# Task 6+7 对账 —— 处方 2 造故障 A / B / must-not-hit ② + 单测逐格核

**环境名 `wt-fault`** · 三元组 `(c1db57b8, n/a, n/a)`

⚠️ **harvest 的输入不是文件，是它自己 spawn 那 11 条闸**（头注释逐字：「它会把那 11 条闸再跑一遍」）
⇒ 造故障只需改闸，⛔ 不需要先跑全链。**先读了才动手，⛔ 没假设。**

## Task 6 预注册对账 —— **3 HIT / 0 MISS**

| # | 预测 | 结果 |
|---|---|---|
| **T6-P1（A）** | `checkedUnits`→0（`findings>0`）⇒ harvest **exit 非 0** 且逐字 `empty-denominator` | **HIT** |
| **T6-P2（B）** | 同上 + 该闸非 0 退出 ⇒ **仍报 `empty-denominator`**，⛔ 不被 `blocked` 吃掉 | **HIT** |
| **T6-P3（MN②）** | `checkedUnits` **缺失** ⇒ ⛔ 不得判 `empty-denominator`，走 fail-closed 抛错 | **HIT** |

### A —— `checkedUnits: files.length` → `0`（`diff --numstat` = `1 1`）

**⛔ 先证故障态成立**（处方 §4 逐字要求「贴出该闸 stdout 的 `totals`」）：
`auditId: no-hardcoded-design-tokens` · `totals.findings = 19` · `totals.checkedUnits = 0` ·
`findings[] 长度 = 19` ⇒ **`findings>0 ∧ checkedUnits===0` 成立** ✅

**harvest 实测**：`EXIT=1`，逐字
```
audit:no-hardcoded-design-tokens  0     19        0    source-file   ⚠️  empty-denominator
```
⚠️ 且**新掉进去的那条与豁免表存量分开显示** —— `audit:figma-conformance` 被单列在
「1 条**具名豁免**的存量异常（打印但不阻断，shrink-only）」段。
⇒ **顺带复证处方 §4.1**「⛔ 不算通过：把新掉进 `empty-denominator` 的闸直接塞进 `KNOWN_ANOMALIES`」
—— 新的那条**没有**被塞进豁免表，正常触发 `exit 1` ✅

### B —— 同上 + `process.exit(0)` → `process.exit(1)`（`diff --numstat` = `2 2`）

⚠️ **这一格改的是闸脚本本身，⛔ 不是闸的输入** —— 前几格都改输入，但处方 §4 造故障 B
逐字要求「**让该闸同时非 0 退出**」，只能改退出行为。**如实登记这个差别。**

**⛔ 先证故障态**：该闸单跑 `EXIT=1` ✅
**harvest 实测**：`EXIT=1`，该闸 `exit 1 · findings=19 · checkedUnits=0` ⇒ **仍判 `empty-denominator`**，
⛔ **未被 `blocked` 吃掉** ✅。诊断文字**强于处方要求**：
> 分母是空的，这条读数不可解读（假绿）；且 `findings>0` 与空分母**自相矛盾**：不可能在 0 个单位里找到问题

### MN② —— 整行删掉 `checkedUnits`（= **完全不传**，⛔ 不是传 0）

`diff --numstat` = `0 1`（纯删一行）· 残留的唯一 `checkedUnits` 命中在**第 284 行注释**里 ⇒ 代码里已删净。

**闸自己 fail-closed 抛错**（`gate-output-contract.mjs:92`）：
```
TypeError: gate-output-contract[no-hardcoded-design-tokens]: `checkedUnits` 必填且必须是非负整数（拿到 undefined）。
没有分母的 findings:0 不可解读 —— 默认成 0 会把「忘了传」伪装成「真的扫了 0 个」。
```
⇒ **这句话逐字就是处方 MN② 的立论。**

**harvest 实测**：`EXIT=1`，该闸判成 **`unparseable`** 而**非** `empty-denominator`：
> stdout 尾部解析不出契约 JSON 块 ⇒ 这条闸的读数收不上来（不是「它没问题」）

⇒ **「没传」与「真的是 0」被分成两个不同的 verdict** ✅
⚠️ `unparseable` **不在** `CROSS_CHECK_VERDICTS` 的 5 个取值里 —— 它是 harvest 自己的另一档，⛔ 别混。

### 还原

`git checkout --` ×3 ⇒ `dirty=0` · 还原后 harvest **`EXIT=0`** ✅

---

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

**结论：全部覆盖，且大幅超额。⇒ §18.8 第 3 条「未逐格核」现已核完。**

### 🔴 但我先报了一个错的结论 —— 本轮**第 9 例**装置故障

初版解析器按 `it(` 切块、用 `toBe\(['"]<verdict>['"]\)` 匹配，打印出：

```
blocked                  0 例  ⛔ 缺
reported-not-blocked     0 例  ⛔ 缺
clean                    0 例  ⛔ 缺
exit=0 × findings=0      0 例  ⛔ 缺
exit≠0 × findings>0      0 例  ⛔ 缺
```

**全是假的。** 真相：单测是**表驱动**的 ——
```ts
const cases = [ ['…', {…}, 'blocked', false], … ]     // ← verdict 在数据里
for (const [name, input, verdict, anomaly] of cases) {
  it(name, () => { expect(r.verdict).toBe(verdict) })  // ← 断言里是变量，不是字面量
}
```
⇒ **容器类型**（表驱动 vs 逐个 `it`）与**标签形态**（变量 vs 字面量）**两项同时错**
—— `lab:N48` 推论五五项里的两项。

⚠️ **差点与对方自称对立**：describe 标题逐字写着「五种 exit×findings 组合各一例」，
而我打印的是「3 种缺」。这与 §18.7 第 4 例（`head -6` 截断，我数 2 / 对方称 4）**同型**。

### 🔴 第 10 例 —— 两个装置共享同一个错误假设，于是**互相印证成假绿**

解析器里我写了一条自查 `if (cases.length !== 27) throw new Error('装置故障')`，**它通过了**。
但 `vitest` 实跑是 **34 tests**。

| 计数 | 值 | 口径 |
|---|---:|---|
| `grep -cE "^\s*(it\|test)\("` | **27** | `it(` **字面量**数 |
| 我的解析器 | **27** | 同上（同一个错误口径） |
| **`vitest run` 实跑** | **34** | **运行时真值** |

差额 **7 = (5−1) + (4−1)** —— 正是两个表驱动块 `for` 展开的差。

⇒ **操作口径（`lab:N48` 推论八）**：**两个独立方法得到同一个数，⛔ 不等于那个数对
—— 如果两者共享同一个错误假设。**
⇒ 与 **AGENTS §3.8**「DS 的收割器与发射端同源 ⇒ 测不出自己那套约定的盲区」**同构**，
只是这次是 lab 自己踩的。⇒ **交叉验证必须换「实现范式」，不只是换「实现」** ——
这里正确的第二把尺是**运行时**（`vitest run` 的计数），不是另一个静态解析器。

### 真实覆盖（重核后）

**T7-P1：五种 `CROSS_CHECK_VERDICTS` —— ✅ 5/5**

| verdict | 用例（`exitCode`/`findings`/`checkedUnits`） |
|---|---|
| `blocked` | `1 / 3 / 100` |
| `blocked-without-findings` | `1 / 0 / 100` |
| `reported-not-blocked` | `0 / 19 / 74` |
| `clean` | `0 / 0 / 644` |
| `empty-denominator` | `0 / 0 / 0` |

⚠️ **外加一条 meta 断言，比逐个断言更强**：
```ts
expect(new Set(cases.map(([,,v]) => v))).toEqual(new Set(CROSS_CHECK_VERDICTS))
```
⇒ 将来**新增第六种 verdict 而不加用例，这条会炸** —— 这正是 `lab:E45`
「控制少钉一维 = 换代时不炸」的**反面正例**。

**T7-P2：`checkedUnits=0` × {exit 0/≠0} × {findings 0/>0} —— ✅ 4/4**

`0/0/0` · `0/9/0` · `1/0/0` · `1/9/0`，全部断言 `verdict === 'empty-denominator'` ∧ `anomaly === true`。

**超出处方要求的额外 6 组：**
1. `exit 2`（跑不起来）+ 分母>0 ⇒ `blocked-without-findings`，⛔ 不当成通过
2. 阴性对照：`exit≠0 ∧ findings>0` 与 `exit0 ∧ findings>0` **判定不同**（证判据不是「有没有 findings」）
3. `why` 必须点名「自相矛盾」**+ 阴性对照**（`findings=0` 时**不该**扣这顶帽子）
4. **MN② 的 6 种坏值全覆盖**：缺失 / `null` / `'0'`（字符串）/ `NaN` / `-1` / `1.5` ⇒ 全部 `toThrow(/checkedUnits/)`
5. must-not-hit：分母>0 时四格**都不得**误报成 `empty-denominator`
6. `CROSS_CHECK_VERDICTS` 长度 = 5 且 `ANOMALIES` 含 `empty-denominator`
7. 每条都断言 `why.length > 10`（「归类必须带可读理由，⛔ 不许只给一个枚举值」）

**`vitest run tests/gate-output-contract.test.ts` ⇒ `EXIT=0` · 34 passed / 34** ✅

⇒ **处方 §4「单测：每种取值各一例 + 四种组合各一例」不但满足，还超额。⛔ 无缺口可登记。**
