# 造故障的工具坏了 ≠ 故障造出来了 —— 兼「spec 也是二手源」

> 2026-08-05 session AD（[[INFRA-F97]] 实施）。本文只记**可复用的两条**，本轮完整叙述在 [STATUS-CHANGELOG](../STATUS-CHANGELOG.md) 当日 session AD 段，不在此复述。

---

## 一、无效注入：第二例，可以定性为 pattern 了

### 事实

给 `audit:render-drift-gate` 立完 S1–S4 后要证明「服务器真死掉时闸会红」。做法是在 scratchpad 写一份临时 playwright config（死端口 5199），跑真 playwright。

第一版 config 照抄了仓库那份的 `import { defineConfig, devices } from '@playwright/test'`。**scratchpad 目录没有 `node_modules`** → `MODULE_NOT_FOUND`，config 根本没被加载。

playwright 退出码 **1**。

那一刻的表象与「故障造成功了」**完全一致**：我要造的就是失败态，拿到的就是非零退出码。

### 为什么这不是个例

上一例：[[INFRA-F86]] 视觉闸那轮，「B 第一次是无效注入 —— `sed` 转义没写对、`grep -c` 仍 2 处命中却看到『7 passed』」。

两例的共同结构：

> **注入动作本身失败了，而失败的表现恰好落在"预期结果"的值域里。**

- F86：注入没生效 → 测试仍绿 → 而那组的预期就是「看看会不会绿」的对照
- 本轮：注入工具没启动 → 退出码 1 → 而那组的预期就是「非零退出码」

⇒ **凡是「期望看到失败」的注入，退出码 / 红绿本身不构成证据** —— 因为造故障失败与故障造成功共用同一个信号。

### 识破手段（这次真正新增的东西）

**别看被测系统的返回值，看它的产物有没有被写过。**

本轮识破靠的是：`figma-data/normalized/render-verification.report.json` 的 `checkedAt` **没变**。report 是那条 spec 每次跑必写的产物；它没被写 ⇒ spec 根本没跑 ⇒ 退出码 1 与被测判据无关。

一般形式：

| 要证的事 | 别只看 | 要看 |
|---|---|---|
| 故障注入真的跑到了被测代码 | 退出码 / 红绿 | 被测系统**必写的产物**是否被更新（时间戳 / 哈希 / 行数）|
| 注入内容真的落进了文件 | `sed` / `node -e` 的退出码 | 改完立刻 `grep` 回读命中数（F86 的教训） |

**判定**：任何「期望红」的注入，先建立一条**与红绿正交**的「它确实执行过」证据，再采信红。

---

## 二、spec 也是二手源 —— 连 owner 认可过的设计文档也是

### 事实

spec §3 写着：「`NAVIGATION_FIELD` 是单一导出常量，catch 块与统计共用同一个 —— **全仓该字面量只有一处**」。

动手前按硬规则 #10 `grep` 核了一遍。同语义域实际有 **4** 处：

| 位置 | 性质 |
|---|---|
| `render-report-summary.ts:23` | 唯一定义（本轮做的） |
| `drift-compare-core.ts:398` | 消费者（分类查表） |
| `render-drift-summary.mjs:29` | 消费者（另一个工具自己再分类） |
| `react-drift-full.spec.ts:189` | **另一个产生者**（React 链） |

⇒ spec 那句话**只在 Vue 这条链上成立**。若照抄进 commit body / backlog entry，就是一句假声明。

### 顺带查出的东西比订正本身更值钱

追 `drift-compare-core.ts:398` 时发现：`classifyFailedCheck` 里有 `|| check.field === 'navigation'` → 归 B。但它只在 `buildChecks` 内被调（`:433`/`:511`），而 `buildChecks` 只在**导航成功后**才跑 —— catch 块那条 check 直接 push、绕过它。

**那半个分支不可达。**

这正是 F97 这个洞的来历：有人本打算把导航失败归类为 B，代码路径走不到，于是落成「无分类」，而闸的判据是 `A ≤ 0` —— **无分类的东西，任何按分类计数的判据都看不见**。

这个因果链 spec 里没有，backlog entry 里也没有。它是「动手前核活源」顺出来的。

### 与既有纪律的关系

memory `entry-restatement-is-secondhand` 讲的是 **backlog entry 的转述**是二手源。本轮把适用面扩到 **spec** —— 哪怕它是当天写的、owner 认可过的、且本轮就是它的执行者。

**判据不变**：文档里凡是**关于当前代码状态的断言**（「只有一处」「已分叉」「加了 X 就归零」），动手前都要自己跑一遍。判据本身（该怎么设计）以 spec 为准，事实陈述不是。

---

## 三、没做的（有意）

`react-drift-full.spec.ts` 那条链**一个闸都没有**，比 F97 本身的缺口大。按「根因牵出大缺口 ≠ 顺手做完」立 [[INFRA-F100]] 报 owner，本轮不扩范围；并在 entry 里写死 ⛔「别照抄 Vue 那份闸改个路径」—— 那会立刻产生第二份会分叉的判据副本（同 [[INFRA-F98]] 的病）。
