# 给活闸补回归面时，「抽成纯函数再单测」买到的比看起来少一层

> 2026-08-24 落地线第二十八轮的复盘。触发条件 = `WRAP-UP.md` §Retrospection 触发判断
> 的「学到通用工程经验 / 发现新 pattern」。
> 本轮的**过程叙述**（六条注入表、覆盖边界、Active 计数）在
> [`STATUS-CHANGELOG.md`](../STATUS-CHANGELOG.md) 顶条，⛔ 本文不复述，只留可迁移的那部分。

---

## 一句话

给一条**已经挂在 L4+L5 上的闸**补测试时，默认反射是「把判据抽成纯函数、对纯函数写单测」——
它覆盖**判据逻辑**，但**结构上覆盖不到接线**；而「把整个脚本拷进一个假 repo root 里跑」
往往**零改动活闸**就能把两者一起覆盖。**先问这个脚本的输入面能不能被整体伪造，再决定要不要动它的结构。**

## 这次是怎么发现的

上一轮把待办登记成了「先把 `audit:status-consistency` 的 C5/C6 两臂抽进 `scripts/lib`，
再对纯函数写单测（抽 lib 属重构活闸，本轮刻意不做）」—— 同一个脚本里 C4 早就是这么做的
（`scripts/lib/backlog-open-entries.mjs`），范式现成、路线看起来无可挑剔。

动手前先问了一句「这条路买到什么」，答案分成两半：

| 失败形态 | 抽 lib + 纯函数单测 | 整脚本 fixture harness |
|---|---|---|
| 判据逻辑被改坏（正则、fail-closed 分支、`!==1` 写成 `>1`） | ✅ 抓到 | ✅ 抓到 |
| **判据块被整块删掉 / 调用点被注释掉**（接线） | ❌ **全绿** | ✅ 抓到 |
| `main()` 的提前 return 排到判据之前 | ❌ 结构上不经过 | ✅ 抓到 |
| 挂载层（hook 触发面 / CI 接线）被摘掉 | ❌ | ❌（两者都不覆盖，见下） |

第二行不是假想：`meta-rules.md` 触发器 R §违反检测 把它逐字列成了一条 ❌ ——
「单测只直接调纯函数、从不经过 `main()` 的提前 return，却据此宣称出口顺序正确」。
实测把 C5 整块从脚本里删掉，**16 条用例里只有 5 条红，而那 5 条全部来自 fixture harness
这一侧**；如果当初按登记的路线做，那 5 条一条都不会存在。

## 那条 harness 长什么样

```
mkdtemp()/
├── package.json                      ← fixture 造
├── CHANGELOG.md                      ← fixture 造
├── docs/STATUS.md                    ← fixture 造
├── docs/internal/{backlog,design-review-queue}.md  ← fixture 造
└── scripts/
    ├── audit-status-consistency.mjs  ← 运行时从真仓库 copyFileSync（同一份字节）
    └── lib -> <真仓库>/scripts/lib    ← 软链，⛔ 不拷
```

跑 `node <tmp>/scripts/audit-<gate>.mjs`，断言 **exit code + stderr 里点名的 check id**。

## 什么时候这条路走得通（前置条件，⛔ 不是所有闸都行）

1. **脚本从 `import.meta.url` 推 repo root，不是 `process.cwd()`** —— 这是它能被「搬到别处
   就读别处」的唯一原因。用 cwd 的脚本改成这条路要先动它，代价就回去了。
2. **输入面只有文件读**：不跑 `git`、不联网、不要凭据。一旦它 `execSync('git diff --cached')`，
   fixture 就得是个真 git 仓库（还能做，但便宜性没了）。
3. **输入面可以被整体伪造** —— 闸读几个文件就造几个。读一棵大目录树的闸（如全仓扫描型）
   造 fixture 的成本会反超抽 lib。
4. **`lib` 软链回真仓库**，⛔ 别拷：拷 = 留一份会漂的副本，lib 侧改坏了这套用例看不见。

不满足 1-3 的，抽 lib 仍然是对的路 —— 本文不是要否掉那条路，是要求**先量再选**。

## 三条如实登记的边界（⛔ 别把这套 harness 读成「闸全测了」）

1. **挂载层不在覆盖面内**：hook 的触发面、CI 的接线，判据面在
   `audit:gate-ci-parity` + `audit:gate-mount-declaration`，跟这套用例是两回事。
2. **fixture 必须同时满足闸的所有臂**才有绿档基线。这是**特性不是缺陷**：将来新增一臂而
   fixture 没跟上 ⇒ 绿档当场红，逼你把新臂写进 fixture。但**反过来不成立** —— 一条只在
   fixture 里不存在的形态上开火的新臂，不会产生任何摩擦，也就没有覆盖。
3. **绿档那条断言必须是 fixture 自己的数**（本轮：`backlog Active 3` / `yes 2` / 计数处 2）。
   断言真仓库的数 = 一旦 harness 退化成跑真仓库，测试照样全绿，整套东西静静作废。

## 另外两条与本主题无关但值得记的

- **取证工具第 N 例造假象，这次是我自己的注入驱动**：把「失败用例名」按 `^\s+[×✗] …$` 解析，
  没匹配 vitest verbose 的真实输出格式（带 ANSI + `FAIL  <file> > <describe> > <test>`），
  于是六条注入全被打成「无用例抓到 = 空断言」，而它们其实都 `EXIT=1`。
  **救回它的是两处读数互相矛盾**（退出码说红、用例名说没红）—— 判据永远是
  「先怀疑解析器，别先记缺陷」。
- **worktree 里三条软链不被 `.gitignore` 命中**：`node_modules/` / `dist-wc/` / `.husky/_/`
  三个 pattern 都带尾斜杠 = 只匹配目录，而**软链在 git 眼里不是目录** ⇒ 主仓忽略、worktree 里
  一律 `??`，`git add -A` 会把三条指向本机绝对路径的软链提交进仓库。实测从未发生
  （`git ls-files` 三者皆空），靠既有的「`commit -F <msg> -- <显式路径>`」纪律兜住。已报 owner。

## 一句话结论（写给下一个接手待办的人）

**待办自带的「实现路线」和它自带的数字一样，是二手源。**
先问「这条路买到什么、买不到什么」，再问「有没有一条不动被测对象就能买到更多的」——
本轮那条**改动最少的路同时覆盖面最大**，⛔ 但这不是「小改动优先」的例子，
它是先量清各条路各买到什么之后的结果。反过来按「哪条改动小」排序，
逐字命中 tracker §排期原则 的反模式表。
