{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://tvu.internal/schemas/page-recipes.schema.json",
  "title": "Page Recipes — AI 机读页面级组合配方",
  "description": "figma-data/page-recipes.json 的结构真源。缘起 = APID-01 (2026-07-22) ship 首个页面配方时留下的 follow-up：该文件此前只被 JSON-parse，字段拼错/漏字段/引用不存在的组件都不会被任何东西拦住。字段集以 data-table-page 的实际形状为准归纳（不是照 _meta.schema_note 的散文猜），additionalProperties:false 是这份 schema 的主要价值——拼错字段名会红。校验器 = scripts/audit-page-recipes.mjs（pnpm audit:page-recipes）。",
  "type": "object",
  "required": ["_meta", "recipes"],
  "additionalProperties": false,
  "properties": {
    "_meta": {
      "type": "object",
      "description": "文件级说明。刻意允许附加键——这里是给人读的文档区，不是数据契约。",
      "required": ["title", "purpose", "is_source_of_truth", "schema_note"],
      "properties": {
        "title": { "type": "string", "minLength": 1 },
        "purpose": { "type": "string", "minLength": 20 },
        "is_source_of_truth": { "const": true },
        "hand_authored": { "type": "boolean" },
        "registered_in": { "type": "string" },
        "source_spec": { "type": "string" },
        "schema_file": { "type": "string" },
        "related": { "type": "object" },
        "schema_note": { "type": "string", "minLength": 20 }
      }
    },
    "recipes": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/recipe" }
    }
  },
  "$defs": {
    "recipe": {
      "type": "object",
      "required": ["id", "summary", "slots", "states", "behaviorOwnedByApp", "layoutTokens"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
          "description": "kebab-case 稳定标识，AI 按它查配方。"
        },
        "summary": { "type": "string", "minLength": 20 },
        "slots": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/slot" }
        },
        "states": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/state" }
        },
        "cellRendering": { "$ref": "#/$defs/cellRendering" },
        "behaviorOwnedByApp": {
          "const": true,
          "description": "本层只描述呈现态。若某天真要出一个由 DS 承担运行时行为的配方，先改 schema 并在 divergences 登记，别偷偷把它设成 false。"
        },
        "behaviorNote": { "type": "string", "minLength": 20 },
        "layoutTokens": {
          "type": "object",
          "description": "语义位置名 → var(--token) 引用。刻意只收 var() 形态：数值字面量会绕过 variables.css 真源，校验器 S3 会核每个 token 真定义过。",
          "minProperties": 1,
          "additionalProperties": {
            "type": "string",
            "pattern": "^var\\(--[a-z0-9-]+\\)$"
          }
        }
      }
    },
    "slot": {
      "type": "object",
      "required": ["slot", "required", "component", "position", "description"],
      "additionalProperties": false,
      "properties": {
        "slot": { "type": "string", "minLength": 1 },
        "required": { "type": "boolean" },
        "component": {
          "type": ["string", "null"],
          "description": "canonical 组件名（校验器 S2 核它真存在于 src/canonical/*.vue），或 null = 纯布局占位、内容由消费方 app 决定。"
        },
        "position": {
          "type": "string",
          "minLength": 1,
          "description": "宽屏（≥ responsive.stacksBelow）下的位置。⚠️ 只写一个位置——「窄屏时改到别处」不许写成散文（\"left (or top)\"），那种写法 AI 解析不出切换点。要表达响应式改位就填 responsive，校验器 S5 核。"
        },
        "responsive": { "$ref": "#/$defs/responsive" },
        "description": { "type": "string", "minLength": 10 }
      }
    },
    "responsive": {
      "type": "object",
      "description": "该 slot 的响应式改位。可选——不写 = 该 slot 在所有宽度下位置不变。",
      "required": ["stacksBelow", "stackedPosition"],
      "additionalProperties": false,
      "properties": {
        "stacksBelow": {
          "type": "string",
          "pattern": "^--bp-[a-z0-9-]+$",
          "description": "视口宽小于该断点 token 的值时，本 slot 改到 stackedPosition。⚠️ 刻意写裸 token 名而非 var(--x)：CSS `@media` 语法吃不了 var()（见 variables.css §Layout breakpoints），消费方要的是数值，从 `./tokens/js` / `./tokens` DTCG 出口解析。校验器 S5 核它真定义过且真属 --bp-* 组。"
        },
        "stackedPosition": {
          "type": "string",
          "minLength": 1,
          "description": "窄屏下的位置（如 \"top\" / \"below master\"）。与 position 同为自由描述，但两者各自只描述一个确定档位。"
        }
      }
    },
    "state": {
      "type": "object",
      "required": ["state", "when", "driven_by"],
      "additionalProperties": false,
      "properties": {
        "state": { "type": "string", "minLength": 1 },
        "when": { "type": "string", "minLength": 5 },
        "driven_by": {
          "type": "string",
          "pattern": "^(prop:[A-Za-z][A-Za-z0-9]*(\\+[A-Za-z][A-Za-z0-9]*)*|slot:[a-z][a-z0-9-]*|[A-Za-z][A-Za-z0-9]*\\.[A-Za-z][A-Za-z0-9]*)$",
          "description": "只三种合法形态：prop:name（可 + 连多个）· slot:name · Component.field（如 column.sortOrder）。写成散文就红——AI 要机械解析这个字段。"
        },
        "note": { "type": "string", "minLength": 10 }
      }
    },
    "cellRendering": {
      "type": "object",
      "required": ["driven_by", "summary", "kinds"],
      "additionalProperties": false,
      "properties": {
        "driven_by": { "type": "string", "minLength": 3 },
        "summary": { "type": "string", "minLength": 20 },
        "kinds": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["kind", "renders", "shape"],
            "additionalProperties": false,
            "properties": {
              "kind": { "type": "string", "pattern": "^[a-z][a-z0-9]*$" },
              "renders": { "type": "string", "minLength": 3 },
              "shape": { "type": "string", "minLength": 5 },
              "note": { "type": "string", "minLength": 5 }
            }
          }
        },
        "interactionDelegation": { "type": "string", "minLength": 20 },
        "note": { "type": "string", "minLength": 10 }
      }
    }
  }
}
