# Rule Registry P1（框架）Implementation Plan

> 🛑 **SUPERSEDED（2026-06-12）** —— owner reframe「重要的是如何做，不是如何约定」否决了"建注册表让 AI 查"。本计划作废，正解见 [`2026-06-12-execution-first-mockup-discipline.md`](./2026-06-12-execution-first-mockup-discipline.md)（执行优先：检查内嵌进执行器）。保留本文件仅作决策史。

> **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:** 建「规则注册表 + 任务路由」框架——一张机器可读 JSON SoT 索引全仓治理规则，派生任务路由视图，加 audit 防漂，让 AI 起手按任务类型确定性加载该跑的规则集。

**Architecture:** 复用 `component-affordances.json` 已验证范式：`rule-registry.json`（SoT）→ `generate-rule-registry.mjs`（渲染只读视图 `rule-registry.md` + `task-routing.md`）→ `audit-rule-registry.mjs`（校验完整性 + 防 stale，挂 pre-commit 条件 + self-audit-phase2）。纯增量，不搬任何规则正文。

**Tech Stack:** Node ESM、zero-dep（仅 `node:` 内建，受 `audit:scripts-stdlib` 约束）；vitest 测纯函数。

**Scope:** 仅 P1 框架 + 全量填充。P2 补闸 / P3 重写模糊规则不在本计划（见 `rule-registry-design-2026-06-12.md`）。

---

## File Structure

| 文件 | 职责 | 动作 |
|---|---|---|
| `docs/internal/rule-registry.json` | 规则索引 SoT（`_meta` + `rules[]`） | Create |
| `scripts/generate-rule-registry.mjs` | 读 JSON → 渲染 `rule-registry.md` + `task-routing.md`（确定性，可被测的纯函数 export） | Create |
| `scripts/audit-rule-registry.mjs` | 校验 schema/引用完整性/防 stale（纯函数 export + CLI runner） | Create |
| `docs/internal/rule-registry.md` | 生成的人读视图（勿手改） | Create（生成物） |
| `docs/internal/task-routing.md` | 生成的任务→规则集视图（勿手改） | Create（生成物） |
| `tests/rule-registry.test.ts` | 测 validate/group/render 纯函数 | Create |
| `package.json` | 注册 `generate:rule-registry` + `audit:rule-registry`；接进 `audit:self-audit-phase2` | Modify:75-121 |
| `.husky/pre-commit` | 条件 gate：staged 命中规则文档/注册表/脚本时跑 audit | Modify |
| `AGENTS.md` | onboarding 指针：判完任务类型 → 加载 task-routing 对应段 | Modify |

**任务类型枚举（写死在 `_meta.task_types`，generator/audit 共用）：**
`mockup-US1/2` · `mockup-US3` · `mockup-US5` · `mockup-US5.S` · `mockup-US6` · `mockup-US7` · `code-S1` · `code-S2` · `code-S3` · `code-S4` · `sync` · `migration` · `audit` · `infra` · `release`

---

## Task 1: rule-registry.json schema + 种子条目

**Files:**
- Create: `docs/internal/rule-registry.json`

- [ ] **Step 1: 写 `rule-registry.json`（`_meta` + ~10 条覆盖各 category/各 task-type 的种子）**

```json
{
  "_meta": {
    "title": "Rule Registry — 治理规则索引 + 任务路由 SoT",
    "purpose": "给定任务类型，确定性吐出该跑的精确规则集 + 流程 + 闸；并把每条规则的 enforcement 层级与机器闸显式化、可 audit。治三根源：AI 没加载 / 规则模糊 / 无机器闸。对标 component-affordances.json。",
    "is_source_of_truth": true,
    "generated_views": ["docs/internal/rule-registry.md", "docs/internal/task-routing.md"],
    "audited_by": "pnpm audit:rule-registry",
    "task_types": ["mockup-US1/2","mockup-US3","mockup-US5","mockup-US5.S","mockup-US6","mockup-US7","code-S1","code-S2","code-S3","code-S4","sync","migration","audit","infra","release"],
    "enforcement_levels": ["L1","L2","L3","L4","L5"],
    "gate_kinds": ["none","pre-commit","prepublishOnly","ci","script"],
    "field_docs": {
      "id": "规则原 id（M35 / R13 / 触发器字母 / hard-rule 序号）",
      "category": "M | R | trigger | hard-rule",
      "source": "正文原处 file#anchor（不复制正文，不搬家）",
      "title": "规则标题",
      "fires_on": "触发它的任务类型（取自 _meta.task_types）",
      "statement": "可测判定句（非『保持一致』这种）",
      "enforcement": "L1-L5（meta-rules 触发器 K）",
      "gate": "none | pre-commit | prepublishOnly | ci | script",
      "gate_ref": "gate≠none 时填真实 package.json script 名或 hook 名；none 时填 null",
      "gap": "enforcement=L1 时必填执行缺口理由"
    },
    "last_authored": "2026-06-12",
    "authored_by": "Claude Code (plan owner)"
  },
  "rules": [
    { "id": "hard-1", "category": "hard-rule", "source": "AGENTS.md#硬规则", "title": "不修改 Figma（设计师真源）", "fires_on": ["mockup-US1/2","mockup-US3","sync"], "statement": "任何 Figma 写操作需显式授权；未授权写=bug", "enforcement": "L4", "gate": "pre-commit", "gate_ref": "check no figma-data/ sync-output writes", "gap": null },
    { "id": "hard-4", "category": "hard-rule", "source": "AGENTS.md#硬规则", "title": "颜色/间距不硬编码,必须用 token", "fires_on": ["code-S1","code-S2","code-S3","code-S4"], "statement": "组件 CSS 不得出现 hex/裸 px;必须 var(--token)", "enforcement": "L3", "gate": "prepublishOnly", "gate_ref": "audit:no-hardcoded-design-tokens", "gap": null },
    { "id": "hard-6", "category": "hard-rule", "source": "AGENTS.md#硬规则", "title": "Figma URL 生成代码必须用 src/canonical/*", "fires_on": ["code-S1","code-S3"], "statement": "从 Figma 还原必须落 src/canonical/*,不用 src/components/*", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "DS 仓内 L1 自律;consumer 侧有 eslint no-base-component-import" },
    { "id": "M35", "category": "M", "source": "docs/internal/mockup-conventions.md#m35", "title": "Affordance-Category Search Discipline", "fires_on": ["mockup-US1/2","mockup-US3"], "statement": "新增/借用基础语义元素前必须按 affordance 分类+同义词扫描+3 行 trace", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "思考产物外化,L1 自律无法机检" },
    { "id": "M0", "category": "M", "source": "docs/internal/mockup-conventions.md#m0", "title": "Phase 0 element→component 映射表", "fires_on": ["mockup-US1/2","mockup-US3"], "statement": "≥3 element 的 mockup 必须先产 element→component 映射表并 user 确认", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "起手 gate,L1 自律" },
    { "id": "R13", "category": "R", "source": "docs/internal/code-conventions.md#r13", "title": "Affordance-Category Search（code 侧）", "fires_on": ["code-S1","code-S3"], "statement": "code 找基础语义元素前同 M35 分类+trace", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "L1 自律;镜像 M35" },
    { "id": "R8", "category": "R", "source": "docs/internal/code-conventions.md#r8", "title": "视觉保真 7 步自检", "fires_on": ["code-S1"], "statement": "Figma 还原后跑 headless 截图 vs Figma ref,diff>5% 必修", "enforcement": "L3", "gate": "script", "gate_ref": "visual-diff", "gap": null },
    { "id": "trigger-O", "category": "trigger", "source": "docs/meta-rules.md#触发器-o", "title": "产品真值优先(实测>onboarding)", "fires_on": ["mockup-US3","code-S2","code-S4","migration"], "statement": "US-3 等既有产品迭代必须 sibling probe 实测取值,不套 onboarding/DS token", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "判断类,L1 自律" },
    { "id": "trigger-K", "category": "trigger", "source": "docs/meta-rules.md#触发器-k", "title": "规则提案必须声明 enforcement 层级", "fires_on": ["infra"], "statement": "任何新规则提案必须声明 L1-L5;视觉/客观规则必须 L3+", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "元规则,L1 自律" },
    { "id": "migration-protocol", "category": "trigger", "source": "docs/internal/migration-protocol.md", "title": "2→3 破坏性迁移四阶段", "fires_on": ["migration"], "statement": "破坏 consumer API 的改动走基线快照→方案+ack→实现→验证+soak 四阶段", "enforcement": "L1", "gate": "none", "gate_ref": null, "gap": "流程,L1 自律" }
  ]
}
```

- [ ] **Step 2: 校验 JSON 合法**

Run: `node -e "JSON.parse(require('fs').readFileSync('docs/internal/rule-registry.json','utf8')); console.log('valid')"`
Expected: `valid`

- [ ] **Step 3: Commit**

```bash
git add docs/internal/rule-registry.json
git commit -m "feat(rule-registry): JSON SoT schema + seed entries (P1 task 1)"
```

---

## Task 2: generate-rule-registry.mjs（渲染两个视图）

**Files:**
- Create: `scripts/generate-rule-registry.mjs`
- Create (生成物): `docs/internal/rule-registry.md`, `docs/internal/task-routing.md`
- Test: `tests/rule-registry.test.ts`

- [ ] **Step 1: 写失败测试（渲染纯函数）**

`tests/rule-registry.test.ts`:
```ts
import { describe, it, expect } from 'vitest'
import { groupByTask, renderRoutingMd, renderRegistryMd } from '../scripts/generate-rule-registry.mjs'

const fixture = {
  _meta: { task_types: ['mockup-US1/2', 'code-S1'] },
  rules: [
    { id: 'M35', category: 'M', source: 'a.md#m35', title: 'T1', fires_on: ['mockup-US1/2'], statement: 's1', enforcement: 'L1', gate: 'none', gate_ref: null, gap: 'g1' },
    { id: 'R8', category: 'R', source: 'b.md#r8', title: 'T2', fires_on: ['code-S1'], statement: 's2', enforcement: 'L3', gate: 'script', gate_ref: 'visual-diff', gap: null },
  ],
}

describe('groupByTask', () => {
  it('groups rule ids under each task type they fire on', () => {
    const g = groupByTask(fixture)
    expect(g['mockup-US1/2'].map(r => r.id)).toEqual(['M35'])
    expect(g['code-S1'].map(r => r.id)).toEqual(['R8'])
  })
  it('lists every declared task type even if empty', () => {
    const g = groupByTask({ _meta: { task_types: ['x', 'y'] }, rules: [] })
    expect(Object.keys(g)).toEqual(['x', 'y'])
    expect(g.x).toEqual([])
  })
})

describe('renderRoutingMd', () => {
  it('is deterministic (no timestamp) and includes task headers + rule ids', () => {
    const a = renderRoutingMd(fixture)
    const b = renderRoutingMd(fixture)
    expect(a).toBe(b)
    expect(a).toContain('mockup-US1/2')
    expect(a).toContain('M35')
  })
})

describe('renderRegistryMd', () => {
  it('renders each rule with enforcement + gate', () => {
    const md = renderRegistryMd(fixture)
    expect(md).toContain('M35')
    expect(md).toContain('L3')
    expect(md).toContain('visual-diff')
  })
})
```

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

Run: `pnpm vitest run tests/rule-registry.test.ts`
Expected: FAIL（`generate-rule-registry.mjs` 不存在 / 函数未定义）

- [ ] **Step 3: 写 `scripts/generate-rule-registry.mjs`**

```js
#!/usr/bin/env node
// generate-rule-registry.mjs
// 读 docs/internal/rule-registry.json (SoT) → 渲染两个只读视图：
//   docs/internal/rule-registry.md  （全量规则表）
//   docs/internal/task-routing.md   （任务类型 → 该跑的规则集）
// 确定性输出（body 无时间戳）；audit:rule-registry 比对防 stale。
// ESM + zero-dep。
import { readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'

const __dirname = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = resolve(__dirname, '..')
export const JSON_PATH = resolve(REPO_ROOT, 'docs/internal/rule-registry.json')
export const REGISTRY_MD_PATH = resolve(REPO_ROOT, 'docs/internal/rule-registry.md')
export const ROUTING_MD_PATH = resolve(REPO_ROOT, 'docs/internal/task-routing.md')

function esc(s) {
  return String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')
}

export function groupByTask(reg) {
  const out = {}
  for (const t of reg._meta.task_types) out[t] = []
  for (const r of reg.rules) {
    for (const t of r.fires_on) {
      if (out[t]) out[t].push(r)
    }
  }
  // 稳定排序：按 id，保证确定性
  for (const t of Object.keys(out)) out[t].sort((a, b) => a.id.localeCompare(b.id))
  return out
}

export function renderRoutingMd(reg) {
  const g = groupByTask(reg)
  const lines = []
  lines.push('# Task Routing — 任务类型 → 该跑的规则集（生成物，勿手改）')
  lines.push('')
  lines.push('> 由 `pnpm generate:rule-registry` 从 `rule-registry.json` 生成。AI 起手判完任务类型，加载本段对应规则集。')
  lines.push('')
  for (const t of reg._meta.task_types) {
    lines.push(`## ${t}`)
    lines.push('')
    if (!g[t].length) {
      lines.push('_（暂无登记规则）_')
      lines.push('')
      continue
    }
    lines.push('| id | 规则 | enforcement | 闸 |')
    lines.push('|---|---|---|---|')
    for (const r of g[t]) {
      lines.push(`| ${esc(r.id)} | ${esc(r.title)} | ${esc(r.enforcement)} | ${esc(r.gate === 'none' ? '—' : (r.gate_ref || r.gate))} |`)
    }
    lines.push('')
  }
  return lines.join('\n')
}

export function renderRegistryMd(reg) {
  const lines = []
  lines.push('# Rule Registry — 全量治理规则索引（生成物，勿手改）')
  lines.push('')
  lines.push('> 由 `pnpm generate:rule-registry` 从 `rule-registry.json` 生成。正文在各 `source` 原处。')
  lines.push('')
  lines.push('| id | cat | 规则 | fires_on | enforcement | 闸 | gap |')
  lines.push('|---|---|---|---|---|---|---|')
  for (const r of [...reg.rules].sort((a, b) => a.id.localeCompare(b.id))) {
    lines.push(`| [${esc(r.id)}](${esc(r.source)}) | ${esc(r.category)} | ${esc(r.title)} | ${esc(r.fires_on.join(', '))} | ${esc(r.enforcement)} | ${esc(r.gate === 'none' ? '—' : (r.gate_ref || r.gate))} | ${esc(r.gap || '')} |`)
  }
  lines.push('')
  return lines.join('\n')
}

function main() {
  const reg = JSON.parse(readFileSync(JSON_PATH, 'utf8'))
  writeFileSync(REGISTRY_MD_PATH, renderRegistryMd(reg) + '\n')
  writeFileSync(ROUTING_MD_PATH, renderRoutingMd(reg) + '\n')
  console.log('generate-rule-registry: wrote rule-registry.md + task-routing.md')
}

if (import.meta.url === `file://${process.argv[1]}`) main()
```

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

Run: `pnpm vitest run tests/rule-registry.test.ts`
Expected: PASS（3 describe 全绿）

- [ ] **Step 5: 跑 generator 产出视图**

Run: `node scripts/generate-rule-registry.mjs`
Expected: `generate-rule-registry: wrote rule-registry.md + task-routing.md`；`docs/internal/task-routing.md` 含 `## mockup-US5.S` 等 15 段。

- [ ] **Step 6: Commit**

```bash
git add scripts/generate-rule-registry.mjs tests/rule-registry.test.ts docs/internal/rule-registry.md docs/internal/task-routing.md
git commit -m "feat(rule-registry): generator + routing/registry md views + tests (P1 task 2)"
```

---

## Task 3: audit-rule-registry.mjs（校验完整性 + 防 stale）

**Files:**
- Create: `scripts/audit-rule-registry.mjs`
- Test: `tests/rule-registry.test.ts`（追加 describe）

- [ ] **Step 1: 追加失败测试（validate 纯函数）**

在 `tests/rule-registry.test.ts` 末尾追加：
```ts
import { validateRegistry } from '../scripts/audit-rule-registry.mjs'

describe('validateRegistry', () => {
  const pkgScripts = { 'audit:no-hardcoded-design-tokens': 'x' }
  const base = { _meta: { task_types: ['mockup-US1/2'], enforcement_levels: ['L1','L3'], gate_kinds: ['none','prepublishOnly'] }, rules: [] }

  it('passes a well-formed L1 rule with gap', () => {
    const reg = { ...base, rules: [{ id:'M35', category:'M', source:'docs/internal/mockup-conventions.md#m35', title:'T', fires_on:['mockup-US1/2'], statement:'s', enforcement:'L1', gate:'none', gate_ref:null, gap:'reason' }] }
    expect(validateRegistry(reg, { pkgScripts, fileExists: () => true })).toEqual([])
  })
  it('flags L1 rule missing gap', () => {
    const reg = { ...base, rules: [{ id:'X', category:'M', source:'a.md', title:'T', fires_on:['mockup-US1/2'], statement:'s', enforcement:'L1', gate:'none', gate_ref:null, gap:'' }] }
    expect(validateRegistry(reg, { pkgScripts, fileExists: () => true }).join()).toMatch(/L1.*gap/i)
  })
  it('flags fires_on value not in task_types', () => {
    const reg = { ...base, rules: [{ id:'X', category:'M', source:'a.md', title:'T', fires_on:['bogus'], statement:'s', enforcement:'L1', gate:'none', gate_ref:null, gap:'r' }] }
    expect(validateRegistry(reg, { pkgScripts, fileExists: () => true }).join()).toMatch(/bogus/)
  })
  it('flags gate_ref pointing to nonexistent package.json script', () => {
    const reg = { ...base, rules: [{ id:'X', category:'M', source:'a.md', title:'T', fires_on:['mockup-US1/2'], statement:'s', enforcement:'L3', gate:'prepublishOnly', gate_ref:'audit:does-not-exist', gap:null }] }
    expect(validateRegistry(reg, { pkgScripts, fileExists: () => true }).join()).toMatch(/does-not-exist/)
  })
  it('flags source file that does not exist', () => {
    const reg = { ...base, rules: [{ id:'X', category:'M', source:'docs/nope.md#x', title:'T', fires_on:['mockup-US1/2'], statement:'s', enforcement:'L1', gate:'none', gate_ref:null, gap:'r' }] }
    expect(validateRegistry(reg, { pkgScripts, fileExists: () => false }).join()).toMatch(/nope\.md/)
  })
})
```

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

Run: `pnpm vitest run tests/rule-registry.test.ts`
Expected: FAIL（`validateRegistry` 未定义）

- [ ] **Step 3: 写 `scripts/audit-rule-registry.mjs`**

```js
#!/usr/bin/env node
// audit-rule-registry.mjs
// 校验 rule-registry.json 完整性 + 生成视图未 stale。退出码非 0 即闸 fail。
// ESM + zero-dep。
import { readFileSync, existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import {
  JSON_PATH, REGISTRY_MD_PATH, ROUTING_MD_PATH,
  renderRegistryMd, renderRoutingMd,
} from './generate-rule-registry.mjs'

const __dirname = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = resolve(__dirname, '..')
const REQUIRED = ['id', 'category', 'source', 'title', 'fires_on', 'statement', 'enforcement', 'gate']
const CATEGORIES = ['M', 'R', 'trigger', 'hard-rule']

// reg: parsed registry; opts.pkgScripts: {scriptName:cmd}; opts.fileExists: (relPath)=>bool
export function validateRegistry(reg, opts) {
  const errs = []
  const { pkgScripts, fileExists } = opts
  const taskTypes = new Set(reg._meta.task_types)
  const levels = new Set(reg._meta.enforcement_levels)
  const gateKinds = new Set(reg._meta.gate_kinds)
  const seen = new Set()
  for (const r of reg.rules) {
    const tag = `[${r.id ?? '?'}]`
    for (const f of REQUIRED) if (r[f] === undefined || r[f] === null || r[f] === '') errs.push(`${tag} 缺字段 ${f}`)
    if (r.id) { if (seen.has(r.id)) errs.push(`${tag} id 重复`); seen.add(r.id) }
    if (r.category && !CATEGORIES.includes(r.category)) errs.push(`${tag} category 非法: ${r.category}`)
    if (Array.isArray(r.fires_on)) for (const t of r.fires_on) if (!taskTypes.has(t)) errs.push(`${tag} fires_on 含未知任务类型: ${t}`)
    if (r.enforcement && !levels.has(r.enforcement)) errs.push(`${tag} enforcement 非法: ${r.enforcement}`)
    if (r.gate && !gateKinds.has(r.gate)) errs.push(`${tag} gate 非法: ${r.gate}`)
    if (r.enforcement === 'L1' && !r.gap) errs.push(`${tag} enforcement=L1 必须填 gap 理由`)
    if (r.gate && r.gate !== 'none' && r.gate !== 'pre-commit') {
      if (!r.gate_ref) errs.push(`${tag} gate=${r.gate} 必须填 gate_ref`)
      else if (!pkgScripts[r.gate_ref] && !pkgScripts[`audit:${r.gate_ref}`] && !String(pkgScripts.prepublishOnly || '').includes(r.gate_ref) && !fileExists(`scripts/${r.gate_ref}.py`) && !fileExists(`scripts/${r.gate_ref}.mjs`)) {
        errs.push(`${tag} gate_ref 指向不存在的脚本/闸: ${r.gate_ref}`)
      }
    }
    if (r.source) { const file = String(r.source).split('#')[0]; if (!fileExists(file)) errs.push(`${tag} source 文件不存在: ${file}`) }
  }
  return errs
}

function main() {
  const reg = JSON.parse(readFileSync(JSON_PATH, 'utf8'))
  const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'))
  const errs = validateRegistry(reg, {
    pkgScripts: pkg.scripts || {},
    fileExists: (rel) => existsSync(resolve(REPO_ROOT, rel)),
  })
  // 防 stale：生成视图必须 == 当前渲染
  const checkStale = (path, rendered, name) => {
    const onDisk = existsSync(path) ? readFileSync(path, 'utf8') : ''
    if (onDisk.trimEnd() !== rendered.trimEnd()) errs.push(`${name} stale —— 跑 pnpm generate:rule-registry 重生成`)
  }
  checkStale(REGISTRY_MD_PATH, renderRegistryMd(reg), 'rule-registry.md')
  checkStale(ROUTING_MD_PATH, renderRoutingMd(reg), 'task-routing.md')

  if (errs.length) {
    console.error('audit-rule-registry FAIL:')
    for (const e of errs) console.error('  ✗ ' + e)
    process.exit(1)
  }
  console.log(`audit-rule-registry OK — ${reg.rules.length} rules, views fresh.`)
}

if (import.meta.url === `file://${process.argv[1]}`) main()
```

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

Run: `pnpm vitest run tests/rule-registry.test.ts`
Expected: PASS（含新 5 个 validateRegistry 用例）

- [ ] **Step 5: 跑 audit 确认绿（种子数据 + 已生成视图）**

Run: `node scripts/audit-rule-registry.mjs`
Expected: `audit-rule-registry OK — 10 rules, views fresh.`（若报 source 文件不存在,修种子条目的 source 路径到真实文件）

- [ ] **Step 6: Commit**

```bash
git add scripts/audit-rule-registry.mjs tests/rule-registry.test.ts
git commit -m "feat(rule-registry): audit (schema/ref integrity/stale) + tests (P1 task 3)"
```

---

## Task 4: package.json 注册 + 接进 audit 链 + pre-commit 条件闸

**Files:**
- Modify: `package.json`（scripts 段 + `audit:self-audit-phase2`）
- Modify: `.husky/pre-commit`

- [ ] **Step 1: package.json 注册两脚本**

在 scripts 段加（紧邻其它 generate:/audit:）：
```json
    "generate:rule-registry": "node scripts/generate-rule-registry.mjs",
    "audit:rule-registry": "node scripts/audit-rule-registry.mjs",
```

- [ ] **Step 2: 接进 `audit:self-audit-phase2`**

把 `audit:self-audit-phase2` 末尾追加 `&& node scripts/audit-rule-registry.mjs`：
```json
    "audit:self-audit-phase2": "node scripts/audit-canonical-compliance.mjs && node scripts/audit-hard-rule-compliance.mjs && node scripts/audit-render-verification-coverage.mjs && node scripts/audit-figma-library-vs-canonical.mjs && node scripts/audit-icon-canonical-names.mjs && node scripts/audit-rule-registry.mjs",
```

- [ ] **Step 3: .husky/pre-commit 加条件 gate**

在 pre-commit 已有条件块范式后追加（参考现有 `staged 命中 X 时跑 Y`）：
```sh
# rule-registry：staged 命中注册表/生成器/审计 或 规则文档时，跑 audit:rule-registry
if echo "$STAGED" | grep -qE 'docs/internal/rule-registry\.json|scripts/(generate|audit)-rule-registry\.mjs|docs/internal/(rule-registry|task-routing)\.md|AGENTS\.md|docs/meta-rules\.md|docs/internal/(mockup|code)-conventions\.md|docs/internal/design-process\.md'; then
  echo "▶ pre-commit: rule-registry (注册表完整性 + 视图未 stale)"
  node scripts/audit-rule-registry.mjs || exit 1
else
  echo "   ✓ no rule-registry-relevant files staged, skipped"
fi
```
（`$STAGED` 变量名对齐 pre-commit 现有写法；若现有用 `git diff --cached --name-only`，照抄其取法。）

- [ ] **Step 4: 验证脚本可跑 + audit 链含新审计**

Run: `pnpm audit:rule-registry && pnpm generate:rule-registry`
Expected: audit OK + 重生成无 diff（`git status` 对 .md 无变化）

- [ ] **Step 5: Commit**

```bash
git add package.json .husky/pre-commit
git commit -m "feat(rule-registry): register scripts + wire self-audit-phase2 + conditional pre-commit (P1 task 4)"
```

---

## Task 5: onboarding 指针（AGENTS）

**Files:**
- Modify: `AGENTS.md`（§Onboarding Gate / task-type 区域加一句指针）

- [ ] **Step 1: AGENTS.md 加指针**

在 task-type 矩阵或 Onboarding Gate 段后加一句：
```markdown
> **任务起手路由（2026-06-12）**：判完任务类型（design-process 起手自判决策树 / code-conventions 场景判定）后，查 [`docs/internal/task-routing.md`](./docs/internal/task-routing.md) 对应段加载该任务该跑的精确规则集 + 闸（SoT = `rule-registry.json`，勿手改生成视图）。
```

- [ ] **Step 2: 校验链接锚点存在**

Run: `test -f docs/internal/task-routing.md && echo ok`
Expected: `ok`

- [ ] **Step 3: Commit**

```bash
git add AGENTS.md
git commit -m "docs(rule-registry): onboarding pointer to task-routing (P1 task 5)"
```

---

## Task 6: 全量填充（subagent 辅助，机械）

**Files:**
- Modify: `docs/internal/rule-registry.json`（补齐全量 M/R/触发器/hard-rule）

- [ ] **Step 1: 列全量规则源**

Run（确认要扫的来源 + 计数）：
```bash
grep -cE '^### 触发器 [A-Z]' docs/meta-rules.md
grep -cE '^#+.*\bM[0-9]' docs/internal/mockup-conventions.md
grep -cE '^## R[0-9]' docs/internal/code-conventions.md
grep -nE '^\| [0-9]+ \|' AGENTS.md | head   # 硬规则表行
```
Expected: 得到 trigger / M / R / hard-rule 各计数，作为填充清单基线。

- [ ] **Step 2: 派 subagent 分文件抽规则 → 回报注册项**

每个规则源文件派一个 subagent（mockup-conventions / code-conventions / meta-rules / AGENTS 硬规则），prompt 要求：逐条输出 `{id, category, source(file#anchor), title, fires_on[(从 _meta.task_types 选)], statement(可测句), enforcement(读规则自带的 L 声明,无则按内容判 + 标注待 owner 复核), gate, gate_ref, gap}`。**硬纪律**：`statement` 必须可测；`gate`/`gate_ref` 必须实证 package.json 真有该 script（不准编造闸）；写不出可测 statement 的标 `"_p3_rewrite": true`。

- [ ] **Step 3: 合并进 rule-registry.json**

把各 subagent 回报的条目合并进 `rules[]`（按 id 去重 + 排序）。

- [ ] **Step 4: 重生成 + 审计**

Run: `pnpm generate:rule-registry && pnpm audit:rule-registry`
Expected: audit OK；若报 gate_ref 不存在 / source 不存在 / L1 缺 gap → 逐条修正（这正是 audit 的价值）。

- [ ] **Step 5: 跑全测 + Commit**

Run: `pnpm vitest run tests/rule-registry.test.ts && pnpm exec vue-tsc --noEmit`
Expected: PASS

```bash
git add docs/internal/rule-registry.json docs/internal/rule-registry.md docs/internal/task-routing.md
git commit -m "feat(rule-registry): full population of M/R/trigger/hard-rule entries (P1 task 6)"
```

---

## Self-Review

- **Spec coverage**：块1 注册表→T1；块2 任务路由→T2;块5 audit 防漂→T3;注册/接链/pre-commit→T4;onboarding 指针→T5(块5);全量填充→T6。块3 补闸/块4 重写=P2/P3，明确不在本计划✓。
- **Placeholder scan**：无 TBD;脚本代码完整;测试有真实断言✓。
- **Type consistency**：`groupByTask`/`renderRoutingMd`/`renderRegistryMd`/`validateRegistry` 跨 T2/T3/测试签名一致;`JSON_PATH`/`REGISTRY_MD_PATH`/`ROUTING_MD_PATH` 从 generator export、audit import✓。
- **风险点**：T4 Step3 的 `$STAGED` 取法依赖 pre-commit 现有写法——执行时先读 `.husky/pre-commit` 头部确认变量名/取 staged 方式再照抄（已在步骤注明）。
