# DG-1 Layout 原语 —— 现状实测 + 三选项 + 推荐（交 owner 选路线）

> **性质**：本文件**只是决策材料**。backlog [`INFRA-F68`](../../internal/backlog.md) 把 DG-1 标为「**Figma-source + owner**」，因此本轮**刻意不往 `src/tokens/variables.css` 写任何 breakpoint / container 数值** —— 那是 owner 拍完路线之后的事。
> **来源**：计划 [`_plans/2026-07-30-v1x-next-batch-ai-consumable-page-layer.md`](../plans/2026-07-30-v1x-next-batch-ai-consumable-page-layer.md) Task 5。
> **日期**：2026-07-30。所有数字都是当日实跑，不是估值；命令逐条附在 §1，可复核。

---

## §0 TL;DR（含一处对计划自身推荐的订正）

- 现状是**三处全零**：code 侧 0 个 breakpoint/container/grid token、0 处 `@media`、0 个 Layout/Grid/Container 组件；**Figma 侧同样零** —— 设计库无 grid style，产品文件里那 219 个 layoutGrid 全是 `pattern:GRID / sectionSize:10` 的**10px 对齐辅助网格**，`pattern:COLUMNS` 响应式列栅格命中 **0**。
- ⚠️ **计划草稿写的「推荐 A（Figma-first 变量）、fallback B」在执行时被实测推翻**：A 的落地机制是「designer 在 Figma 建变量 → `pnpm generate` 按名同步」，而**那条同步管道当前是断的** —— `figma-data/raw/variables.json` 冻结在 **2026-04-22**（`9a4a52ca`），拉取端点稳定 **403**，缺的 `file_variables:read` 是 **Enterprise-only scope**（[[INFRA-F79]]，ack 到期 2026-10-28）。designer 就算把变量建好，代码也拉不到。
  > 🔴 **2026-09-01 订正：上面这个前提已作废，⛔ 但结论不变 —— 别据此重开 A。** 变量层刷新管线当日建成（走 `use_figma` = Plugin API 执行面，不受那个 Enterprise-only scope 限制；`raw/variables.json` 已刷新到 live 98 条，`normalize.mjs` 的自环也修了），**管道不再是断的**。⇒ 当时否掉 A 的那条**可执行性**理由没了。<br>**但 B 仍是这一族的答案，理由换了一条更硬的**：owner **2026-07-31 显式拍定 layout 断点 / 容器宽是 code-first**，已登记在 `divergences-decisions.json`；2026-09-01 那条「Figma 是真源」的裁定管的是**另一个 token 族**（`Spacing/Radius/Size/Basic Size/Module Width`），两条并存不矛盾。另有一条实测：Figma 的 `Module Width/*` 是 200–1200 一档，与 `--container-*` 的 720/840/1600/1800 **没有一个值重合** ⇒ 就算走 A，那边也没有可用的源。
- **因此推荐改为 B（code-first token）作为当下路线、A 作为终点**，且迁 A 时是纯换真源方向、**不改 token 名**。详见 §4（含推荐自审 4 问留痕）。
- **三个选项都不改变一件事**：**数值由 owner/designer 定，AI 不发明**。选项之争是「真源放哪」，不是「谁挑数字」——这两件事正交（§4.1）。

---

## §1 现状（原始命令 + 原始输出）

### ①  code 侧：breakpoint / container / grid token = 0

```bash
$ grep -cE "breakpoint|--container|--grid-" src/tokens/variables.css
0
```

### ②  `src/` 内 `@media` = 0 处

```bash
$ grep -rl "@media" src/ | wc -l
0
```

含义：**36 个组件全都是宽度不敏感的** —— 要么流式撑满、要么由 prop 定死宽度（如 dropdown 的 L/M/S variant）。组件层没有任何"到某个宽度就改布局"的表达。

### ③  无 layout 类 canonical 组件

```bash
$ ls src/canonical/ | grep -iE "layout|grid|container|row|col|space"
(none)
```

### ④  Figma 侧同样没有响应式栅格（**这条是本轮最容易被误读的**）

设计库 `figma-styles.json` 的顶层只有 `textStyles`(14) / `effectStyles`(4) / `scaleTokens`(2)，`grid` / `breakpoint` / `container` 关键词命中**全 0**：

```bash
$ node -e "const s=require('./figma-data/normalized/figma-styles.json');
  console.log(Object.keys(s));
  const a=JSON.stringify(s);
  for(const k of ['grid','Grid','GRID','breakpoint','container'])
    console.log(k, (a.match(new RegExp(k,'g'))||[]).length);"
[ '_meta', 'textStyles', 'effectStyles', 'scaleTokens', '_alignment_decisions' ]
grid 0 / Grid 0 / GRID 0 / breakpoint 0 / container 0
```

**⚠️ 广谱复扫后的订正**：`figma-data/` 全目录里 `layoutGrid` 关键词其实有 **484** 处命中，集中在两个**产品 mockup 文件**。一眼看去像「designer 已经在用栅格、把值抄过来就行」——**逐条测下去是反的**：

```bash
$ grep -rhoE '"pattern"\s*:\s*"[A-Z]+"' figma-data/ | sort | uniq -c
 219 "pattern": "GRID"
```

219 个带 `layoutGrids` 的节点**全部是同一条规格**：

```json
{ "pattern": "GRID", "count": -1, "gutterSize": 0, "offset": 0, "sectionSize": 10, "alignment": "MIN" }
```

即 **10px 见方的像素对齐辅助网格**（画稿时吸附用），不是响应式列栅格。真正的列栅格形态 `pattern: "COLUMNS"`（配 `count: 12` + gutter + margin）命中 **0**；另一个产品文件带 `layoutGrids` 的节点数是 **0**。

> **结论**：**响应式栅格在设计侧也不存在**，不是"存在但没同步过来"。这决定了选项 A 不是"去读 designer 已有的东西"，而是"请 designer 从零建一套新东西"。
> **留痕**：本条起初被我读成「设计侧已有栅格、可直接采值」，是靠 `pattern` 分布实测证伪的 —— 关键词命中数不等于概念存在（[[feedback_name-search-absent-fallacy]] 的同型）。数据是 **2026-07-28 / 2026-07-17 的缓存快照**，owner 若要据此拍板，建议让 designer 在 live 文件确认一句。

### ⑤  唯一真的需要响应式的地方，自己就地发明了数值

仓库里唯一有 `@media` 的是 docs 站（`playground/`）—— **5 处、4 个各不相同的断点值，全是就地硬写**：

```bash
$ grep -rn "@media" playground/ --include='*.css' --include='*.vue'
playground/CanonicalShowcase.vue:780:@media (max-width: 1100px) {
playground/docs/docs.css:429:@media (max-width: 640px) {
playground/docs/docs.css:918:@media (max-width: 1100px) {
playground/docs/docs.css:939:@media (max-width: 720px) {
playground/docs/pages/ColorPage.vue:594:@media (max-width: 960px) {
```

**这就是"没有契约会发生什么"的现成样本**：同一个站里 1100 / 960 / 720 / 640 四个值并存，没有任何一处解释为什么是这几个数。消费方 app 会重演一遍，而且每个 app 演一套。

### ⑥  选项 A 的管道口当前是断的（**决定推荐方向**）

```bash
$ git log -1 --format="%h %ad" --date=iso -- figma-data/raw/variables.json
9a4a52ca 2026-04-22 14:06:59 +0800

$ cat figma-data/variables-sync-status.json
"lastSuccess": null,
"lastFailure": { "at": "2026-07-30T05:47:01.169Z",
  "message": "Figma API 403 ... This endpoint requires the file_variables:read scope" }
```

同步链是 `extract → raw/variables.json → sync:normalize-variables → normalized/variables.json → generate-tokens → variables.css`（`figma-sync/sync-figma-library.mjs:20-21`）。**第一步就 403**，`file_variables:read` 是 **Enterprise-only**，本账号是 Professional，两次独立签发 PAT 的 scope 列表逐字相同且都无此项（[[INFRA-F79]] / STATUS §2b；⛔ 别再重签 PAT、别再问 owner 确认 plan）。

**A 的解锁路径只有两条**：(a) Figma plan 升级 = **采购问题，不是工程问题**；(b) **Figma plugin 侧导出**变量（绕开 REST，F79 entry 已登记为可评估项，未做可行性验证）。

---

## §2 为什么这是能力 4 的瓶颈（以及**它不是什么**的诚实边界）

`PROJECT_GOAL.md` 能力 4 要求「AI 拿文字/链接/截图/代码 → 生成符合本规范的 UX 效果图 + 可运行代码网页」。页面级合成缺可组合的布局契约时，AI 只能把组件拼在一起，**无法表达「两列在窄屏下如何变一列」**。

**本轮刚 ship 的配方给出了一个不能更具体的证据**（`figma-data/page-recipes.json` `master-detail-page`，`46752944`）：

```json
{ "slot": "master", "position": "left (or top)"  }
{ "slot": "detail", "position": "right (or below)" }
```

那两个 **"(or …)"** 就是缺口本体 —— 配方能说"主列表在左**或**在上"，但**说不出在什么条件下是哪一种**，因为词汇表里没有断点这个概念。AI 读到它只能猜，或者沿用它在训练语料里见过的别家断点值。

**诚实边界（订正计划初稿的一处半编依赖）**：

- ❌ **DG-1 不是整个 pattern 层的前置**。三条配方的 `layoutTokens` 实测**只引用 `--sp-*`**（间距维度已经够表达，token 全在 `variables.css` 真定义过，S3 闸在核），出第四、第五个配方**不需要**任何 breakpoint token。
- ✅ **DG-1 只在"响应式断点/容器宽度"这一维上是真缺口**。把它说成 PAT-01 的关键路径是错的（该说法已在计划 §排期依据 里自我订正过一次，此处再钉一遍）。

---

## §3 三个选项

> 每条含：改哪些文件 / 谁是真源 / 破坏性 / 验收方式 / 谁能落地 / **当前可执行性**。

### 选项 A — `figma-first-variables`

| 项 | 内容 |
|---|---|
| **做法** | 请 designer 在 Figma 建 breakpoint / container 变量集合 → `pnpm generate` 按名同步 → `audit:token-contract`（L5）自动纳管 |
| **真源** | Figma Variable（与现有 54 个变量同一机制） |
| **改哪些文件** | 无需新增闸：`variables.css` 的 primitive tier 由 `generate-tokens` 写入；`audit:token-contract` 自动把新变量纳入「不可手改」保护 |
| **破坏性** | 无（纯 additive token） |
| **验收** | 改 Figma 变量值 → 跑 `pnpm sync:figma-library --with-extract` → `variables.css` 跟着变；手改代码值 → `audit:token-contract` 必须红 |
| **谁能落地** | designer（建变量）+ owner（排期） |
| **当前可执行性** | 🟡 **管道已通、但源不存在**（2026-09-01 订正，⛔ 别据此重开 A）—— 原写「🔴 不可执行：REST 403，管道第一步就断」，那条**已作废**：当日走 (b) plugin 侧（`use_figma`）把管线建起来了，raw 已刷到 live 98 条。**现在挡住 A 的是另外两条**：① owner 2026-07-31 **显式拍定** layout 断点/容器宽 code-first（`divergences-decisions.json`）② Figma 的 `Module Width/*`（200–1200）与 `--container-*`（720/840/1600/1800）**零值重合** ⇒ 那边没有可用的源 |
| **符合度** | ✅ 最符合硬规则 #1 + `FIGMA_AS_SOURCE_OF_TRUTH.md` 的默认方向 |
| **额外未知** | Figma Variable 适不适合表达断点（变量是「值」，断点是「条件」）需 designer 判断——这不是纯排期问题 |

### 选项 B — `code-first-tokens`

| 项 | 内容 |
|---|---|
| **做法** | 按 `--sp-*` 的既有先例做 **code-authored token**（如 `--bp-*` / `--container-*`），走 `divergences-decisions.json` 登记 code-first |
| **真源** | code（`src/tokens/variables.css`） |
| **改哪些文件** | `variables.css`（新分组，受 `audit:sort-tokens` I1/I2 约束）· `divergences-decisions.json`（类别 = 「用户显式 code-first 拍板」，注释含 `user approved code-first YYYY-MM-DD`）· page-recipes schema 若要收断点维度则一并扩 |
| **破坏性** | 无（纯 additive） |
| **验收** | `audit:sort-tokens` 绿 · `audit:token-contract` 绿（无 Figma 上游、不受该闸管，同 `--sp-*` 性质） · `audit:no-hardcoded-design-tokens` 可扩一档抓「写死 px 断点」 |
| **谁能落地** | **数值由 owner/designer 给**，AI 只负责接线与上闸 |
| **当前可执行性** | 🟢 **可执行**（不依赖任何被 403 挡住的东西） |
| **代价** | 布局真源落在 code 侧，与「Figma 是真源」的默认相反 —— `FIGMA_AS_SOURCE_OF_TRUTH.md` §合法差异第 6 条「用户显式 code-first 拍板」是**唯一合法出口**，需 owner 显式拍板 |

### 选项 C — `recipe-only-no-tokens`

| 项 | 内容 |
|---|---|
| **做法** | 不建 token，只在 page-recipes 里用**语义档位名**（如 `compact` / `regular` / `wide`）描述断点意图，具体像素交消费方 app |
| **真源** | 无（各 app 各自解释） |
| **改哪些文件** | 只改 `page-recipes.schema.json` + 配方数据 |
| **破坏性** | 无 |
| **验收** | schema 闸能校档位名在枚举内 —— 但**校不了跨 app 一致性**（没有可校的分母） |
| **当前可执行性** | 🟢 可执行 |
| **代价** | **把不一致外包给消费方**：AI 生成的同一个页面在两个 app 里断点不同，正是 §1⑤ 那 4 个值并存的现象被制度化。与能力 4「生成符合本规范的可运行网页」目标相反 |

---

## §4 推荐：**B 作当下路线、A 作终点；不推荐 C**

**推荐 = B**，理由三条：

1. **A 当前不可执行，且阻塞点是采购不是工程**（§1⑥）。这不是"A 更贵所以选 B"，是"A 的第一步返 403"。在 plan 升级或 plugin 导出路径验证之前，选 A 等于选"什么都不做"，而缺口会继续由每个消费方各自发明数值来填（§1⑤ 已经在 docs 站上演了一遍）。
2. **B 迁 A 的成本被刻意压到最低**：token **名字不变**，只把 `--bp-*` 从 code-authored 转成 Figma-synced，即**换真源方向、不换 API**。所以 B 不是与 A 竞争的岔路，是 A 的可回收过渡态。
3. **C 会把问题制度化**。它唯一的好处是"不用决定数值"，但数值终归要有人定；C 只是把决定权散给 N 个 app，换来 N 套互不相同的断点。

**明确交回 owner 的**：数值本身（§4.1）+ 走不走 B 这条 code-first 出口（`FIGMA_AS_SOURCE_OF_TRUTH.md` 第 6 条要求"用户显式拍板"，AI 不得代拍）。

### §4.1 「真源放哪」与「谁挑数字」是两个正交问题

三个选项都**不改变**这一条：**AI 不发明断点数值**。因为 §1④ 证明设计侧也没有现成栅格可抄，所以数值必须新定，而定它需要产品判断（TVU 的媒体类 dashboard 实际在什么屏宽被使用、主从页在哪个宽度该塌成一列）——那是 owner/designer 的活。

若 owner 选 B，落地任务的**输入前提**就是一张由 owner/designer 给出的档位表；AI 的工作从"接到这张表"开始，不在之前。

### §4.2 推荐自审 4 问（[`meta-rules.md`](../../meta-rules.md) 触发器 F）

1. **理由经得起反驳吗？** —— §1⑥ 是可复核的原始输出（403 报文 + 冻结 commit 号 + 同步链行号），不是推测。**并且我推翻了计划自己写的推荐**，方向是从"符合默认原则的 A"改到"当下可执行的 B"，不是反过来给自己找省事的理由。
2. **是否把最省事的包装成 #1？** —— 三条里最省事的是 **C**（只改 JSON、不碰 token、不需要 owner 定数值），它被明确列为**不推荐**。B 比 C 贵（要 owner 定值 + 要开 divergence + 要扩闸）。
3. **有没有更稳健的选项被压成脚注？** —— A 是最稳健的，它**没有**被压成脚注：它被写成"终点"，且 §1⑥ 逐条给出了它的两条解锁路径（plan 升级 / plugin 导出）。B 的设计目标就是让迁 A 时不改 token 名。
4. **该我拍还是 owner 拍？** —— **owner 拍**。这也是本 task 只出 spec、`git diff --stat src/tokens/variables.css` 必须为空的原因。

---

## §5 明确不在本 spec 范围

| 不含 | 理由 |
|---|---|
| **Layout / Grid / Container 组件** | 属 [[INFRA-F68]] **COV-02 系统级缺件（Figma-first + owner）**；AI 不得自造组件设计。token 与组件是两件事，可分别决策 |
| **TKN-01 算法主题** | 与 DG-1 同属「要不要在 Figma 建新变量集合」的同一个 owner 决策簇 —— 等本条拍完再排（计划 §不做清单原文） |
| **任何 breakpoint / container 数值** | backlog 明写 DG-1 是「Figma-source + owner」；见 §4.1 |
| **`page-recipes` 收断点维度的 schema 改动** | 依赖本条选路线的结果（选 C 才需要档位枚举，选 A/B 则是 token 引用） |

---

## §6b owner 裁定（2026-07-31）—— **路线已定 = B 当下 + A 终点**

owner 2026-07-31 ack 推荐，即 §4 原文：**选项 B（`code-first-tokens`）作当下路线、选项 A（`figma-first-variables`）作终点、不走 C**。

**这条裁定解开的和没解开的，分开写**（教训来源：[`retrospection/2026-07-31-…`](../../internal/retrospection/2026-07-31-gate-criteria-ink-vs-box-and-entry-restatement-trust.md) §6「暂缓 ≠ 前提已不存在」）：

| 项 | 状态 |
|---|---|
| **走 B 这条 code-first 出口**（`FIGMA_AS_SOURCE_OF_TRUTH.md` §合法差异第 6 条要求「用户显式拍板」）| ✅ **已拍** —— 落地时在 `divergences-decisions.json` 写 `user approved code-first 2026-07-31` |
| **断点 / 容器的具体数值** | ⚠️ **本行已被同日的 [§6c](#6c-owner-追加裁定2026-07-31-同日-不等新表值取实测现值--做成唯一定义) 推翻，别按它去等表** —— owner 当日追加裁定「根据实际信息来显示即可 + 要有唯一值」，改为**取仓库实测现值做成单一定义**（一个新值都不发明），token 层当日已 ship。以下是被推翻的原文：⛔ 仍在 owner/designer 手里，未给 —— 见 §4.1：设计侧也没有现成栅格可抄，所以数值必须新定，而定它需要产品判断（TVU 媒体类 dashboard 实际在什么屏宽被用、主从页在哪个宽度塌成一列）。**AI 不得自拟视觉值**，落地任务从「拿到这张表」开始 |
| §6 问题 3（要不要先花一次性成本验证 **plugin 侧导出**能否绕开 403）| ⛔ 未答（属 A 的解锁路径，不阻塞 B）|
| §6 问题 4（「解锁 A」要不要立独立 entry）| ⛔ 未答（当前仍挂在 [[INFRA-F79]] 残余里，性质 = 采购 + 一条未验证的技术旁路）|

**因此本条现在的状态是「等一张表」，不是「等选路线」** —— 下一个 session 别再把三个选项重新摆一遍。

---

## §6c owner 追加裁定（2026-07-31 同日）—— **不等新表，值取实测现值 + 做成唯一定义**

> ⚠️ **这一段推翻了 §6b 表格里「等一张 owner/designer 给的档位表」那一行。** 别再按 §6b 去等表；
> 也别把本段读成「AI 可以自拟视觉值」—— 恰恰相反，本段的做法是**一个新值都不发明**。

owner 原话：**「根据实际信息来显示即可，是不是得有个唯一值，这样多个其他地方引用它，不容易有遗漏」**。

**解开了什么**：把问题从「谁来定新数值」换成「已经在用的那些数值，凭什么散在 N 处各写一遍」。
§1⑤ 早就摆出了证据（docs 站 4 个并存断点），本轮把范围扫全后更难看：**首方 CSS/Vue 里 10 处
`@media` 用了 7 个互不相同的宽度值，另有 21 处 `max/min-width ≥600px` 字面量、14 个不同值**。
owner 要的不是第八个值，是**让这些值只有一处定义、别处引用**。

**据此落地（当日 ship）**：

| 项 | 内容 |
|---|---|
| **值的来源** | 八个值**逐字取自改动前仓库真实在用的值**：`--bp-sm/md/lg/xl` = 640/960/1100/1280（640/960/1100 各出现 2 处；1280 = docs 站与 `test:visual` 的 viewport 宽）· `--container-narrow/prose/page/wide` = 720/840/1600/1800（实测容器宽）。**纯命名化，零渲染变化** —— 刻意不规整成 600/900/1200/1500，那会移动窄屏布局、属 owner/designer 的视觉裁定。同 `--z-*` 组当年的做法（`variables.css` 里那段注释就是先例）。 |
| **唯一定义在哪** | `src/tokens/variables.css` 两个 code-authored 组。非 CSS 消费方（AI / 非 web 工具 / consumer JS）经 `./tokens` DTCG + `./tokens/js` 出口拿到它们 —— `generate-token-exports` 从同一份文件派生，**不存第二份**。 |
| **怎么"引用"** | 容器宽可以真引用：5 处已改成 `max-width: var(--container-*)`。**断点不能** —— CSS `@media` 语法吃不了 `var()`，所以可校验的引用形式 = **字面量 ∈ token 值集合**，由 `pnpm audit:layout-tokens` 机械核。 |
| **闸** | `scripts/audit-layout-tokens.mjs`：**S1** 分母 fail closed（解析不出 `--bp-*`/`--container-*` → 红，防把 token 删了让闸空转成假绿）· **S2** 扫描面为 0 → 红 · **S3** 每个 `@media` 宽度必须 ∈ 值集合，认不出的宽度形态（range syntax）也算红 · **S4** 豁免表 shrink-only（不再命中 → 红、要求删行）· **S5** report-only 印出剩余 `≥600px` 字面量 + 计数。Enforcement = **L4** pre-commit 条件 gate + **L5** prepublishOnly（→ 同时进 Gitea `pr-checks` 与 master-push CI）。23 单测 + **4 组故障注入实测转红**（ad-hoc 1024px · `(width <= 640px)` range syntax · 删整组 `--bp-*` · 豁免源消失），复原后与备份逐字节相同、闸回绿。 |
| **合法性** | `divergences-decisions.json` → `layout-breakpoint-container-tokens-code-first-2026-07-31`，`user approved code-first 2026-07-31`（`FIGMA_AS_SOURCE_OF_TRUTH.md` §合法差异第 6 条唯一出口）。 |

**没解开的、如实留着**（别读成"布局已收口"）：

1. **3 个存量一次性断点**（`docs.css` 720 / `progress-demo.css` 900 / `button-demo.css` 980）走闸里
   **具名 + 带日期 + 带修法方向的 shrink-only 豁免**。归并到最近档会改 640–960 / 900–960 / 960–980
   区间的行为，而 `test:visual` 只在 1280 宽拍照、**看不到那些区间** → 需 owner/designer 视觉裁定。
   表空着是终态不是待办；修完由闸自己宣布（S4 会要求删行）。
2. **容器宽未收口** —— S5 只 report-only 印出剩余 **19** 处字面量。它们是 demo/页面**内容宽**
   （600/760/860/900/980/1200…各 1-3 处），硬塞进容器档位就是把偶然值当契约（meta-rules 反模式 #6 的同型）。
3. **§6 问题 3 / 4 仍未答**（plugin 侧导出能否绕开 403 · 「解锁 A」是否立独立 entry）—— 属 A 的解锁路径，不阻塞。
4. **`page-recipes` 收断点维度**仍未做（§5 原文：依赖选路线的结果）。现在有 token 名可引用了，
   那两个 `"(or …)"` 可以改成引用 `--bp-*`，但那是独立一步。

---

## §6 owner 需要回答的

1. **A / B / C 选哪个？**（推荐 **B 当下 + A 终点**，理由见 §4）
2. 若选 **B**：请给一张断点/容器档位表（§4.1）——AI 从拿到它开始接线，不自行拟值。
3. 若选 **A**：要不要先花一次性成本**验证 plugin 侧导出**能否绕开 403（F79 残余项）？在那之前 A 落不了地。
4. 是否要把「解锁 A」这件事本身立成独立 entry？（当前它挂在 [[INFRA-F79]] 的残余里，性质是采购 + 一个未验证的技术旁路）
