# 假红假绿，成因常常是你自己上一步的动作（2026-08-03 · session O）

> **触发**：一个 session 内，三个不同的闸/探针报出与代码无关的结果，**三次的成因都是我前一步的操作**。三次形态各异、都不难查，但如果第一反应是"去查代码"，每一次都会走进死胡同并可能"修"出真的 bug。
> **相邻但不同的旧复盘**：[`2026-07-31-probes-that-cannot-see-their-own-blind-spot`](./2026-07-31-probes-that-cannot-see-their-own-blind-spot.md) 讲的是**探针结构上看不见自己的盲区**；本篇讲的是**探针是好的，被观察者（环境）被我自己污染了**。

---

## 一 · 三次实例

### ① `npx vite build` 单跑清空 `outDir` → `audit:consumer-contract` 假红 11 条

为了核 `page-recipes` 的 S4（token 有没有真进 `dist/style.css`），我只跑了 DS 库那一步 `npx vite build`。vite 的 `emptyOutDir` 默认开 —— 它把完整 `pnpm build` 链里**后续步骤**产出的 `dist/chart.js` / `dist/tokens/` / `dist/composition/` 一并抹掉。于是下一个闸立刻报 11 条「`exports` 声明指向不存在的文件」。

那不是仓库缺陷，是我三分钟前的动作。补跑构建链剩余步骤即复原。

### ② `dist/` 陈旧 → S4 报「token 没随包发」

同一轮里 S4 先报 `--bp-lg` 在源码定义了但不在 `dist/style.css`。这**恰好是 S4 被造出来要抓的那类真 bug**（同型前例 `ship-token-css` 潜伏六个版本），所以最不该草率下结论。

判据不是推理，是两个数：`stat` 出本机 `dist/style.css` 构建于 13:03，而 `variables.css` 的 token 是 18:22 才落的；重建后 dist token 声明数 **346 → 354，正好 +8**（4 个 `--bp-*` + 4 个 `--container-*`）。**"陈旧产物"与"真的没随包发"症状一模一样**，必须查到数字才能分开。

### ③ playwright 共用 page + `goto` 只改 hash → 阴性对照读出滚动位置

验证 F90 段落锚点时，第一版探针所有用例共用一个 `page`。`page.goto` 到只有 hash 不同的 URL **不会真重载** —— 于是"不存在的 slug 应停在页顶"这条读出 `scrollY=8336`，正是上一条用例留下的位置。

**阴性对照被污染 = 最危险的一种**：它会让你以为新功能有 bug（其实没有），进而去"修"一个不存在的问题。改成每条用例开新 `page` 才拿到真值 `0`。

---

## 二 · 可操作的判据

看到闸红/探针异常，**先问一句再动手**：

1. **这一轮我动过被观察者本身吗？** 构建产物、缓存、dev server、浏览器会话状态、`.git/index` —— 这些都是"被观察者"。
2. **这个信号有没有可能是"陈旧"而不是"错误"？** 判据是**时间戳与计数**（`stat` 比 mtime、重建后看增量），不是"看起来像"。
3. **阴性对照本身干净吗？** 共用会话/共用进程的探针，阴性对照往往最先被污染 —— 而它一旦失真，整套判据的信息量归零。

反过来，这条**不能**被用来解释掉真缺陷。区分方法很朴素：**给出让它变绿的具体动作，并复跑**。①补跑构建链 ②重建 dist ③每条开新 page —— 三次都是"做一个具体动作、信号变了"，而不是"我觉得它是环境问题"。

---

## 三 · 另外两条同样来自这一天

### 「执行一次流程」比「读十遍流程」更能发现流程写错了

[`RELEASING.md`](../../RELEASING.md) Step 4 把 `pnpm check:docs-site-version` 排在 `git commit` **之前**，还配了句「应从 exit 1 变成 exit 0（构建产物已含新版本）」。**那句是错的**：该脚本测的是 **git 推导**的「最后一次动 `playground-dist` 的 commit + 它当时的 `package.json`」，这正是它「不另存一份会漂的构建戳」的设计 —— 所以 `pnpm build` 之后它**仍然 exit 1**，照旧顺序做的人会误判成构建失败。

要命的地方在于：这段流程**被读过、被引用过、还是 SoT 归属表里指定的唯一真源**，前一天刚被 owner 裁定统一口径。纸面审查抓不到它，真跑一遍五秒钟就撞上。

> **推论**：一份"唯一真源"文档的可信度，与它**最近一次被真实执行**的时间有关，而不只与它被评审过几次有关。

### 落到 master ≠ 交付

F90（docs 站段落锚点）的价值**全部在真站上** —— 它存在的理由就是"走查/签核时能发一条直达某段的链接"。把实现推进 master 而线上仍是旧 bundle 时，这条 entry 实际上一点没兑现，但从 git 历史上看它"完成了"。

> **判据**：一条改动的价值落点在哪里（npm 包 / 线上站 / 仓库内工具），**验收就必须发生在那里**。同理，这一天另一条纪律的由来也是它：`push` 绿 ≠ 站点更新，必须打开真站亲验。

---

## 四 · 这一天做对的（同等重要，别只囤教训）

- **新闸落地前先跑基线**：`page-recipes` 的 S5 在写完的第一次运行就抓到那两处存量散文（exit 1）。**头一次就绿的闸，绿是没有信息量的** —— 分不清"没有缺陷"和"闸没生效"。
- **不照搬仓库里的二手记录**：发 docs 站要 `VISUAL_COMMIT_APPROVED`，STATUS 里写着上一轮 `test:visual` 33/33，但那是别人的记录 —— 自己重跑了一遍才签。
- **删 entry 前先问"里面有没有仍生效的约束"**：INFRA-F84 删档时，`build:wc` 必须插在 `build:playground` 之前这条约束被搬进了 `check-dist-wc-freshness.mjs` 头注释。**必须先搬**，因为计划文档里那句是**写错的版本**，删了 entry 就只剩错的那句。
- **消除重复要看代价**：视觉闸里 `slugify` 有两份，删掉了全仓无人 import 的那份死孪生，但**没有**去动 `page.evaluate` 里内联那份 —— 392 张已签 baseline 的文件名由它派生。改用文本漂移闸钉住，**并如实声明它只拦得住"改了正则"、拦不住"整段重写成等价实现"**。
