# 处方 — 交叉校验的真值表少了一格：`findings > 0` 时分母无人看

> 小处方，一处改动。发现方式：**造故障**（AGENTS §3.8），不是 must-hit/must-not-hit。
> 实测出处：[`reports/2026-08-25-round2-step7-7b-delta.md`](../reports/2026-08-25-round2-step7-7b-delta.md) §5 故障 B。
> pin `4a68e02d`。

- **执行状态**：🟢 **已由 DS 落地** —— 亲验 pin **`7fcca917`**（2026-09-01，`lab:N93`）。`scripts/lib/gate-output-contract.mjs` 的 `crossCheckExitVsTotals`（`:258`）已把 `checkedUnits === 0` 提为**第一顺位判断**（`:267`），⛔ 不再只在 `exit 0 ∧ findings 0` 那一格看分母；`:273` 逐字「且 findings>0 与空分母**自相矛盾**：不可能在 0 个单位里找到问题」= 本处方 §0 点名的那句；`empty-denominator` 已进 `CROSS_CHECK_ANOMALIES`（`:305`）。⚠️ 另有一处**超出本处方**的加强：`:259-262` 把「没传 / 传坏了」与「真的是 0」拆成两件事（前者当场抛），⛔ 这不是本处方提的
- **执行状态重取**：@`d07be8e9`（2026-09-11 §30 全量重取，量具 `metrics/proposal-execution-status-refresh.mjs`）—— 锚 1/1 仍成立（`scripts/lib/gate-output-contract.mjs`） ⇒ **结论不变**。

---

## 0. 一句话

`scripts/lib/gate-output-contract.mjs` 的 `crossCheckExitVsTotals` 只在
**`exit 0 ∧ findings 0`** 那一格看 `checkedUnits`，另外三格标 `—`（不看）。
⇒ **一条闸只要报出了 findings，它的分母就完全不受校验** ——
包括 `findings=9 ∧ checkedUnits=0` 这种**自相矛盾**的读数，收割器照样 PASS、exit 0。

---

## 1. 故障复现（已实测，含还原）

在 pin 的 worktree 里，把 `figma-sync/audit-docs-site-readiness.mjs` 的
`checkedUnits: pages.length` 改成 `checkedUnits: 0`：

```
故障态自证：该闸打出  findings=9 · checkedUnits=0
DS 收割器：            verdict=reported-not-blocked · exit 0 · ✅ PASS · 零异常
lab 独立量具：         verdict=⚠️ empty-denominator
```

（改动已 `git checkout` 还原，`git status` 为空。）

**根因，源码 `:230-236` 的真值表**：

| exit | findings | checkedUnits | verdict |
|:---:|:---:|:---:|---|
| ≠0 | >0 | **—** | `blocked` |
| ≠0 | 0 | **—** | `blocked-without-findings` ⚠️ |
| 0 | >0 | **—** | `reported-not-blocked` |
| 0 | 0 | >0 | `clean` |
| 0 | 0 | 0 | `empty-denominator` ⚠️ |

三个 `—`。实现（`:240-268`）与表逐字一致 —— **不是实现跑偏，是表本身少了一格。**

---

## 2. 为什么这是真缺口，不是设计取舍

契约 §1.1 的立论逐字是：

> **没有分母的 `findings: 0` 不可解读。** 收割器必须能区分「检查了 812 个单位，0 命中」
> 和「检查了 0 个单位，0 命中」。

这条论证里 **`findings: 0` 只是举例，不是条件**。真正的命题是
**「没有分母的读数不可解读」** —— 与 findings 是不是 0 无关。

而 `findings > 0 ∧ checkedUnits = 0` 比「不可解读」更强一层：它是**自相矛盾**。
你不可能在 0 个单位里找到 9 处问题。⇒ 这是契约白送的一条**一致性不变量**，
目前被放在桌上没捡。

**现实失效路径**（不是纯理论）：`checkedUnits` 与 findings 往往来自**不同的变量** ——
一个是扫描面长度，一个是问题数组。作者接错线、或扫描面走了降级分支塌成空、
而 findings 来自缓存或另一条路径时，就长成这样。这正是契约要消灭的那类假绿的**变体**。

**当前暴露面**：`4a68e02d` 在链的 11 条契约闸里，**3 条 findings > 0**：

| npm key | findings | checkedUnits |
|---|---:|---:|
| `audit:no-hardcoded-design-tokens` | 19 | 74 |
| `audit:docs-site` | 9 | 38 |
| `audit:component-tokens` | 3 | 8,095 |

这 3 条今天的分母是对的，但**没有任何机械校验在守着它**。

---

## 3. 处方

`crossCheckExitVsTotals` 里，**把 `checkedUnits === 0` 的判断提到最前面**：

```
if (checkedUnits 不是非负整数) → 抛（fail closed，⛔ 不降级）
if (checkedUnits === 0)        → empty-denominator ⚠️   ← 提前，不再区分 findings
… 其余四格维持原样 …
```

- 真值表随之从「五格三个 `—`」变成「`checkedUnits=0` 一律异常 + 其余四格按 exit×findings 分」。
- `CROSS_CHECK_VERDICTS` / `CROSS_CHECK_ANOMALIES` 两个导出**不需要加新取值** ——
  `empty-denominator` 已经在异常表里。⇒ 收割器、单测、豁免表**都不用动结构**。

### 3.1 一个必须一起处理的连带

`audit:figma-conformance` 今天就是 `findings 0 · checkedUnits 0`，已具名豁免
（`KNOWN_ANOMALIES`，`since 2026-08-24`，挂 INFRA-F139）。本改动**不影响它** ——
它本来就走 `empty-denominator` 那一格。

⚠️ 但要**核一遍**：改动后是否有**新的**闸掉进 `empty-denominator`。
若有，那就是本处方**当场抓到的存量假绿**，⛔ 不许顺手加进豁免表了事 ——
按 F139 同样的形态立条目、指 owner。

---

## 4. 验收标准

⛔ 每条都要可复核证据（AGENTS §4）。

- [ ] **造故障 A（本处方要治的那个）**：把任一条在链契约闸的 `checkedUnits` 改成 0
      （保持 findings > 0），验证 `report:gate-output-harvest` **exit 非 0 且逐字报出
      `empty-denominator`**。⛔ 先证故障态成立（贴出该闸 stdout 的 `totals`），改回后 `git diff --stat` 为空。
- [ ] **造故障 B（exit≠0 那一格）**：同上但让该闸同时非 0 退出，验证仍报 `empty-denominator`
      （⛔ 不许被 `blocked` 那一格吃掉）。
- [ ] **must-not-hit ①**：分母正常时**不得**误报 —— 全链跑一次，
      `empty-denominator` 只有 `audit:figma-conformance` 一条（即豁免表那条），
      **数量与改动前一致**。
- [ ] **must-not-hit ②**：`checkedUnits` **缺失**（不是 0）时**不得**判成 `empty-denominator`，
      必须走 fail-closed 的抛错路径 —— 「没传」与「真的是 0」是两件事，⛔ 不许合并。
- [ ] 单测：`CROSS_CHECK_VERDICTS` 的**每一种取值各一例**，且新表的
      `checkedUnits=0 × {exit 0/≠0} × {findings 0/>0}` **四种组合各一例**。
- [ ] `buildGateOutput` 的 `checkedUnits` 必填校验**一字不动**（它已经是 fail closed 的）。

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

- **只改了注释里的真值表，没改实现**（或反之）。两处必须同批改，且单测覆盖新格。
- **把新掉进 `empty-denominator` 的闸直接塞进 `KNOWN_ANOMALIES`。**
  豁免表是 shrink-only 的存量快照，⛔ 不是新缺陷的垃圾桶。

---

## 5. 登记的边界

1. 本处方**只改分类，不改任何闸的判据强度**，也不改 `exit code` 语义。
2. **只覆盖已上契约的 11 条。** 另外 27 条在链步骤（含 25 条 `scripts/` 闸）压根没有 `checkedUnits`，
   本处方对它们无效 —— 那是 v2 棘轮的事。
3. `checkedUnits` 的单位各闸自定义 ⇒ 本处方只做**同闸判空**，⛔ 不做跨闸比较。
4. **这条缺口 must-hit/must-not-hit 抓不到。** 收割器自己的 6 条内建控制全绿 ——
   它们测的是「判据按真值表跑对了没有」，而缺的是**真值表少了一格**。
   ⇒ 这是 AGENTS §3.8（造故障）与 §3.2（控制）**不可互相替代**的一个干净实证，
   建议一并写进 spec §16。
