# INFRA-F96 — `superpowers/plans` + `specs` 的生命周期判据设计

> 立于 2026-08-03（session V）。回答 [[INFRA-F96]] 指定的第一个决策点：**「plan 的两半分别该活多久」**。
> 与 [[INFRA-F95]] **不重叠**：F95 守 `STATUS.md` 的两个段（段级形态契约），本条守 `docs/superpowers/` 两个目录的**保留规则**。
> ⛔ 本 spec 不改 F95 的任何判据、不碰它的 5-task 计划。

---

## 0. 一句话结论

**两半不用「拆开存放」—— 它们本来就分在两个目录里。真正缺的是：两个目录该有不同的保留规则，而其中一条必须机械化。**

- `specs/`（设计推理）→ **无限期保留，不归档、不瘦身**。
  - ⚠️ **一条例外（owner 2026-09-09 拍）**：**为其它 AI 工具（Codex / claude.ai 等）准备的、状态已 DEFERRED 的跨工具 spec**
    可归档进 `docs/_archive/superpowers/specs/`。owner 逐字：「之前想着后续可以跨工具使用，但是目前来看，难度有点大，
    考虑到当前主要用 Claude Code 来设计，所以现在专注于一个工具就可以，其他 AI 工具的兼容等后续当前这个设计系统完善后再考虑。」
    ⛔ **两个前置，缺一不可**：① 归档 ≠ 删除，一律 `git mv`；② **入站指针先改写成归档路径**再移
    —— 下方 §3 理由 2（「spec 丢了 = 已拍过的决策被重新拍一遍」）对归档件**仍然成立**，靠的正是这条改写后的指针
    （实例：`skills/tvu-design-pipeline/SKILL.md` 指向 TRIG-03 的那条防重造指针）。
    ⇒ ⛔ 本例外**不适用于**在用的 spec、也不适用于「看着旧」的 spec；判据是「为别的工具写的 + 自陈 DEFERRED」两条同时成立。
- `plans/`（执行脚手架）→ **寿命 = 到「仓库里没有任何东西再指向它」为止**，到期 `git mv` 进 `docs/_archive/superpowers/plans/`。

---

## 1. 本轮实测（全部可复核，2026-08-03）

### 1a. 基线

| 项 | 实测 |
|---|---|
| `docs/superpowers/plans` | **1.0 MB / 28 份** |
| `docs/superpowers/specs` | **404 KB / 29 份** |
| 合计 | **1.4 MB ≈ `docs/` 7.5 MB 的 19%** |
| checkbox | 未勾 **1 069** / 已勾 **43** / 总 **1 112** → 勾选率 **3.9%** |

> **订正 F96 entry 一处数字口径**：entry 写「1 069 个 checkbox 只勾了 43（4.0%）」，其中 `1 069` 实为**未勾数**，总数是 `1 112`，勾选率 **3.9%**。量级不变，结论不变。

### 1b. 两处推翻 F96 判断的实测

**① checkbox 不是体量的主要构成 —— 差了约 6 倍。**

```
plans 总字节   1 006 155
checkbox 行      73 718   →  7.3%
fenced 代码块   425 041   → 42.2%
其余散文        507 396   → 50.4%
```

F96 entry 原写「1 069 个里至少 1 026 个是纯仪式……**它们是 1.4 MB 的主要构成**」——**不成立**。把全部 checkbox 删光只省 plans 的 7.3%、总盘的 5.3%。
⇒ **勾选率 3.9% 是有效症状，但「删 checkbox」是无效杠杆。生命周期的单位必须是「文件」，不是「半个文件」。**

**② 归档规则不是「grep 不到任何一条」—— 兄弟目录早就有，只是没被执行。**

F96 entry 写「`WRAP-UP.md`/`AGENTS.md`/`meta-rules.md` 里 grep 不到任何一条」——那三份里确实没有，但 [`docs/internal/_plans/README.md`](../../internal/_plans/README.md)（立于 **2026-07-17**，REDUN-05）**逐字写着同一条规则**，判据还恰好就是可机械核的那个：

> 「归档：session 跑完、pickup 被后续 supersede、且**全仓 0 引用**后，`git mv` 到 `docs/_archive/_plans/`」
> 「归档前先核引用：`grep -rln <pickup-basename> docs/ AGENTS.md CLAUDE.md`，0 命中才移。」

而且 `docs/_archive/superpowers/plans/` **已经存在**（2 份，`8237d609` 移入）。

### 1c. 那条既有规则有没有自执行？——没有

| 观察 | 值 |
|---|---|
| 约定立于 | 2026-07-17 |
| 此后发生的归档事件 | **1 次**，`8237d609`（2026-07-23，message 自称 "move historical documents out of live tree" = **人工大扫除**，不是约定在日常运转） |
| 07-23 之后新变成 0 引用的 `_plans/` 文件 | **2 份**（`2026-07-27-p2-rubric-batch-retest.md`、`2026-07-28-INFRA-F71-gitea-self-hosted-publish-ci.md`） |
| 其中已归档的 | **0 份**（11 天） |

**这与 [[INFRA-F95]] 的核心发现是同一个病**：规则早就写下了，缺的只有 enforcement；靠人偶发发起，术后立刻回填。
⇒ **给 `superpowers/` 再写一份 README 就完事 = 写第三份已被证明不自执行的规则。必须上闸。**

### 1d. 两个目录的性质实测不同

| 维度 | `specs/` | `plans/` |
|---|---|---|
| 散文占比 | **95.8%** | 50.4% |
| fenced 代码 | 3.5% | **42.2%** |
| checkbox | 0.7% | 7.3% |
| 零引用份数 | 11 / 29 | **18 / 28**（**536 KB**，占 plans 字节 **54%**）|
| 被**代码/闸/测试**引用 | **有**：`tests/parity/framework-parity.spec.ts` · `scripts/audit-artifact-routing.mjs` · `scripts/audit-sort-tokens.mjs` · `skills/design-walkthrough/SKILL.md` · `figma-data/page-recipes.json` · `divergences-decisions.json` · `AGENTS.md` · `docs/meta-rules.md` | **几乎没有**：唯一一条代码级引用是 `scripts/check-dist-wc-freshness.mjs` → `2026-07-30-v1x-next-batch` |
| 写完后还被改吗 | — | **28 份里 22 份只有 1 个 commit**；有多个 commit 的 6 份全部在 **1 天内**改完 → 经验上是 **write-once** 产物 |

**那唯一一条代码级引用，恰恰是本判据最强的证据**：它之所以存在，是 [[INFRA-F84]] 删档时抢救约束留下的；而 `STATUS.md` §三 18 现在明写着**同一份 plan 的 Task 6 Step 2③「那句『串尾加』是错的，别照抄」**。
⇒ **一份活在 live tree 里的已完成 plan，此刻正在提供一条错误指令。** 这就是归档要治的真正损害 —— 不是字节。

---

## 2. 判据设计

### 2a. 为什么判据是「可达性」，不是「完成度」也不是「体量」

三个候选判据，按**被规避时内容流向哪**来选（沿用 F95 的选法）：

| 候选 | 机械可判？ | 被规避的方式 | 逃逸方向 |
|---|---|---|---|
| **完成度**（plan 做完了没） | ❌ 不可 —— checkbox 勾选率 3.9%、三个已发布 plan 勾 0，勾选状态与真实完成度**已证无关** | 不用规避，本来就测不准 | — |
| **体量上限**（plans 目录 ≤ N KB） | ✅ | 把内容挤进 `specs/` 或 `docs/internal/` | ❌ **总量不降**，只是换个目录 |
| **可达性**（有没有东西指向它） ← **选它** | ✅ | 想让一份 plan 留在 live tree，就得从 `STATUS`/`backlog`/代码**指向它** | ✅ **指向 = 声明它在飞**，正是 owner 「唯一真源、别处引用」裁定要的动作 |

可达性判据还有一个不可替代的性质：**它同时是「这份 plan 还有没有人管」的定义**。一份没有任何东西指向的 plan，按定义就不在任何工作流里 —— 无论它做没做完。

### 2b. 判据（`pnpm audit:plan-lifecycle`）

| # | 判据 | 阻塞 | fail-closed 点 |
|---|---|---|---|
| **S1** | `plans/` 下每个 `.md` 文件名必须是 `YYYY-MM-DD-<slug>.md` | ✅ | 解析不出日期前缀 → 红（输入形态错不放行）|
| **S2** | 文件名日期早于「今天 − GRACE_DAYS」的 plan，**必须有 ≥1 条仓库内入站引用**；没有 → 红，要求 `git mv` 进 `docs/_archive/superpowers/plans/` | ✅ | — |
| **S3** | 扫描面 fail closed：`plans/` 一个文件都没扫到，或归档目录路径解析不出 → 红 | ✅ | 防「把目录清空让闸空转成假绿」|
| **S4** | 豁免表 **shrink-only**：表里某条已不再命中 → 红，要求删行 | ✅ | 「修完由闸自己宣布」；**表空着是终态，不是待办** |
| **S5** | report-only：印出 live/archived 份数、字节、每份的入站引用数 | ❌ | 让 drift 可见 |

**入站引用的定义（精确，防两种假绿）**：
- 扫描面 = 仓库全域（`docs/` · `scripts/` · `src/` · `tests/` · `skills/` · `figma-data/` · `.design-sync/` · 顶层 `*.md` · `*.json` / `*.yml`），按 **basename 字面匹配**。
- **排除该 plan 文件自身**（否则每份都自引用、闸恒绿）。
- **排除 `docs/_archive/**`**（否则一份已归档文档能把一份活 plan 永久钉在 live tree 里）。
- 来自**其它 live plan** 的引用**算数** —— 你可能正在执行 B 而 B 指向 A。
  ⇒ 连带效果：归档是**迭代收敛**的（archive → 某些 plan 掉到 0 ref → 下次 commit 闸再红）。这是正确行为，不是缺陷；闸会自己把下一批点名。
- **溯源引用不算活指针**（`isProvenanceMention`）：引用文本里带 `_archive/` 路径前缀的，视为**历史出处**而非「它还在飞」。

> ### 2b-1. 这条溯源规则是实现期实测撞出来的，不是设计时想到的
>
> 写完前三条抢救注释（每条都注明「抢救自哪份 plan」）后重跑闸，**点名数从 18 掉到 15** —— 那三条注释把它们抢救自的 plan 变成了「有入站引用」。
> **这是判据的自锁**：越是认真执行 §4a 的抢救搬迁，被搬迁的那份 plan 越归档不掉。
> 修法**不是**在注释里回避写文件名（那会丢掉溯源），而是让判据能区分两类引用：**指向归档后真实路径的 = 溯源**。
> 这条同时是**可验证的**：写了 `_archive/` 前缀却没真归档 = 死链，人一点就发现。
> 已由单测钉住（`isProvenanceMention` 三向 + 一条回归钉复现该自锁场景）。

### 2c. `GRACE_DAYS = 1` 的来源（实测推导，非拍脑袋）

判据本身是**布尔量**（有引用 / 没引用），**不存在噪声底可量** —— 唯一的数值参数是 grace 期。它按「既有合规样本的最大值」推：

```
created=2026-07-14  firstRef=2026-07-14   single-source-demos
created=2026-07-21  firstRef=2026-07-21   form-validation-engine
created=2026-07-22  firstRef=2026-07-22   table-data-grid-spec
created=2026-07-23  firstRef=2026-07-23   ds-merge-two-systems
created=2026-07-29  firstRef=2026-07-30   f73-publish-and-mainline-scheduling   ← 最大滞后
created=2026-07-30  firstRef=2026-07-30   sync-b-retirement
created=2026-07-30  firstRef=2026-07-30   v1x-next-batch
created=2026-08-03  firstRef=2026-08-03   f58-sot-dedup-and-inventory-gate
created=2026-08-03  firstRef=2026-08-03   infra-f95-doc-shape-gate
```

9 个可测样本：**8 个同日，最大滞后 1 天** → `GRACE_DAYS = 1` 覆盖 100% 的既有合规写法。

**日期取自文件名而不是 git**：CI checkout 常是 shallow（`depth: 1`），`git log --diff-filter=A` 在那里拿不到创建日 → 会变成环境相关的假绿/假红。文件名日期是**带内**的、确定性的，且顺带把命名约定变成受检事实（S1）。

### 2d. 余量检查（防「不可满足的闸」）

上闸当天把 18 份零引用 plan 全部归档后，live tree 剩 **10 份 / 445 KB**，全部 ≥1 引用 → **闸落地即绿，豁免表为空**。
新写一份 plan 的合规成本 = 与它同一个 commit 里从 `STATUS`/`backlog`/pickup 指它一次 —— 这**正是本仓库当前已经在做的事**（2026-08-03 那两份 plan 在第 1 天就是 ref=3）。⇒ 闸不制造新负担，只把既有习惯钉死。

---

## 3. `specs/` 为什么不设归档

> ⚠️ **2026-09-09 补**：本节结论仍成立，但有 §0 那条**跨工具 spec 例外**（owner 拍）。
> 下面三条理由逐条对例外的适用性：① 断链 —— 例外要求**先改写入站指针**，不断链；
> ② 决策被重拍 —— 指针改写后仍指得到，且归档件全文保留；③ 字节 —— 与例外无关。
> ⇒ 例外不推翻本节，它是本节三条理由**在指针已改写的前提下**的一个受控出口。
> 实施记录：2026-09-09 归档 2 份（TRIG-03 跨工具编排 · cross-ai-authoring-gate），DS `bdb9ae31`。

三条理由，按强度排：

1. **它有真实的机器消费者。** 18/29 被引用，其中 6 处来自**代码、测试、闸、skill**（见 §1d）。归档会直接断链。
2. **失效模式不对称。** plan 丢了 = 少一份已完成工作的执行记录（且它经验上 write-once、且可能提供错误指令）；spec 丢了 = **一个已经拍过的决策被重新拍一遍，或者被以「已被否决过的理由」推翻**。后者在本仓库有实证代价（`rollback-action-not-safer-than-forward` 那类回滚提案，靠的正是 spec 里记着「这条路被否过、理由是什么」）。
3. **它不在任何 onboarding 读取路径上，且只有 404 KB。** L-core 四份 90 KB 里没有 specs；它的字节不吃 onboarding 预算。花力气瘦身 404 KB 的决策记录 = 负收益。

### ⛔ 明确不做：不建「spec 索引」

看起来很顺的一个补充是「建一份 `superpowers/README.md` 列出每份 spec 设计了什么」。**不做**，理由是它正是本项目一直在治的病：**手工维护的清单 = 会漂的第二副本**（[[INFRA-F58]] 全部工作、`rule-inventory` 闸的存在理由、owner「唯一真源、别处一律引用」裁定，指的都是这件事）。
11 份零引用 spec 的正解**不是**给它们建索引，也**不是**归档它们，而是**在它们所设计的那件东西旁边留一个指针**（就像 `audit-sort-tokens.mjs` 头部指回自己的 spec 那样）。那是逐条的判断题，**本轮不做，作为独立项登记**（见 §6）。

---

## 4. 落地物

| # | 产出 | Enforcement（meta-rules 触发器 K）|
|---|---|---|
| 1 | `docs/superpowers/README.md` —— 两个目录的保留规则 + 判据 + 归档怎么做 | L1（规则真源，供人读）|
| 2 | `scripts/audit-plan-lifecycle.mjs` + `pnpm audit:plan-lifecycle` | **L4** `.husky/pre-commit` + **L5** `prepublishOnly`（→ 同时进 Gitea `pr-checks` 与 master-push CI，满足 gate 平权 [[INFRA-F61]]）|
| 3 | 单测 `tests/plan-lifecycle.test.ts` —— must-fire / must-not-fire 双向 | 随 `pnpm test` 跑（本身也在 pre-commit 里）|
| 4 | 一次性清零：18 份零引用 plan（536 KB）`git mv` 进 `docs/_archive/superpowers/plans/` | —— |
| 5 | 清零**前**的抢救扫描：确认没有「只活在这些 plan 里的仍生效约束」（[[INFRA-F84]] 先例）| —— |

### 4a. 抢救扫描协议（⛔ 不可跳过）

F96 entry 的 ⛔ 第 ② 条：**已发布 plan 里可能夹着只活在那里的约束**。归档虽然可逆（`git mv`，历史完整），但一条约束一旦离开 live tree 就**搜不到**了，等于失效。
⇒ 移动前对每份待归档 plan 做一遍分类扫描，把判为「仍生效 + 仓库别处无第二份」的约束**先搬到它约束的那个对象旁边**（脚本头注释 / 闸的判据注释 / `AGENTS.md`），再移动。搬运结果逐条记进本 spec 的执行附录。

---

## 5. 验证协议

沿用本仓库既有纪律（pickup §3 + F95 ⛔③）：

1. **落地前先跑基线** —— 在归档**之前**跑一次闸，必须 **exit 1 且精确点名 18 份**。这一步证明它不是空过。
2. **故障注入，逐条只红对应判据**：
   - 造一份 `2026-01-01-fake-plan.md`（无引用、超 grace）→ 只 S2 红。
   - 造一份 `no-date-prefix.md` → 只 S1 红。
   - 把 `plans/` 扫描面指向空目录 → S3 红（**不是 exit 0**）。
   - 往豁免表加一条不命中的 → S4 红。
   - 给 fake plan 从 `STATUS.md` 加一条引用 → 转绿（证明判据真的读引用，不是数文件）。
   - **阴性对照**：引用只写在 `docs/_archive/` 里 → **仍然红**（证明 `_archive` 排除生效）。
3. **复原后 `sha256` 逐字节比对**，确认注入无残留。
4. 全量回归：`pnpm test` + `pnpm run audit:status-consistency` + `audit:rule-inventory` + `audit:stale-anchors`。
   > `audit:stale-anchors` 的 `SCAN_FILES` 只有 3 份（`mockup-conventions` / `code-conventions` / `meta-rules`），**都不在 `superpowers/`** → 移动 plan 不会撞它。已实读源码确认，非推断。

---

## 6. 明确不做（本轮 scope 外，逐条给理由）

| 不做 | 理由 |
|---|---|
| **删除**（而非归档）任何 plan | 不可逆；F84 抢救先例说明风险真实存在；且归档已经达成目的（「一眼分辨谁在飞」+ 错误指令离开 live tree）。⚠️ **归档不降仓库字节**，只把 `plans/` 从 982 KB 降到 445 KB（`superpowers/` 合计 1.4 MB → 0.83 MB） —— 若 owner 要的是真降字节，那是另一个（不可逆的）决定，本轮不替他拍。 |
| 给 11 份零引用 spec 补指针 | 逐条判断题（每份要判「它设计的东西现在在哪个文件」），11 次判断；且与本轮的机制无耦合。作为独立项登记。 |
| 精简 plan 内部（删 checkbox / 压代码块）| 实测只值 7.3% / 42.2%，而单位应该是文件。且改动量大、收益低。 |
| 并进 [[INFRA-F95]] | F96 entry ⛔① 明令禁止；F95 的 spec/plan 已 commit、scope 冻结。 |
| 动 [[INFRA-F58]] 残余② 规则三层化 | 2026-08-03 已测出器械与目的不匹配，STATUS §一 13 待 owner 拍。 |

---

## 7. 与既有约定的关系

`docs/internal/_plans/README.md`（pickup 归档约定）与本 spec 是**同一条规则的两个实例**，判据字面相同（全仓 0 引用）。
本轮**不合并、不改写**那份 —— 它管 `_plans/`（session pickup），本条管 `superpowers/plans/`（实施计划），两者的活跃判据虽同，归档落点不同（`_archive/_plans/` vs `_archive/superpowers/plans/`）。
**但闸的实现按可扩展写**：扫描配置是一张表，将来要把 `_plans/` 也纳入同一条闸，改一处即可（反模式 #5）。⚠️ 现在**不**顺手纳入 —— `_plans/` 的活跃判据里还有一条「被 STATUS 固定指针引用的留原位」，与本判据的交互没测过。
