# 一条闸走哪一支，可能取决于**谁的机器**，而两边都 exit 0

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

---

## 1. 现象

`audit-page-recipes` 有五层判据。第四层 S4 问的是「配方引用的 token 是不是**真的随包发了**」，分母取 `dist/style.css`：

```js
export function readShippedTokens(root = REPO_ROOT) {
  const abs = resolve(root, SHIPPED_CSS_PATH)
  if (!existsSync(abs)) return null      // ← 取不到 ⇒ 无法判定
  …
}
export function validateShippedTokens(recipes, shippedTokens) {
  if (shippedTokens === null) {
    return { skipped: true, ok: true, errors: [] }   // ← 不算失败
  }
  …
}
```

这是一个**设计良好、如实登记过的 fail-open 出口**：闸不假装「查不到 = 通过」，它印三行警告，其中一行逐字写着「⚠️ 这一层 SKIP 掉时，本闸**不**保证配方引用的 token 随包发 —— 别按「S4 绿」理解」。文件头注释也写了。没有任何东西是藏着的。

问题不在这里。问题是 **`dist/` 是 gitignored 构建产物**，于是同一条闸、同一份数据、同一个 commit：

| 在哪跑 | S4 走哪一支 | 退出码 |
|---|---|---|
| 主工作树（build 过） | 真判 —— `359 个 dist token 声明` | 0 |
| worktree（按纪律 ⛔ 不软链 `dist`） | **SKIPPED** | 0 |
| CI（`prepublishOnly` / `prepare` 链上 build 已跑） | 真判 | 0 |
| 谁的机器刚 clone、还没 build | **SKIPPED** | 0 |

两棵树各跑一次，输出**逐字不同**，退出码**逐字相同**。

---

## 2. 为什么这值得单独记

本仓此前已登记过三种「闸看起来在守、其实没守」的形态（全在 `INFRA-F138`）：

1. 测试只 `import` 判据函数 ⇒ **看不见接线**（摘掉 `main()` 的接线，全量测试无一转红）
2. 入口守卫在 symlink 下恒假 ⇒ **整个不执行、exit 0、零输出**
3. 判据全在，而**真仓库的数据只覆盖其中一条路径**（另一条分支永不进入 ⇒ 摘掉它零信号）

第 4 种与前三种都不同：**判据在、接线在、数据也能覆盖到它 —— 分叉在环境里。**

前三种可以靠「在真仓库上跑一次」发现（虽然贵）。第 4 种**结构上**不能：你在自己机器上跑，只会看到自己那一支；另一支的存在与否，取决于你有没有 build 过。而两支都 exit 0，`grep '✓|PASS'` 也看不出差别 —— 唯一的差别是 stdout 里那几行字。

推而广之，命中这个形态的判据长这样：

> **分母取自一个「可能不在」的东西，取不到时诚实地降级，而那个东西的在不在由环境决定。**

本仓里同型的还有：需要 `.env` 的闸（memory `worktree-lacks-node-modules-and-uses-main-hooks` 记过「缺 `.env` + `figma-data/mockup/` ⇒ 闸走 fail-open 分支假通过」）、需要网络/凭据的闸、需要 `dist-wc` 的渲染面。**它们的共同点不是「会失败」，是「会安静地少判一层，而少判的那层没有任何计数被记录下来」。**

---

## 3. 修法：不是去掉 fail-open，是把三态都钉住

⛔ **不该做的**：把 `skipped` 改成 fail-closed。那会让每个没 build 过的 clone 上的 pre-commit 直接红，而闸头注释已经论证过为什么不这么做（`prepublishOnly` / `prepare` 链上 build 已跑过，那里是真判）。这是有意的取舍，不是缺陷。

✅ **该做的**：给这一层补一个 **fixture**，在里面把三态各造一次 ——

```ts
// dist 有、两个 token 都在 ⇒ 绿，且摘要印 fixture 自己的 `2 个 dist token 声明`
// dist 有、缺一个       ⇒ 红，且精确点名是哪个 token、哪个 recipe、哪个 slot
// dist 不在             ⇒ exit 0，但三行 SKIPPED 警告全印、且 ⛔ 不印 `✓ S4`
```

第三条是关键，它钉的不是「闸红了」，是**「闸在降级时说了它在降级」**。这条断言在真仓库上永远跑不到（主仓有 dist），在 CI 上也跑不到（CI build 过）。

配套两条纪律，本轮各付过一次代价：

- **每条 `.not.toContain` 都要配正向锚点。** 「SKIPPED 时不印 `✓ S4`」在闸压根没跑时**照样成立** ⇒ 必须同时断言一个「只有真跑过才有的读数」（本轮用 fixture 自己的 `✓ S5 响应式改位机读`）。
- **绿档要钉 fixture 自己的数，且配反向钉。** 本轮用三个来自**不同输入文件**的读数（`3 个可消费组件名` / `3 个源码 token 声明` / `2 个 dist token 声明`），并断言 stdout 不含真仓库的 `41` / `305` / `359`。三个同时对上，结构上不可能是「退回真仓库读」。

---

## 4. 怎么发现的：靠故障注入，不是靠读代码

我是先把回归面写完、跑六向注入时才看清这一层的分量的。最值钱的一注是 ④：

> 把 `readShippedTokens()` 改成恒返回 `null`。

结果：闸**看起来完全正常** —— exit 0，还印了那段善意的警告。而 `healthy` 与 `s4-red`（真违例）两个场景的 `STDOUT_LEN` 从 `429 / 358` 变成**一模一样的 `623`**。真违例被静默放行，没有任何一处红。

新面 5 条转红，**旧面 43 条纯函数用例全绿** —— 因为它们直接 `import validateShippedTokens` 传参调用，看不见 CLI 里是谁在给它喂分母。

⇒ **「这一层有没有在守」这个问题，只有 spawn 整个脚本才答得出。**

---

## 5. 附带的一条：封路措辞连续第六轮被量具证伪

交接把这条闸判成「成本高、不划算」，两条理由：

- ✅ **真**：它 `import Ajv2020 from 'ajv/dist/2020.js'`，fixture 里 bare specifier 解析不到 ⇒ `ERR_MODULE_NOT_FOUND`、**exit 1 且 stdout 零字节**。
  —— 但解法是 harness 既有 `linkDirs` 里**一个**元素（`node_modules`），不是「多软链」。
- ❌ **假**：「S4 依赖 `dist/` 而纪律 ⛔ 不软链 dist」。
  —— 那条纪律禁的是**软链真仓库那份**（并行线会重建它 ⇒ 报假缺陷）。**fixture 里自己写一份，与那条纪律无关。**
  而且正因为 S4 依赖 `dist/`，它才**更**需要 fixture —— 见 §1。「它依赖 X」被当成了「所以做不了」，实际是「所以非做不可」。

**可迁移的判据**：交接 / entry 里凡「只剩 / 已用完 / 走不通 / 没得挑」这类**封路措辞**，动手前先问一句「这条禁令**逐字**禁的是什么？我要做的事落在它的字面范围里吗？」本轮那条纪律的字面是「⛔ 别把 `dist` 放进 `linkDirs`」，而我要做的是 `files: { 'dist/style.css': …}` —— 两个不同的 harness 参数、两种不同的机制。

（同族：memory `entry-restatement-is-secondhand` 的「最贵的方向是封路理由」；连续第六轮同一形态，前五轮记在 `INFRA-F138` 各次核销里。）

---

## 6. 自己犯的两处判据缺陷（当场作废，留档防复发）

- `pnpm audit:page-recipes 2>&1 | tail -8; echo "EXIT=$?"` —— 那个 `$?` 取的是 **`tail` 的**退出码，不是闸的。已重取真实退出码（0）并另钉「末行是闸自己那句话」。同族 memory `verification-criterion-must-be-terminal-state`，本仓 2026-08-25 用 `grep` 撞过同一个。
- 一处**恒真 echo**：核完 sha256 后写「(空 diff 上面这行不会有输出)」。自查一句就废：**「结论相反时这行还会照样打印吗」** —— 会。真判据是 sha256 与 `git diff --stat` 本身。

以及一处**断言写错不是闸的问题**：新面初版断言「S2/S3/S5 红时 S4 仍绿」，而源码里没定义的 token 在 dist 里当然也没有 ⇒ S4 跟着红（闸的报错文本自己逐字写了这是设计接受的重复报）。⇒ **新用例第一次红时先问「它红的理由是我想测的那个吗」**，本轮答案是「不是」，改的是断言、不是闸。
