# 处方 — mockup conformance 闸合并为「引擎 + 规则模块」，并接上执行点

- 日期：2026-08-24 · 对应 spec §14 **第二轮第 7 步**（§10.2 合并原则）
- 目标 sha：`71ac2711`（lab 的 pin；DS 侧执行时按当时 HEAD 重核行号）
- 执行方：**DS 侧 session 在真源执行**。lab 只出处方（§8 只读边界）
- 依赖：与 `rule-hit` **已解耦**（勘误 E13）；与 7b 输出契约**可并行**
- 立项理由与成本实测：`reports/2026-08-24-round2-preconditions.md` §2（⛔ 不在此复制证据）
- **执行状态**：🟢 **已由 DS 落地** —— pin **`4a68e02d`**（2026-08-27 I 格补登记，见下方）
- **执行状态重取**：@`d07be8e9`（2026-09-11 §30 全量重取，量具 `metrics/proposal-execution-status-refresh.mjs`）—— 落地物现取仍在（`scripts/audit-mockup-conformance.mjs` + `scripts/mockup-rules/*`） ⇒ **结论不变**。

---

> ### 🟢 2026-08-27 执行状态回改（I 格，`lab:N59`）—— **本处方已被执行，⛔ 上面第 5 行那句是未来时态，别照它读成「还没做」**
>
> **lab 亲验读数（pin `4a68e02d`，⛔ 不是转引 status）**：
>
> | 处方要求 | pin `4a68e02d` 上的实测 | 判 |
> |---|---|:-:|
> | 合并为「引擎 + 规则模块」 | `scripts/mockup-rules/` 存在，含 **10 个规则模块** + `_shared.mjs` + `index.mjs` | ✅ |
> | 规则清单真源取父闸 `AUDITS`（§1 那条 ⚠️） | 总闸 `audit-mockup-conformance.mjs:15` 逐字「规则条数 **9 → 10**（新并入 `geometry-consistency` —— 它此前既不在 AUDITS 数组里…）」 | ✅ |
> | **B1 已接**（复用 DS 已有的 PostToolUse hook） | `.claude/settings.json:70` 有 `PostToolUse` · `.claude/hooks/post-figma-write.sh` 存在 | ✅ **hook 存在** |
> | **B2 已交付**（§5.5 第 3 条） | `templates/audit-mockup-workflow.yml` 存在（9,141 B，job `mockup-conformance` `:72`，`:132` 读 `secrets.FIGMA_TOKEN`）· `consumer-postinstall.mjs:16-17` 逐字「**新交付的** `templates/audit-mockup-workflow.yml`」+ `:75-76` 指向它 | ✅ **两项都成立** |
>
> ⛔ **本次未做的事（如实登记，⛔ 别把上面读数读成「§5.5 验收通过」）**：
> - ⛔ **lab 不代执行方勾验收框**（§8 单向纪律）⇒ `§5.5` 那 4 个 `- [ ]` **原样不动**。
> - ~~⛔ **B1 只验到「hook 文件存在」**~~，⛔ **没有**满足 §5.5 第 1 条要求的那份证据
>   （「一次真实的 `use_figma` 写操作后…贴出 hook 实际注入的文本 + 引擎的 report 路径」）
>   —— ~~那需要 **live Figma**，`lab` 够不到。~~**⇒ 「hook 存在」与「hook 真触发过引擎」两句话⛔ 不可互相冒充。**
>   - 🔴 **2026-09-15 第十轮订正 ①（谓语写窄了两档）**：lab 2026-08-25 实际验到的是**静态接线可证**
>     （`reports/2026-08-25-round2-step7-7b-delta.md:212-215` 逐字给了 `post-figma-write.sh:79,83,117-118`
>     与 `detect-figma-task.sh:37-50`），⛔ 不是「只验到 hook 文件存在」。**⛔ 但第 1 条那份证据仍然没有。**
>   - 🔴 **2026-09-15 第十轮订正 ②（那句有两半，只有一半过期 ⇒ ⛔ 不整句删）**：
>     「**需要 live Figma**」**成立**；「**`lab` 够不到**」**实测不成立** ——
>     lab 现持有**已鉴权的 Figma MCP 通道**（`whoami` = `Nancy Zeng` · TVU Networks · pro · Full seat）
>     且 `.env.example:13` 的真 fileKey `YbsPRUVmNdsbN40NNwh1Gn` **实读成功**（返回 3 个顶层页面）；
>     hook 的 matcher 逐字 `mcp__(claude_ai_Figma|plugin_figma_figma)__use_figma` —— **第一个分支
>     正是 lab 持有的那个工具名**。⇒ **真正够不到的是【owner 的授权】**（F62 spec 禁令逐字
>     「不要修改 Figma 里面的内容，**除非我主动让你写**」）⇒ 阻碍**从两个减到一个**。
>     ⚠️ **⛔ 别据此以为可以绕**：**无只读旁路** —— `post-figma-write.sh:33` 有写信号正则、
>     `:108` 逐字「⛔ **只读调用在 :30/:34 已提前 exit 0**」⇒ 要那档证据**必须真写**。
>     ⛔ **边界**：只证了读通道 + 鉴权 + fileKey，⛔ **未证**一次写调用一定成功。
>     读数见 [`reports/2026-09-15-round10-blockers-recheck.md`](../reports/2026-09-15-round10-blockers-recheck.md) §4。
>   - 🔴 **2026-09-15 第十轮·续（订正 ③，⛔ 同日推翻了订正 ② 的处置建议）**：owner 反问
>     「**为什么需要写这个？是为了解决什么问题**」⇒ 重核后 **⛔ 不需要为这份证据专门写 Figma**。
>     ① `stop-figma-conformance.sh:128` 逐字会在「有 Figma 写操作但**零机检**」时**阻塞收尾并报出**
>     ⇒ **这条链断了会自己喊出来，⛔ 不会静默**；② 本机 `${TMPDIR}/tvu-mockup-conformance/`
>     下 **13 个 session 目录** ⇒ ⛔ 不是死链（⚠️ 边界：含 `testsess`/`t3` 等自测名 ⇒ 只证「跑过」，
>     ⛔ 不证「真实写操作下跑过」）；③ ⇒ 专门造一次写只买到「**提前几天知道**」。
>     **⇒ §5.5 第 1 条的取证方式改为：下次在本仓做一次【本来就要发生】的 Figma 写操作时，
>     顺手贴两样 —— hook 实际注入的文本 + 引擎 report 路径
>     （`${TMPDIR}/tvu-mockup-conformance/<session>/report-*.json`）。** 零额外代价。
>     详见报告 §6′（§6 留痕但已被推翻）。
> - ⛔ 未验 §5.5 第 4 条（§4 两个既有缺陷的复现 → 修复前后对照）。
> - ⇒ **`- [ ]` 保持未勾选是正确的**；但「整份处方待执行」这个读法**已失效**。
>
> 📌 **为什么这条迟到**：`docs/round2-status.md:14` 早在第 7 步落地当天就记了「✅ 已落地 `4a68e02d` · lab 已验收（§7）」，
> 而**本处方正文此前零处登记**（I 格实测：执行状态类留痕 0 处 —— `:14` 与 `:130` 那两处「DS 已…」讲的是别的事）。
> 🔴 **根因不是「忘了」**：9 份处方**结构性都没有「执行状态」这一栏** ——
> `status` 里「待 DS 执行」有 **7 处**，`proposals/` 全 9 份 **0 处**。⇒ 已立 `lab:N59`。

## 0. 一句话

**合并的第一价值不是省时间，是让这批闸有可能被接上执行点。**
而「接上执行点」今天卡在 `fileKey` 从哪来 —— 本处方给出的答案是 **B1：复用 DS 已有的 PostToolUse hook**，
它的触发点**今天就已经是自动的**，只差从 URL 里捕获 fileKey 这一个动作。

---

## 1. 范围

**合并 10 条规则到 1 个引擎**：父闸 `scripts/audit-mockup-conformance.mjs` 的 `AUDITS` 数组 9 条
+ `audit-mockup-geometry-consistency.mjs`（N3：它打同样的 API，却既不在 `AUDITS` 数组也无执行点）。

**不并进来的 2 条**（第一轮 §4.1 已定分界线：**要不要外部凭据**）：

| 闸 | 为什么留在外面 |
|---|---|
| `audit-mockup-handoff-evidence` | 读本地 markdown 交付卡，无需 Figma 凭据 —— 这正是它能挂上 `.husky/pre-commit` 的原因 |
| `audit-mockup-html-conformance` | 读本地 HTML 产物，同上 |

> ⚠️ **规则清单的真源必须取父闸的 `AUDITS` 数组**，不能取 npm key 面、也不能取头注释：
> - npm key 面会漏掉 `audit-mockup-connector.mjs`（它**没有自己的 npm key**，只经 `AUDITS` 挂载）
> - 头注释自己 stale 过（父闸头注释曾写死「five」，而活源已是 9 条）

## 2. 目标结构

```
scripts/audit-mockup.mjs              # 引擎：取数一次、解析一次、共享 payload
  └─ rules/
       integrity.mjs        colors.mjs           typography-icon.mjs
       library-origin.mjs   binding-fidelity.mjs bilingual-spacing.mjs
       overlap.mjs          connector.mjs        library-binding.mjs
       geometry-consistency.mjs
```

**§10.2 原则不动：合并执行，不合并规则。** 每条规则保留独立 id 与独立开关（`--rule=binding-fidelity`）。
规则条数 10 → 10，进程 10 → 1。

**取数从 12 次请求（8 次大 payload）降到 3 次（1 次大 payload）**：

| 保留的请求 | 服务的规则 |
|---|---|
| `GET /files/<key>?plugin_data=…`（父闸的 `?depth=1` 探测并入） | integrity · colors · typography-icon · library-origin · binding-fidelity · bilingual-spacing · overlap · connector · geometry-consistency |
| `GET /files/<key>/variables/local` | colors · binding-fidelity |
| `GET /files/<lib>/components` | library-origin |

`library-binding` 不打 API（只读 `figma-data/` 离线缓存），取数拓扑不变。

---

## 3. 执行点方案（**验收必需项**）

合并只拆掉「太慢，挂不上」这一层。「pre-commit 给不出 fileKey」那一层要单独解 ——
**且 DS 自己已经把这条障碍写在闸里了**：`scripts/audit-gate-mount-declaration.mjs:24-29` 逐字
「`audit-mockup-*` 那批子闸……**零挂载是它们的正常形态**（要 Figma token + 具体 fileKey 才能跑，
pre-commit / prepublishOnly 都给不出这个输入）」。

⇒ 处方**不是**「把它挂进 pre-commit」，而是走下面 B1 + B2 两条。

### 3.1 B1（主路径）—— 复用已有的 PostToolUse hook。触发点今天已经是自动的

| 项 | 现状 |
|---|---|
| 触发点 | `.claude/settings.json:70-79` 已注册 PostToolUse hook，matcher = `mcp__(claude_ai_Figma\|plugin_figma_figma)__use_figma`，command = `.claude/hooks/post-figma-write.sh` |
| 已经在做的 | `post-figma-write.sh:33` 正则判「这次是写操作」；`:38` **已经在抽 nodeId**（`grep -oE "[0-9]+:[0-9]+"`） |
| 已经在做的（另一半） | `.claude/hooks/detect-figma-task.sh:48` 的正则**已在匹配** `figma\.com/(design\|file)/` |
| **唯一缺口** | 那条正则**没有捕获**紧跟在后面的 fileKey；hook 今天只注入提醒文本，不执行任何闸（`post-figma-write.sh:41`） |
| 凭据 | 本机 `.env`（`scripts/lib/figma-env.mjs` 已能读）—— **这是 DS 的正常本机形态，不触碰「别往 CI 引凭据」那条政策** |

**要做的三件事**：

1. `detect-figma-task.sh` 的 URL 正则加一个捕获组取 fileKey，落到 session-scoped 文件；
   `post-figma-write.sh` 读它 + 已抽到的 nodeId，拼出 `--file K --node N`。
2. **决定同步还是异步**：整文件跑 140s（`audit-mockup-conformance.mjs:51-59` 自记），
   同步会把 hook 变成两分半的阻塞。⇒ 建议**后台跑 + 下一轮注入结果**。
3. 注入文本里必须写清 `--node` 的**真实覆盖**（见 §3.4 第 1 条），否则会得到一个「看起来验过了」的绿。

> ⚠️ 这条路要改 DS 的 `.claude/hooks/` 与 `settings.json`。按本机规则，**`.claude/` 下的文件必须在前台完成编辑**，
> 不要交给后台 agent。

### 3.2 B2（并行交付）—— 消费产品 CI 模板。这是 DS 自己已推荐的答案

`docs/CONSUMER_AUDIT_SETUP.md:73` 已给出形态：consumer 在自己的 `package.json` 里**写死 fileKey**
（`--file YOUR_FIGMA_FILE_KEY`），`:207-242` 给出 `on: [push, pull_request]` 的 CI 示例，
凭据走 consumer 仓自己的 secret（`:240` `FIGMA_PERSONAL_ACCESS_TOKEN: ${{ secrets.FIGMA_TOKEN }}`）。

**缺的只是模板文件**：现有两份模板 `templates/audit-workflow.yml:11-13` 与
`docs/templates/consumer-audit-ci.yml:11-13`（内容逐字相同）都**明文声明不跑 mockup**。

⇒ 交付一份**带 mockup job 的 workflow 模板**，并修 `scripts/consumer-postinstall.mjs:64` 的指向
（已登记缺陷 `docs/internal/backlog.md:94`：它教 consumer copy 的那份模板 `secrets.` 零命中，
照做的人会配一个那份 workflow 永远不读的 secret）。

**这条正是 `V1_RELEASE_CHECKLIST.md:190` 那条 Recommendation 的可执行版本**
（「Redefine L5 trigger as 'tooling ships + at least 1 consumer actually consumes the gate'」）——
DS 仓自己不需要接线，只交付模板。

### 3.3 明确**排除**的三条（不是漏，是有理由不走）

| 路径 | 排除理由（都有硬证据） |
|---|---|
| **B3 注册表 + 定期跑** | 注册表**已存在**且已被消费（`docs/internal/ds-health-mockup-targets.json:66-109`，6 条 target，schema `{fileKey,label,product,sourceRef,addedAt}`；消费者 `scripts/ds-health-scan-mockups.mjs`）。但那个脚本 `:14-17` 逐字「⛔ **刻意零挂载，这是设计不是缺陷**」，理由正是「pre-commit / CI 里网络+凭据两者都不保证在场」；且它是**观测器不是闸**（always exit 0）。把 conformance 挂上去等于推翻一条 DS 明文的设计决策 ⇒ 需 owner 拍板，不放进本处方 |
| **B5 `gates.yml` + dispatch inputs** | `gates.yml:25-26` **一个 input 都没有**；要新加 Figma secret，而 `pr-checks.yml:197-198` 逐字「**接受它，别为它往 CI 引凭据**」；且 `gates.yml:22-24` 自陈 Actions 配额是不可靠资源（正是 INFRA-F71/F73 把发布迁走的原因）。⚠️ 那条政策的原文语境是 handoff gate，**是否覆盖新建 dispatch workflow 未明文** ⇒ 属 owner 决策，不是 lab 能代拍的 |
| **B6 CI cron** | 全仓 4 个 workflow 的 `on:` 块逐个核过，**零 `schedule:`、零 `cron`**，无先例；凭据/配额问题同 B5。另：Gitea 侧根本没有 `workflow_dispatch`（`.gitea/workflows/publish.yml:47-52` 逐字：这台是 **1.22.6**，而 `workflow_dispatch` 是 **1.23.0** 才加的） |

> **B4「从改动的交付卡抽 fileKey」不是独立路径，是 B2 的变体。** 交付卡 frontmatter 里确实有
> `figma.file`（`templates/consumer-product/docs/handoffs/_handoff.template.md:12-15`），
> 但①它逐字标注「可选；无则删本段」、②无任何校验闸（`build-request-index.mjs:12` 的 `REQUIRED` 不含它）、
> ③**DS 仓判定面恒空** —— `.gitea/workflows/pr-checks.yml:186` 逐字「真 handoff 落在消费产品仓库」，
> `docs/handoffs/` 下 0 份 `.md`。⇒ 有素材的地方是消费产品仓，那就是 B2。

### 3.4 三条不能在合并中丢的语义（否则「更快的闸」会变成「更快的假绿」）

1. **`--node` 只约束 7/9**（2026-08-14 DS 实测，`.claude/agents/design-review.md:111-119`）。
   不吃 `--node` 的两条：`library-binding`（按 positional fileKey 调，只读离线缓存 = 某天的**整文件**快照）、
   `connector`（live REST，但**计数是全文件**）。
   ⇒ 引擎必须**逐规则声明它的 scope**，`--node` 不得被表述成「已把范围缩到这个节点」。
2. **缺参 exit 2**。现状 11/11 条缺 `--file` 一律 `exit 2`（不是静默 exit 0）。
   ⛔ 引擎里不得退化成 exit 0 —— 那会造出「无参恒绿」的假绿，
   而 DS 已为同类问题付过学费（`audit-mockup-handoff-evidence.mjs:23` 逐字：
   「⛔ CI 里**不许裸调无参** —— 无参 files=[] 恒 exit 0，会得到一个永远绿的 step，比 L4-only 更糟」）。
3. **`unverified` 是独立一档，`exitCode=0 ≠ 验过了`**（`.claude/agents/design-review.md:122-138`；
   实测反例：connector 在字段取不到时优雅降级后仍 exit 0 ——「那个 ✅ 成立于**空集**」）。
   缓存时效的三态 fail-closed/fail-open 分档（`audit-mockup-conformance.mjs:173-194`）必须原样保留。

### 3.5 合并动作本身会撞上的一条元闸

`scripts/audit-gate-mount-declaration.mjs:73-99` 的 `KNOWN_SILENT` 豁免表里逐字列着这 10 条中的 **10 条**，
而该表是 **shrink-only**（`:32-33`：某个存量补了声明后它不再命中 ⇒ 本闸报 FAIL 要求删行）。

⇒ **合并必须同批更新豁免表**：旧条目变 stale 会让元闸判红；新文件不在表里则按 D1/D2 立即受判。
新引擎的头注释走 **D2 形态**（`:24-29`）：显式声明「没有自有挂载 / 经 X 挂载」。

⛔ 且别把新文件改名成非 `audit-*` 前缀来绕开元闸 —— 那三个前缀正是元闸判定「这是一条闸」的扫描面。

---

## 4. 顺带修掉的两个既有缺陷（合并时几乎免费）

| # | 缺陷 | 证据 |
|---|---|---|
| 1 | **`audit:consumer-mockup` 这条 npm 别名结构上跑不通**（不是假绿，是必红）。`pnpm run <别名> --file K` 把参数追加到**整条 shell 串末尾** ⇒ 只有最后一个脚本收到 `--file`，第一个 `audit-mockup-integrity.mjs` 在任何 fetch 之前就 `die('missing required --file')` exit 2，`&&` 直接短路 | `package.json:112`；已登记 `docs/internal/backlog.md:485-486`（INFRA-F132） |
| 2 | **comma 形态的 `--node` 两条路径口径矛盾**。父闸落 report 时 `NODE_ID.split(',')`（支持逗号列表），却把 `NODE_ID` **原样**透传给子闸，而 `audit-mockup-integrity.mjs:438-441` 是 `data.nodes?.[NODE_ID]` 精确键查找 ⇒ 传 `--node "a,b"` 会 not found。⚠️ **读码所得，未实跑验证** | `audit-mockup-conformance.mjs:393` vs `:283-304` vs `audit-mockup-integrity.mjs:438-441` |

合并成单进程后，参数解析只有一处，这两条自然消失 —— 但**必须在验收里显式验**，否则「自然消失」只是期望。

---

## 5. 验收标准

⛔ **每一条都要有可复核证据，不接受「已合并 / 通过」式断言**（AGENTS §4）。

### 5.1 结构

- [ ] 规则模块数 = **10**，逐条与合并前的 `AUDITS` 数组 + `geometry-consistency` 对齐（**逐条列出对照表**，不给总数）
- [ ] 每条规则可独立开关（`--rule=<id>`），且 `--rule` 传未知 id 时 **exit≠0**（不是静默跑全量）
- [ ] 进程数 10 → 1（用 `ps` / 引擎自印证据，不用「应该是」）

### 5.2 取数

- [ ] 同一次运行里 `api.figma.com` 请求数 = **3**（不含重试）。
      ⚠️ **必须用请求计数器自证，不能用耗时代理** —— lab 实测过「12s 的 run 同样 steps 为空」这类代理判据失效
- [ ] 大 payload（`/files/<key>`）请求数 = **1**

### 5.3 语义未丢（§3.4 三条各一项）

- [ ] 缺 `--file` ⇒ **exit 2**（不是 0）
- [ ] 每条规则自印其 scope；`--node` 下 `library-binding` / `connector` 明确标注为**全文件口径**
- [ ] `unverified` 仍是独立一档：构造一个「比较输入缺失」的场景，验其**不被报成 pass**

### 5.4 元闸

- [ ] `pnpm audit:gate-mount-declaration` 绿，且 `KNOWN_SILENT` 表**已同批缩表**（列出删/增行）
- [ ] `pnpm audit:gate-ci-parity` 绿

### 5.5 **执行点（缺此项则本步不算完成）**

- [ ] **B1 已接**：一次真实的 `use_figma` 写操作后，hook 自动拼出 `--file`/`--node` 并触发了合并后的引擎，
      **贴出 hook 实际注入的文本 + 引擎的 report 路径**作为证据
- [ ] B1 的 fileKey 是**从 URL 捕获的**，不是写死的常量
- [ ] **B2 已交付**：带 mockup job 的 consumer workflow 模板存在，且 `consumer-postinstall.mjs` 指向它
- [ ] §4 的两个既有缺陷各有一条复现 → 修复的前后对照

### 5.6 ⛔ 不算验收通过的情形

- 闸绿但**分母是空的**（无参 / 节点不存在 / 缓存缺失走了降级）。
  **「闸绿」不是校验结论** —— 必须同时给出「这次实际检查了多少个节点 / 多少条规则真跑了」。
- 只测了 `--node` 模式就宣布 140s 问题解决 —— 带 `--node` 的耗时**DS 侧至今无实测数**（lab 已三轮搜索确认查不到），
  合并后的整文件耗时要**重新实测**并记进头注释。

---

## 6. 登记的边界与已知未知

1. **合并后的整文件耗时无法事先预估。** preconditions §2.4 里「余量 ≈135s ≈ 96%」是**按差值倒推**的，
   不是直接测的（直接测要真调 Figma REST，DS 明文禁止 lab 这么干）。⇒ 量级可用，别当精确值。
2. **带 `--node` 的绝对收益更小**：payload 小得多，但**请求条数的 8→1 不变**。
3. `binding-fidelity` 整文件模式实测可达 **1009 条 findings**（父闸因此把 maxBuffer 从 1MB 放大到 64MB）
   ⇒ 引擎的输出缓冲要按这个量级设计。
4. 离线缓存单份实测 **402 MB**，禁止整份 `JSON.parse`（父闸只读头 4KB 取 `_meta.extractedAt`）。
5. **本处方不解决「谁来维护 fileKey 注册表」**。B1 走 URL 捕获，绕过了注册表；
   B3 那条注册表本身自陈「人工维护 —— 漏登记一个产品文件 = 静默少测，读起来却像『全测了』」
   （`ds-health-mockup-targets.json:4`）。这条风险随 B3 一起被排除在外，不是被解决。
