# 收口 §M49.1 悬空引用 + 补上让它潜伏两个月的那个盲区（2026-08-21）

**来源**：V4-2392 轮回流（DS `331aedf7`）收尾时的遗留项 —— retrospect §6 末登记为
「留给 owner 定：立成文 vs 统一改指向」。本计划把它判掉并落地。

**owner 授权**：2026-08-21，选「收口 §M49.1 悬空引用」。

---

## 0 · 判定：走「统一改指向」，⛔ 不立新节

两条候选路径里 **(A) 立一条真的 §M49.1 是错的**，硬证据三条：

1. `docs/internal/mockup-conventions.md:3125` —
   `### M52 — Library Fidelity Umbrella…（2026-06-12 新增；**原 M49**，2026-06-25 B-P4 因与
   design-process.md §M49 撞号…）`
2. 同文件 `:3129` = `#### M52.1 库样式 / 变量 Fidelity` —— **正是 §M49.1 的内容**，内容未变、只改了号。
3. 同文件 `:3169` Legacy ID Map 有行：
   `| M49 | Library Fidelity Umbrella | §M52 / §M52.1–.4 | 2026-06-25 B-P4：与 design-process.md §M49 撞号 |`

⇒ 立新 §M49.1 会**重新制造 2026-06-25 已经修掉的撞号**，并造出「同一事实两份副本」（本仓视为缺陷）。

## 1 · 活引用 vs 历史记录（决定改哪些）

| 出现处 | 性质 | 处置 |
|---|---|---|
| `scripts/audit-mockup-binding-fidelity.mjs`（8 处，含每次运行自印的 console 行） | **活引用** —— 自称 "Machine enforcement for mockup-conventions.md §M49.1" | 改指 §M52.1 |
| `scripts/audit-mockup-conformance.mjs`（注释 1 处） | **活引用** | 改指 §M52.1 |
| `docs/internal/retrospection/design-spec-canonical-alignment-tracker.md:361` | 历史记录 | ⛔ 不动 |
| `docs/internal/_metrics/phase0-ledger.md`（多轮） | 历史记录 | ⛔ 不动 |
| `docs/_archive/_plans/…`、`docs/superpowers/specs/2026-06-25-…` | 归档 | ⛔ 不动 |

「记录不改」的依据不是偷懒：`scripts/audit-rule-inventory.mjs` 头注释自己写着
「plans / retrospection / STATUS-CHANGELOG 里引用历史枚举是**合法的历史记录**，不该被判违规」，
且与 V4-2392 上一轮「未擅自改历史 ledger 行」的处置一致。

## 2 · 为什么潜伏两个月 —— 两个死判据，各自都拦不住

`audit-stale-anchors.mjs` 本该抓这个，但：

| # | 盲区 | 实证 |
|---|---|---|
| ① | `SCAN_FILES` 只含 3 份 `.md`，**不扫 `scripts/*.mjs`** | 探针输出 `SCAN_FILES 是否含任何 scripts/*.mjs = false` |
| ② | `resolveRuleRef` 的**父族 fallback**：`M49.1` 精确不命中 → 回退族根 `M49` → 而 `M49` 在 `DEF_FILES` 里的 design-process.md 存在（是**毫不相干**的 Design Quality Contract）⇒ 放行 | 探针：`§M49.1` = **PASS**，`ids.has('M49.1')=false`，`families.has('M49')=true`；已知必假的 `§M99.1` = FAIL（证明探针有鉴别力、不是永绿） |

⚠️ **只补 ① 或只补 ② 都是空断言**：只扩扫描面，`§M49.1` 仍被 fallback 放行；只收紧 fallback，
scripts 不在扫描面。两者必须同时成立才可能报红。

### ⛔ 两条被实测否决的修法

- **全局收紧父族 fallback** —— 实测会让 5 个 id（`§I5`/`§I6`/`§I7`/`§M-LIFECYCLE`/`§M23.15`，
  约 40 处出现）变红，**全是合法用法**（`§M-LIFECYCLE` → 定义为 `M-LIFECYCLE-CRUD` 的前缀族；
  `mockup-conventions.md:794` 的 `原 §M23.15（…）并入此处` 是历史叙述）。正是 owner 那条 FP 纪律要保的。
- **「每个 Q 都要有 jump 行」的路由覆盖闸** —— 实测 `figma-technical-reference.md` 有 **29 个 Q 标题、
  jump 表只 3 个**（Q15/Q22/Q29），而 `AGENTS.md:137/666` 是把该文件**整份路由**的
  （"Figma API quirks，实现时查"），per-Q 路由从来不是契约 ⇒ 该闸会产出 26 条非缺陷 findings，
  ~90% 假阳性，属触发器 K 该退档的形态。**本轮不做**（这一项是我起手时承诺过的，量完撤回）。

## 3 · 要做的（新判据：`retired-id`）

在 `audit-stale-anchors.mjs` 新增一类检查，**不新建脚本**：

- **真源** = `mockup-conventions.md` 的 Legacy ID Map 表（旧 ID → 现行落点），⛔ 不在代码里复制其内容。
- **扫描面** = 代码层 `scripts/*.mjs` **+ `tests/*.ts`**。选代码层而不是散文层是**实测决定**的：
  散文合法地叙述改号（`原 §M23.15`），而代码里写退役号一律是缺陷。

  ⚠️ **`tests/` 是第二轮补的 —— 第一版只扫 `scripts/`，当场漏掉一处真缺陷**：
  `tests/mockup-binding-fidelity.test.ts:12` 的 `describe` 名里带 `§M49.1`，闸报绿而缺陷仍在。
  「扫描面比缺陷面窄」正是本闸要治的病，我自己先犯了一次 —— 而且是**用闸自己的函数**
  跑了一遍 `tests/` 才发现的，不是靠回想。纳入实测只多 2 条命中，两条都是真缺陷。
- **判据**：script 里出现 `§<id>`，其 `<id>` 精确命中退役表、**或**其族根命中退役表
  （`§M49.1` 的根 `M49` 已退役）→ FAIL，并印出该表登记的现行落点。
- **fail-closed**：退役表解析出 0 行 → FAIL（分母为 0 闸会空转成假绿），沿用本仓既有措辞。
- **豁免表**：无。空着是终态不是待办（触发器 K）。
- **enforcement 层级**：跟 `audit:stale-anchors` 现状同级，不自行升档；层级如实登记，不写 L4+L5。

### 已知的一处必须一并处理（不是豁免掉）

`audit-stale-anchors.mjs` 自己的注释用 `§M23.15` 当算法示例（第 18/20 行）。它会被新判据判红——
而这是**真的该改**：拿一个已退役的号去示范 fallback 语义本身就误导读者。改成非退役号，⛔ 不加豁免。

## 4 · 故障注入清单（触发器 R L2 —— 「若把 X 改坏，哪几条会变红？」）

| # | 注入 | 期望 | 它证明什么 |
|---|---|---|---|
| i | 把 binding-fidelity 的 `§M52.1` 改回 `§M49.1` | 红 | 本轮修的 bug 有回归保护 |
| ii | 把 Legacy ID Map 整表删空 | 红（fail-closed） | 「删空真源让闸空转成假绿」这条被堵住 |
| iii | 任一 script 新写 `§M23.15` | 红 | 精确命中那一支非空 |
| iv | 把判据从「族根匹配」收成「仅精确匹配」 | (i) **变绿** | 族根那一支不是空断言（`§M49.1` 本身不是表里的行） |
| v | 整段删掉 `retired-id` 检查 | (i)(ii)(iii) 全绿 | 整体不是空断言 |

⚠️ (iv) 是双向变异：只做 (i)(ii)(iii) 会漏掉「族根匹配写成了同义反复」这种情况。

## 4a · 故障注入实测结果（2026-08-21，全部亲跑，退出码先存变量再读）

| # | 注入 | 期望 | 实测 |
|---|---|---|---|
| i | binding-fidelity `§M52.1` → `§M49.1` | 红 | **EXIT=1** ✅ |
| ii | Legacy ID Map 表头破坏（等价删表） | 红 fail-closed | **EXIT=1** ✅ |
| iii | 脚本新写 `§M23.15` | 红 | **EXIT=1** ✅ |
| iv | 判据收成「仅精确匹配」+ 同时注入 (i) | **绿** | **EXIT=0** ✅ 族根支非空断言 |
| v | `isRetiredRef` 恒 false + 注入 (i) | 绿 | **EXIT=0** ✅ 整体非空断言 |

扩面到 `tests/` 后补的两条（同日第二轮）：

| # | 注入 | 期望 | 实测 |
|---|---|---|---|
| vi | `tests` 面改成不存在的目录（面塌成空集） | 红 fail-closed | **EXIT=1** ✅ |
| vii | 移除 `tests` 面 + 把 tests 里的引用改回退役号 | 绿（漏掉） | **EXIT=0** ✅ 扩面是真断言，不是装饰 |

⚠️ (vii) 里我用 `git checkout -- <file>` 做 restore，**把自己对该测试文件的修改一并冲掉了**
（那时改动还没 commit）。已复检并重新落上 —— 记在这里：注入用的 restore 要用备份，
⛔ 不要对「还有未提交改动」的文件用 `git checkout --`。

每次 patch 均确认「命中数」符合预期后才读退出码 —— 排除「patch 没打上、绿灯毫无意义」那类假成功。
每次注入后 restore 并复跑 baseline EXIT=0，且 `grep -c "注入："` = 0（零残留）。

**单测本身也做了变异检验**（测试可能是同义反复，绿不代表有鉴别力）：

| 变异 | 结果 | 红的是哪条 |
|---|---|---|
| 去掉族根那一支 | 恰好 1 条红 | `isRetiredRef > 族根退役 + 自身无活定义 → 报` |
| 去掉 `reg.ids.has(id)` 活定义守卫 | 恰好 1 条红 | `isRetiredRef > ⚠️ 族根虽退役，但该号自身仍有活定义 → 不报` |

两次都是**精确一条**、且正是对位那条，不是一片连坐。

## 4b · 实施中被实测推翻的一个设计（记下来，别下轮重犯）

第一版闸跑出来立刻报 **6 条**，其中 5 条是**我自己刚写的说明性注释** —— 它们必须点名那个退役号
才能把「这里以前写错了」讲清楚。这与 §2 里「叙述 vs 活断言不可词法区分」是同一个坑，只是从另一侧撞上来。

⛔ 没走豁免表（触发器 K：豁免表空着是终态），⛔ 没加「叙述标记」启发式（本仓已实测否决三次）。
改用本文件**自己第 20 行早就有的那条纪律**并把它显式化：

> **闸脚本里 `§` 是活指针，裸号是历史提及。** 要讲「某号已退役」，写裸号（`M49.1`），⛔ 不加 `§`。

这不是绕闸：加了 `§` 就是在声明「去这儿读规则」，而那儿没有规则。收口后闸绿、零豁免。

## 5 · 验收

全部亲跑（退出码**先存变量再读**，⛔ 不经管道）：

| 项 | 结果 |
|---|---|
| `vitest run tests/stale-anchors.test.ts` | **30 passed** EXIT=0（新增 9 条，含 §4 i–iv 的等价断言） |
| `node scripts/audit-stale-anchors.mjs` | EXIT=0 · coverage `retired-id 89` / TOTAL 618 |
| `node --check` ×3（stale-anchors / binding-fidelity / conformance） | 全 EXIT=0 |
| `audit:rule-inventory` · `audit:artifact-routing` | 全 EXIT=0 |
| `pnpm test` 全量 | **1920 passed / 15 skipped** EXIT=0 |
| `grep -c "§M49.1"` 两个闸脚本 | 各 **0** |
- ⛔ 全程禁 `--no-verify`；无冲突 auto-merge 不跑 pre-commit ⇒ 闸自己补跑
- push 后 `git ls-remote` **逐个远端**核 SHA（`origin` 挂了 gitea + github 两个 push URL，EXIT=1 ≠ 没推上）

## 6 · 不在本轮

- Legacy ID Map 里其余退役号（M10/M25/M26/M27/M28/M30/M34/M23.13）在散文层的引用治理 —— 属散文层，
  本轮判据刻意不铺到散文。
- `figma-technical-reference.md` 的 Q 编号纳入 inventory / 路由闸 —— 见 §2 撤回理由。
- ⛔ 清零期不新增 backlog entry，故上面两项**只记在本计划**，不进 backlog。

---

## 7 · §6 第一项的后续：量完了，⛔ 不铺闸，但抓出 1 处真缺陷（2026-08-21 同日）

§6 把「其余退役号在散文层的引用治理」留成待判。**先量再判**，用闸自己的
`collectRetiredIds` / `isRetiredRef` / `resolveRuleRef`（⛔ 不复制实现）铺到全仓 399 份 `.md`。
每支探针都带正负两条对照，对照不过就 `exit 2`、数字一律不采信。

### 7a · 测量结果 —— 散文层假阳性率 97%

| 层 | 判据命中 | 真缺陷 | 假阳性 |
|---|---|---|---|
| 活散文 | 1（`mockup-conventions.md:794` 的 `原 §M23.15`）| 0 | 1 |
| 历史记录层（plans / specs / ledger / _archive / .changeset）| 33 | 0 | 33 |
| **合计** | **34** | **0** | **34 ⇒ 100%** |

逐号明细更关键 —— **§6 点名的 8 个号里，7 个在散文层根本没有 `§` 引用**，无可治：

| 退役号 | 现行落点 | 同号活定义 | 活散文 §引用 |
|---|---|---|---|
| M25 M26 M27 M28 M30 M34 M23.13 | §C2/§C3/§I1/§I2/M32+M35/§I3/§M23.12 | 无 | **0** |
| M10 | §C1（M-COLOR）| **domain-tvu.md**（List row Inactive，毫不相干）| 1 |
| M49 | §M52 / §M52.1–.4 | **design-process.md**（Design Quality Contract）| 9 |
| M23.15 | §M23.14 (B) | 无 | 1（`:794`，合法叙述）|
| M49.1（族根退役）| — | — | **0**（本轮已收口）|

⇒ 判据铺到散文会产出 34 条 findings、真缺陷 0 条。**与 §2 那条 Q 编号闸同形（触发器 K 该退档）
⇒ ⛔ 不上散文层闸**，`RETIRED_SCAN_SURFACES` 维持代码层两面不动。

### 7b · 但测量本身抓出 1 处真缺陷（已修）

`docs/working-principles.md:145`：

> 单色非品牌图标 path fill 必须绑 `Color Type/Icon/Default` Variable，不写 hex literal
> ——详见 `docs/internal/mockup-conventions.md` **§M10**

四条硬证据说明它是**活的悬空指针**，不是历史叙述：

1. `mockup-conventions.md` **没有 M10 heading**（正对照 M52 有 ⇒ 不是命令假 false）；
2. 退役表 `:3162` 登记 `M10 → §C1（M-COLOR）`；
3. `:306` 的 C1 标题就是「Brand / Token color paint 必须绑 Color Variable，不写 hex literal（原 M10）」
   —— 正是那句话描述的规则；
4. 掩住它的同号活规则在 **`domain-tvu.md`**（List row Inactive），而那句话**点名了 mockup-conventions.md**
   ⇒ 同号根本不适用。

与 `:794` 的分野是**语义而非词法**：`:794` 叙述改号并说明去处，`:145` 不叙述、直接指路。
⇒ 改为 `§C1（M-COLOR 下；…该号已于 2026-05-26 退役，见该文件 §Legacy ID Map）`，保留 2026-05-09 的历史号留痕。

⚠️ **锚点闸单靠自己永远抓不到它**：实测 `resolveRuleRef({type:'id',id:'M10'})` = **true** ——
domain-tvu 的同号活规则把它吸收了，与 §M49.1 被 design-process §M49 吸收**完全同形**。
真正找到它的是「退役表判据 + 人读语义」，不是解析器。
且 `working-principles.md` **不在任何闸的 `SCAN_FILES` 里** —— 补跑扫描面外那一遍
（116 条 §引用、悬空 0 条）才是发现它的动作。

### 7c · 本轮验证过、以后照用的两个做法（不只记教训）

- **「用闸自己的判据函数扫没纳入扫描面的地方」是真能出货的动作**，不是形式主义：本轮它直接产出
  §7b 那处真缺陷，而闸自己报绿。
- **先量假阳性率再决定铺不铺**：34 : 0 这个比一出来，「散文层要不要上闸」就不用再讨论了。
  ⛔ 不要先写闸再看红多少。
