# 双框架 Phase 0（生成器）+ Phase 1（低风险批 15）复盘

> **日期**：2026-06-30
> **承接**：[`dual-framework-react-pilot-findings-2026-06-29.md`](./dual-framework-react-pilot-findings-2026-06-29.md)（pilot + §7 owner 决策）
> **计划**：[`docs/superpowers/plans/2026-06-30-dual-framework-rollout-generator-and-low-risk-batch.md`](../../superpowers/plans/2026-06-30-dual-framework-rollout-generator-and-low-risk-batch.md)
> **执行**：subagent-driven-development，全程在 master 直 commit（Owner override），逐 task implementer→review-package→reviewer→fix→ledger。

---

## TL;DR

pilot 验证路线 A 后，owner 拍板全量分批。本 arc 交付 **Phase 0 生成器基础设施** + **Phase 1 低风险 15 组件**：
- 生成器（config 单源 → WC 注册 + React wrapper + 类型）证明 ROI——重生成 3 个 pilot 过同一 React gate **0 A_TRUE_DRIFT**，与手写逐项一致。
- 15 组件中 **13 个 render-gate 验证 0 A_TRUE_DRIFT**（cross-gate 逐条等价 Vue），**2 个（PillCounter/Breadcrumb）无 manifest → 生成 + smoke 但 gate-unverified**（owner 已知共享覆盖缺口）。
- 最终 whole-branch review（opus）= Ready to merge，0 Critical/Important。

---

## 阶段

### Phase 0 — 生成器基础设施（6 task，commits 96f3c41e→98cd0f82，已推）
1. config schema + `reserved-names.ts`（`style`→`variant` 单源重映射）。
2+3. 配置驱动生成器 + 纯模板函数 + TDD 单测（golden 目标=复现 3 pilot wrapper）。
4. 重生成 3 pilot → React gate 180/0（生成器 ROI 坐实）。
5. 抽共享 comparator `drift-compare-core.ts`（Vue + React 同源，双 gate 不变）。
6. React token 入口 `styles.css` + config↔SFC parity audit + pre-commit 挂钩。

### Phase 1 — 低风险批 15（commits 88d8a17e→8f74f2f3）
按 interop 形态分批：A 纯 props/slot 6 → B v-model:status 3 → C v-model:modelValue 4（含 number）→ D 无 manifest 2。

---

## 关键决策

1. **生成器优先**（§7 决策 4）：把 pilot 证明高度机械的 wrapper 层抽成 config 驱动生成器。回报=15 组件几乎零手写、单源扩展、3 次模板加固一次性惠及全部。
2. **按 interop 形态分批**（非 findings §6 的笼统"复合/slot"）：纯 props/slot → v-model:status → v-model:modelValue → 无 manifest。每批暴露并修复一类生成器类型缺陷。
3. **2 个无 manifest 组件诚实标 gate-unverified**（owner 拍板）：不为共享覆盖缺口阻塞 parity；保真靠 parity audit + smoke + 同管线。

---

## 核心技术学习：生成器的 3 次 root-fix（由组件多样性暴露）

生成器在 pilot（Button 无 v-model / Input string v-model / FormItem 无 v-model）上的 string-centric 假设，随组件多样性扩大逐一暴露，**全部修在模板根因层，后续批自动继承**：

| # | 缺陷（哪批暴露） | 根因 | 修法 |
|---|---|---|---|
| 1 | TopBar 5 具名 slot → wrapper prop-only（Batch A） | 模板只在 `hasDefaultSlot` 渲染 children | `hasDefaultSlot \|\| namedSlots.length>0` → 渲染 `{children}`（具名 slot 靠 light-DOM 投影，FormItem 已证） |
| 2 | union/number v-model 默认 `''` 越界（Batch B/C） | 模板硬编码 `= ''` 默认（对 string 对、union/number 错） | type-aware：`''` 仅 string，否则无默认 + 守卫 down-push |
| 3 | Vue emit handler 收到 raw Event 非 detail[0]（Batch C Pagination，Critical） | events[] 模板直挂 listener（对 native 对、Vue emit 错） | `EventConfig.detailIndex`：有=unwrap detail[idx]，无=native 透传 Event |

**教训**：分批 + 每批过 gate/review 正是为了在低风险阶段把这些类型缺陷挤出来，而非到高风险组件才发现。pilot 的 3 组件覆盖面不足以暴露它们。

---

## 路线 A 保真度：cross-gate 验证方法

每批 controller 不只看 React gate 的 `A_TRUE_DRIFT=0`，而是**逐组件对比 React gate vs Vue gate 的分类**（同一 manifest 条目、共享 comparator）。结论：**React 与 Vue 对每个组件逐条等价**——所有 B_RESIDUAL 两侧数字完全相同（Switch 90-B、Slider 10-B 等），证明这些是继承自 Vue 侧的预存 Figma schema gap，**不是 React 引入的漂移**。这是路线 A"单源继承不叠加偏差"论证的实测坐实。

---

## 过程学习

- **subagent 越权改 SoT**：Batch B implementer 自作主张 commit 了一条 STATUS.md ㊱ 条目（未要求、过早、不全）。controller 核 git log 抓出（agent 摘要未提）。修正：后续 dispatch 显式禁止碰 docs/STATUS 等 SoT + 限定单 commit；STATUS 综合条目由 controller 在 wrap-up 统一重写。**"核磁盘实物别信摘要" 再次验证有效。**
- **commit/push 纪律**：Phase 内本地 commit，phase 边界一次性 push（避免每 commit 触发 master CI）。
- **生成器加载 TS config**：Node 24 原生 strip types，`.mjs` 可 import `.ts` config；生成器是 dev 工具（CI 不跑 gen，故 `engines>=20` 不动以免误伤消费者）。

---

## Minor follow-up（whole-branch review 清的 cosmetic，未阻塞 merge）

单独 `chore` commit 处理：① 生成 smoke-test 描述 `'renders without throwing'` 名实不符（只 check toBeDefined，未 mount）→ 统一改名（注意 write-if-absent，15 个现存需手改）；② 模板 `vModelConst=''` inert dead code 删除；③ 测试 describe 标签错挂；④ vModel down-push 注释硬编码 "value" 即使 prop 是 status。

---

## 剩余工作（14 高/中风险组件，走后续独立计划）

- **Plan 2 — overlay/teleport**：PopupBox、SelectBoxBase/Line/Filled、DropDownListSelect、Tooltip。关键未知：teleport 节点落 document.body（shadow root 外）仍继承 token（应可，token 在根）；comparator 定位 teleported 根。Button 已 teleport 且过闸 → 基础已证。
- **Plan 3 — provide/inject 多元素组合**：Tab/TabList/TabItem、Steps/StepItem。**最可能触发 route-B**：若父子注册成独立 custom element，provide/inject 不跨 shadow 边界——须早验（选项：整族注册成单 element，子件为内部 Vue 组件）。
- **Plan 4 — canvas**：Chart（chart.js）。canvas 在 shadow DOM 内尺寸/渲染 + gate 范围诚实界定。
- **Plan（medium）**：Notification/Message（inline 内容，非 overlay——纠 findings §6）、Table（复合无 teleport/inject）。
- **Plan 5（并行非阻塞）**：node-level 覆盖（`data-figma-node-id`，INFRA-F41）+ 给 PillCounter/Breadcrumb 补 manifest（两端共享收益）。

---

## 工作区状态

- Phase 0 已推 Gitea+GitHub（…98cd0f82）；Phase 1（…8f74f2f3）+ 本复盘 + STATUS 本 commit 推送。
- 生成器 + 18 wrapper（3 pilot + 15）+ config 单源 + parity audit + pre-commit gate 全在仓库。
- 未碰已发布 Vue 包；react-pilot / src/web-components / tests/render-verification-react 独立。
- ledger：`.superpowers/sdd/progress.md`（gitignored scratch）。

---

# Batch E / F / G — 剩余 14 组件全落地（2026-06-30 续，29/29 完成）

> 承上：上文「剩余工作」的 Plan 2/3/4/medium 全部完成。最终 React gate **930 entries / A_TRUE_DRIFT=0（全 29 组件）**，cross-gate 对 Vue 逐组件 IDENTICAL。

## Batch E — provide/inject 家族（Tab/TabList/TabItem/Steps/StepItem，commit 168906f2）
- **route-B 担忧未成立**：父子作为各自 custom element 注册，孤立渲染时 inject 落默认值，TabList/TabItem/StepItem 有 manifest 条目过闸 A=0；Tab/Steps 容器无 manifest → 生成 + smoke 但 gate-unverified（同 D 的 PillCounter/Breadcrumb，owner 已知共享缺口）。

## Batch F — overlay/teleport（PopupBox/SelectBoxLine/SelectBoxFilled/DropDownListSelect/Tooltip，commit 94205752）★最大架构学习
- **pickup 的 `shadowRoot:false` 修法是【未验证假设】，实测被否决再纠正**。三段实证：
  1. 纯 shadow 模式（并行 session 的 ac63dffb）：PopupBox 12 A（teleport 内容逃出 shadow root 拿不到 scoped CSS）+ DropDownList 1 A（`html[data-theme]` 祖先选择器跨不了 shadow 边界）。
  2. 仅 `shadowRoot:false`：PopupBox **仍 12 A**、DropDownList **反而 1→134 A**（更糟）。根因 = **Vue 3.5 在 light-DOM 模式明确拒绝注入 styles，只 `warn`**（runtime-dom `_applyStyles` 守卫：`if(this.shadowRoot) _applyStyles(styles); else if(styles) warn(...)`）。
  3. 正解 = **`shadowRoot:false` + 手动把组件编译后 `styles` 注入 `document.head`**（`injectLightDomStyles`，由生成器从 config 生成）。PopupBox 的模态/Teleport/样式全在 `BasePopupBox`（canonical 仅转发、无 `<style>`）→ 经新 config 字段 `lightDomStyleSources` 把 base 的 styles 一并注入。→ 全部 A=0。
- **教训**：pickup/handoff 里的"修法"若未被 gate 实证，一律当假设，spike 验证后再落；"核磁盘实物别信摘要"在 git 层再次应验（见下）。
- **生成器扩展**：`ComponentConfig` 新增 `shadowRoot?`（per-component CE shadow 开关）+ `lightDomStyleSources?`（样式在 base 的组件的额外样式来源）；模板 `renderRegisterEntry`/`renderRegisterFile` 生成 light-DOM 注入。一处缺漏会以 A_TRUE_DRIFT 暴露在 gate（前向 enforced，非静默）。
- SelectBoxLine/Filled（闭合态）+ Tooltip（position:absolute 非 teleport）留默认 shadow，0 A。

## Batch G — inline/composite/canvas（Notification/Message/Table/Chart，commit 17c0b971，→29/29）
- **全默认 shadow DOM**：4 个组件均无 teleport、无 `html[data-theme]` 祖先选择器（scoped CSS + CSS 变量穿透 shadow 边界）→ 不需 light-DOM 注入。shadowRoot:false 的复杂度被隔离在 F。
- Notification：3 个零-payload emit（close/cancel/confirm）同 PopupBox 同款 `() => void` 无 detailIndex（handler 忽略 CustomEvent）。Message：canonical 不转发 base 的 close emit → events 留空（镜像 canonical 表面）。
- **Chart（canvas）gate scope 诚实界定**：chart.js 画到 `<canvas>`，gate 只读 `.tvu-chart` wrapper computed style（box/padding/radius/bg/legend-text），**canvas 像素（slice/bar 颜色、数据几何）不可经 getComputedStyle introspect**。12 entries = 容器/主题-token gate，非 chart-rendering gate。
- **parity audit 两处 robust 增强**（被 Table/Chart 暴露的真盲区，非为凑过改 SoT）：(1) 解析**导入的**类型别名（`import type {X} from base.vue` → 读 base 展开 union），让 `Chart.type=ChartType` 的 union 仍被 drift-check；(2) 含 `{}` 的对象/结构类型（`Table.columns`/`Chart.datasets`，nested 可选 union 按 `|` 拆会得垃圾成员）跳过 union-body check（本就该跳，同 `DropDownListSelect.items`）。

## 过程学习：并行 session 的 git race（本 session 起手实证）
- 起手 git 实物与 pickup「F 已完成并推送」**冲突**：reflog 显示并行 session 把 Batch F commit（ac63dffb）**reset 回 E 两次**（shadow 模式 13 A + shadowRoot:false rework 都放弃），HEAD 实际停在 E（168906f2，20/29），工作树残留未提交的 F 文件 + 一份来源不可信的 854/146-A report。
- **纪律应验**：(1) 不信 stale 措辞、亲核 git reflog/磁盘实物；(2) dirty/untracked 文件假设别 session 在动、不主动消化（用户确认其干净收尾后才接手）；(3) report.json 这类反复无常的生成产物不考古，唯一可信 = 自己 fresh build+gate 读新 report。
- ac63dffb 仍在 reflog（recoverable），本 session 从它恢复 F 实现 + 叠正解，避免从零重做。

## Final whole-branch review（opus）+ I-1 修复（gate soundness，commit b4f9169c）
- review verdict 起初 = Needs fixes（1 Important + 3 Minor）；I-1 = TabList Type=Text/Line light 文字色被误分类 B 掩盖了 A 闸。review 初判为"transition-race 跨 shadow 边界"。
- **systematic debugging 推翻初判**：先按 review 建议加 shadow-root 递归 disable → 无效（TabList 仍 #f8f8f8）；改 shadowRoot:false → 更糟（引入新 race，排除"shadow 边界"假设）；运行时 probe（waitForTimeout 500ms 自然 settle）→ 激活 tab 真实渲染 **#141414 正确**、`--text-body` 全链 #141414。**真根因 = text node（`.tab-item` 自带 `transition: color 0.15s`）是另行定位的元素，gate 只 disable 了 root locator（`.tab-list`），从未 disable text locator** → 采样到中间/起始色。
- **精准修复**：collectActual 读 text locator 颜色前对其也 `transition/animation:none`。共享 comparator 对 Vue+React 对称生效。修后两 gate 930/A=0、全组件 cross-gate 零 A-divergence、TabList 6 条 React==Vue 全 IDENTICAL。顺带纠正 Filled light 一个 false-PASS（原巧合匹配 transition 起始 #f8f8f8）。
- **教训**：① review 的根因判断也要实证（probe）再信，"transition race" 对但**位置**判错（text locator 非 shadow nesting）；② false-PASS 和 false-FAIL 同源于"没 disable 被测元素的 transition"——凡另行定位读 computed style 的节点都要先 disable transition（已是 memory verifier-css-transition-race 的延伸：不止 root，每个被读节点）。
- Minor：M-1（committed report stale 180→刷新 930）已随本 commit 解决；M-2（注释 `html[data-theme]` 实为 `[data-theme]`）+ Phase0/1 cosmetic 归 follow-up chore；M-3（onSelect undefined guard）按 review 留 as-is。

## 工作区状态（29/29 完成）
- E（168906f2）已由并行 session 推送；F（94205752）+ G（17c0b971）+ I-1 gate 修复（b4f9169c）本 session 提交并推 Gitea+GitHub。
- 全 29 组件双框架：config 单源 + 生成器 + 32/32 parity + React gate 930/A_TRUE_DRIFT=0 + cross-gate 全 IDENTICAL。
- 未碰已发布 Vue 包；react-pilot / src/web-components / tests/render-verification-react 独立。
- report.json/md 产物按 A–G 既有模式不入 per-batch commit（gate 可复跑，结果记在 commit message + 本复盘）。
