# 第二轮 A 格实施计划 —— ② 造故障那一类（处方 1 / 2 / 3）

> **给执行者：** 逐 Task 执行，每个 Task 结束必须有可复核证据（AGENTS §4：⛔ 不接受「已完成 / 通过」式断言）。
> 步骤用 checkbox 跟踪。⛔ 未打勾的不得写进报告的 ✅ 列。

**Goal：** 在**一次性 worktree** 上补齐 DS 本轮 5 份处方里**全部造故障类验收条目**，
不破坏四个 pin 的 `dirty=0` 不变式。

**Architecture：** 新开第 5 个 `git worktree`（detached at `c1db57b8` = P3）作**可弄脏的沙盒**。
每格走同一套五步闭环：**预注册期望 → 先证故障态成立 → 跑判据 → 还原 → 证还原干净**。
做完拆掉 worktree，四个 pin 起止 `dirty=0`。

**Tech Stack：** Node 24 · pnpm · DS 仓库自带闸脚本（`scripts/*.mjs`、`figma-sync/*.mjs`）·
lab 侧量具 `adapters/ds-gate-chain-run.mjs`、`adapters/ds-gate-output-contract.mjs`

### ⚠️ 两个装置，⛔ 别用错（本计划起草时踩过一次，见 §0.4）

| 装置 | 命令 | 它是什么 | 能验什么 |
|---|---|---|---|
| **被测对象** | `node scripts/run-gate-chain.mjs`（= `pnpm run prepublishOnly`），在 `wt-fault` 内跑 | DS 自己的**收集式 runner** | 处方 1 §5.1/§5.2/§5.3 的**全部**格 —— 它们验的正是这个 runner 的行为 |
| **lab 独立第二实现** | `node adapters/ds-gate-chain-run.mjs --ds <wt-fault>` | **逐步独立跑** 39 步，每步各起一个进程 | 成本画像 · 契约读数 · 交叉核对 runner 的逐步表（§3.7 双向夹逼） |

⛔ **`ds-gate-chain-run.mjs` 结构性地验不出 §5.1。** 它头注释逐字写着「⛔ 为什么逐步独立跑，
而不是 `pnpm run prepublishOnly`」——**每步都必然被执行**。拿一个「必然全跑」的装置去证
「是不是全跑了」，返回的 39 会是**装置的属性、不是被测对象的属性**（`lab:N48` 同族：
装置故障的返回值恰好是一个合法读数）。

**Spec（本计划实现的验收面）：**
- [`proposals/2026-08-25-chain-failfast-denominator.md`](../proposals/2026-08-25-chain-failfast-denominator.md) §5.1–§5.5
- [`proposals/2026-08-25-crosscheck-denominator-gap.md`](../proposals/2026-08-25-crosscheck-denominator-gap.md) §4
- [`proposals/2026-08-24-stale-anchors-codespan-gap.md`](../proposals/2026-08-24-stale-anchors-codespan-gap.md) §5
- 上轮对账结果：[`reports/2026-08-26-round2-repin-part2.md`](../reports/2026-08-26-round2-repin-part2.md) §4

---

## 0. 覆盖面盘点（⛔ 计数器口径已自查，见 §0.2）

### 0.1 26 条验收条目的三态

| 处方 | 条目 | 上轮状态 | 本计划 |
|---|---|---|---|
| **1** §5.1-a | 第 1 步失败 ⇒ 第 2…39 步全部仍执行，逐步表 39 行都有 exit+耗时 | ⛔ | **Task 2** |
| **1** §5.1-b | 同一次运行 runner 总退出码非 0 | ⛔ | **Task 2** |
| **1** §5.1-c | must-not-hit：全绿时 exit 0 且仍 39 行 | ✅ 上轮已过 | Task 1 复证（同环境基线） |
| **1** §5.2-a | `package.json` 新增一步、**不改 runner 一个字** ⇒ 逐步表 40 行 | ⛔ **§18.8 漏登** | **Task 3** |
| **1** §5.2-b | must-not-hit：npm key 改成不存在 ⇒ 当场抛/非 0，⛔ 不静默跳过 | ⛔ **§18.8 漏登** | **Task 3** |
| **1** §5.3-a | 第 1 步与第 39 步同时失败 ⇒ 摘要**两条都列**（步号+npmKey+exit+stderr 尾） | ⛔ | **Task 4** |
| **1** §5.3-b | must-not-hit：只 1 条失败时摘要**不得**出现第二条 | ⛔ | **Task 4** |
| **1** §5.4 ×3 | 11 条契约闸各打 `findings`+`checkedUnits` / `lint:ds` 不进闸计数 / 收割面仍 11 | ✅ 上轮已过 | — |
| **1** §5.5-a | 全绿热态总耗时（`--repeat≥3` 丢弃第一次） | ✅ 上轮已过 | — |
| **1** §5.5-b | **失败 run 的耗时**，与「改造前失败即停」对照 | ⛔ **§18.8 漏登** | **Task 5** |
| **2** A | 契约闸 `checkedUnits`→0（`findings>0`）⇒ harvest exit≠0 且逐字报 `empty-denominator` | ⛔ | **Task 6** |
| **2** B | 同上 + 该闸同时非 0 退出 ⇒ 仍报 `empty-denominator`（⛔ 不被 `blocked` 吃掉） | ⛔ | **Task 6** |
| **2** MN① | 分母正常时不误报，只 `audit:figma-conformance` 一条 | ✅ 上轮已过 | Task 1 复证 |
| **2** MN② | `checkedUnits` **缺失**（≠0）⇒ ⛔ 不得判 `empty-denominator`，走 fail-closed 抛错 | ⛔ **§18.8 漏登** | **Task 6** |
| **2** 单测 | `CROSS_CHECK_VERDICTS` 每取值各一例 + `0 × {exit}×{findings}` 四组合各一例 | ⚠️ 只确认文件存在 | **Task 7**（= §18.8 第 3 条） |
| **2** 校验 | `buildGateOutput` 的 `checkedUnits` 必填校验一字不动 | ✅ 上轮已过 | — |
| **3** 1 | 未改动仓库跑 `audit:stale-anchors` ⇒ exit 0 且锚点 TOTAL > 529 | ✅ 上轮已过（767） | Task 1 复证 |
| **3** 2 | 删 `rule-execution-discipline-gap.md` ⇒ exit≠0，孤儿清单含该路径 + 指出引用方 | ⛔ | **Task 8** |
| **3** 3 | 删 `v4-1865-rules-backflow.md` ⇒ exit≠0 | ⛔ | **Task 8** |
| **3** 4 | must-not-hit：外部仓路径 code-span ⇒ exit 0 | ⛔ | **Task 9** |
| **3** 5 | must-not-hit：无目录分量（`Badge.figma.ts` / `x.md`）⇒ exit 0 | ⛔ | **Task 9** |
| **3** 6 | must-not-hit：非段边界子串 ⇒ exit 0 | ⛔ | **Task 9** |
| **3** 7 | 恢复后复跑 ⇒ 回到判据 1 的读数 | ⛔ | **Task 9** |
| **3** 8 | 报那两份文件的**实际 exit code + 孤儿行原文** | ⛔ **§18.8 漏登** | **Task 8** |

**合计 26 条 = 已过 8 + 本计划 17 + 已在别格 1**（处方 2 单测 = §18.8 第 3 条，顺手一起做）。

### 0.2 计数器口径自查（`lab:N48` 推论五，⛔ 五项各查一遍）

| 项 | 查法 | 结果 |
|---|---|---|
| **截断** | 三份 proposal 全文行数 186 / 137 / 108，提取脚本读全文非 `head` | ✅ 未截断 |
| **容器类型** | 处方 1/2 用 `- [ ]` checkbox；**处方 3 用表格行** `\| N \|` —— 两种都数 | ✅ 两种都覆盖 |
| **节边界** | 按 `^#{2,4}` 归属，⛔ 不用跨节正则 | ✅ |
| **标签形态** | 处方 3 是 `1`–`8` 数字判据，非 `D1–D11` 式；未重编号 | ✅ |
| **字段路径** | n/a（本次不读 JSON 字段） | — |

⇒ **17 条这个数是 lab 自己的清单，⛔ 不与任何对方自称对立** —— §18.8 第 1 条登记的 12 条是 lab 自己漏登的 5 条，不是 DS 的问题。

### 0.4 起草本计划时踩到的第 6 例装置误用（`lab:N48` 同族，⛔ 要写进报告）

计划初稿把 Task 2/3/4 的取证装置全写成了 lab 的 `adapters/ds-gate-chain-run.mjs`。
核实其头注释后发现：**它逐步独立跑，每步各起一个进程，⛔ 刻意不跑 `prepublishOnly`。**
⇒ 它在任何情况下都会打出 39 行 —— **拿它去证「第 1 步失败后第 2…39 步仍执行」，
得到的 39 是装置的属性，不是被测对象的属性。**

⚠️ 与前五例的差别：前五例是**计数器口径**错（数错了），这一例是**装置与命题不匹配**
（数对了，但数的不是那个东西）。⇒ **操作口径**：造故障验收前，先问
「**这个装置在故障不存在时和存在时会给出不同的读数吗**」——答不出「会」，装置就不成立。

⇒ 由此加一条**独立旁证**（⛔ 不依赖 runner 自称，处方 §5.1 逐字要求「证据不是『runner 说它跑了』」）：
**第 1 步失败时 runner 的总墙钟耗时**。若 ≈ 全绿全链耗时 ⇒ 后 38 步确实跑了；
若 ≈ 第 1 步单步耗时 ⇒ 仍在 fail-fast。这条旁证与 Task 5 的代价读数**同一次测量取到**。

### 0.3 已避免的装置故障（`lab:N48` 主条，⛔ 必须写进报告）

处方 3 判据 2 逐字写引用方 `mockup-conventions.md:3237`。**在 P3 上 `docs/mockup-conventions.md` 不存在。**
若照原文直接 `rm` 目标文件跑闸，拿到的任何 exit code 都会被误归因。

独立确认判定面非空的结果：

| 事实 | 实测 |
|---|---|
| `docs/_archive/retrospection/2026-06-01-rule-execution-discipline-gap.md` | **TRACKED 在** |
| `docs/_archive/retrospection/2026-06-01-v4-1865-rules-backflow.md` | **TRACKED 在** |
| 引用方真身 | **`docs/internal/mockup-conventions.md`**，⛔ 不是 `docs/mockup-conventions.md` |
| 引用行号 | **3241**（处方称 3237 —— 处方写于 08-24，P3 是 08-25，⛔ 不算差异） |
| 引用形态 | 相对路径 `` `../_archive/retrospection/…` ``，⛔ 不是 `docs/_archive/…` |
| 另有引用方 | `docs/internal/STATUS-CHANGELOG.md`、`docs/_archive/_reports/…roadmap-2026-06-08.md`、`tests/stale-anchors.test.ts` |

⚠️ **判据 2 的期望要按实测形态写**：孤儿清单里出现的很可能是**相对路径原文**而非仓根路径。
⛔ 期望写死成仓根路径会把「闸正常工作」误判成「闸漏了」。

---

## Global Constraints（⛔ 每个 Task 都适用，违反即本轮作废）

1. **⛔ 四个 pin 一个都不碰。** 起止各跑一次 `git -C <pin> status --porcelain`，必须**空**。
   造故障只在一次性 worktree 里做。
2. **⛔ 验 fail-closed 不带管道单跑**（`lab:N47`）。`node x.mjs | tail` 的 `$?` 是 `tail` 的。
   要看输出就 `> out.txt 2>&1; echo "EXIT=$?"` 再读文件。
3. **⛔ 判据返回 0 / 空集时，先用独立方式确认判定面非空**（`lab:N48`），再解释那个 0。
4. **⛔ 把自己的计数与对方自称对立之前，先证自己的计数器口径** —— 截断 / 标签形态 / 节边界 /
   字段路径 / 容器类型五项各查一遍（`lab:N48` 推论五）。
5. **⛔ 报性能 delta 必须带环境名**（`lab:E47`，AGENTS §2 第 14 条）。本轮唯一合法环境名 =
   **`wt-fault`（一次性 worktree，无 `figma-data/mockup`）**。⛔ 不得与 DS 主工作树读数并池。
6. **⛔ 每格造故障后必须还原并证还原干净**：`git -C <wt> status --porcelain` 回到空。
   ⛔ 不许「跑完下一格再一起还原」——中间态污染无法归因。
7. **预注册先于实测**：每个 Task 的 Step 1 就是把期望写进 `runs/faultinjection-prereg.md`
   并 **commit**，⛔ 跑完再补写不算预注册。
8. 每条读数记 §5.2 三元组 `(c1db57b8, claude-opus-5, pt-v1)`；纯 T1 确定性项后两位记 `n/a` 但**显式记**。
9. **⛔ 取证前先问「这个装置在故障存在与不存在时会给出不同读数吗」**（§0.4 新增）。
   答不出「会」⇒ 装置与命题不匹配，换装置。处方 1 的三格一律用 **DS runner**，
   ⛔ 不用 lab 的逐步独立跑 adapter。

---

## 文件面

| 动作 | 路径 | 职责 |
|---|---|---|
| Create | `docs/round2-faultinjection-plan.md` | 本计划（= 预注册的载体之一） |
| Create | `runs/faultinjection-prereg.md` | 逐 Task 盲预测，⛔ 跑前 commit |
| Create | `runs/faultinjection-evidence/` | 每格的 stdout/stderr 原文 + `EXIT=` 行 |
| Create | `reports/2026-08-26-round2-faultinjection.md` | 交付报告 |
| Modify | `docs/round2-status.md` | 新增 §19；订正 §18.8 第 1 条的条数（12 → 17） |
| Modify | `AGENTS.md` | 若本轮产出新勘误/纪律则加 |
| **⛔ 不碰** | `~/.ai-ds-lab/pins/*` | 四个 pin |
| **⛔ 不碰** | `tasks/tvu-ds/v1/*.json` | golden set（本轮不涉冻结面） |

---

## Task 1：建一次性 worktree + 同环境全绿基线

**Files:** Create `~/.ai-ds-lab/wt-fault`（一次性）· `runs/faultinjection-evidence/task1-*.txt`

**Interfaces — 后续 Task 全部依赖本 Task 产出的三个常量：**
- `WT=/Users/nancy/.ai-ds-lab/wt-fault`（环境名 **`wt-fault`**）
- `BASELINE_STEPS=39`、`BASELINE_EXIT=0`
- `BASELINE_MS`（全绿热态总耗时均值，`wt-fault` 口径）

- [ ] **Step 1：预注册**。写进 `runs/faultinjection-prereg.md` 并 commit：
  > **T1-P1**：`wt-fault` 上全绿全链 = 39 步 / exit 0 / `unresolved`=0，与 P3 pin 逐步一致。
  > **T1-P2**：`wt-fault` 的全链热态总耗时与 P3 pin 的 8,976 ms **落在同一噪声地板内**
  > （2σ=526 ms），因两者都是 `git worktree` ⇒ 都无 `figma-data/mockup`。
  > **T1-P3**：`audit:stale-anchors` 锚点 TOTAL = **767**、exit 0（复现上轮读数）。

- [ ] **Step 2：起止不变式 —— 先记四个 pin 的干净态**

```bash
for p in 71ac2711 339a72bf 4a68e02d c1db57b8; do
  printf '%-12s dirty=%s\n' "$p" "$(git -C ~/.ai-ds-lab/pins/tvu-ds-$p status --porcelain | wc -l | tr -d ' ')"
done
```
Expected：四行全 `dirty=0`。⛔ 任何一行非 0 ⇒ **停，先查清再动**。

- [ ] **Step 3：建 worktree**

```bash
DS=/Users/nancy/Documents/AICoding/VS_Code/tvu-design-system
git -C "$DS" worktree add --detach /Users/nancy/.ai-ds-lab/wt-fault c1db57b8
git -C /Users/nancy/.ai-ds-lab/wt-fault rev-parse HEAD   # 期望 c1db57b8759f…
git -C /Users/nancy/.ai-ds-lab/wt-fault status --porcelain | wc -l   # 期望 0
```

- [ ] **Step 4：装 node_modules（⛔ 不得改 lockfile）**

主选项（APFS clone，近乎零耗时零占用）：
```bash
cp -Rc ~/.ai-ds-lab/pins/tvu-ds-c1db57b8/node_modules ~/.ai-ds-lab/wt-fault/node_modules
```
fallback（clone 不可用时）：`cp -R`；再不行 `pnpm install --frozen-lockfile --offline`。
装完立刻：
```bash
git -C ~/.ai-ds-lab/wt-fault status --porcelain   # 期望空（node_modules 是 gitignored）
```
⛔ 若 `pnpm-lock.yaml` 出现在 status 里 ⇒ **回滚该文件**，换 `cp` 路线。

- [ ] **Step 5：全绿基线 —— 两个装置各取一份（⛔ 都不带管道单跑）**

(a) **被测对象**（DS runner，后续 Task 2/3/4 的取证装置）：
```bash
cd ~/.ai-ds-lab/wt-fault
/usr/bin/time -p node scripts/run-gate-chain.mjs \
  > /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab/runs/faultinjection-evidence/task1-runner-green.txt 2>&1
echo "EXIT=$?" >> /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab/runs/faultinjection-evidence/task1-runner-green.txt
```
Expected：`EXIT=0`、逐步表 **39 行**、`lint:ds` 标为「非闸」、结尾**无失败摘要**（§5.1-c must-not-hit）。
⚠️ 跑 ≥3 次**丢弃第一次**（AGENTS §3.6 冷启动 3.4 倍），记 `BASELINE_MS`（`wt-fault` 口径）。

(b) **lab 独立第二实现**（成本画像 + 契约读数 + 交叉核对）：
```bash
cd /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab
node adapters/ds-gate-chain-run.mjs --ds ~/.ai-ds-lab/wt-fault --repeat 3 \
  --out runs/faultinjection-evidence/task1-labadapter.json \
  > runs/faultinjection-evidence/task1-labadapter.txt 2>&1
echo "EXIT=$?" >> runs/faultinjection-evidence/task1-labadapter.txt
```
参数面已核实（[adapters/ds-gate-chain-run.mjs:49](../adapters/ds-gate-chain-run.mjs#L49)）：
`--ds <worktree>`（**⛔ 不是 `--repo`**）· `[--repeat 3]` · `[--out <json>]` · `[--only <npmKey>]` ·
`[--chain-key <key>]` · `[--no-controls]`。`--repeat` 必须 ≥2（量具自己会抛）。

- [ ] **Step 5b：交叉核对两个装置**（§3.7 双向夹逼）——
  两份的**步数**、**每步 npmKey**、**每步 exit**、11 条契约闸的 `findings/checkedUnits` 应一致。
  ⚠️ **耗时不比**：一个在链里跑、一个各起进程，OS 缓存热度不同 ⇒ ⛔ 不同口径不并池。
  不一致就逐条对账并登记，⛔ 不默认哪一份对。

- [ ] **Step 6：对账 T1-P1/P2/P3**，逐条 HIT/MISS 写进证据文件。
  ⛔ MISS 不得改预测，只记 MISS + 原因。
  ⚠️ 若 T1-P2 MISS（`wt-fault` 与 P3 差超过 2σ）⇒ **两个环境不可比** ⇒
  Task 5 的耗时对照必须**全部在 `wt-fault` 内部做**，⛔ 不得跨到 P3。

- [ ] **Step 7：commit**（`docs:` 前缀，证据文件 + 预注册）

---

## Task 2：处方 1 §5.1 —— 第 1 步失败，第 2…39 步仍全跑

**Files:** Modify（临时）`~/.ai-ds-lab/wt-fault/<第 1 步闸的输入文件>`

**Interfaces — Consumes:** Task 1 的 `WT`、`BASELINE_STEPS=39`

- [ ] **Step 1：预注册**
  > **T2-P1**：造第 1 步 `audit:icon-fill-currentcolor` 失败后，逐步表仍是 **39 行**，
  > 且第 2…39 行**每行都有 exit 与耗时**（⛔ 不是「runner 说它跑了」）。
  > **T2-P2**：runner 总退出码 **非 0**。
  > **T2-P3**：失败行只有第 1 行，第 2…39 行 exit 全 0（⛔ 若有连带失败，说明闸之间有隐藏耦合 ⇒ 那是新发现）。

- [ ] **Step 2：勘察造故障点 —— ⛔ 改闸的输入，不改闸脚本本身**

```bash
sed -n '1,60p' ~/.ai-ds-lab/wt-fault/figma-sync/audit-icon-fill-currentcolor.mjs
```
找出它扫哪个目录、判什么。选**一个** icon 文件，把 `fill="currentColor"` 改成硬编码色值。
⛔ 不得用 `process.exit(1)` 伪造失败 —— 那验的是 runner 不是分母恒定。

- [ ] **Step 3：先证故障态成立（⛔ 不带管道单跑）**

```bash
cd ~/.ai-ds-lab/wt-fault && pnpm run audit:icon-fill-currentcolor \
  > /tmp/t2-single.txt 2>&1; echo "EXIT=$?" >> /tmp/t2-single.txt
```
Expected：`EXIT` **非 0**，且 stdout 逐字指出被改的那个文件。
⛔ `EXIT=0` ⇒ 故障没造成，**停**，换造故障点，⛔ 不许带着假故障往下跑。

- [ ] **Step 4：跑被测对象取逐步表（⛔ 必须是 DS runner，⛔ 不是 lab adapter —— 见 §0.4）**

```bash
cd ~/.ai-ds-lab/wt-fault
/usr/bin/time -p node scripts/run-gate-chain.mjs \
  > /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab/runs/faultinjection-evidence/task2-step1-fail.txt 2>&1
echo "EXIT=$?" >> /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab/runs/faultinjection-evidence/task2-step1-fail.txt
```

- [ ] **Step 5：程序化核逐步表（⛔ 不肉眼数，`lab:N48` 推论五）**
  用脚本数：总行数 / 有 exit 字段的行数 / 有耗时字段的行数 / exit≠0 的行号集合。
  三个计数必须**都等于 39**；exit≠0 的行号集合必须**恰好 = {1}**。
  ⚠️ 先确认逐步表的**行形态**（前缀/分隔符），⛔ 别用会把摘要段也数进去的正则（截断/节边界两项自查）。

- [ ] **Step 5b：独立旁证 —— ⛔ 不依赖 runner 自称（§0.4）**
  取本次 `/usr/bin/time -p` 的 `real`，与 Task 1 的 `BASELINE_MS`、以及第 1 步单步耗时三者并排：
  - ≈ `BASELINE_MS` ⇒ 后 38 步**确实跑了**（T2-P1 独立成立）
  - ≈ 单步耗时 ⇒ **仍在 fail-fast**，逐步表的 39 行是假的 ⇒ 这是**推翻性发现**
  ⛔ 三个数都带环境名 `wt-fault`（`lab:E47`）。本读数同时交付 Task 5 的 (b)。

- [ ] **Step 6：还原 + 证还原干净**

```bash
git -C ~/.ai-ds-lab/wt-fault checkout -- .
git -C ~/.ai-ds-lab/wt-fault status --porcelain   # 期望空
```

- [ ] **Step 7：对账 + commit**

---

## Task 3：处方 1 §5.2 —— 步骤面不是第二真源（⛔ §18.8 漏登的两格）

- [ ] **Step 1：预注册**
  > **T3-P1**：在 `package.json` 的 `gate-chain` 末尾追加第 40 步（一个必绿的 npm key），
  > **不改 `scripts/run-gate-chain.mjs` 一个字**，逐步表变成 **40 行**。
  > **T3-P2**：把某步 npm key 改成不存在的名字 ⇒ runner **当场抛/非 0**，⛔ 不静默跳过。
  > **T3-P3（新增，会推翻 T3-P2 的形态）**：`gate-chain` 是 `&&` 拼的 `pnpm run X` 串 ⇒
  > 不存在的 key 很可能由 **pnpm 本身**报错而非 runner 检测 ⇒
  > 「fail closed」成立但**责任方是 pnpm 不是 runner**。这两种要分开记，⛔ 别合并成一句「通过」。

- [ ] **Step 2：40 行那格**。在 `wt-fault/package.json` 的 `gate-chain` 值末尾追加
  ` && pnpm run <一个已存在且必绿的 key>`（挑 Task 1 基线里 exit 0 且最快的那步，重复一次即可）。
  ⛔ 不新增依赖、不改 lockfile。

- [ ] **Step 3：证 runner 未被改**

```bash
git -C ~/.ai-ds-lab/wt-fault status --porcelain   # 期望只有 package.json 一行
git -C ~/.ai-ds-lab/wt-fault diff --stat scripts/run-gate-chain.mjs   # 期望空
```

- [ ] **Step 4：跑 DS runner，程序化数逐步表行数** ⇒ 期望 **40**。
```bash
cd ~/.ai-ds-lab/wt-fault && node scripts/run-gate-chain.mjs > /tmp/t3-40.txt 2>&1; echo "EXIT=$?" >> /tmp/t3-40.txt
```
⚠️ lab adapter 也应从 `gate-chain` 读出 40 步（它同样以 `package.json` 为真源）⇒
**两个装置都报 40 才算 HIT**；只有一个变说明另一个抄了清单，那本身是发现。

- [ ] **Step 5：还原 package.json，证干净**（`git checkout -- package.json` + status 空）

- [ ] **Step 6：坏 key 那格**。把 `gate-chain` 里某步改成 `pnpm run audit:this-key-does-not-exist`。

- [ ] **Step 7：⛔ 不带管道单跑，分别取两条证据**
  - runner 路径：`node scripts/run-gate-chain.mjs > /tmp/t3-runner.txt 2>&1; echo "EXIT=$?"`
  - 链路径：`pnpm run gate-chain > /tmp/t3-chain.txt 2>&1; echo "EXIT=$?"`

  两者都要非 0。**在报告里分开写「谁抓到的」** —— runner 自己检测出未知 key，
  还是 pnpm 报 `Missing script`。⛔ 别写成一句「fail closed 通过」。

- [ ] **Step 8：还原 + 证干净 + 对账 + commit**

---

## Task 4：处方 1 §5.3 —— 失败可定位（两条都列 / 只 1 条时不得多列）

- [ ] **Step 1：预注册**
  > **T4-P1**：第 1 步与第 39 步（`audit:figma-env-single-source`）同时失败 ⇒
  > 结尾摘要**恰好两条**，每条含 **步号 + npmKey + exit + stderr 尾部**四个字段。
  > **T4-P2**：只让第 1 步失败时，摘要**恰好一条**，⛔ 不得出现第二条（不得把 warning 混进失败摘要）。

- [ ] **Step 2：勘察第 39 步的造故障点**（读 `scripts/audit-figma-env-single-source.mjs`，改其输入）。

- [ ] **Step 3：先分别单跑证两个故障态各自成立**（⛔ 不带管道，各自 `EXIT` 非 0）。

- [ ] **Step 4：双故障跑 DS runner**（⛔ 被测对象，非 lab adapter），取**结尾摘要段原文**进证据文件。
```bash
cd ~/.ai-ds-lab/wt-fault && node scripts/run-gate-chain.mjs > /tmp/t4-both.txt 2>&1; echo "EXIT=$?" >> /tmp/t4-both.txt
```

- [ ] **Step 5：程序化核摘要**：条数 = 2；对每条断言四个字段**都非空**。
  ⛔ 「摘要里出现了 39」不等于「步号字段存在」—— 按字段路径取，别按子串匹配（`lab:N48` 推论五）。

- [ ] **Step 6：还原第 39 步的故障，只留第 1 步故障**，重跑 ⇒ 摘要条数**必须 = 1**。

- [ ] **Step 7：全部还原 + 证干净 + 对账 + commit**

---

## Task 5：处方 1 §5.5-b —— 失败 run 的耗时（⛔ §18.8 漏登；⛔ 带环境名）

**⚠️ 这是本处方的代价侧，处方 §5.5 逐字要求「⛔ 不许只报收益」。**

- [ ] **Step 1：预注册**
  > **T5-P1**：`wt-fault` 口径下，**第 1 步失败**的全链耗时 ≈ 全绿耗时（因为改造后 39 步都要跑完），
  > 与「改造前失败即停」的耗时（≈ 第 1 步单步耗时）相差 **接近全链总时长**。
  > **T5-P2**：该 delta **远大于噪声地板 2σ** ⇒ 是真实代价，⛔ 不是噪声。

- [ ] **Step 2：三个读数全部在 `wt-fault` 内部、且全部用 DS runner 的墙钟取（⛔ 不跨环境，`lab:E47`）**
  - (a) 全绿全链：Task 1 Step 5(a) 的 `BASELINE_MS`
  - (b) 第 1 步失败的全链（改造后语义）：**复用 Task 2 Step 5b 已取的那次**，补到 ≥3 次丢弃第一次
  - (c) 「改造前失败即停」的等价耗时：第 1 步单步耗时（`pnpm run audit:icon-fill-currentcolor` 的墙钟，
    ≥3 次丢弃第一次）
    ⚠️ 这是**等价口径模拟**，不是真跑旧 runner ⇒ **必须在报告里逐字标为「等价口径，非旧 runner 实跑」**。
  ⛔ 三个数都用同一把尺（`/usr/bin/time -p` 的 `real`），⛔ 不与 lab adapter 的逐步耗时并池。

- [ ] **Step 3：算噪声地板** = 合并 σ 的 2 倍，与 delta 并排报（AGENTS §2 第 8/9 条：并排报 MDE/mean）。

- [ ] **Step 4：写句子时带环境名** —— 「在 **`wt-fault`（git worktree，无 `figma-data/mockup`）** 口径上，
  fail-fast 改造的代价是 +X ms」。⛔ 不写不带口径名的裸数。

- [ ] **Step 5：还原 + 证干净 + 对账 + commit**

---

## Task 6：处方 2 造故障 A / B + must-not-hit ②

- [ ] **Step 1：预注册**
  > **T6-P1（A）**：把某条在链契约闸的 `checkedUnits` 改成 0（保持 `findings>0`）⇒
  > `report:gate-output-harvest` **exit 非 0** 且逐字出现 `empty-denominator`。
  > **T6-P2（B）**：同上 + 该闸同时非 0 退出 ⇒ **仍报 `empty-denominator`**，⛔ 不被 `blocked` 吃掉。
  > **T6-P3（MN②）**：`checkedUnits` **缺失**（不是 0）⇒ **⛔ 不得**判 `empty-denominator`，
  > 必须走 fail-closed 抛错路径 —— 「没传」与「真的是 0」是两件事。

- [ ] **Step 2：选造故障闸并先证故障态成立**。挑一条 `findings>0` 的契约闸
  （Task 1 基线里 `audit:no-hardcoded-design-tokens` = 19/74 是候选）。
  改 `buildGateOutput` 调用处的 `checkedUnits` 实参 ⇒ **单跑该闸**，
  贴出 stdout 的 `totals` 段原文证明 `checkedUnits=0 ∧ findings>0`（处方 §4 逐字要求）。

- [ ] **Step 3：跑 harvest（⛔ 不带管道）**

```bash
cd ~/.ai-ds-lab/wt-fault && pnpm run report:gate-output-harvest \
  > /tmp/t6a.txt 2>&1; echo "EXIT=$?" >> /tmp/t6a.txt
grep -c "empty-denominator" /tmp/t6a.txt
```
⚠️ harvest 可能需要先跑一遍全链产出 gate output ⇒ **先读 `scripts/gate-output-harvest.mjs` 头部**
确认它的输入从哪来，⛔ 别假设。

- [ ] **Step 4：B 格** —— 同一条闸再让它非 0 退出，重跑 harvest。
  断言 `empty-denominator` **仍在**，且 verdict **不是** `blocked`。
  ⛔ 按字段路径取 verdict，别按子串。

- [ ] **Step 5：MN② 格** —— 把该实参改成**完全不传**（删掉 `checkedUnits` 键）。
  期望：**抛错**（`buildGateOutput` 的必填校验），⛔ 不得静默变成 `empty-denominator`。
  ⚠️ 若这一格返回 0 / 空集，先按 Global Constraint 3 独立确认判定面非空。

- [ ] **Step 6：还原 + 证干净**（`git checkout -- .` + status 空）+ 对账 + commit

---

## Task 7：处方 2 单测逐格核（= §18.8 第 3 条）

- [ ] **Step 1：预注册**
  > **T7-P1**：`tests/gate-output-contract.test.ts` 里，`CROSS_CHECK_VERDICTS` 的**每一种取值**都有 ≥1 例。
  > **T7-P2**：`checkedUnits=0 × {exit 0/≠0} × {findings 0/>0}` **四种组合各有 ≥1 例**。

- [ ] **Step 2：先取 `CROSS_CHECK_VERDICTS` 的全集**（从实现里按字段路径取，⛔ 不从注释抄）。

- [ ] **Step 3：程序化把单测用例映射到 verdict / 组合**，出一张覆盖矩阵。
  ⛔ 「文件里出现了这个字符串」不算覆盖 —— 要落到具体 `it()` 用例。

- [ ] **Step 4：跑一次单测确认全绿**（⛔ 不带管道，`echo "EXIT=$?"`）。

- [ ] **Step 5：矩阵有空格 ⇒ 如实登记为缺口**，⛔ 不替 DS 补写单测（那是 DS 的活）。

- [ ] **Step 6：commit**

---

## Task 8：处方 3 判据 2 / 3 / 8 —— 该拦的拦住了

**⚠️ 先读 §0.3。目标文件路径与引用方路径都与处方原文不同。**

- [ ] **Step 1：预注册**
  > **T8-P1**：删 `docs/_archive/retrospection/2026-06-01-rule-execution-discipline-gap.md` ⇒
  > `audit:stale-anchors` **exit ≠ 0**，孤儿清单含该文件。
  > **T8-P2**：孤儿行里指出的引用方是 **`docs/internal/mockup-conventions.md:3241`**
  > （⛔ 不是处方写的 `mockup-conventions.md:3237`）。
  > **T8-P3**：孤儿清单里出现的路径形态很可能是**相对路径原文** `../_archive/retrospection/…`，
  > ⛔ 不必然是仓根路径 —— 两种形态都算 HIT，**报告里如实登记是哪种**。
  > **T8-P4**：删 `2026-06-01-v4-1865-rules-backflow.md` ⇒ 同样 exit ≠ 0。
  > **T8-P5（判据 8）**：贴出两次的**实际 exit code 数值 + 孤儿行原文**。

- [ ] **Step 2：判据面复证（⛔ 删之前，Global Constraint 3）**

```bash
WT=~/.ai-ds-lab/wt-fault
git -C "$WT" cat-file -e HEAD:docs/_archive/retrospection/2026-06-01-rule-execution-discipline-gap.md && echo "目标1 在"
git -C "$WT" cat-file -e HEAD:docs/_archive/retrospection/2026-06-01-v4-1865-rules-backflow.md && echo "目标2 在"
grep -c "rule-execution-discipline-gap" "$WT/docs/internal/mockup-conventions.md"   # 期望 ≥1
```

- [ ] **Step 3：基线复证** —— 未改动时 `audit:stale-anchors` exit 0、锚点 TOTAL = 767（判据 1）。

- [ ] **Step 4：删第一份，单跑（⛔ 不带管道）**

```bash
cd ~/.ai-ds-lab/wt-fault
rm docs/_archive/retrospection/2026-06-01-rule-execution-discipline-gap.md
pnpm run audit:stale-anchors > /tmp/t8-1.txt 2>&1; echo "EXIT=$?" >> /tmp/t8-1.txt
```
⚠️ **若 `EXIT=0`** ⇒ ⛔ **先别写「闸漏了」**。按 `lab:N48` 先查：
闸是否只扫**现行层**而 `_archive` 在豁免面里？（处方 3 §5 提到 `LIVE_TREE_EXEMPTIONS`=2）
⇒ 独立确认「该文件在闸的判定面内」，再解释那个 0。

- [ ] **Step 5：程序化提孤儿行**，按 T8-P2/P3 断言引用方与路径形态。

- [ ] **Step 6：还原 + 证干净**，再删第二份，重复 Step 4–5。

- [ ] **Step 7：全部还原 + 证干净 + 对账 + commit**

---

## Task 9：处方 3 判据 4 / 5 / 6 / 7 —— 不该拦的没拦（⚠️ 处方逐字说这一侧更重要）

> 处方 §5 逐字：「**只验「该拦的拦住了」抓不到「不该拦的也拦了」**——DS 当初刻意不校验就是怕这一侧，
> 所以这一侧的实测输出比另一侧更重要。」

- [ ] **Step 1：预注册**
  > **T9-P1（判据 4）**：在 `docs/internal/mockup-conventions.md` 插一行含
  > `` `MicroApps Console/docs/handoffs/x.md` `` ⇒ **exit 0**（外部仓路径不得报）。
  > **T9-P2（判据 5）**：插一行含 `` `Badge.figma.ts` `` 或纯 basename `` `x.md` `` ⇒ **exit 0**（无目录分量不得报）。
  > **T9-P3（判据 6）**：插一行含 `` `tion/2026-06-01-v4-1865-rules-backflow.md` ``（非段边界）⇒ **exit 0**。
  > **T9-P4（判据 7）**：全部恢复后复跑 ⇒ **回到判据 1 的读数**（exit 0，锚点 TOTAL = 767）。
  > **T9-P5**：三格**逐格独立做**，⛔ 不三行一起插 —— 一起插时任一格误报都会被归到错误的那格。

- [ ] **Step 2–4：逐格执行**（每格：插一行 → ⛔ 不带管道单跑 → 记 `EXIT=` → `git checkout -- .` → 证 status 空）。
  ⚠️ 插入位置选**文件末尾新起一行**，避免破坏既有段落结构影响其它判据。

- [ ] **Step 5：判据 7** —— 全部还原后复跑，断言 exit 0 **且** 锚点 TOTAL **逐位等于** Task 1 基线。
  ⛔ 「exit 0」不够，TOTAL 必须回到同一个数（否则说明某格没还原干净）。

- [ ] **Step 6：⚠️ 若某格 exit ≠ 0（= must-not-hit 命中）** ⇒ 这是**真发现**，
  但按 Global Constraint 4，先证自己的口径：插入的 code-span 形态是否与处方逐字一致？
  反引号是否被 markdown 吃掉？确认后才写进报告。

- [ ] **Step 7：对账 + commit**

---

## Task 10：收尾 —— 拆 worktree + 不变式复证 + 交付

- [ ] **Step 1：拆一次性 worktree**

```bash
DS=/Users/nancy/Documents/AICoding/VS_Code/tvu-design-system
git -C ~/.ai-ds-lab/wt-fault status --porcelain    # ⛔ 必须先是空，非空则先查清
rm -rf ~/.ai-ds-lab/wt-fault/node_modules
git -C "$DS" worktree remove ~/.ai-ds-lab/wt-fault
git -C "$DS" worktree list | grep -c wt-fault      # 期望 0
```

- [ ] **Step 2：四个 pin 不变式复证**（与 Task 1 Step 2 同一条命令）⇒ 期望四行全 `dirty=0`。

- [ ] **Step 3：golden set 复证**（硬约束，每次用前必跑）

```bash
cd /Users/nancy/Documents/AICoding/VS_Code/ai-ds-lab
node metrics/golden-set-freeze.mjs --pin ~/.ai-ds-lab/pins/tvu-ds-c1db57b8 \
  > runs/faultinjection-evidence/task10-goldenset.txt 2>&1
echo "EXIT=$?" >> runs/faultinjection-evidence/task10-goldenset.txt
```
Expected：`EXIT=0`。

- [ ] **Step 4：写报告** `reports/2026-08-26-round2-faultinjection.md`：
  逐格 HIT/MISS 对账表 · 每条读数带**环境名 `wt-fault`** · 新增勘误 · ⛔ 未覆盖如实登记。

- [ ] **Step 5：更新** `docs/round2-status.md` 新增 §19 **并订正 §18.8 第 1 条**（12 条 → **17 条**，
  注明多出的 5 条是 lab 自己漏登，⛔ 不是 DS 的问题）。

- [ ] **Step 6：若产出新纪律** ⇒ 加进 `AGENTS.md` §2 / §3。

- [ ] **Step 7：commit + push**

---

## 已登记的边界（⛔ 本计划不覆盖，别读成漏）

1. **CI 侧跑不出来**（GitHub Actions 窗口 2026-08-14 已关闭，`lab:N14`）⇒ 处方 1 §5.6
   的两套 workflow 只能静态核（上轮已做：4 处调用点）。
2. **`grader-changed` 档仍未在真实世界发生过**（§18.8 第 4 条）。
3. **加大 N / 处方 4 守恒 / actionability / 铁律 2 / first-shot T2 / manual r006** 全部不在本计划内。
4. **严/宽口径**仍等 DS 裁，⛔ lab 不催、不替裁。
5. Task 5 的「改造前失败即停」是**等价口径模拟**，⛔ 不是旧 runner 实跑。
