# F4 分母双分支预演 —— 实施计划

> **For agentic workers:** 本计划按 `superpowers:writing-plans` 写就。步骤用 `- [ ]` 勾选。
> ⛔ **本计划含预注册常量，必须「先 commit 再跑」**（§30.5 N80.1 纪律）。

## ✅ 执行状态（2026-08-31 收尾回填）

**全部 Task 已执行完毕。** 结论**推翻了本计划自己的立项前提** —— 原以为是「给 owner 两栏数字选一栏」，
实测发现那个二选一问题**本身不成立**（13 段无一是真的空）。

| Task | 状态 | 实际产出 / 偏差 |
|:-:|---|---|
| 1 · 量具骨架 | ✅ | `metrics/f4-denominator-branches.mjs`，预注册先 commit（`9c08773`）后跑 |
| 2 · 控制组 | ✅ | **13 条控制 + 1 条 skip**（计划里是 9 条；⛔ 不照抄 triage 的 `must-hit-0`，改两臂） |
| **2′ · 形态判据** | ✅ **计划外新增** | 读了 3 条 gated 零核查项原文后加 —— 正是它证伪了立项前提。阈值预注册后另行 commit |
| 3 · 双 pin 实跑 | ✅ | P4/P6 读数逐字相同；跨 pin 比 **id 集合**（13 vs 13，差集 0）；跑后两 pin `git status` 均空 |
| 4 · 回写处方 | ✅ | 新增 §2.6.1 三条订正；原 §2.6 表**留痕不删**；§5 第 2 项改判为「问题本身作废」 |
| 5 · status 登记 | ✅ | **N81**（主结论）+ **N81.1**（`must-hit` 恒真，新形态） |

---

**Goal:** 把处方 §5 执行状态栏第 2 项（F4 那 10 条要不要从 126 的分母里去掉）从「一道 owner
要自己推演后果的题」变成「两栏数字选一栏」—— 用可复跑量具产出 126 分支与 `126−k` 分支下
体检报告第四节的**全部**占比，并排回写处方 §2.6。

**Architecture:** 新增 `metrics/f4-denominator-branches.mjs`。复用 `acceptance-coverage-dump.mjs`
的 `build()` 与闸自己的 `parseAcceptanceSections`（⛔ 不复制判据副本），把「段内 0 条核查项」这条
**结构判据**从**只跑 67 条 unclassified** 扩到**全 126 段**，按三态分别读数，再算两分支占比。
双 pin 跑（P4 = 报告基线 / P6 = 处方基线），用 `--compare` 交叉比 id 集合。

**Tech Stack:** 纯 Node ESM、零依赖（D4）。只读 pin worktree。

**Spec:** [`proposals/2026-08-31-unclassified-67-fix-directions.md`](../proposals/2026-08-31-unclassified-67-fix-directions.md) §2.6 + §5 第 2 项；
[`docs/round2-status.md`](./round2-status.md) §30.5 N80.4。

---

## Global Constraints

- **lab 只审查、不裁定**（`AGENTS.md` §8）⇒ 本量具产出**两栏读数**，⛔ 不推荐选哪栏。
- **只在 pin 住的 worktree 上测量**；pin dirty ⇒ **fail closed** 抛错，⛔ 不许 `allowDirty`。
- **预注册先行**：`PREREG` 常量写死进源码，**先 commit 再跑**；判据失败即如实登记并降级，
  ⛔ 不许拿同一份读数反复调阈值到通过（N80.1）。
- **比 id 集合，⛔ 不比总数**（N80.2：两个判据都得 33 但集合差 8 条）。
- **每条 `must-not-hit` 必须配一条同构的 `must-hit` 阴性对照**（N79.2：否则它可能恒真）。
- **凡输出「空 / 零」先证判别力**（§30.5.2）。
- **剥注释再判**：核查项抽取走闸自己的 `parseAcceptanceSections` + dump 的 `normalizeText`，
  ⛔ 不许自己写 grep（§30.5.2 教训，同一形态已累计 4 次）。
- 取退出码 ⛔ 不接管道（zsh 下 `${PIPESTATUS[0]}` 是空的）⇒ 一律 `cmd; echo "exit=$?"`。
- **不可复现的格子记 `unreproducible`，⛔ 不许写成算出来的数**（N79.1 口径纪律：
  `unlocated` ≠ `corrected`）。

**pin 与 dump（均已存在，⛔ 不重建）：**

| 角色 | sha | worktree | dump |
|---|---|---|---|
| **P4** 体检报告基线 | `19e55102` | `~/.ai-ds-lab/pins/tvu-ds-19e55102` | `subjects/tvu-ds/inventory/acceptance-coverage-19e55102.json` |
| **P6** 处方基线 | `b3728d38` | `~/.ai-ds-lab/pins/tvu-ds-b3728d38` | `subjects/tvu-ds/inventory/acceptance-coverage-b3728d38.json` |

---

## File Structure

- **Create** `metrics/f4-denominator-branches.mjs` —— 唯一新文件。职责：全 126 段零核查项读数
  + 两分支占比 + 内建控制 + Markdown 渲染。
- **Create** `subjects/tvu-ds/inventory/f4-denominator-19e55102.json`（产物）
- **Create** `subjects/tvu-ds/inventory/f4-denominator-b3728d38.json`（产物）
- **Modify** `proposals/2026-08-31-unclassified-67-fix-directions.md` §2.6（并排双分支表）+ §5 第 2 项状态
- **Modify** `docs/round2-status.md` §30.5（登记 N81）

---

## 🔴 预注册（Task 1 里写死进源码，先 commit 再跑）

```js
const PREREG = {
  // 处方 §1 已登记 F4 = 10（P6 上的 unclassified 零核查项段）
  // ⇒ 本器在 P6 上重算，条数与 id 集合都必须复现它。不复现 = 抽取面没对齐，整份读数作废。
  p6UnclassifiedZeros: 10,

  // 🔴 以下两项 lab 【事先不知道】——它们正是本次要回答的问题。
  //    ⛔ 不设期望值，跑出多少记多少；设了就是在给自己留调参空间。
  gatedZeros: null,
  exemptZeros: null,
}
```

**为什么只预注册第一项**：`p6UnclassifiedZeros = 10` 是**已发表的读数**，可证伪 ⇒ 当对照用。
而「gated / 只能人看 那 55 段里有几段 0 核查项」**从未测过**，任何预设值都是编的
⇒ 显式写 `null` 并留痕，比编一个数再「碰巧命中」诚实。

### ⚠️ 留痕：读数是**先由探针跑出来的**，⛔ 本量具不是「盲跑发现」的

写本计划期间先跑了一支一次性探针（scratchpad，不入库）确认可行性，**因此下面的读数在量具落地前就已看到**：
`gated 3 · not-machine-checkable 0 · unclassified 10`。

⇒ 必须说清楚这不构成 N80.1 违规，理由是**预注册的那一项没有被调过**：
- 唯一带期望值的 `p6UnclassifiedZeros = 10` 是**处方早已发表**的数，看不看探针都一样；
- 另两项从一开始就写死 `null`（**拒绝设期望值**），⛔ 没有「看到数再回填期望值」的空间。

⇒ 本量具的职责是让这三个数**可复跑 + 有控制**，⛔ 不是宣称「盲测发现」。

### 🔴 探针撞出的两条，必须进 Task 2 与 Task 4

**（a）`unclassified-fix-triage.mjs` 的 `must-hit-0/itemcount-matches-dump` 对它自己宣称的
事（「抽取面没对齐」）零信息量。** 实测：现行 A 路 **0/126** 对不齐；扰动成窄尺 `stripNoise`
后 **2/126** 对不齐 ⇒ **比较算子是活的**，但现行代码两边是**同一表达式的两次求值**
（`acceptance-coverage-dump.mjs:251-254` 与 `unclassified-fix-triage.mjs:326-327` 都是
`normalizeText(raw)` → `gate.parseAcceptanceSections`）⇒ 它**永远不会红**。
⚠️ 措辞纪律：**⛔ 不许说成「它是坏的」** —— 它弱保了行集完整性（`file:line` 键的存废）；
不成立的是它 `why` 里那句「不同 ⇒ 抽取面没对齐」。
⇒ **本器 ⛔ 不照抄这条控制**，改为显式扰动臂（Task 2 Step 1），并把措辞降到它真能证的那一档。

**（b）🔴 `k = 13`，⛔ 不是 10 —— 处方 §2.6「分母要减 10」措辞不成立。**
零核查项段按三态：**gated 3 · not-machine-checkable 0 · unclassified 10**。
⇒ 后果比原措辞重一档：**有 3 段现在被算作「🟢 有人管」，而它一条核查项都没有**
（即：一支闸声称覆盖了一条**没有可检查内容**的规则）⇒ 分支 B 里**分子分母同时变**，
⛔ 不是「只把分母从 126 改成 116」。
⚠️ `not-machine-checkable = 0` 是**零输出** ⇒ §30.5.2 硬要求先证判别力
（Task 2 Step 3 的三态合成探针正是为此，**该控制在见到读数之前就已写进本计划**）。

---

## Task 1: 量具骨架 + 全 126 段零核查项读数（先 commit 再跑）

**Files:**
- Create: `metrics/f4-denominator-branches.mjs`

**Interfaces:**
- Consumes: `build as buildDump, normalizeText` from `./acceptance-coverage-dump.mjs`；
  pin 内 `scripts/audit-acceptance-coverage.mjs` 的 `SCAN_FILES` / `parseAcceptanceSections`
  （与 `unclassified-fix-triage.mjs:312-335` 同一条路径）
- Produces: `run(pinAbs, dumpPath) → { subjectSha, totals, zerosByState, zeroIds, branches, ... }`

- [ ] **Step 1: 写文件头注释 + `PREREG` 常量**（照上面「预注册」原文抄，⛔ 不改数）

- [ ] **Step 2: 写 `run()` —— 复用 dump 真代码路径取全 126 段**

```js
async function run(pin, dumpPath) {
  const gatePath = join(pin, GATE_REL)
  if (!existsSync(gatePath)) throw new Error(`[fail-closed] 闸不存在于 pin：${gatePath}`)
  const gate = await import(pathToFileURL(gatePath).href)
  const d = await buildDump(pin)
  if (d.dirty) throw new Error('[fail-closed] pin 不干净 ⇒ 读数不可复现')

  // 核查项正文 —— ⛔ 走闸自己的 parser，不自己 grep
  const itemsByPos = new Map()
  const lineCountByFile = new Map()
  for (const f of gate.SCAN_FILES) {
    const raw = readFileSync(join(pin, f), 'utf8')
    lineCountByFile.set(f, raw.split('\n').length)
    const { text } = normalizeText(raw)
    for (const s of gate.parseAcceptanceSections(f, text)) itemsByPos.set(`${f}:${s.line}`, s.items)
  }
  const allSections = d.rows.map((r) => ({
    ...r,
    items: itemsByPos.get(`${r.file}:${r.line}`) || [],
    dumpItemCount: r.itemCount,
    lineExists: r.line <= (lineCountByFile.get(r.file) || 0),
  }))
  return finish(allSections, d)
}
```

- [ ] **Step 3: 写 `finish()` —— 三态分别数零核查项段**

```js
const STATES = ['gated', 'not-machine-checkable', 'unclassified']

function finish(allSections, d) {
  const byState = Object.fromEntries(STATES.map((s) => [s, allSections.filter((x) => x.state === s)]))
  const zerosOf = (arr) => arr.filter((s) => s.items.length === 0)

  const zerosByState = Object.fromEntries(
    STATES.map((s) => [s, { scanned: byState[s].length, zeros: zerosOf(byState[s]).length }]))
  const zeroIds = Object.fromEntries(
    STATES.map((s) => [s, zerosOf(byState[s]).map((x) => x.id).sort()]))

  const k = STATES.reduce((a, s) => a + zerosByState[s].zeros, 0)
  const totals = Object.fromEntries(STATES.map((s) => [s, byState[s].length]))
  totals.all = allSections.length

  return { subjectSha: d.subjectSha, totals, zerosByState, zeroIds, k,
           branches: branchTable(totals, zerosByState, k),
           allSections, dumpRows: d.rows }
}
```

- [ ] **Step 4: 写 `branchTable()` —— 两分支占比**

```js
// 分支 A：维持现状（126 全算规则）
// 分支 B：0 核查项的段不算规则 ⇒ 各态分别减掉自己那部分零段
function branchTable(totals, z, k) {
  const pct = (n, dd) => (dd ? Math.round((n / dd) * 1000) / 10 : null)
  const A = { denom: totals.all }
  const B = { denom: totals.all - k }
  for (const s of STATES) {
    A[s] = { n: totals[s], pct: pct(totals[s], A.denom) }
    const nB = totals[s] - z[s].zeros
    B[s] = { n: nB, pct: pct(nB, B.denom) }
  }
  return { A, B, k }
}
```

- [ ] **Step 5: `git add` + commit（⛔ 此刻还没跑过）**

```bash
git add metrics/f4-denominator-branches.mjs
git commit -m "feat(metrics): F4 分母双分支量具 + 预注册（先提交再跑，⛔ 不许跑完调期望值）"
```

---

## Task 2: 内建控制（must-hit / must-not-hit **配对**）

**Files:**
- Modify: `metrics/f4-denominator-branches.mjs`

- [ ] **Step 1: 抽取对齐（带扰动臂）+ 非量具故障两条**

> 🔴 ⛔ **不照抄 triage 的 `must-hit-0`** —— 实测它对「抽取面没对齐」零信息量（见上）。
> 改成**两臂**：恒等臂 + 扰动臂。扰动臂会红才说明这个比较有判别力。

```js
{
  id: 'must-hit-0/itemcount-identity-and-perturbation',
  why: '⚠️ 措辞已降级：本条【只】证「比较算子是活的」，⛔ 不证抽取面独立正确 —— 本器与 dump 同源'
     + '（都是 normalizeText → gate.parseAcceptanceSections）。恒等臂必 0；扰动臂（换成窄尺 stripNoise）'
     + '必须 >0，否则说明连换了输入都不变 ⇒ 这个比较是死的',
  run: (c) => {
    const idn = c.allSections.filter((s) => s.items.length !== s.dumpItemCount).length
    const per = c.allSections.filter((s) => s.narrowItemCount !== s.dumpItemCount).length
    return { pass: idn === 0 && per > 0,
             detail: `恒等臂对不齐 ${idn}（须 0）· 扰动臂对不齐 ${per}（须 >0）` }
  },
},
{
  id: 'must-hit-1/zero-sections-lines-exist',
  why: '每个零核查项段的行号必须在文件里真实存在 ⇒ 证明「0 条」是真读数，不是解析失败',
  run: (c) => {
    const zs = c.allSections.filter((s) => s.items.length === 0)
    const ghost = zs.filter((s) => !s.lineExists)
    return { pass: zs.length > 0 && ghost.length === 0,
             detail: `零核查项段 ${zs.length} 个，其中行号不存在的 = ${ghost.length}` }
  },
},
```

- [ ] **Step 2: 预注册对照（可证伪）**

```js
{
  id: 'must-hit-2/p6-f4-idset-reproduces',
  why: '在 P6 上，本器的 unclassified 零核查项【id 集合】必须逐字复现处方已发表的 F4 10 条。⛔ 只比条数不算复现（N80.2）',
  run: (c) => {
    if (c.subjectSha.slice(0, 8) !== 'b3728d38') return { skip: true, detail: '非 P6，本条不适用' }
    const mine = c.zeroIds.unclassified
    const eq = mine.length === PREREG.p6UnclassifiedZeros
      && JSON.stringify(mine) === JSON.stringify(c.proposalF4Ids.slice().sort())
    return { pass: eq, detail: `本器 ${mine.length} 条 · 处方 ${c.proposalF4Ids.length} 条 · id 集合逐字相等 = ${eq}` }
  },
},
```

- [ ] **Step 3: 🔴 判别力控制 —— 合成探针，每个 state 各注一个零核查项段**

> **为什么非有不可**：若真实 `gatedZeros = 0`，这个输出与「判据根本没扫 gated 那 27 段」
> **在输出上完全不可区分**（§30.5.2）。本条把「扫过了」证出来。

```js
{
  id: 'must-hit-3/synthetic-zero-caught-in-every-state',
  why: '给三态各注入一个 0 核查项的合成段，判据必须【三个都抓到】⇒ 证明它对三态都有判别力，某态读数为 0 是真的没有、不是没扫',
  run: (c) => {
    const caught = STATES.filter((st) => {
      const probe = { state: st, items: [], id: `@probe:${st}`, lineExists: true }
      const z = [...c.allSections, probe].filter((s) => s.state === st && s.items.length === 0)
      return z.some((s) => s.id === `@probe:${st}`)
    })
    return { pass: caught.length === STATES.length,
             detail: `三态合成探针被抓到 ${caught.length}/${STATES.length}：${caught.join(' / ')}` }
  },
},
```

- [ ] **Step 4: must-not-hit + 它的**同构阴性对照**（N79.2 硬要求，⛔ 不许只写前者）**

```js
{
  id: 'must-not-hit-4/nonzero-never-counted',
  why: '有核查项的段⛔不许被算进零核查项。防止判据把「解析不出」当成「本来就没有」',
  run: (c) => {
    const probe = { state: 'unclassified', items: [{ line: 1, text: 'x' }], id: '@probe:nonzero', lineExists: true }
    const z = [...c.allSections, probe].filter((s) => s.items.length === 0)
    return { pass: !z.some((s) => s.id === '@probe:nonzero'),
             detail: `带 1 条核查项的合成段被误算 = ${z.some((s) => s.id === '@probe:nonzero')}` }
  },
},
{
  id: 'must-hit-5/same-probe-caught-when-emptied',
  why: '🔴 must-not-hit-4 的【同构阴性对照】（N79.2）：同一个合成段，把核查项清空后必须被抓到。'
     + '不配这条，must-not-hit-4 的「没命中」既可能是判据对、也可能是这个探针根本没进扫描面 ⇒ 恒真',
  run: (c) => {
    const probe = { state: 'unclassified', items: [], id: '@probe:nonzero', lineExists: true }
    const z = [...c.allSections, probe].filter((s) => s.items.length === 0)
    return { pass: z.some((s) => s.id === '@probe:nonzero'),
             detail: `同一探针清空核查项后被抓到 = ${z.some((s) => s.id === '@probe:nonzero')}` }
  },
},
```

- [ ] **Step 5: 分支算术闭合 + 分支有差**

```js
{
  id: 'must-hit-6/branch-arithmetic-closes',
  why: '两分支各自三态相加必须等于各自分母。不闭合 ⇒ 占比全错',
  run: (c) => {
    const sum = (br) => STATES.reduce((a, s) => a + br[s].n, 0)
    const okA = sum(c.branches.A) === c.branches.A.denom
    const okB = sum(c.branches.B) === c.branches.B.denom
    return { pass: okA && okB, detail: `A ${sum(c.branches.A)}/${c.branches.A.denom} · B ${sum(c.branches.B)}/${c.branches.B.denom}` }
  },
},
{
  id: 'must-hit-7/branches-actually-differ',
  why: '两分支若逐字相同，本次预演零信息（第 25 条恒绿）。k=0 时本条【应当红】—— 那说明没有第二个分支可预演',
  run: (c) => {
    const diff = STATES.some((s) => c.branches.A[s].pct !== c.branches.B[s].pct)
      || c.branches.A.denom !== c.branches.B.denom
    return { pass: c.k > 0 && diff, detail: `k = ${c.k} · 两分支存在差异 = ${diff}` }
  },
},
```

- [ ] **Step 6: 跨 pin id 集合比对（`--compare` 给了才算数，没给显式 `skip`⛔ 不算绿）**

```js
{
  id: 'must-hit-8/cross-pin-idset-not-just-count',
  why: '🔴 N80.2 那个坑：条数相同不代表集合相同。若 P4 与 P6 的零核查项条数相等，则 id 集合必须也逐字相等；'
     + '不等则必须能逐条列出差集。⛔ 只比总数不算比对过',
  run: (c) => {
    if (!c.compare) return { skip: true, detail: '未给 --compare ⇒ 本条未执行（⛔ 不计为绿）' }
    const mine = STATES.flatMap((s) => c.zeroIds[s]).sort()
    const other = STATES.flatMap((s) => c.compare.zeroIds[s]).sort()
    const onlyMine = mine.filter((x) => !other.includes(x))
    const onlyOther = other.filter((x) => !mine.includes(x))
    const ok = mine.length === other.length ? onlyMine.length === 0 && onlyOther.length === 0 : true
    return { pass: ok, detail: `本 pin ${mine.length} · 对比 pin ${other.length} · 只在本 pin ${onlyMine.length}（${onlyMine.join(',') || '—'}）· 只在对比 pin ${onlyOther.length}（${onlyOther.join(',') || '—'}）` }
  },
},
```

- [ ] **Step 7: `runControls` 把 `skip` 与 `pass` 分开渲染，`--self-check` 里 `skip` 显示 `⊘` ⛔ 不显示 `✓`**

- [ ] **Step 8: commit（仍未跑）**

```bash
git add metrics/f4-denominator-branches.mjs
git commit -m "feat(metrics): F4 分母量具 9 条控制 —— must-not-hit 全部配同构 must-hit 阴性对照（N79.2）"
```

---

## Task 3: 双 pin 实跑 + 交叉比对

- [ ] **Step 1: 跑 P6（处方基线），先验预注册对照**

```bash
node metrics/f4-denominator-branches.mjs \
  --pin ~/.ai-ds-lab/pins/tvu-ds-b3728d38 \
  --dump subjects/tvu-ds/inventory/acceptance-coverage-b3728d38.json \
  --triage subjects/tvu-ds/inventory/unclassified-fix-triage-b3728d38.json \
  --self-check; echo "exit=$?"
```

期望：`must-hit-2` 绿（复现处方 F4 10 条的 **id 集合**）。
🔴 **若它红** ⇒ 抽取面没对齐，**停下**，⛔ 不许改 `PREREG` 迁就，按 N80.1 如实登记并诊断。

- [ ] **Step 2: 出 P6 JSON**

```bash
node metrics/f4-denominator-branches.mjs \
  --pin ~/.ai-ds-lab/pins/tvu-ds-b3728d38 \
  --dump subjects/tvu-ds/inventory/acceptance-coverage-b3728d38.json \
  --triage subjects/tvu-ds/inventory/unclassified-fix-triage-b3728d38.json \
  --out subjects/tvu-ds/inventory/f4-denominator-b3728d38.json; echo "exit=$?"
```

- [ ] **Step 3: 跑 P4（体检报告基线）+ 跨 pin 比对**

```bash
node metrics/f4-denominator-branches.mjs \
  --pin ~/.ai-ds-lab/pins/tvu-ds-19e55102 \
  --dump subjects/tvu-ds/inventory/acceptance-coverage-19e55102.json \
  --compare subjects/tvu-ds/inventory/f4-denominator-b3728d38.json \
  --out subjects/tvu-ds/inventory/f4-denominator-19e55102.json --self-check; echo "exit=$?"
```

- [ ] **Step 4: 跑完验 pin 仍干净（只读证明）**

```bash
git -C ~/.ai-ds-lab/pins/tvu-ds-19e55102 status --short; echo "exit=$?"
git -C ~/.ai-ds-lab/pins/tvu-ds-b3728d38 status --short; echo "exit=$?"
```
期望：两处均**无输出**。

- [ ] **Step 5: commit 产物**

---

## Task 4: 回写处方 §2.6 与 §5

**Files:**
- Modify: `proposals/2026-08-31-unclassified-67-fix-directions.md`

- [ ] **Step 1: §2.6 追加「分母双分支」并排表**，含三栏：分支 A（126）· 分支 B（`126−k`）· 差值。

  🔴 **已实测 `k = 13`，⛔ 不是 10** ⇒ 必须在表上方**单独一行**订正处方原措辞，⛔ 不许并进表格
  一笔带过。要写明的是**后果，不只是数字**：

  - 「分母减 10」这句**不成立**。零核查项段共 **13** 段：`gated 3` · `not-machine-checkable 0`
    · `unclassified 10`。F4 那 10 条只是其中落在「没人管」里的部分。
  - 🔴 **更重的一条：分支 B 里分子分母【同时】变。** 那 3 段现在被算作「🟢 有人管」——
    即**一支闸声称覆盖了一条没有任何可检查内容的规则**。⛔ 这不是「把分母改小」，
    它同时让「有人管」的条数下降。
  - `not-machine-checkable = 0` 是**零输出**，其判别力由 `must-hit-3` 三态合成探针证明
    （⛔ 不许写成「查了没有」就完事）。

  ⚠️ **口径**：lab 给的是**这 13 段是什么**，⛔ 不是「该不该减」。拍板仍是 owner。

- [ ] **Step 2: §4 好消息表那 3 个历史时点** —— 无 dump 不可复现 ⇒ 标 `unreproducible`
  并写明原因（N73：时点对比此前是人工写的表格，dump 工具晚于它们）。⛔ 不许推算填数。

- [ ] **Step 3: §5 第 2 项状态由「⬜ 待拍」改为「🟡 待拍（lab 已备双分支读数）」+ 更新日期**，
  ⛔ 状态**不许**改成「已完成」—— 拍板的仍是 owner。

- [ ] **Step 4: commit**

---

## Task 5: status 登记 + 收尾

- [ ] **Step 1: `docs/round2-status.md` §30.5 登记 N81**（表格行，与 N80.x 并列），写明：
  k 的实测值 · 三态分布 · 是否推翻处方「减 10」的措辞 · 跨 pin id 集合比对结果。
- [ ] **Step 2: 若量具自撞出缺陷 ⇒ 另起 N81.1 如实登记**（照 N74 / N77 / N79.1 体例）。
- [ ] **Step 3: `git status` 确认干净 + push**
- [ ] **Step 4: work-log 追加**

---

## Self-Review（写完计划后自查，已执行）

**1. Spec 覆盖**：§5 第 2 项 → Task 3/4；N80.4「分母一动第四节占比全要重算」→ Task 1 Step 4 + Task 4 Step 1；
§2.6「或者判定这一段根本不该算一条 Acceptance」→ 分支 B 的定义。✅ 无缺口。

**2. 占位扫描**：无 TBD / 无「类似 Task N」/ 每个代码步骤都给了可直接粘的实现。✅

**3. 类型一致性**：`STATES` 三个字面量在 Task 1 Step 3/4 与 Task 2 Step 3/5/6 中逐字相同；
`zerosByState[s].zeros` / `zeroIds[s]` / `branches.A[s].pct` 的字段名跨任务一致；
`c.proposalF4Ids` 由 `--triage` 注入（Task 3 Step 1 的命令里已带该参数）。✅

**4. 🔴 一处已知风险**：`--triage` 指向的 `unclassified-fix-triage-b3728d38.json` **未确认存在**
⇒ Task 3 Step 1 前先 `ls`；不存在则先用 `unclassified-fix-triage.mjs --out` 重出一份
（该器 11 条控制全绿过，重跑应逐字复现）。
