# Q5 预注册 —— R20 / R21 建规则 + 同批补闸；R22 判「不建」

> owner 2026-09-07 拍 `build-with-gate`（建规则**且**同批补闸）。
> 本文件在**动手之前**钉死判据。理由与 Q3 那次同形：`build-with-gate` 的「补闸」是可以
> 事后叙述成任何样子的 —— 一条只在 fixture 上跑的闸也能被讲成「已上闸」。
> ⛔ 判据不先钉死，「同批补闸」就退化成事后找理由。

---

## 0 亲验读数（⛔ 全部单跑、不带管道；这些读数直接改变了落法）

| # | 读数 | 命令 / 出处 |
|---|---|---|
| **A** | `R20` / `R21` / `R22` 在 `docs scripts tests figma-sync` 下命中 **12 处，全部是「它不存在」的登记**，零规则定义 | `grep -rnE "\bR2[012]\b" docs scripts tests figma-sync` |
| **B** | code 侧 R-rule 的既有范式 = `eslint-plugin/rules/*.js`（AST 层）**＋** `scripts/audit-product-code.mjs` 的 CLI probe（文本层），两者互为 mirror | `eslint-plugin/rules/no-native-element.js:10-12` 逐字「code-side enforcement of R15 … Stays aligned with CLI R15 in scripts/audit-product-code.mjs」 |
| **C** | `audit-product-code.mjs` **自己没有挂载声明**，且是元闸 38 条具名豁免之一 ⇒ **它在 DS 仓没有牙** | `node scripts/audit-gate-mount-declaration.mjs` ⇒ `扫描面 91 · 已声明 53 · 沉默 38 · 具名豁免 38`，`sed -n 1,45p` 该文件无「挂载层」段 |
| **D** | `audit:consumer-code` / `audit:consumer` **不在** `.husky/pre-commit`、不在 Gitea / GitHub CI | `grep -n product-code package.json .husky/pre-commit .gitea/workflows/*.yml .github/workflows/*.yml` ⇒ 只有 package.json 三行（`files` + 两个 script 别名） |
| **E** | 既有 R14 / R19 / R9 的**真牙**在 `tests/audit-product-code-r14-r19-r9.test.ts`（planted-drift 单测），跑在 `pnpm test` 下 ⇒ pre-commit **L8–9 无条件** + Gitea `pr-checks.yml` | 同 Q2 棘轮那次亲验过的两层 |
| **F** | `no-hardcoded-color.js` 只认 **hex**（`HEX_RE`），CLI `R2` 也只认 hex ⇒ **裸 `rgba(...)` 全仓无人管** | `eslint-plugin/rules/no-hardcoded-color.js:12` + `audit-product-code.mjs:27-31` 头注释 |
| **G** | `src/` 里裸 `rgba()` 共 **2 处**：`src/tokens/variables.css:374`（`--mask-overlay` 的**唯一定义点**）+ `src/canonical/PillStatus.vue:17`（**注释文字**里的 `rgba(hue,0.18)`） ⇒ 上 R21 的边际摩擦 ≈ 0 | `grep -rnoE "rgba\([^)]*\)" src` |
| **H** | `src/` 里 `#<index 派生>` 形态的 list 标号 **0 处**。仅 2 处 `#${…}` 都在 `playground/docs/DocsShell.vue`，且是 **URL hash / 锚点**（`#${routeWithSection}` · `#${section.id}`），⛔ 不是 `#N` 标号 | `grep -rnE "[\"'\`]#\$\{\|'#' *\+" src playground react-pilot` |
| **I** | 🔴 **M41 正文的 `0.2` 与活体真源冲突**：`src/tokens/variables.css:374` = `rgba(0, 0, 0, 0.6)`；Figma 侧该变量名逐字 **`Mask blur8 #000-60%`**（`token-aliases.ts:68,158`）；而 M41 逐字写「opacity **0.2**（**不 0.5-0.6**，避免黑过重）」+ 其 Code 端镜像行逐字 `background: rgba(0,0,0,0.2)` | 三处单跑 grep |
| **J** | `playground/docs/pages/atomicAssets.ts:532` 逐字「**当前运行时唯一收进系统的遮罩样式**，业务页面应避免再随意发明新的 rgba 遮罩」 | 同上 |
| **K** | `code-conventions.md` **在** `audit:acceptance-gate-coverage` 的 `SCAN_FILES` 里（第 116 行）⇒ 新增带 `**Acceptance**` 的规则段会让 `unclassified` +1/条，撞 `BASELINE_UNCLASSIFIED = 51` ⇒ **不补自陈就当场红** | `scripts/audit-acceptance-gate-coverage.mjs:110-119,166` |
| **L** | 🔴 **`audit-product-code.mjs` 跑在 DS `src/` 上本来就是红的**：单跑 ⇒ **EXIT=1**，`scanned 189 files`，`R1 29 条 + R2 9 条 + …`。而那 29 条 R1 **全部**是 `src/icons/catalog/generated/*.ts` 与 `src/icons/raw.ts` —— **DS 就是 icon 的出处**，内联 SVG 在这里是产物不是违例。⇒ 读数 C/D 的「没挂载」**不是遗漏，是结构性的**：这条闸的对象是**消费仓**，整条挂进 DS 必然恒红 | `node scripts/audit-product-code.mjs`（单跑、不带管道，EXIT 读自 `$?`） |

⚠️ **读数 L 直接否掉了一个我原本会选的落点**：不能「把 `audit:consumer-code` 挂进 DS 的 pre-commit / CI」来给 R20/R21 上牙 —— 那会让 38 条存量违例当场把全仓提交打死，且那些违例里至少 29 条**根本不是违例**。
⇒ 牙**只能**落在「单独跑 R20/R21 两个纯函数扫 `src/`」的测试上（§2 表里那一行），⛔ 不是整条 CLI。

---

## 1 读数把三条规则分成了三种落法（⛔ 不是交接假设的「三条都建」）

### 1.1 R20 —— 建，且判据用 owner 的问法

owner 2026-09-07 定的问法在 code 侧才真可机检（Q1-b 小节已登记）：
**label 由 index 派生 ⇒ `sequence`；label 是存储字段 ⇒ `name`。**

- **断言**：源码里出现「`#` 紧跟 index 派生表达式」的 list 标号 ⇒ 必须在**相邻行**声明
  `#N semantic: sequence` 或 `#N semantic: name`。
- **值域逐字取自活源**：闭集 `{sequence, name}` 取自 `mockup-conventions.md#M40` 语义类型表，
  声明串形态取自 owner 2026-09-07 选定的候选 2（DS `bb9146af` 已落）。⛔ 未发明新值域。
- ⚠️ **如实登记的分母事实**（读数 H）：**DS `src/` 今日触发命中 0**。
  ⇒ 「扫 DS src ⇒ 0 违例」这一半**今日恒绿、零信息**，⛔ 不许把它宣传成「DS 已被守住」。
  R20 的真实射程是**消费仓**（`audit:consumer-code --dir <消费仓>`）与**未来新写的 DS 代码**。
  它是**前向 fail-closed 闸**：分母可以长大，第一次有人写就红 —— 而这一点**必须由造故障证明**（见 P1）。

### 1.2 R21 —— 只建「两个真源都同意」的那一半；`0.2 vs 0.6` ⛔ 不由 agent 裁

读数 I + J 是本轮**最要紧的发现**，它不在交接里：

| 真源 | 说什么 | 站得住吗 |
|---|---|---|
| `mockup-conventions.md#M41` | scrim = `rgba(0,0,0,**0.2**)` + blur 8，逐字「**不 0.5-0.6**」 | ✅ 逐字标注 `user-validated 2026-05-26 V`（owner 当面校过） |
| Figma 库变量 `Mask blur8 #000-**60%**` / `src/tokens/variables.css:374` | scrim = `rgba(0,0,0,**0.6**)` + blur 8 | ✅ owner 已拍定「Figma 库是权威且不改」；且 `atomicAssets.ts:532` 逐字称它是**系统唯一收编的遮罩** |

⇒ 这**正是**队列 §1.1 表里的第 ③ 类「两个都站得住的真源在冲突」⇒ **真该进队列（C3）**，
⛔ agent 不得替 owner 选 0.2 还是 0.6。⚠️ 反向也要守住：**这不构成「R21 整条不建」的理由** ——
两个真源在**另一半上完全一致**：遮罩必须走那**一个** token，⛔ 不许在业务代码里另写裸 `rgba`
（读数 J 逐字就是这句）。⇒ 本轮建的就是这一半。

- **断言**：overlay / scrim / mask 语义的 `background(-color)` 与 `backdrop-filter: blur(...)`
  必须用 `var(--mask-overlay)` / `var(--mask-overlay-blur)`；写裸 `rgba(...)` 或裸 `blur(<N>px)` ⇒ 违例。
- **豁免**：`src/tokens/variables.css`（token 的唯一定义点，读数 G）+ 行内 `// AUDIT-IGNORE-R21: <reason>`。
- **⛔ 刻意不判**的：那个数值本身（0.2 / 0.6）。理由 = 上表，并同步立成队列新条目 **Q11**。
- **分母今日 ≥ 1**（`src/components/PopupBox/PopupBox.vue:280-281`，合规）⇒ 见 P4 的**分母地板**。

### 1.3 R22 —— 判「⛔ 不建」，并把 M42 那行改成真指针

M42 的 Code 端镜像逐字要求：「fork / copy existing component file 时必检查其引用的所有 token，
不允许 hardcoded color 沿用」。拆成两半后**没有一半需要新规则号**：

| 半 | 判定面 | 现状 |
|---|---|---|
| 结果文件里不许有硬编码颜色 | 源码 | **已被覆盖**：hex ⇒ 既有 `R2`（CLI）+ `no-hardcoded-color`（ESLint）；rgba ⇒ **本轮新建的 R21** |
| 「clone 前必 probe 源」+「probe 结果入 handoff 表」 | 过程 + handoff 文本 | **判定面不落仓** —— 与已登记的 handoff 族天花板同处境（`audit-mockup-handoff-evidence.mjs` 自印「DS 仓一份真 handoff 都没有」） |

⇒ 建 R22 = **加一条规则面、换来零新覆盖**，而那**正是 Q5 自己正文点名的病**
（逐字：「加规则不加闸会让那个比例更差」）。
⚠️ 这不是「我觉得不必要」，是读数唯一确定的：**修法唯一** ⇒ agent 自己拍（A 档）。
- 动作：M42 的「Code 端镜像」行改成真指针（指 `R2` + 新 `R21`）+ 显式写清 process 那一半为什么不可机检
  + **写重开条件**（见 §4）。

---

## 2 落点（⛔ 判据算式只有一份）

| 落点 | 放什么 | 为什么是这里 |
|---|---|---|
| `docs/internal/code-conventions.md` | 新增 `## R20` · `## R21` 两条规则（含 `**Acceptance**`） | 规则真源。⚠️ 读数 K：不补自陈会当场撞棘轮 |
| `scripts/audit-product-code.mjs` | 纯函数 `findR20Violations` / `findR21Violations` + 接进 CLI 的 `violations` 表与报告 | 读数 B：这是 code 侧 CLI probe 的既有真源。⛔ 不新建脚本 —— 那会多一条沉默闸（读数 C） |
| 同文件头 **60 行内** | `// covers-acceptance: docs/internal/code-conventions.md#R20 [N]` · `#R21 [N]` | 读数 K；`parseCoversDeclarations` 只读前 60 行 |
| `tests/audit-product-code-r20-r21.test.ts` | ① planted-drift 单测（必须命中 / 不可误报）② **扫真 `src/` 的零违例断言** ③ **R21 分母地板** | 读数 D + E：CLI 本身没牙，牙只能落在 `pnpm test`（pre-commit L8–9 无条件 + Gitea CI），同 Q2 棘轮那次的落法 |
| `docs/internal/mockup-conventions.md` | M40 / M41 / M42 三行「Code 端镜像」改成真指针 | 三行自己逐字写着「谁建了 R2x 就同批改成真指针」 |

⛔ **不做的两件事**（各有理由，⛔ 别当 TODO）：
1. ⛔ 不给 `audit-product-code.mjs` 补挂载声明 / 从元闸豁免表里摘出来 —— 那是**第 6 项待办**
   （C1 元闸扫描面）的一部分，它有自己的口径，混做会让两边的读数都说不清。
2. ⛔ 不建 ESLint 侧的 R20/R21 规则 —— 既有范式里 ESLint 与 CLI 是**两条判据两份实现**
   （`no-hardcoded-spacing.js:16` 逐字承认 `<style>` 块由 CLI 那半兜），本轮先落 CLI 那半；
   ESLint 那半属独立提案。**⚠️ 这是登记在案的覆盖边界，不是「已全覆盖」。**

---

## 3 判据（⛔ 跑之前写死。每条都要「造故障时它红并点名 / 还原后逐字一致」两侧）

### P1 · R20 会红，且点名（治读数 H 那个恒绿）
喂一段 `\`#${i + 1}\`` 形态的模板文本给 `findR20Violations` ⇒ **必须** 返回 1 条，
带正确 `line`，且 `hint` 里含 `#N semantic:`。
⇒ **同时**把这段代码真写进一个临时 `.vue` 落在 `src/` 下，跑扫真 src 的那条测试
⇒ **必须红并逐字点出该文件与行号**。还原后必须绿。
⛔ 只有 fixture 命中不算 —— 那只证明纯函数会算，不证明它在真扫描面上会红。

### P2 · R20 不误报（三条阴性对照）
- URL hash / 锚点：`` `#${section.id}` `` 与 `` `#${routeWithSection}` `` ⇒ **0 条**
  （这两条逐字取自读数 H 的真实代码，⛔ 不是我编的阴性样本）
- hex 颜色：`#fff` / `#1a2b3c` ⇒ **0 条**
- 已声明：同行或相邻行有 `#N semantic: sequence` ⇒ **0 条**

### P3 · R20 值域是闭集
相邻行写 `#N semantic: auto`（不在闭集）⇒ **必须**红，且 `hint` 指出合法值只有 `sequence` / `name`。
⛔ 「有声明就放行」不算实现了 M40 —— M40 的闸判的是**存在性 + 值域二选一**两条。

### P4 · R21 有牙、且分母不许静默变小
- **零违例断言**：扫真 `src/` ⇒ R21 违例 **0**（今日读数 G 支持它现在就成立）。
- **造故障**：把 `PopupBox.vue:280` 的 `var(--mask-overlay)` 改成裸 `rgba(0,0,0,0.6)`
  ⇒ **必须**红并点名该文件行号。
- 🔴 **分母地板**：`findR21Violations` 在真 `src/` 上匹配到的 **overlay 上下文单元数必须 ≥ 1**。
  ⇒ 有人删/改名让探针再也看不见任何 overlay ⇒ **红**，含义是「闸的分母塌了」。
  ⚠️ 这条是本仓两次点名过的病（`collectStateFrameGroups` 分母静默变小 · Q8 身份变了）的直接防线，
  ⛔ 没有它，R21 会在某次重构后静默变成恒绿。

### P5 · R21 不误报（三条阴性对照）
- token 定义点 `src/tokens/variables.css` 的那行 `--mask-overlay: rgba(0,0,0,0.6)` ⇒ **0 条**
- 注释里的 `rgba(hue,0.18)`（`PillStatus.vue:17`，真实代码）⇒ **0 条**
- 非 overlay 语义的 `blur()`（如 `filter: blur(2px)` 用在缩略图）⇒ **0 条**

### P6 · 棘轮不许被抬（读数 K）
`node scripts/audit-acceptance-gate-coverage.mjs` ⇒ **EXIT=0**，且
`unclassified` **仍 = 51**、`BASELINE_UNCLASSIFIED` **仍 = 51`（⛔ 一个字都不许改这个常量）。
新增两段必须以 `gated` 落地。
**造故障**：删掉 `covers-acceptance: …#R20 [N]` 那一行 ⇒ **必须**红并印 `[S2] unclassified 52 > BASELINE 51`。

### P7 · `[N]` 必须等于段内核查项 Σ
按队列已钉的纪律，`[N]` = 段内 `·` 分隔的**核查项总和**，不是条数。
`node scripts/audit-acceptance-gate-coverage.mjs` 的 `[S4]` 会当场对账 ⇒ 写错必须红并逐字印出两个数。

### P8 · 全量回归零误伤
`pnpm test` ⇒ 与开工前基线相比 **零新增失败**；新文件确实被收进 `pnpm test`（跑数增加）。

### P9 · 三行「Code 端镜像」不再说假话
M40 / M41 / M42 三行改完后，全仓 grep `没有 R2[012]` ⇒ **0 命中**。
⚠️ M41 那行必须**同时**写清「数值那一半仍在 C3 排队（Q11）」，
⛔ 不许写成「已完全镜像」—— 那会把读数 I 的冲突藏起来。

### P10 · 还原后零残留
每次造故障后 `git diff` / `cmp` ⇒ 与造故障前**逐字一致**（`checkedAt` 类时间戳除外）。

---

## 4 R22 「不建」的重开条件（按仓规：被否也要写重开条件）

任一条成立 ⇒ 重开「要不要建 R22」：
1. 出现**一条硬编码颜色形态既不被 hex 判据也不被 R21 的 rgba/blur 判据抓住**的实例
   （如 `hsl()` / `color-mix()` / `oklch()` 写死品牌色）。
2. 「clone 前 probe 源」那一半的判定面**落进仓库**了 —— 例如 handoff 模板被投放进
   `templates/consumer-product/` 且带机检（那正是 Q1 §3B handoff 族登记的独立提案）。
3. 有人**真的**因为「clone 传播了坏物料」返工，且上面两条判据当时都是绿的
   （= 覆盖假设被实证推翻）。

---

## 5 ⛔ 本轮不能说的话（先写下来，免得收尾时自己滑过去）

- ⛔ 不许说「M40 的 code 侧镜像已经守住 DS 仓」—— 读数 H：DS `src/` 今日触发命中 **0**。
- ⛔ 不许说「R21 让 scrim 合规了」—— 数值那一半（0.2 vs 0.6）**本轮未裁**，已立 Q11。
- ⛔ 不许说「Q5 三条都建了」—— R22 判**不建**，是有读数支撑的省略，不是遗漏。
- ⛔ 不许说「code 侧 R-rule 现在有闸了」—— CLI 那条闸在 DS 仓**仍然没有挂载**（读数 C/D）；
  本轮拿到的牙是 `pnpm test` 那一层，覆盖面是「判据本身 + DS `src/`」，
  ⛔ 不覆盖「消费仓真的跑过它」。
