# 2026-07-30 复盘 — 闸的「分母口径」怎么选，以及计划草稿有多不可信

> 触发条件（[`WRAP-UP.md`](../../WRAP-UP.md) §Retrospection 触发判断）= **学到通用工程经验 / 发现新 pattern** → 必写。
> 本 session（C）范围：v1.x 下一批排期 + 计划文件 + 执行 Task 2/3。commit `611b05c2` / `e31a4a3f` / `bf0048e1` / `cd6040bd`。

---

## 1. 最有价值的一条：**闸的分母要选「被保护的那一方能感知的层」，不是最容易拿到的层**

### 病灶

`audit:page-recipes` 的 S2 判据 = 「`slots[].component` 必须是真实存在的 canonical 组件」。首版实现取分母的方式是最顺手的那个：

```js
readdirSync('src/canonical').filter(f => f.endsWith('.vue')).map(f => basename(f, '.vue'))
```

第一个配方 `data-table-page` 只引用 `Table` / `Pagination` —— 这两个**恰好**文件名与导出名同名，所以闸绿得毫无异样。缺陷直到写第二个配方、需要引用按钮时才暴露：

- 公开导出名是 **`Button`**（`src/canonical/index.ts:1` → `export { default as Button } from './ButtonBridge.vue'`）
- 文件名是 **`ButtonBridge`**

于是首版 S2 的行为是**双向错**：

| 写法 | 首版 S2 | 应该 |
|---|---|---|
| `component: "Button"` | ❌ 判为不存在 | ✅ 合法 |
| `component: "ButtonBridge"` | ✅ 放行 | ❌ 该拒（未被任何 barrel 导出） |

第二行才是真正危险的：`page-recipes.json` 存在的意义是**告诉 AI 该用哪个组件**，AI 照它写的是 `import { X }`。一条放行 `ButtonBridge` 的闸，会把 AI 稳定地引向非公开 API —— 闸从"防错"变成"教错"。

### 通用教训

**给一条 gate 选分母时，先问「这条闸保护的是谁的感知」。**

- S2 保护的是**消费方/AI 写代码时的正确性** → 分母只能是「能 `import` 到的名字」。
- 文件系统那一层（`readdirSync`）是**实现细节**，跟被保护方的感知隔了一层导出映射。两层大部分时候重合，**分叉时错的恰好是容易拿到的那层**。

同型追问（下次设计闸时机械过一遍）：

| 闸要断言的事 | 容易拿到的分母 | 被保护方感知的分母 |
|---|---|---|
| 「这个组件存在」 | `src/**/*.vue` 文件名 | barrel 的导出名 |
| 「这个 token 存在」 | CSS 里 `--x:` 声明 | 打包后 `dist/style.css` 里真发出去的声明 |
| 「这个 export 可用」 | `package.json` 的 `exports` 字段 | 真 `npm pack` 后 tarball 里的文件 + 真 `require.resolve` |

后两行本 session 没做（S3 用的是 `variables.css` 而非 `dist/style.css`；计划 Task 6 里 export 面**已经**写成"真 pack + 真 fresh install 验证"而不是读 `package.json` 自证）。**S3 那条是已知的、有意接受的近似**：`variables.css` 是 token 真源，打包只是复制；但如果哪天出现"定义了却没进 dist"的 bug（v0.9.0 的 `ship-token-css` 正是此类，潜伏六个版本），S3 就会跟着放行。已在此留档，不现在扩。

### 最终落地

分母 = **两个 barrel 的并集**，逐个实测定的，不是猜：

- `src/index.ts`（npm 包入口）独有 `PillCounter` / `Logo`
- `src/canonical/index.ts` 独有 `Chart`（经 `./chart` 子路径分发，不在主入口）
- 并集 41 个名字，每个都确实可 import；两边都不含 `ButtonBridge`

**刻意保留的两处"不完美"，都写进了脚本注释而不是悄悄处理：**

1. `UserMenu` / `MenuList` / `InputBoxBase` / `SelectBoxBase` 被拒 —— 这是**正确信号不是误报**。将来配方真要引用，正解是先导出或改引用宿主组件，不是放宽闸。
2. 包入口另导出 6 个图标/Logo 单体（`IconAdd` / `LogoTVU` 等）被放行 —— 它们同样真能 import。**没有**用"第几个 export 块"这种位置启发式去剔：那种判据会在有人重排 `src/index.ts` 时静默失效，比这点宽松更糟。

---

## 2. 第二条：**计划草稿里的具体值，执行时命中率可能是 0**

本 session 上半场写的计划文件（10 Task）在 Task 3 的配方草稿里写了 4 个具体断言，执行时**逐条对活源核，3 条是错的**：

| 草稿写的 | 活源实测 | 病根 |
|---|---|---|
| `submitting` → `prop:loading` | **Button 没有 `loading` prop** —— loading 是 `status` 枚举的一个值（配 `icon='loading'`） | 照"别的组件库通常这样"想的 |
| `layoutTokens` 含 `fieldToFieldGap` | `.tvu-form` **自己内建** `gap: var(--sp-m)`，app 再加会叠双倍 | 没读组件的 `<style>` 就假设间距是外部责任 |
| `field-invalid` → `prop:rules` | 实测是 `FormItem.error`（`error?: string` + `status?: 'Error'\|'Normal'`） | 把"什么开启了校验"当成"什么驱动了这个视觉态" |
| `states[]` 用 `{state, drivenBy, description}` | 实际是 `{state, when, driven_by, note}`；`layoutTokens` 是**对象**不是数组 | 照 `_meta.schema_note` 的**散文**归纳，没读 JSON 本体 |

第 4 条在写计划时就被自己预防了（计划 Step 1/2 明确要求"先读实际 JSON 再定稿 schema，以数据为准，不反过来改数据"）—— **那条预防生效了**。前 3 条没有对应的预防，就全错了。

### 通用教训

**计划文件里凡是"具体值"（prop 名 / token 名 / 字段名 / 文件路径 / 行号），都要标成「执行时必须对活源核」，而不是当成已确认的事实写下去。** 计划的价值在**顺序、判据、验收方式**——那些是我推理出来的；具体值是我从上下文里"想起"的，命中率不保证。

已落地的两条动作：

1. Task 3/4 的草稿在执行时就地改成实测形状，并**抽出来真喂进闸校过**（S1/S2/S3 全绿），不是"照文档改文档"。
2. Task 6 的 `dist-wc` 入口文件名在计划里本来是占位示例 —— 实测发现真名是 `tvu-web-components.js`，已回填，并加了一条"`types` 指向的 `.d.ts` 是否真存在要用 `find dist-wc` 确认，别写一个指向不存在文件的 `types` 字段"。

---

## 3. 第三条：**单测耦合到「真 fixture 恰好有 N 条」是会定时炸的脆性**

Task 2 写的 23 条单测里有 5 条形如：

```ts
bad.recipes[0].layoutTokens.toolbarToTableGap = 'var(--sp-invented)'
const r = validateLayoutTokens(bad.recipes, ['--sp-m'])   // ← 窄分母 + 整个数组
```

只改 `recipes[0]`，却把**整个** `real.recipes` 喂给一个手写的窄分母。文件只有 1 条 recipe 时全绿；Task 3 加了第二条配方后，其中 2 条立刻假失败（`accepts component: null` / `unwraps var()`）—— 因为窄分母不覆盖新配方，报的是"新配方的 token 没定义"，跟被测的判据毫无关系。

**正解**：传窄分母的用例只喂**被测那一条**（`[bad.recipes[0]]`）。已全部改掉并把这条纪律写进测试文件头注。

**教训**：`must-fire` 测试用手写窄分母是对的（隔离判据），但**喂给它的数据也必须同样窄**。两者不匹配 = 用例的失败原因会随 fixture 增长而漂移。

---

## 4. 流程侧：一次并行写对撞，与 worktree 的净负担

### 4.1 共享 index 把我的文件卷进了别人的 commit

我 `git add` 计划文件后跑 `git commit -F msg`（**无 pathspec**），并行 session 在同一秒先 commit 成功 → 我的 1917 行计划文件被并进他们的 `558b914a`「feat(tools): host-monitor…」。文件内容无损、已 push，不能改写历史，只能用后续 commit 补记 rationale。

根因：**同一仓库的多个 session 共享同一个 `.git/index`**，别人 `git add` 过的东西就在你的 index 里。`git status --short` 起手就显示了 `M `/`A `（第一列是 index 列）——信号在眼前却没读。

对策（后续 4 个 commit 全部照做，无一次再出问题）：

```bash
git commit -F <msgfile> -- <显式路径…>    # -F 必须在 -- 之前，否则 msgfile 被当 pathspec
```

路径限定 commit **完全忽略** index 里的其他条目。已存 memory `feedback_git-index-shared-across-sessions`。

### 4.2 worktree 这次是净负担

按纪律"明知有并行 session 就起手开 worktree"开了 worktree，连撞三坑：

1. 无 `node_modules` → `vue-tsc not found`
2. 无 `dist-wc/` → `@tvu/wc` 解析失败（93 files pass，只这一个 suite 红 = 缺产物，不是回归）
3. **husky 的 `core.hooksPath` 解析到主仓的 `.husky/`**，而 `pnpm run` 的 cwd 是 worktree → 主仓新加的 F79 hook 块配 worktree 旧 base 的 `package.json`，报 `Missing script: audit:variables-freshness`

第 3 坑反而有用：它暴露出 master 已被并行 session 推进 3 个 commit、worktree base 全过期。最终退回主仓在最新 base 上重做（baseline 已在新 base 复验，全部成立）。

**结论**：worktree 隔离的是工作树，**隔离不了 STATUS.md 那类共享文件的语义冲突**，也换不来免费的 clean commit（4.1 才是那个问题的解）。**只产出 `.md` 的 session 不该开 worktree。** 已存 memory `feedback_worktree-lacks-node-modules-and-uses-main-hooks`。

---

## 5. 做对了、以后照做的

- **闸先于数据**：`audit:page-recipes` 排在补第二/第三个配方**之前**落地（APID-01 自己留的 follow-up 原文就是"多实例前补 validator"）。如果反过来，S2 的分母缺陷会被两条配方的数据固化，修的时候要回改数据。
- **故障注入证「各只红对应那一条」**：四组注入（拼错字段名 / 非 kebab id / 组件不存在 / 编造 token）不只验了"会红"，还验了**其余判据保持 ✓**。这是区分"真闸"与"见改动就红的假闸"的关键 —— 后者会逼人写豁免注释绕过它，最后没人当真。
- **推荐自审 4 问抓出自己两条编的依赖故事**：「DG-1 unblock PAT-01」（半编 —— `layoutTokens` 实测已在引用 `--sp-*`，出新配方不需要 breakpoint token）、「React npm 路径解锁 F76」（假 —— MicroApps 是 vue-app）。自审不是形式，这两条如果留在计划里会误导排期。
- **发现该做的事不属于本闸时，写进注释而不是悄悄扩范围**：divergence 那条（见下）。

## 6. 一处「计划让做、执行时证伪」的判断

计划 Task 3 Step 5 要求给新配方登记 code-first divergence。执行时实测推翻：

- 第一个配方 `data-table-page` 本身**没有** divergence 条目（40 条里零命中）
- `divergences-decisions.json` 登记的是硬规则 #5 意义上的「Figma↔Code **不一致**」；页面配方不新增代码 API 面、不产生新差异，只引用已各自登记过的组件
- 证据 = **不加任何新条目**，`audit:translation-completeness` 就是 exit 0

给它开条目是 category error，且会与第一个配方的处理不一致。已作废该步骤、在计划里写明理由，Task 4 的同型步骤一并改掉。

**教训**：计划是自己写的也一样要被证伪。"计划让我做"不是做的理由，"实测支持"才是。

---

## 附：本 session 的可复核证据位

| 内容 | 位置 |
|---|---|
| 排期决策 + 推荐自审 4 问留痕 + §不做清单 | [`_plans/2026-07-30-v1x-next-batch-ai-consumable-page-layer.md`](../../superpowers/plans/2026-07-30-v1x-next-batch-ai-consumable-page-layer.md) §排期依据 |
| S2 分母口径为何是并集（含反例留档） | `scripts/audit-page-recipes.mjs` → `readPublicComponentNames()` 的 JSDoc |
| 「窄分母只喂被测那一条」纪律 | `tests/audit-page-recipes.test.ts` 头注 |
| 三层判据 + 明确不覆盖 | `figma-data/page-recipes.schema.json` 的 `description` + `PROJECT_MAP.md` §2 |
| 故障注入原始输出 | 本 session commit message（`611b05c2` / `bf0048e1` / `cd6040bd`）逐条列了 exit code |
