---
share_worthy: maybe
audience: UX, Dev
one_liner: 这一轮四次误判全部出在我自己临时写的量具上，产品代码零失误；三条可复用的自检点与两条验证有效的做法。
evidence: 37f6c709, b1dbea94, docs/internal/backlog.md INFRA-F145 (c)
---

# 当缺陷在我自己的量具里（2026-09-14）

> 本轮两个 commit：`b1dbea94`（Drop down List 面板容器接进 render-verification）·
> `37f6c709`（INFRA-F145 (c) 第一批 —— 8 个手写控件换成 DS 组件）。
> 叙述在 [STATUS 顶条](../../STATUS.md)，⛔ 本文不复述做了什么，只记**怎么发现自己错的**。

## 0. 一句话

**四次误判，全部出在我为了取证临时写的脚本里；产品代码与两次 commit 的内容零失误。**
每一次的症状都不是报错，而是**一个看起来很合理的结论**。

---

## 一 · 教训（下次别再犯）

### ① 跨行标签让「按行扫描 + 先行断言」大幅漏数 —— 三个数互相矛盾才暴露

数 `playground/docs/**` 里的手写原生元素，我先后得到 **11 / 2 / 17** 三个互相矛盾的数。
根因：模板里标签是**跨行写**的

```html
<button
  ref="searchButtonRef"
  class="docs-search"
>
```

而我的正则是 `/<button(?=[\s/>])/` 配**按行**扫描 —— 行尾 `<button` 后面没有字符，
先行断言失配，整行被跳过。DocsShell 有 10 个按钮，我数出 2 个。

**是 `/usr/bin/grep` 的独立读数把它比下去的**，不是我读代码读出来的。

> 🔑 **可复用的自检点**：**我刚写的量具与既有工具矛盾时，默认错的是我写的那个。**
> 这条在 [[feedback_one-off-scripts-are-the-unreliable-part]] 里已经记过，本轮是又一个实例 ——
> 说明它值得在「开始数任何东西之前」就跑一次交叉验，而不是等矛盾出现。

### ② hash 路由不触发重载 ⇒ 截图状态泄漏到下一张，标签全错

docs 站是 hash 路由。我以为 `page.goto('…/#/form')` 会重新加载页面，于是在**同一个
context** 里连续截 8 张图，每张之前按需切主题 / 切禁用开关。

实际：换 hash **不触发真实重载**，Vue 应用状态（主题、禁用开关）留在上一张的状态里。
于是「深色·禁用」那张其实是浅色·常态，「浅色·常态」那张其实是别的组合 —— **标签全错**。

**暴露它的不是眼睛，是 sha256**：我比对时发现 `form-actions-disabled--before--dark.png`
与 `form-actions--before--light.png` **逐字节相同**。两个本该不同的状态得到同一张图，
只可能是状态没切。

> 🔑 **可复用的自检点**：**截图取证时，"截到了" 与 "截对了" 是两件事。**
> 修法有两条，都要：① **每张图一个全新 context**（别指望 SPA 的 goto 会重置状态）
> ② 切完状态**断言它真的切到了**（读 `data-theme`、读按钮的 `disabled` 属性），
> 断言不过**直接 throw、不出图** —— 而不是默默截一张看起来没问题的。

### ③ 差点拿一份更早的构建当「改动前」

要做 before/after 并排，我想用仓库里已提交的 `playground-dist/`（静态构建）当「改动前」。
探测发现：**它的 Form demo 里 disabled 开关还是 `<input type="checkbox">`**，
而当前代码早已是 `<Switch>`。那是更早的快照。

拿它并排，会把**与本次无关的改动**（checkbox → Switch）一起算进对比，
让 owner 在一份混进噪声的材料上签核。

> 🔑 **可复用的自检点**：**「手边有一份旧产物」不等于「它是这次改动的对照组」。**
> 用它之前先找一个**与本次改动无关、但你知道正确答案**的点去验它的年代
> （这里就是「disabled 开关是 checkbox 还是 Switch」）。验不过就别用 ——
> 宁可不给 before 那一列，也不给一列会误导的 before。
> 本轮的处置：去掉 before 列，把「改动前」交给 `git diff`（精确且无歧义），并在证据页上
> **写清楚为什么没有那一列**。

### ④ entry 里写着「不存在」的东西，往往真的存在

`INFRA-F145` entry 逐字写着「demo/docs 面**只有这一条**手写 disabled 样式规则」。
现取实证：还有第二条 —— `docs.css` 的 `.framework-switch--disabled { opacity: 0.4 }`，
而 `opacity: 0.4` 恰恰是 `FIGMA_AS_SOURCE_OF_TRUTH.md` §不允许的差异 **逐字举的那个例子**。

**漏掉的原因是结构性的、可复现的**：那条用的是 **modifier class**（`--disabled`），
不是 `:disabled` 伪类。只搜伪类形态的扫描**永远**抓不到它。

> 🔑 这是「判据只对真源的**一种书写形态**灵敏」的又一个实例
> （[[feedback_new-gate-acceptance-three-questions]] 第 9 问）。
> 与 [[feedback_entry-restatement-is-secondhand]] 记的「最贵的三个方向」之一
> —— **「这个问题不存在」**（纠正刚抓到一个错 ⇒ 没人再查它）——完全同型。

### ⑤ 断言写在 shadow DOM 外面 —— 同一次修改里**假红**与**恒真假绿**一起出现（第四轮补记）

给 `INFRA-F148` 的截图量具加两条新断言（「AFTER 必须显示数值」+「AFTER 必须不再是
`value-hidden` 档」）时，我用的是 `c.querySelector('.slider__value')`。
Vue 侧是普通组件、查得到；**React 侧是 `<tvu-slider>` CE，两个类名都在它的 `shadowRoot` 里**
⇒ 从 light DOM 一律取不到。两条断言因此走向**相反**的错：

| 断言 | 方向 | 为什么危险 |
|---|---|---|
| 「必须**有** `.slider__value`」 | **假红** | 会让我以为 `showValue` 没送进 CE，去改一段本来就对的代码 |
| 「必须**没有** `.slider--value-hidden`」 | **恒真假绿** | 永远取不到那个类 ⇒ 永远「通过」，**这条断言从写下起就没有判别力** |

**可迁移的判据**：写「某元素不存在」型断言时，先问一句「**如果我的查询根本够不到那一层，这条会读成什么？**」——
答案是「读成不存在 = 通过」的，就是一条恒真断言。
⇒ 配套做法：**同一个探针同时报出它用的是哪个根**（本轮加了 `sliderProbeRoot: 'shadow' | 'light'`），
让「查得到/查不到」变成读数的一部分，而不是沉默的前提。

> 与 [[feedback_one-off-scripts-are-the-unreliable-part]] 第五条（判据自己恒真）同型，
> 但**新增的那一半是「同一次修改同时产生两个方向的错」** —— 假红把注意力吸引过去，
> 假绿在旁边安静通过。⇒ 一条断言红时，**顺手复核同一批里方向相反的那条**。

---

## 二 · 验证过确实管用的做法（下次遇到同类场景直接用）

⚠️ 本节与上一节**同等重要**。只囤教训会让判断力朝「过度谨慎」单向漂移
（[[feedback_auto-apply-when-inferable-and-low-risk]] 的成因）。

### ✅ A. 量具自带「已知向量自测」，让读数自己证明自己可信

每次算对比度，脚本开头先印两个**答案已知**的数：

```
黑白 = 21.00（应 21.00） ·  #777 on white = 4.48（应 4.48）
```

对不上就说明算法或取色链坏了，后面所有数字一概不可信。
本轮跑了 3 次对比度测量，每次都先过这一关 —— **成本接近零，但它是「这批数字能不能拿去做决定」的唯一凭据**。

### ✅ B. 想证明「不是我引入的」，就去跑对照组，别只讲道理

render gate 的 Vue 链一直被一条间歇导航失败打红。我本可以用三条推理说服自己「与我无关」
（失败条目与我的改动无关 · 位置在我新增条目之前 · 上一轮也有同症状）——
**但那三条都只是推断**。

实际做的：把 HEAD 那份 944 条 manifest 换回磁盘，**跑 3 次对照**。结果 1 次也命中同样的失败
⇒ **「基线也会抖」成了实证事实**，而不是我的说法。

顺带的诚实边界也是这么来的：含改动 7 跑 2 净 vs 基线 3 跑 2 净，**两个样本都太小**，
所以我只敢说「基线也会抖」，**不敢说两者频率相同** —— 有对照才谈得上这种分寸。

### ✅ C. 做「该不该换」这类分诊时，先量清楚再给结论，且结论允许是「大部分不该做」

(c) 原文是「demo 面禁止手写按钮，一律用 DS Button，可上 lint 规则」。
逐条看完 30 处实物后，结论是**只有 8 处该换、22 处该留**，并因此**反对**那条 lint：
落地当天它就是一张 22 行豁免表，而**全是豁免的闸也不是闸**。

> 关键在于**分诊结论允许推翻原提案的形态**。如果我默认「任务是上那条 lint」，
> 就会去凑一张豁免表，把一个没有拦截力的闸挂上去 —— 那比不上闸更糟，因为它看起来像有保护。

---

## 三 · 留给下一轮的（都已写进 backlog，⛔ 别当已完成）

1. **「手写控件不许自己定义禁用/hover 态样式」这条判据未上闸** —— 等 owner 拍。
2. **`react-pilot/src/demos/**` 还有 27 处**同类元素未分诊（本轮只动了与 Vue 侧配对的 8 个）。
3. **render gate 的 Vue 链间歇导航失败** —— 既有缺陷，怎么治等 owner 拍。
4. **INFRA-F145 (b)** 的新提案（判「禁用态 token 是否取自组件自己那一族」）仍未起工；
   本轮抓到的 `opacity: 0.4` 正是它的一个实例（连 token 都没取，对比度阈值路线对它同样抓不准）。
