# INFRA-F58 元层 SoT dedup + 规则清单单一真源闸 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 把 [[INFRA-F58]] 残余的「元层 SoT 小型 dedup」六小项做完，并给其中唯一有功能后果的一类（规则清单被多处枚举 → 必漂 → scoped-load 漏规则）配一条机械闸，让修完的状态不能无声漂回来。

**Architecture:** 先建闸、让它在**现状**上直接红（现状本身就是故障样本，不必人工造），再逐项 dedup 把它修绿。闸沿用仓库最新的可测闸范式（`scripts/audit-layout-tokens.mjs`：纯函数 export + `IS_MAIN` guard + vitest 单测 + CLI 故障注入）。清单类真源统一成「目标文件自己头部的唯一 inventory，别处只放指针」——即 owner 2026-07-31 那句裁定的形态：**一个值只有一处定义，多处引用它。**

**Tech Stack:** Node ESM (`.mjs`) audit 脚本 · vitest · husky pre-commit（L4）· `prepublishOnly`（L5）· 纯 Markdown 编辑

## Global Constraints

- **不碰 `.css` / `.vue`**：本 plan 全部产出物是 `.md` + 一个 `.mjs` + 一个 `.test.ts`。视觉门（`VISUAL_COMMIT_APPROVED=1`）不适用，也不得自设。
- **不碰 Figma、不碰 `figma-data/`**（AGENTS 硬规则 #1）。
- **活源优先**（AGENTS 硬规则 #10）：所有清单内容从目标文件的**实际标题**派生，禁止照抄任何现有镜像里的枚举——现有 5 处镜像已实测全部 stale。
- **entry 里的行号是二手数**：`backlog.md` §INFRA-F58 残余 worklist 写的 `AGENTS §398-417` / `AGENTS §312-316` 均已漂移，一律按活源重新定位。
- **A7 例外**：`working-principles.md` 决策 Log 里 **F25 那条是活 rationale，勿删**（entry 显式标注）。
- **commit 纪律**：`git commit -F <msgfile> -- <逐条内联路径>`，提交后紧跟 `git reset -- <同一批路径>`。push 落地只信 `git ls-remote`。
- **闸类改动的验收**：先造故障看它变红 → 再反向验单测非空过 → 再验空/畸形输入 fail closed。PASS 本身不是证据。
- **豁免表判据**：若不得不开豁免，必须**具名 + 带日期 + 带修法方向**，且配 shrink-only 判据（不再命中 → 闸自己 FAIL 要求删行）。**目标是豁免表为空**。

---

## 现状事实（本 plan 的依据 · 已对活源实测 2026-08-03）

### 六小项活源核对结果

| 项 | entry 原描述 | 实测 | 性质 |
|---|---|---|---|
| **D3** | 「AGENTS M-number 3 处枚举」 | **5 处镜像 · 4 种不同内容 · 全部对活源 stale** | **活缺陷**（scoped-load 漏规则） |
| **C1** | 「session-handoff/multi-session 第二 onboarding 链路→指针化 STATUS」 | `docs/session-handoff.md` = 2026-04-28 化石，**自带唤醒词** + 9 文件清单（**0 份 L-core**）+ stale 基线/阻塞/阶段 | **活缺陷**（绕过 Onboarding Gate） |
| **C2** | 「AGENTS §398-417 ↔ meta-rules §4 dup」 | 真重复，但实际位置 = **AGENTS 371-394** ↔ **meta-rules 117-139**（398-417 是 Sprint Self-Audit，归属已标对） | 低杠杆 dedup |
| **D1** | 「目标/能力 4 处」 | 4 处确认：`PROJECT_GOAL.md:10`(SoT) · `AGENTS.md:97` · `PROJECT_MAP.md:10` · `STATUS.md:56` | 低杠杆 dedup |
| **D2** | 「颜色规则 4 处」 | **6 处**：`AGENTS.md:145`(SoT,带 enforcement 列) · `PROJECT_MAP.md:94` · `working-principles.md:30` · `code-conventions.md:253`(已指针) · `mockup-conventions.md:857/2420`(引用) | 低杠杆 dedup |
| **A7** | tombstone | `working-principles.md:125` §决策 Log 的 `6.2`–`6.8` 全是早已 ship 的阶段计划 | 低杠杆 dedup |

### D3 的量化损失（活源分母 vs 5 处镜像）

活源（H2 标题实测）：`docs/internal/design-process.md` **25 条** · `docs/internal/domain-tvu.md` **12 条**。

> ⚠️ **执行期订正 ①：这里原写 26，错的。** 那个数来自 `grep -c '^## '`，**没剥代码围栏** —— 第 858 行
> `## User Journey Map — <Feature Name>` 落在 857–867 的 ```markdown 围栏里，是 report 模板不是 section。
> 闸的 `extractHeadingIds` 剥围栏后得 25，把我自己的二手数纠正了。**教训与 [[entry-restatement-is-secondhand]] 同型：
> 我一边写着「entry 里的数字是二手的」，一边自己又用了一个没验的 grep 数。**

5 处镜像：

| 位置 | 枚举内容 | 对活源漏 |
|---|---|---|
| `AGENTS.md:130` | M22 / Pre-Phase 0 / **Stage 0.5** / M11 / M14 / M15 / M16 / M21 / M6 / Lazy Reference Loading | 漏 16/26（含 **Phase 0**） |
| `AGENTS.md:604` | M22 / Pre-Phase 0 / M11 / M14 / M15 / M16 / M21 / M6 | 漏 18/26（含 Stage 0.5 + Phase 0 + Lazy Reference Loading） |
| `code-conventions.md:49` | M22 / Pre-Phase 0 / **Phase 0** / M11 / M14 / M15 / M16 / M21 / M6 | 漏 16/26（含 **Stage 0.5**） |
| `mockup-conventions.md:81` | 同上 | 漏 16/26 |
| `mockup-conventions.md:193` | M22 / Pre-Phase 0 / Stage 0.5 / M21 / Library-First + Evidence Discipline（M2）/ M6 / M11 / M14 / M15 / M16 / Lazy Reference Loading | 漏 15/26 |
| `AGENTS.md:605` · `code-conventions.md:50` · `mockup-conventions.md:82` · `mockup-conventions.md:195` | M3 / M4 / M5 / M7 / M8 / M9 | domain-tvu 漏 **6/12**（M10 / M11 / M12 / M13 / M17 + Persona 表） |

**五处一致漏掉的真实 section**（一处都没列）：`M48 起手强制 Gate` · `M49 Design Quality Contract Gate` · `M50 One-Page Design Kickoff Packet` · `M51 Change Control` · `M-PRD-EG` · `M-CARDINALITY` · `M-IA-FIRST` · `M-LIFECYCLE-CRUD` · `Convention Priority Hierarchy` · `Scope Handling` · `Delivery Cleanup Gate` —— 其中 M48/M49/M50 被 `design-process.md` 自己的 §完整产品需求设计流程图标为 mandatory。

**为什么这不是洁癖**：这两份文件在 mockup 任务里是 mandatory 全读（AGENTS §必读链路 7-10），全读场景下漂移无害。受害面是 **`design-process.md` §Lazy Reference Loading 的 scoped-load** —— 按任一份镜像清单做按需加载，都会漏掉一半以上真实存在的规则（含标着「起手强制 Gate」的 M48）。

### C1 的具体形态

`docs/session-handoff.md`（142 行）含四类内容，处置各不相同：

| 段 | 内容 | 处置 |
|---|---|---|
| §唤醒词 + §收到唤醒词后的标准动作 | 唤醒词 `继续 TVU handoff` → 读 9 文件（**无 FIGMA_SOT / 无 STATUS**）+ 跑 2 个 audit + 4 项输出 | **指针化**：唤醒词保留，动作改成走 STATUS §起手必读链路 |
| §当前最新基线（2026-04-28） | `17 approved pages` / `105 tests` / `36 matched` / Phase 6.x 进度 | **删**（版本/状态真源 = STATUS + tracker；历史在 git log 与 STATUS-CHANGELOG） |
| §当前阶段 / §当前阻塞点 / §下一步默认 | `Path B 6.3 → v0.1 上线` / `Popup Box 进 v0.1 还是 v0.2` | **删**（同上） |
| §必守规则（图标体系 / docs shell / Tooltip / 工作纪律） | 逐条判：部分已升成机械闸，部分仍是活规则 | **逐条核，勿盲删**；已有闸的改指闸，仍活且无别处登记的搬去真源文件 |

---

## File Structure

| 文件 | 责任 | 动作 |
|---|---|---|
| `scripts/audit-rule-inventory.mjs` | 新闸：规则清单/起手链路的单一真源判据（S1–S6） | **Create** |
| `tests/audit-rule-inventory.test.ts` | 新闸单测（must-fire / must-not-fire / fail-closed） | **Create** |
| `package.json` | 注册 `audit:rule-inventory` + 挂 `prepublishOnly` | Modify |
| `.husky/pre-commit` | 条件 gate 块（staged 命中注册文档或闸自身时触发） | Modify |
| `docs/internal/design-process.md` | 头部加**唯一 inventory**（26 条，活源派生） | Modify |
| `docs/internal/domain-tvu.md` | 头部加**唯一 inventory**（12 条，活源派生） | Modify |
| `AGENTS.md` | D3 两处降指针 · D1 §项目一句话 收成一句+指针 · A7 tombstone | Modify |
| `docs/internal/code-conventions.md` | D3 两处降指针 | Modify |
| `docs/internal/mockup-conventions.md` | D3 四处降指针 | Modify |
| `docs/session-handoff.md` | C1 指针化 + 删 stale 段 | Modify |
| `docs/multi-session-collaboration-rules.md` | C1b handoff 模板「起手必读」改指 STATUS + 只列增量 | Modify |
| `docs/meta-rules.md` | C2 §4 两个行为约束子表降指针 | Modify |
| `docs/PROJECT_MAP.md` | D1 / D2 镜像降指针 | Modify |
| `docs/working-principles.md` | D2 判定 + A7 决策 Log tombstone（**F25 勿删**） | Modify |
| `docs/internal/backlog.md` | F58 entry 残余 worklist 更新 | Modify |
| `docs/STATUS.md` | Last updated + 当日摘要 | Modify |

---

## Task 1: 规则清单单一真源闸（先建闸，让它在现状上红）

**Files:**
- Create: `scripts/audit-rule-inventory.mjs`
- Create: `tests/audit-rule-inventory.test.ts`
- Modify: `package.json`（scripts 段）
- Modify: `.husky/pre-commit`

**Interfaces:**
- Produces（Task 2 / 3 / 4 依赖这些名字）：
  - `export const INVENTORIES` — 注册表，每条 `{ id, target, level, markerId, pointerScanExts }`
  - `export const ONBOARDING_AUTHORITIES` — 起手链路的两个合法真源（STATUS §起手必读链路 · AGENTS §Mockup 任务额外必读）
  - `export const DURABLE_ONBOARDING_DOCS` — S6 的扫描面（**只含耐久规则文档，不含 plans / handoffs**）
  - `export function extractHeadingIds(mdText, level)` → `string[]`（活源分母）
  - `export function extractInventory(mdText, markerId)` → `{ found: boolean, ids: string[] }`
  - `export function findEnumerationMirrors(mdText, targetBasename, knownIds, minIds)` → `{ line, text, ids }[]`
  - `export function findSelfDeclaredChains(mdText)` → `{ line, text, paths }[]`
  - `export function evaluate({ inventories, mirrors, chains, exemptions })` → `{ failures, coverage }`

**判据（每条都要能单独红）**

| # | 判据 | 阻塞 |
|---|---|---|
| **S1** | 活源分母非空：注册目标文件的 H2 标题解析出 **0 个 id** → FAIL（文件被改名/重构导致闸变盲，fail closed） | ✅ |
| **S2** | inventory 存在：注册目标文件里必须有 `<!-- rule-inventory:<id> -->` 标记块 | ✅ |
| **S3** | inventory **等于**活源：缺一个 → FAIL 并点名；多一个（phantom）→ FAIL 并点名 | ✅ |
| **S4** | 别处不得再枚举：任何注册的 `.md` 里，**同一行既链接到目标文件、又出现 ≥3 个已知 id** → FAIL（除非行内 `<!-- rule-inventory-ok: <理由+日期> -->`） | ✅ |
| **S5** | 豁免 shrink-only：登记的豁免不再命中 → FAIL 要求删行 | ✅ |
| **S6** | 起手链路只许两个真源：`DURABLE_ONBOARDING_DOCS` 里出现「起手必读式枚举」（framing 关键词 + ≥3 个 `.md` 路径）且不在 `ONBOARDING_AUTHORITIES` → FAIL | ✅ |

**为什么不折进现有 `audit-doc-sync.mjs`**（写进脚本头注释）：那个脚本是顶层即执行（无 `main()`/`IS_MAIN` guard），`import` 它就会跑，因此现在**没有也无法有单测**；把新判据折进去要先重构一条已在 L4+L5 的活闸，风险与收益不对等。新脚本沿用 `audit-layout-tokens.mjs` 的可测范式。

- [ ] **Step 1: 写失败测试（活源分母 + inventory 一致性）**

```ts
// tests/audit-rule-inventory.test.ts
import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  INVENTORIES,
  extractHeadingIds,
  extractInventory,
  findEnumerationMirrors,
  findSelfDeclaredChains,
  evaluate,
} from '../scripts/audit-rule-inventory.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const read = (rel: string) => readFileSync(resolve(REPO_ROOT, rel), 'utf-8')

describe('extractHeadingIds — 活源分母', () => {
  it('H2 标题取分隔符前的首段作 id（— / ： / （ 三种分隔）', () => {
    const md = [
      '## M22 — User Design Intent Acknowledgment（前置 Gate）',
      '## Pre-Phase 0：Product Definition Gate',
      '## Lazy Reference Loading',
    ].join('\n')
    expect(extractHeadingIds(md, 2)).toEqual(['M22', 'Pre-Phase 0', 'Lazy Reference Loading'])
  })

  it('只收指定层级：H3 子条目不进分母（M11.1 不与 M11 并列）', () => {
    const md = '## M11 — Self-audit Discipline\n### M11.1 — Probe-based 自查\n'
    expect(extractHeadingIds(md, 2)).toEqual(['M11'])
  })

  it('代码围栏里的 ## 不算标题', () => {
    const md = '```\n## M99 — 假的\n```\n## M3 — 真的\n'
    expect(extractHeadingIds(md, 2)).toEqual(['M3'])
  })

  it('真仓库两份目标文件分母都非空（S1 回归钉）', () => {
    for (const inv of INVENTORIES) {
      expect(extractHeadingIds(read(inv.target), inv.level).length).toBeGreaterThan(0)
    }
  })
})

describe('extractInventory', () => {
  it('取标记块里的 id 列表（斜杠分隔、去空白）', () => {
    const md = '<!-- rule-inventory:design-process -->\n> 清单：M6 / M11 / Phase 0\n<!-- /rule-inventory -->\n'
    expect(extractInventory(md, 'design-process')).toEqual({ found: true, ids: ['M6', 'M11', 'Phase 0'] })
  })

  it('没有标记块 → found:false（S2 fail closed，不是当成空清单通过）', () => {
    expect(extractInventory('# 无标记\n', 'design-process')).toEqual({ found: false, ids: [] })
  })
})

describe('findEnumerationMirrors — S4', () => {
  const ids = ['M22', 'M11', 'M14', 'Phase 0']

  it('同行既链接目标文件又列 ≥3 个 id → 命中', () => {
    const md = '| [`design-process.md`](./design-process.md) | 通用 process 规则（M22 / M11 / M14）| 所有任务 |'
    expect(findEnumerationMirrors(md, 'design-process.md', ids, 3)).toHaveLength(1)
  })

  it('纯指针（零 id）不命中', () => {
    const md = '| [`design-process.md`](./design-process.md) | 通用 process 规则（清单见该文件头）| 所有任务 |'
    expect(findEnumerationMirrors(md, 'design-process.md', ids, 3)).toEqual([])
  })

  it('1-2 个 id 的散文引用不命中（引用 ≠ 清单）', () => {
    const md = '`design-process.md` M22 / Phase 0 同理，无需新增。'
    expect(findEnumerationMirrors(md, 'design-process.md', ids, 3)).toEqual([])
  })

  it('列了 id 但没链接目标文件 → 不命中（避免撞到别的文件自己的规则表）', () => {
    const md = '| Monitoring | M22 / M11 / M14 | 触发 |'
    expect(findEnumerationMirrors(md, 'design-process.md', ids, 3)).toEqual([])
  })

  it('行内豁免标记放行', () => {
    const md =
      '| [`design-process.md`](./design-process.md) | M22 / M11 / M14 | x | <!-- rule-inventory-ok: 历史引用 2026-08-03 -->'
    expect(findEnumerationMirrors(md, 'design-process.md', ids, 3)).toEqual([])
  })
})

describe('findSelfDeclaredChains — S6', () => {
  it('framing 关键词 + ≥3 个 .md 路径 → 命中', () => {
    const md = [
      '## 收到唤醒词后的标准动作',
      '1. 先阅读：',
      '   - `docs/a.md`',
      '   - `docs/b.md`',
      '   - `docs/c.md`',
    ].join('\n')
    expect(findSelfDeclaredChains(md)).toHaveLength(1)
  })

  it('只提 1 个路径的指针不命中', () => {
    const md = '## 起手必读\n走 [`docs/STATUS.md`](./STATUS.md) §起手必读链路。\n'
    expect(findSelfDeclaredChains(md)).toEqual([])
  })

  it('无 framing 关键词的普通文件清单不命中', () => {
    const md = '## 产出物\n- `docs/a.md`\n- `docs/b.md`\n- `docs/c.md`\n'
    expect(findSelfDeclaredChains(md)).toEqual([])
  })
})

describe('evaluate — 判据编码', () => {
  const okInv = { id: 'x', target: 't.md', liveIds: ['A', 'B'], inventory: { found: true, ids: ['A', 'B'] } }

  it('全绿时零 failure', () => {
    expect(evaluate({ inventories: [okInv], mirrors: [], chains: [], exemptions: [] }).failures).toEqual([])
  })

  it('S1: 活源零 id → fail closed', () => {
    const f = evaluate({
      inventories: [{ ...okInv, liveIds: [] }],
      mirrors: [], chains: [], exemptions: [],
    }).failures
    expect(f.map((x) => x.code)).toContain('S1')
  })

  it('S2: 没有 inventory 标记块 → 红', () => {
    const f = evaluate({
      inventories: [{ ...okInv, inventory: { found: false, ids: [] } }],
      mirrors: [], chains: [], exemptions: [],
    }).failures
    expect(f.map((x) => x.code)).toContain('S2')
  })

  it('S3: 缺项与 phantom 各自点名', () => {
    const f = evaluate({
      inventories: [{ ...okInv, inventory: { found: true, ids: ['A', 'Z'] } }],
      mirrors: [], chains: [], exemptions: [],
    }).failures
    const s3 = f.filter((x) => x.code === 'S3')
    expect(s3).toHaveLength(2)
    expect(s3.some((x) => /缺.*\bB\b/.test(x.message))).toBe(true)
    expect(s3.some((x) => /phantom.*\bZ\b/.test(x.message))).toBe(true)
  })

  it('S5: 登记但零命中的豁免 → 红并要求删行', () => {
    const f = evaluate({
      inventories: [okInv], mirrors: [], chains: [],
      exemptions: [{ file: 'gone.md', line: 1, reason: 'x 2026-08-03', hit: false }],
    }).failures
    expect(f.map((x) => x.code)).toContain('S5')
  })
})
```

- [ ] **Step 2: 跑测试确认失败**

Run: `pnpm vitest run tests/audit-rule-inventory.test.ts`
Expected: FAIL —— `Failed to resolve import "../scripts/audit-rule-inventory.mjs"`

- [ ] **Step 3: 实现 `scripts/audit-rule-inventory.mjs`**

按 `scripts/audit-layout-tokens.mjs` 范式：文件头写 why + 覆盖面声明 + 「为什么不折进 audit-doc-sync」；纯函数全部 `export`；`main()` 印覆盖面（每份 inventory 的 id 数 / 扫描面文件数 / 豁免数与命中数 / S6 扫描面）；末尾 `IS_MAIN` guard。

关键实现约束（踩过的坑，别重犯）：
- `extractHeadingIds` 必须**先剥掉 ``` 代码围栏**再匹配标题（`design-process.md` §完整流程图里有围栏内容）。
- id 切分用 `/^#{level}\s+(.+?)(?:\s*[—:：(（]|$)/`，取 group 1 后 `.trim()`；不要按空格切（`Pre-Phase 0` / `Lazy Reference Loading` 含空格）。
- S4 的 id 匹配必须**整词**（`M1` 不能命中 `M11`）：对每个已知 id 生成 `(?<![\w.-])<escaped>(?![\w.-])`。
- 豁免标记与 de-mirror 闸保持同形态（行内 HTML 注释），但用独立关键字 `rule-inventory-ok`，别复用 `de-mirror-ok`（两闸语义不同，混用会互相放行）。

- [ ] **Step 4: 跑测试确认通过**

Run: `pnpm vitest run tests/audit-rule-inventory.test.ts`
Expected: PASS，全部用例绿。

- [ ] **Step 5: 注册 CLI + 挂闸**

`package.json`：
```json
"audit:rule-inventory": "node scripts/audit-rule-inventory.mjs",
```
并加进 `prepublishOnly` 链（L5，紧邻 `audit:doc-sync`）。

`.husky/pre-commit`（L4，仿 `doc-de-mirror` 块形态，注册文档 + 闸自身 staged 时触发）：
```sh
echo "▶ pre-commit: rule-inventory (规则清单/起手链路单一真源 — INFRA-F58)"
if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/internal/design-process\.md|docs/internal/domain-tvu\.md|docs/internal/code-conventions\.md|docs/internal/mockup-conventions\.md|docs/session-handoff\.md|docs/multi-session-collaboration-rules\.md|AGENTS\.md|docs/PROJECT_MAP\.md|scripts/audit-rule-inventory\.mjs)'; then
  pnpm run audit:rule-inventory
else
  echo "   ✓ no rule-inventory-relevant files staged, skipped"
fi
```

- [ ] **Step 6: 在现状上实跑 —— 必须红（这是「先造故障」的等价物：现状即故障样本）**

Run: `pnpm audit:rule-inventory`
Expected: **exit 1**，且输出必须逐条点名：
- S2 两条（`design-process.md` / `domain-tvu.md` 都还没有 inventory 标记块）
- S4 命中 `AGENTS.md:130` / `AGENTS.md:604` / `code-conventions.md:49` / `mockup-conventions.md:81` / `mockup-conventions.md:193`（design-process 侧）+ 对应 4 处 domain-tvu 侧
- S6 命中 `docs/session-handoff.md`（9 文件自建链路）

⚠️ 若某条判据在现状上**没**红，说明它的谓词写错了 —— 停下修谓词，别改现状去迁就闸。

- [ ] **Step 7: 空/畸形输入 fail closed 三问**

```bash
pnpm audit:rule-inventory --                    # 空参数透传，不得把 "--" 当参数
node scripts/audit-rule-inventory.mjs extra-arg # 多余参数
```
Expected: 两条都不得因参数形态而 exit 0；判据结果与不带参数时一致。

再验闸变盲的情形：临时把 `docs/internal/domain-tvu.md` 改名 → 期望 **S1/S2 红且明说「读不到目标文件，fail closed」**，而不是静默 skip。复原后 `git status` 干净。

- [ ] **Step 8: Commit**

```bash
printf '%s\n' "feat(rule-inventory): 规则清单/起手链路单一真源上闸" "" \
  "现状即故障样本：design-process 26 个 H2、domain-tvu 12 个，5 处镜像最多列 10 个，" \
  "M48/M49/M50/M51 等 11 个真实 section 一处都没列 —— 按镜像做 scoped-load 会漏掉标着" \
  "「起手强制 Gate」的规则。闸落地时刻即红，dedup 由后续 commit 修绿。" > /tmp/msg-rule-inventory.txt
git commit -F /tmp/msg-rule-inventory.txt -- \
  scripts/audit-rule-inventory.mjs tests/audit-rule-inventory.test.ts package.json .husky/pre-commit
git reset -- scripts/audit-rule-inventory.mjs tests/audit-rule-inventory.test.ts package.json .husky/pre-commit
```

⚠️ 这个 commit 会让 pre-commit 自己跑新闸并**红**。用 `--no-verify` 并在 message 里写明理由（闸落地时刻按设计必红，绿由 Task 2 提供），这是本 plan 唯一允许 `--no-verify` 的一次。

---

## Task 2: D3 — inventory 单一化，把闸修绿

**Files:**
- Modify: `docs/internal/design-process.md`（头部，`---` 之前）
- Modify: `docs/internal/domain-tvu.md`（头部，`---` 之前）
- Modify: `AGENTS.md:130-131` + `AGENTS.md:604-605`
- Modify: `docs/internal/code-conventions.md:49-50`
- Modify: `docs/internal/mockup-conventions.md:81-82` + `:193` + `:195`

**Interfaces:**
- Consumes: Task 1 的 `INVENTORIES` 注册表（`markerId` = `design-process` / `domain-tvu`）

- [ ] **Step 1: 从活源生成两份 inventory 内容（不手抄）**

```bash
node -e "
const {readFileSync}=require('fs');
for (const f of ['docs/internal/design-process.md','docs/internal/domain-tvu.md']) {
  const ids=readFileSync(f,'utf8').replace(/^\`\`\`[\s\S]*?^\`\`\`/gm,'')
    .split('\n').filter(l=>/^## /.test(l))
    .map(l=>l.replace(/^## /,'').split(/\s*[—:：(（]/)[0].trim());
  console.log(f, ids.length); console.log(ids.join(' / '));
}"
```
把 stdout 逐字用进下一步 —— **不准凭印象补漏或改名**。

- [ ] **Step 2: 在 `design-process.md` 头部插入唯一 inventory**

插在文件头 quote 块之后、第一个 `---` 之前：

```markdown
<!-- rule-inventory:design-process -->
> **本文件规则清单（唯一真源 · 机械核 `pnpm audit:rule-inventory`）** —— scoped / lazy 加载按本清单挑，别处的枚举一律已降为指针：
>
> <Step 1 stdout 的 26 条，逐字>
<!-- /rule-inventory -->
```

- [ ] **Step 3: `domain-tvu.md` 同形态插入（12 条）**

markerId 用 `domain-tvu`。

- [ ] **Step 4: 5 处镜像降纯指针**

每处把括号里的 M-number 枚举替换成「清单见该文件头」，链接保留。例：

```markdown
7. **[`docs/internal/design-process.md`](./docs/internal/design-process.md)** — 通用 process 规则（**完整清单见该文件头 `rule-inventory` 块**；此处不再枚举——曾 5 处各枚举一套、4 种内容、全部 stale）
```

⚠️ `mockup-conventions.md:193` 那处原文多出 `Library-First + Evidence Discipline（M2）` —— 活源里该 section 标题是 `## Library-First + Evidence Discipline`（**无 M2**）。降指针时顺带记下：`M2` 是镜像里发明的编号，活源无此 id。

- [ ] **Step 5: 闸转绿**

Run: `pnpm audit:rule-inventory`
Expected: **exit 0**，覆盖面行印出 `design-process: 26 ids` / `domain-tvu: 12 ids` / `豁免 0 条`。

- [ ] **Step 6: 反向验非空过 —— 三组精确注入**

每组改完实跑、断言**只**红对应那一条，然后复原并 `git diff --quiet` 确认逐字节还原：

```bash
# (a) inventory 删掉一个 id → 只 S3「缺」红且点名那个 id
# (b) inventory 加一个不存在的 id（如 M77）→ 只 S3「phantom」红且点名 M77
# (c) 在 AGENTS.md:130 把枚举写回去 → 只 S4 红且点名 AGENTS.md:130
```

- [ ] **Step 7: 全量单测 + 相关闸链**

Run: `pnpm vitest run` · `pnpm audit:rule-inventory` · `pnpm audit:doc-sync` · `pnpm audit:doc-de-mirror` · `pnpm audit:stale-anchors` · `pnpm audit:rule-load-map`
Expected: 全部 exit 0；`vitest` 数字逐字记录（对比基线 1124 passed / 11 skipped，只应增不应减）。

- [ ] **Step 8: Commit**

```bash
printf '%s\n' "fix(rule-inventory): 五处规则清单收成两处唯一 inventory，补回 11 个从未被列的 section" "" \
  "design-process 26 / domain-tvu 12 全部从活源 H2 派生；AGENTS/code-conventions/" \
  "mockup-conventions 的 9 处枚举降为纯指针。M2 是镜像里发明的编号，活源无此 id。" > /tmp/msg-d3.txt
git commit -F /tmp/msg-d3.txt -- \
  docs/internal/design-process.md docs/internal/domain-tvu.md AGENTS.md \
  docs/internal/code-conventions.md docs/internal/mockup-conventions.md
git reset -- docs/internal/design-process.md docs/internal/domain-tvu.md AGENTS.md \
  docs/internal/code-conventions.md docs/internal/mockup-conventions.md
```

---

## Task 3: C1 — session-handoff.md 化石链路指针化

**Files:**
- Modify: `docs/session-handoff.md`

- [ ] **Step 1: 逐条判 §必守规则 四组是否已有真源/机械闸**

对每条跑一次活源核对，把结论写进 commit message：

```bash
grep -rn "内联 SVG\|inline-svg" docs/internal/code-conventions.md | head
pnpm audit:component-no-inline-svg   # 图标体系 1
pnpm audit:icon-canonical-names      # 图标体系 3
pnpm audit:docs-single-theme         # docs shell 1 + Tooltip 1/2
```
判定：命中机械闸的 → 改成一行「已升机械闸，见 `pnpm audit:*`」；仍活且**别处没登记**的 → 搬去对应真源文件（`code-conventions.md` / `mockup-conventions.md`）再指针化；已被别处覆盖的 → 删。

⚠️ 「工作纪律 1-3」（Codex 不改 figma-data / cleanup dry-run / auto-suggest 状态）需先 grep AGENTS §Figma 库同步 与 §默认安全 vs 显式破坏性，确认已覆盖再删。

- [ ] **Step 2: 改写 §收到唤醒词后的标准动作**

唤醒词 `继续 TVU handoff` 保留（用户可能仍在用），动作改为：

```markdown
## 收到唤醒词后的标准动作

1. **走 [`docs/STATUS.md` §起手必读链路](./STATUS.md#起手必读链路)** —— L-core 四份全文 + 命中触发的 L-ref。
   该节是 onboarding 链路的**唯一 SoT**；本文件不再自列必读清单
   （2026-08-03：原先那张 9 文件清单里 0 份 L-core，会绕过 Onboarding Gate）。
2. 再按 [`docs/multi-session-collaboration-rules.md`](./multi-session-collaboration-rules.md) 判并行/隔离。
3. 输出：读到的事实 / 当前阻塞点 / 本轮先做哪层 / 为什么这样排 —— 状态真源是 STATUS.md，不是本文件。
```

- [ ] **Step 3: 删 stale 段**

删除 §当前最新基线（2026-04-28）· §当前阶段 · §当前阻塞点 / 待用户决策 · §下一步默认 四段，替换为一句：

```markdown
## 当前状态 / 阻塞 / 下一步

**不在本文件**。真源：[`docs/STATUS.md`](./STATUS.md)（状态 + 等 owner 拍板项）+
[`docs/internal/backlog.md`](./internal/backlog.md) §Active。
历史基线可 `git log docs/session-handoff.md` 回溯。
```

- [ ] **Step 4: S6 转绿 + 反向验**

Run: `pnpm audit:rule-inventory`
Expected: exit 0。

反向注入：把 9 文件清单临时写回 → 断言 **S6 红且点名 `docs/session-handoff.md`**；复原后 `git diff --quiet`。

- [ ] **Step 5: Commit**

```bash
printf '%s\n' "fix(session-handoff): 拆掉 2026-04-28 化石竞争链路" "" \
  "该文件自带唤醒词「继续 TVU handoff」，动作是读一张 9 文件清单 —— 里面 0 份 L-core," \
  "并把 17 approved pages / 105 tests / 「Popup Box 进 v0.1 还是 v0.2」当现状喂给 AI。" \
  "唤醒词保留，动作改指 STATUS §起手必读链路；stale 基线/阻塞/阶段四段删除。" > /tmp/msg-c1.txt
git commit -F /tmp/msg-c1.txt -- docs/session-handoff.md
git reset -- docs/session-handoff.md
```

---

## Task 4: C1b — handoff 模板不再生产竞争链路

**Files:**
- Modify: `docs/multi-session-collaboration-rules.md`（§Handoff 必须包含的段 + §反模式）

- [ ] **Step 1: 改模板的「起手必读」段**

```markdown
## 起手必读
**先走 [`docs/STATUS.md` §起手必读链路](./STATUS.md#起手必读链路)**（唯一 SoT）。
本段只列**本任务增量**：这次额外要读的 spec / plan / backlog entry 路径，绝对路径或仓库相对路径。
⛔ 不要在这里复制 L-core 清单 —— 每份 handoff 各抄一份 = 又一条会 stale 的竞争链路
（2026-08-03 实证：`session-handoff.md` 就是这么变成化石的）。
```

- [ ] **Step 2: §反模式 加一条**

```markdown
- ❌ Handoff 自列一整套 onboarding 必读清单（而非指向 STATUS §起手必读链路 + 只列增量）—— 清单会 stale，
  且给 AI 一条绕过 Onboarding Gate 的入口（`pnpm audit:rule-inventory` S6 机械核耐久文档面）
```

- [ ] **Step 3: 闸 + 单测**

Run: `pnpm audit:rule-inventory` · `pnpm vitest run tests/audit-rule-inventory.test.ts`
Expected: 均 exit 0。⚠️ 模板段本身含 framing 关键词但只 1 个路径 → 按 S6 谓词不应命中；若命中说明谓词过宽，回 Task 1 收窄，**别开豁免**。

- [ ] **Step 4: Commit**

```bash
printf '%s\n' "fix(handoff-template): 起手必读改指 STATUS，只列本任务增量" "" \
  "原模板要求每份 handoff 自列一张必读表 = 竞争链路的再生产装置；C1 那份化石就是这么来的。" > /tmp/msg-c1b.txt
git commit -F /tmp/msg-c1b.txt -- docs/multi-session-collaboration-rules.md
git reset -- docs/multi-session-collaboration-rules.md
```

---

## Task 5: C2 — meta-rules §4 行为约束降指针

**Files:**
- Modify: `docs/meta-rules.md:117-139`

**方向判定**：`AGENTS.md:371-394` 是**超集**（多了 task-type 矩阵接线 / Discovery-vs-Apply 的写法差异 / 独立 verify 三条），`meta-rules.md:124-139` 是子集。SoT 归属表把「AI 协作硬规则」判给 AGENTS.md → **保 AGENTS，meta-rules 降指针**。

- [ ] **Step 1: 保留 §4 的角色互换四条原则，两个子表换成指针**

```markdown
### 角色行为约束

**真源 = [`AGENTS.md` §executor 角色行为约束 / §plan owner 角色行为约束](../AGENTS.md)**（该处是超集：含
task-type 矩阵接线、Discovery-vs-Apply 的 prompt 写法差异、审产出必独立 verify 三条）。
本节此前镜像了一份子集 —— 已于 2026-08-03 降为指针（[[INFRA-F58]] C2）。
```

- [ ] **Step 2: 核对没有内容净损失**

```bash
diff <(sed -n '124,139p' docs/meta-rules.md) <(sed -n '371,394p' AGENTS.md)
```
逐条确认 meta-rules 侧每一句都在 AGENTS 侧有对应（**executor「不加料式评论、不发明新轨道/阶段/框架」这句在 AGENTS 侧措辞不同**——若确实缺，把它补进 AGENTS 而不是留在 meta-rules）。

- [ ] **Step 3: 闸 + Commit**

```bash
pnpm audit:rule-inventory && pnpm audit:doc-sync && pnpm audit:rule-load-map
printf '%s\n' "docs(meta-rules): §4 角色行为约束降指针到 AGENTS（超集侧）" > /tmp/msg-c2.txt
git commit -F /tmp/msg-c2.txt -- docs/meta-rules.md AGENTS.md
git reset -- docs/meta-rules.md AGENTS.md
```

---

## Task 6: D1 — 一句话目标四处收口

**Files:**
- Modify: `AGENTS.md:95-107`
- Modify: `docs/PROJECT_MAP.md:10`

**现状判定**：`STATUS.md:56` 已是「一句 + 指针」（合规，不动）；`PROJECT_MAP.md:10` 是一句话改写（基本合规，只需加指针）；`AGENTS.md:95-107` 是**全量镜像**（5 条能力 + 能力 3 的长注），是唯一要收的。

- [ ] **Step 1: `AGENTS.md` §项目一句话 收成一句 + 指针**

```markdown
## 项目一句话

把 **Figma 已发布 Library**（组件 / Token / 图标 / Styles）做成 **AI 与开发可消费的双向 Figma↔Code 桥**。

> **5 个能力的完整定义 + 各能力前置约束 + 能力 3 的边界分层，真源 = [`docs/PROJECT_GOAL.md`](./docs/PROJECT_GOAL.md) §一句话目标**。
> 能力**成熟度/当前状态** → [`docs/STATUS.md`](./docs/STATUS.md) §能力 × 成熟度表。本节不再镜像（[[INFRA-F58]] D1，2026-08-03）。
```

- [ ] **Step 2: `PROJECT_MAP.md:10` 句尾加指针**（`（定义真源见 PROJECT_GOAL §一句话目标）`）

- [ ] **Step 3: 闸 + Commit**

```bash
pnpm audit:doc-de-mirror && pnpm audit:doc-sync && pnpm audit:rule-inventory
printf '%s\n' "docs(agents): §项目一句话 从全量镜像收成一句 + 指针（D1）" > /tmp/msg-d1.txt
git commit -F /tmp/msg-d1.txt -- AGENTS.md docs/PROJECT_MAP.md
git reset -- AGENTS.md docs/PROJECT_MAP.md
```

⚠️ `audit:doc-sync` 的 `literal-count` 事实里可能登记了「5 个能力」之类的 pattern；若改动后报 `doc-sync-literal-nomatch`（pattern 零命中），说明我把它引用的那句删掉了 —— 修 `doc-sync-map.json` 的 `referencedIn` 指向新落点，**不要**把句子加回来凑闸。

---

## Task 7: D2 — 颜色规则六处定 SoT

**Files:**
- Modify: `docs/PROJECT_MAP.md:94`
- Modify: `docs/working-principles.md:30`（**先判再动**）

**判定**：SoT = `AGENTS.md:145` 硬规则 #4（唯一带 Enforcement 层级列 + 违反后果的那处）。

- `code-conventions.md:253` 已是「同 AGENTS 硬规则 #4」→ 合规，不动。
- `mockup-conventions.md:857 / 2420` 是**引用**（论证某处取值合规 / 列 review 时机）→ 合规，不动。
- `PROJECT_MAP.md:94` 是平行复述 → 降指针。
- `working-principles.md:30` 属「原则 4」正文的一行 —— ⚠️ working-principles 是「实现原则」的 SoT，硬规则 #4 与它是**同一事实的两个层**（规则 vs 原则）。**保留正文，但加一行指明 enforcement 真源在 AGENTS 硬规则 #4**，不要删（删了会让「原则 0-8」出现空洞）。

- [ ] **Step 1: `PROJECT_MAP.md:94` 降指针**

```markdown
2. **颜色硬编码是 bug** —— 真源 = [`AGENTS.md` 硬规则 #4](../AGENTS.md)（含 Enforcement 层级 L5 与违反后果）。
```

- [ ] **Step 2: `working-principles.md:30` 加 enforcement 指针**

```markdown
- 颜色硬编码 = bug，必须用 token（**enforcement 真源 = [`AGENTS.md` 硬规则 #4](../AGENTS.md)**：L5 `audit:no-hardcoded-design-tokens`）
```

- [ ] **Step 3: 实跑确认闸仍绿 + Commit**

```bash
pnpm audit:no-hardcoded-design-tokens && pnpm audit:doc-sync && pnpm audit:rule-inventory
printf '%s\n' "docs(d2): 颜色硬编码规则六处收口，SoT = AGENTS 硬规则 #4" "" \
  "PROJECT_MAP 平行复述降指针；working-principles 原则 4 保留正文但标明 enforcement 真源" \
  "（两者是规则 vs 原则的两层，删正文会让原则 0-8 出现空洞）。" > /tmp/msg-d2.txt
git commit -F /tmp/msg-d2.txt -- docs/PROJECT_MAP.md docs/working-principles.md
git reset -- docs/PROJECT_MAP.md docs/working-principles.md
```

---

## Task 8: A7 — tombstone 处置

**Files:**
- Modify: `docs/working-principles.md:125-140`（§决策 Log）

- [ ] **Step 1: 读全 §决策 Log，逐条判活/死**

Run: `sed -n '120,175p' docs/working-principles.md`

判据：`6.2`–`6.8` 是 2026-04/05 的阶段计划编号，全部早已 ship（`6.8 Button canonical 完成迁移` 在 v1.0.0 就锁 API 了）。**死的是「计划态措辞」，不是决策本身** —— 决策内容若仍是活 rationale（如某个 prop 为什么拆）必须留。

⛔ **F25 那条是活 rationale，勿删**（entry 显式标注）。动手前先 `grep -n "F25" docs/working-principles.md` 确认它的位置，避开。

- [ ] **Step 2: 把「阶段计划清单」形态改成「决策 + 落点」形态**

保留每条的决策内容与理由，去掉 `6.x` 阶段编号与「待做 / 计划」措辞，改成一行结论 + 指向 tracker 的排期真源。stale 的进度叙述删除（真源 = tracker §轨道 A 已完成）。

- [ ] **Step 3: 核 F25 未被动 + Commit**

```bash
grep -n "F25" docs/working-principles.md   # 必须仍在
git diff docs/working-principles.md | grep -n "^-.*F25" && echo "!! F25 被动了，回退" || echo "F25 完好"
pnpm audit:doc-sync && pnpm audit:rule-load-map
printf '%s\n' "docs(working-principles): 决策 Log 去掉 6.x 阶段计划态措辞，保留决策与 rationale" "" \
  "6.2-6.8 是 2026-04/05 的阶段编号、早已 ship；排期真源在 tracker。F25 条未动（活 rationale）。" > /tmp/msg-a7.txt
git commit -F /tmp/msg-a7.txt -- docs/working-principles.md
git reset -- docs/working-principles.md
```

---

## Task 9: 收尾 —— 全量验证 + entry/STATUS 更新 + push

**Files:**
- Modify: `docs/internal/backlog.md`（§INFRA-F58 残余 worklist）
- Modify: `docs/STATUS.md`（Last updated + 当日摘要）
- Modify: `docs/internal/STATUS-CHANGELOG.md`（prepend 旧摘要）

- [ ] **Step 1: 全量验证（数字逐字记录，不概括）**

```bash
pnpm vitest run                    # 基线 1124 passed / 11 skipped，只应增
pnpm audit:rule-inventory
pnpm audit:doc-sync
pnpm audit:doc-de-mirror
pnpm audit:stale-anchors
pnpm audit:rule-load-map
pnpm audit:status-consistency
pnpm audit:hard-rule-compliance
```
⚠️ `audit:status-consistency` 有存量红（Active 计数），只挂 `prepublishOnly` —— 若它红，先确认是**存量**那条而不是我新引入的。

- [ ] **Step 2: 更新 F58 entry 残余 worklist**

把「元层 SoT 小型 dedup」六小项标掉，并如实登记两条**订正**：D3 实际是 5 处/4 种内容/漏 11 个 section（不是「3 处枚举」）· C2/A7 的行号原本已漂。**残余里若还剩「待 owner 定 policy」与「规则三层化」两块，原样留着**（不属本轮）。

- [ ] **Step 3: 更新 STATUS**

顶部 `Last updated` 改 2026-08-03，旧摘要 prepend 进 STATUS-CHANGELOG，新摘要按 §编辑判据只写「还有人要做的事」+ 本轮留下的**状态性事实**（新闸的覆盖面与豁免表为空是终态；S6 只覆盖耐久文档面、不覆盖 plans/handoffs）。

- [ ] **Step 4: Commit + push + 用 `git ls-remote` 验落地**

```bash
printf '%s\n' "docs(f58+status): 元层 SoT dedup 六小项收口 + STATUS 当日摘要" > /tmp/msg-wrap.txt
git commit -F /tmp/msg-wrap.txt -- docs/internal/backlog.md docs/STATUS.md docs/internal/STATUS-CHANGELOG.md
git reset -- docs/internal/backlog.md docs/STATUS.md docs/internal/STATUS-CHANGELOG.md
git push
git ls-remote origin -h refs/heads/master   # 落地唯一判据
```

- [ ] **Step 5: Sprint 收尾 Self-Audit（AGENTS §协议，mandatory）**

跑 A/B/C 三类扫描 + D/E/F/G 四个 audit，主动输出 finding（不等 user 问）。

---

## Self-Review

**Spec coverage**：六小项 C1 / C1b / C2 / D1 / D2 / D3 / A7 各有对应 Task（C1 拆成 Task 3 + 4）；闸 = Task 1；收尾 = Task 9。✅

**Placeholder scan**：无 TBD / 无「适当处理」/ 每个代码步都有真实代码块。Task 3 Step 1 与 Task 8 Step 1-2 是**判定步**（逐条核活源后才知道删/搬/留），已写明判据与禁区（F25 勿删 / 工作纪律先核 AGENTS 覆盖），不是占位。

**Type consistency**：`extractHeadingIds` / `extractInventory` / `findEnumerationMirrors` / `findSelfDeclaredChains` / `evaluate` 五个名字在 Task 1 的 Interfaces、测试代码、Task 2 Step 6 注入验证里一致；`markerId` 值 `design-process` / `domain-tvu` 在 Task 1 注册表与 Task 2 Step 2/3 一致；判据码 `S1`–`S6` 在判据表、测试断言、Task 1 Step 6 期望输出里一致。

**已知风险 + 处置**：S6 谓词过宽会误伤合法的 handoff/plan → 扫描面**只注册耐久规则文档**，且 Task 4 Step 3 显式要求「若模板段被命中，回去收窄谓词而不是开豁免」。

---

## 执行期订正与偏离（2026-08-03 实跑后回填）

**② 首版谓词有 3 个假阳，全部靠收窄谓词解决，豁免表仍为空**（不是开豁免 —— 那会让新闸落地第一天就带一张表）：

| 假阳 | 原因 | 收窄后的判据 |
|---|---|---|
| `AGENTS.md:22`（Mockup Write Gate step 2） | `Read design-process.md § Pre-Phase 0 + § Stage 0.5 + § Scope Handling + § Delivery Cleanup Gate` 是**prescriptive 读取范围**，不是内容复述；删掉会丢真信息 | S4 **不计 `§` 前缀的 id**。⚠️ 这是形态代理不是语义判定：全加 `§` 就能规避 —— 拦的是复述形态的自然漂移 |
| `multi-session-collaboration-rules.md:191` | §反模式 里举例的 `../foo.md` + §实证 的两个 handoff 路径被「framing 词往下 15 行」凑成一条链路 | S6 改要求**连续列表项**（真链路是一串路径列表，不是散落在论述里的路径） |
| `CLAUDE.md:7` | 散文指针（「起手第一份必读 STATUS.md，然后读 AGENTS.md」）+ 15 行后一条无关 bullet 里的 `PROJECT_GOAL.md` 凑够 3 份 | 同上 |

收窄后另修一处**灵敏度**缺口：按「带路径的项」收连续段，会被清单中间那行 `site-review-manifest.json`（非 `.md`）截断，`session-handoff.md` 的 8 份缩报成 6 份 → 改成按**连续列表项**收段、段内容许无路径的项。

**③ 偏离计划：Task 1 不单独 commit，与 Task 2/3/4 合并落地。** 原计划让闸先以红状态落地（"现状即故障样本"），需要一次 `--no-verify`。实跑时判定不可取 —— master 有 `push:master` CI 跑 `prepublishOnly`，落一个红闸会当场把 master CI 打红，代价落在并行 session 头上。闸落地即红的证据改为**留档 stdout**（13 条违例：2×S2 + 10×S4 + 1×S6），不靠一个红 commit 承载。

**④ 顺手修一处 C1 的连带死引用**：`session-handoff.md` §历史方法论 指向 `docs/internal/retrospection/2026-04-28-design-system-audit.md`，该文件已在 `8237d609` 移到 `docs/_archive/`，引用悬空。⚠️ **这不是 `audit:stale-anchors` 的漏** —— 那个脚本源码里明写「反引号文件路径**刻意**不校验（basename 提及 / 外部仓路径混杂，误报率过高），导航锚点只由 markdown 链接覆盖」，是**声明过的覆盖边界**。修法 = 重指 `_archive` 落点 **且改写成 markdown 链接**，让它进入那条闸的覆盖面。

**⑤ 测试基线订正**：STATUS 记 session P 的全量是 `1124 passed / 11 skipped`，但 `a5539e98` 实测是 **1133 / 11** —— 那个 1124 是在 session P 最后一个 commit（`a5539e98`，加了 S6/S7 单测）**之前**测的。本轮 +43 → **1176 / 11**，对得上，无缺口。

**⑥ 并行 session**：本轮期间 `docs/internal/backlog.md`(mtime 15:21) 与 `docs/internal/figma-component-catalog.md`(15:27) 被另一条线写（V4-2333 走查轮十五 / CANONICAL-F92–F94 打包裁定）。全程用显式 pathspec 隔开、不消化其改动；Task 9 改 backlog 时**只用 Edit 改 F58 entry 区域，禁止整文件 Write**。
