# backlog F 号复用闸 —— 口径预注册（B 档）

> 队列 Q9 尾段撤回「属 C1」后判 **B 档**（三个读数否掉了摩擦这个理由：`backlog.md` 已在
> pre-commit 触发面内 · 范式已有 · 修法唯一）。**须预注册口径** ⇒ 本文件。
> ⛔ 判据先写死再动手。

---

## 0 亲验读数（⛔ 单跑、不带管道）

| # | 读数 | 出处 |
|---|---|---|
| **A** | 既有闸 `audit-rule-number-collision.mjs` 的头注释**自己就点名了这个洞**，逐字：「③ **已删档号被重新占用** —— backlog 的纪律是「完成即删档」，被删的 ID 在 `origin/master` 的文本里已不存在 ⇒ 有人拿它新建 entry 时**本闸看不见**。那一条仍归 L1 纪律 + `pnpm new-backlog`」 | 该文件头注释「覆盖面」段 |
| **B** | S1 的判据是「新增 ID ∈ **`origin/master` 的标题行 ID**」⇒ 结构上看不到已删档号（标题行没了） | 同文件 `remoteIds = extractor.ids(remoteText)` |
| **C** | 🔴 **它给建议号用的扫描面正是判它所需的输入**：`BACKLOG_ID_SOT_FILES` 三份（backlog + STATUS + tracker）× (本地 ∪ remote) 全文，函数 `backlogNextIdFromTexts`，返回值里就有 `used`（该前缀已用过的号）。⇒ **机制已存在，只是没被当判据用** | 同文件 `BACKLOG_ID_SOT_FILES` + `scripts/lib/rule-ids.mjs:242` |
| **D** | **分母**：`origin/master` 上 backlog.md 标题行 ID **26** 个；三份 SoT 全文提及 **137** 个 ⇒ **已无标题行但仍被提及 = 111 个**。**这 111 个就是 S1 完全看不见、而本次新增判据要保护的号池** | 探针 `probe-f-numbers.mjs`（复用 lib 的 `extractBacklogIds` + `backlogIdPattern`）|
| **E** | `INFRA-F130`：标题行 **false** · 被提及 **true** ⇒ **可用作真造故障靶**（不必合成） | 同上；且该闸源码注释里逐字提过它「已落地删档、仍被 STATUS/tracker 引用」 |
| **F** | `INFRA-F145`：标题行 **true** ⇒ 它此刻是活 entry（正是队列记的那次复用的结果）。⇒ 再复用它 S1 就够；**F145 那次之所以漏，是因为当时它没有标题行** | 同上 |
| **G** | 挂载面：**L4** `.husky/pre-commit:210`（`^docs/internal/backlog\.md$` staged 即触发）+ **L5** `.gitea/workflows/pr-checks.yml:355`（带 `--require-remote`）⇒ 新判据**自动继承真牙**，边际挂载成本 0 | 两处单跑 grep |
| **H** | 该闸**没有任何既有测试**（`ls tests \| grep rule-number` ⇒ 空）⇒ 本轮要连测试一起建 | — |

---

## 1 口径（⛔ 这是本文件的核心，落地后不许悄悄改）

**新增判据 S5「号一旦发出去就不许被第二条 entry 占用（含已删档历史号）」**

- **判定对象**：本地**新增**的 backlog entry ID（= 不在 `merge-base` 上的），与 S1 同一个集合
  （`newLocal`），⛔ 不重新定义「新增」。
- **判据**：该 ID 的号 ∈ `backlogNextIdFromTexts(prefix, **remoteTexts**).used` ⇒ **红**，
  并给出 `nextId` 建议。
  - `remoteTexts` = `BACKLOG_ID_SOT_FILES` 三份在 **`origin/master`** 上的全文。
  - 🔴 **⛔ 只用 remote，不掺本地** —— 这不是省事，是防一个必然的假阳：
    新 entry 自己就写在**本地** `backlog.md` 里，掺进本地文本 ⇒ 任何新号都「已被提及」⇒ **恒红**。
    恒红 = 零信息，且它长得像 fail-closed（本仓 `AGENTS.md` 第 25 条已命名此病）。
- **算式复用**：⛔ 不新写 mention 正则 —— 直接调 `backlogNextIdFromTexts`，
  与建议号那一半、与 `new-backlog.mjs` **共用同一份**（判据抄第二份就会漂）。
- **去重**：已被 S1 报过的 ID ⛔ 不再由 S5 重报（S1 的消息已点名并给了建议号）。
- **范围展开的条目**（`e.viaRange`）跳过，与 S4 既有语义一致。
- **降级语义继承 S2**：拿不到 `origin/master` ⇒ 整条闸 fail-open（CI 上 `--require-remote` 反转为
  fail-closed）。⛔ 不给 S5 单独发明一套降级。

### ⛔ 如实登记的天花板（不是 TODO，是这条判据能看到的上限）

号被删档、**且三份 SoT 里所有提及也被一并删掉** ⇒ remote 全文无痕 ⇒ **S5 看不见**。
这与 `pnpm new-backlog` 的天花板同源（它扫的是同三份）。
⇒ ⛔ 不许说「号复用不会再发生」，只能说「**只要历史提及还在这三份里，复用就会被拦**」。
当前该保护面 = **111 个已删档号**（读数 D）。

---

## 2 判据（⛔ 跑之前写死。每条两侧）

### P1 · 真造故障：拿一个**真的已删档号**新建 entry ⇒ 必须红并点名
往 `docs/internal/backlog.md` 插一条 `### INFRA-F130 — <标题>`（读数 E：该号无标题行、有提及）
⇒ 闸 **EXIT=1**，消息里必须**逐字出现** `INFRA-F130`，并给出一个建议号。
⛔ 只验「红了」不算 —— 必须点名是哪个号，否则修的人不知道改什么。

### P2 · 阴性对照：一个**真正没被用过**的号 ⇒ 绿
用 `backlogNextIdFromTexts('INFRA', remoteTexts).nextId` 现算出的那个号新建 entry
⇒ 闸 **EXIT=0**（⛔ 不许出现该号的任何 finding）。
🔴 **这一条是本批最要紧的阴性对照**：没有它，「恒红」也能通过 P1。

### P3 · 自指不许成为假阳
P2 那条 entry 写进**本地** backlog 后，S5 仍绿 ⇒ 证明判据没掺本地文本（口径 §1 那条）。
**造故障**：把实现改成 `[...remoteTexts, localBacklogText]` ⇒ P2 **必须转红**
⇒ 证明「只用 remote」这个选择真的在承重，⛔ 不是一句注释。

### P4 · S1 仍然独立生效（⛔ 新判据不许把旧的挤掉）
拿一个**当前有标题行**的号（如 `INFRA-F145`，读数 F）新建第二条 entry
⇒ 仍由 **S1** 报（`collisions`），且 S5 ⛔ 不重复报同一个 ID。

### P5 · 不误伤既有仓库状态
不做任何改动时单跑该闸 ⇒ **EXIT=0**，且输出里 S5 的 finding 数 = **0**。
（⚠️ 当前本地相对 merge-base 的新增 backlog ID 数可能为 0，那样 P5 只证明了「没炸」；
真正的承重在 P1/P2/P3。**⛔ 别把 P5 的绿当成判据有效的证据。**）

### P6 · 形态坏了要 fail-closed（继承 S3）
把 `extractBacklogEntries` 的返回改成空数组 ⇒ 仍走既有 S3 的 fail-closed 出口（exit 1）。
⛔ S5 不许在「一个 ID 都解析不出来」时静默绿。

### P7 · 测试与全量回归
新建 `tests/audit-rule-number-collision.test.ts`（读数 H：此前零测试），P1–P4 逐条落成用例；
`pnpm test` ⇒ 零新增失败（基线 3221 passed）。

### P8 · 还原后零残留
每次造故障后 `git diff` / `cmp` ⇒ 逐字一致。

---

## 3 ⛔ 本轮不能说的话

- ⛔ 不许说「号复用已根治」—— 见 §1 天花板：提及被一并删净的号仍不可见。
- ⛔ 不许说「111 个号都被验过」—— 验的是**判据**，造故障用了其中 1 个（`INFRA-F130`）。
- ⛔ 不许把 P5 的绿当证据（见 P5 自带的警告）。
