# 一条闸可以在同一次输出里印两个互相矛盾的结论，而没有任何东西会去比那两句

> 2026-08-27（落地线第五十七轮）· 起因 = 给 `scripts/audit-gate-ci-parity.mjs` 补整脚本回归面（[[INFRA-F138]]）。
> 本文只留**可迁移**的那部分；本轮逐条读数、六向注入表、逐格 diff 在
> [`STATUS-CHANGELOG`](../STATUS-CHANGELOG.md) 顶条与 [backlog `INFRA-F138`](../backlog.md) 的「最近核销」。

---

## 1. 假绿的一种新形态：摘要诚实，结论说谎

`audit-gate-ci-parity` 的输出分两半：先印**读数**，再印**结论**。

摘掉主判据那块接线（`if (unexcused.length)` → `if (false)`）后，它对一个**真违例**打出的完整 stdout 是：

```
[audit:gate-ci-parity] husky 钩子上的闸 4 条 · CI 可达 4 条 · 差集 1 条 · 具名豁免 0 条
   …（算法自印三行）
✅ PASS — husky 钩子上没有 CI 跑不到的闸；0 条具名豁免全部仍然命中（表只许缩）。
```

`EXIT=0`、stderr 零字节。

读数那半是**对的**：差集真的是 1 条。说谎的只有结论那半。

**为什么这一档比「输出逐字不变」更难抓**：本仓此前记过两档 ——「退出码变、输出不变」（注入只摘 `process.exit`）与「输出变、退出码不变」（摘某条子判据的点名）。这一档是第三种：**输出变了、退出码变了、而变化恰好落在没人比的地方**。

- 靠 grep 摘要判健康 → 摘要是诚实的，看不出问题。
- 靠「输出跟健康态比有没有变」判 → `差集 0 条` → `差集 1 条` **恰好等长**，`STDOUT_LEN` 与健康态同为 555 B。按长度比对完全一致。
- 靠退出码判 → 那是唯一变了的量，但只有**造出真违例并 spawn 整脚本**才看得到，而旧面是 import 判据函数。

⇒ **可迁移的判据**：凡一条闸**同时印「读数」与「结论」**，回归面必须断言一条**跨两半的蕴含**：

> 读数 > 0 ⇒ 结论必须是 ❌ 那一块 **且** 退出码非 0。

⛔ 不是「分别断言读数对」+「分别断言绿档能绿」。那两条各自都会通过。

同族：memory `verification-criterion-must-be-terminal-state` 记的四种「判据取了容易观测的代理」，本条是它在**同一次输出的两个半边之间**的形态。

---

## 2. 用例名字里写着「真仓库」，不等于输入面取自被测程序

旧面（`tests/audit-gate-ci-parity.test.ts`）里有一条用例，名字逐字是：

> `真仓库：commit-msg 上那条闸确实在 A 里（非空过钉）`

它长这样：

```ts
const L4_HOOKS = ['pre-commit', 'commit-msg']          // ← 测试里另抄的一份副本
function realInputs() {
  const hookTexts = L4_HOOKS.map((h) => readFileSync(join(REPO, '.husky', h), 'utf8'))
  …
}
it('真仓库：commit-msg 上那条闸确实在 A 里（非空过钉）', () => {
  expect(computeParity(realInputs()).a).toContain('scripts/audit-commit-affordance-trace.mjs')
})
```

把**闸自己**读钩子那行改坏（`L4_HOOKS.map(…)` → `L4_HOOKS.slice(0, 1).map(…)`，只读 `pre-commit`），实测：

- 探针：只挂 `commit-msg` 的闸从 `EXIT=1` 变成 **`✅ PASS`** —— 正是该闸 2026-08-24 扩成钩子家族要治的那个「永久逃逸换个钩子名重演」当场复发。
- 那条名字里写着「真仓库」的用例：**照样绿**。

原因很朴素：它读的是真仓库的**数据**，但那份数据是**测试自己喂进去**的。闸读不读第二个钩子，它结构上看不见。

⇒ **机械自查（一句话）**：把被测程序**读取输入的那一行**改坏，这条「真仓库钉」会不会红？

- 会 → 它钉的是程序的**读取路径**。
- 不会 → 它钉的只是**数据**，别把它算进「这条闸的接线有覆盖」。

这条同时解释了为什么 [[INFRA-F138]] 的口径盯的是「接线零覆盖」而不是「有没有测试文件」：非空过钉可以完全真实，而仍然对接线零敏感。

---

## 3. 全 must-not-hit 的用例，必须配一个「只有真跑过才有的读数」——哪怕它只有 1 字节

`--list` 模式在差集为空时**什么行都不印**。我第一版这样写：

```ts
expect(run.status).toBe(0)
expect(run.stdout.trim()).toBe('')     // ← 空过
expect(run.stderr).toBe('')
```

注入「入口守卫恒假」后，23 条里**只有这一条没红**：闸整个不执行时 `EXIT=0` + stdout 零字节，三条断言全部满足。

这个档位没有任何「读数」可钉 —— 除了一个：`console.log('')` 会写出**一个换行**，而没跑过的进程写出**零字节**。

```ts
expect(run.stdout).toBe('\n')   // 精确到字节地区分「跑了但没有行要印」与「压根没跑」
```

⇒ 本仓 memory 里那条「任何 must-not-hit 都要配一个只有真跑过才有的读数」在这里被推到极限：**读数可以只有 1 字节，但不能没有**。若连 1 字节都找不到，那条用例应该改成红绿配对，或者干脆承认它是空过并如实登记。

---

## 4. 转述最贵的方向仍是「封路」，而它有一种低调形态：把判据的集合定义**写窄**

触发器 S 连续第七轮抓到同一类。本轮交接给的四个数（BASELINE / 随包面 / 闸清单 / 接线有覆盖）加测试面，逐项现取**一个都没漂**。假的是判据本身：

| 出处 | 判据 |
|---|---|
| [[INFRA-F138]] entry（真源） | `P2` 干净 + `P3` **非 `TREE`** |
| 交接 prompt | `P2` 干净 + `P3` **∈ {`NAMED`, `DIRLIST`}** |

两句在**当时的**取值集合上等价过一阵，但它们不是同一个判据：一个是**补集**，一个是**枚举**。`P3` 实际有四个取值，第四个是 `UNCLEAR`（逐字含义是「量具判不了输入面，**必须读原文**」）。按补集算候选 **20 条**，按枚举算 **17 条**，被静默排掉的三条里有一条（`audit-demo-framework-parity`）挂载层数与头部同档。

**为什么这种改写特别难发现**：它不说「做不了」。它只是让那批对象**不出现在清单里** —— 没有任何闸会报「你的候选集少了 3 条」，因为闸看不到你心里的判据。

⇒ **可迁移的自查**：交接 / entry 里凡出现 `∈ {A, B}` 这种**枚举式**判据，回去读真源那句是枚举还是「非 X」（补集）。两者在**出现第三个取值**时行为相反：补集自动纳入，枚举自动排除。⛔ 而 `UNCLEAR` / `MIXED` / `UNKNOWN` 这类「判不了」档**天生是后来才出现的取值**，所以它们恰恰是最容易被枚举式转述吃掉的一批 —— 本 entry 第八轮已实测过一次（那条兑现的闸 `P3` 就是 `UNCLEAR`）。

同族：memory `entry-restatement-is-secondhand` 的「最贵的方向是封路理由」；本条是它在**集合定义**这一层的形态。

---

## 5. 一条小的、但当场兑现了的纪律

复原注入 ② 时，锚点 `process.exit(0)` 在闸里命中 **2 次**（`--list` 分支另有一处）。我用的注入器是「命中即改、**不命中或多命中即抛**」的，于是它当场拒绝并要求我加缩进锚点。

若当时用的是「静默 replace 第一处」，被改的会是 `--list` 那个出口 —— 而那处改动**不会让任何用例变红**（`--list` 的两条用例都只断言 `status === 0`）。⇒ 复原动作本身可以引入一个静默的缺陷，而它长得跟「复原成功」一模一样。判据是 **每次改写后逐次比对 `sha256`**，不是「命令没报错」。

---

## 6. 结论清单（给下一个人）

1. 闸同时印「读数」与「结论」时，回归面要断言**跨两半的蕴含**，不是各自断言。
2. 判一条「真仓库钉」钉住了什么：改坏被测程序**读输入那一行**，它红不红。
3. 全 must-not-hit 的用例必须有一个只有真跑过才有的读数，哪怕 1 字节。
4. 交接里 `∈ {A, B}` 形态的判据，回真源核它是枚举还是补集；「判不了」档最容易被枚举吃掉。
5. 复原注入必须「命中即改、不命中即抛」+ 逐次核 `sha256`。
