# 双框架（Vue + React）路线 A Pilot — Findings & 全量决策论证

> **日期**：2026-06-29
> **Spec**：[`docs/superpowers/specs/2026-06-29-dual-framework-react-pilot-design.md`](../../superpowers/specs/2026-06-29-dual-framework-react-pilot-design.md)
> **Plan**：[`docs/superpowers/plans/2026-06-29-dual-framework-react-pilot.md`](../../superpowers/plans/2026-06-29-dual-framework-react-pilot.md)
> **受众**：owner（决定是否从 pilot 扩到全量 26 组件）。

---

## 1. 一句话结论

**路线 A（Vue `defineCustomElement` → Web Component + 薄 React 包装）在 Button / Input / FormItem 三个 pilot 组件上全部验证通过：React 渲染与 Vue 逐条等价、0 个 React 特有的真实漂移（A_TRUE_DRIFT_CANDIDATE = 0），四大互操作轴（主题 / props / v-model / slot-组合）全部成立。无需为这三个组件退回路线 B。** 建议据此推进全量，但带一份明确的"代价 + 待决策"清单（§5/§6）。

---

## 2. 验证了什么（逐风险轴）

| 风险轴 | 结论 | 证据 |
|---|---|---|
| **主题穿透 shadow DOM** | ✅ 成立（但机制与初判不同） | CSS 自定义属性从 **document 根**继承穿透 shadow DOM；dark `rgb(67,67,67)` vs light `rgb(219,219,219)` 实测。**关键**：token 必须装在 document 根（消费者 `import variables.css`），不能注入 shadow root（`:root` 在 shadow root 内不匹配）。 |
| **props 映射** | ✅ 地道 | Vue `defineCustomElement` 为每个声明 prop 装 getter/setter；wrapper 经 ref+effect 设 element property。Button 8 props 全通。 |
| **v-model 双向绑定** | ✅ 干净（最怕的一轴） | React 受控 `value`/`onChange` ↔ Vue `modelValue`/`update:modelValue`。Vue CE 同时分发 `update:modelValue` + `update:model-value`，detail[] 载 emit 参数。**无反馈环**（property set 不会 re-emit）。 |
| **slot / 组合** | ✅ 成立 | `<FormItem><Input/></FormItem>` light-DOM 组合 + named `label` slot 两路径；**shadow-DOM slot/fallback projection 经 render-verification 在真实 Chromium 中坐实**（FormItem 64/64）。 |

---

## 3. 保真度闸结果（诚实解读）

React render-verification（`tests/render-verification-react/`，180 条 manifest，复用 Vue 同一套 comparator + 同一 5% 容差 / ±1px，逐变体 getComputedStyle vs `expectedFromFigma`）：

| Total | PASS | PASS_BY_MODE_SKIP | FAIL | A_TRUE_DRIFT | B_RESIDUAL_SCHEMA_GAP |
|---:|---:|---:|---:|---:|---:|
| 180 | 0 | 176 | 4 | **0** | 14 |

**三个数字必须正确理解，否则会误读：**

1. **"0 PASS / 176 PASS_BY_MODE_SKIP" 不是"176 条没比对"。** 条目级标签只要有**任一** check 是合法 mode-skip（如 `gap` 在 <2 子节点时不适用、或 HUG 宽度由内容驱动）就降级为 PASS_BY_MODE_SKIP——但该条目**所有实质比对（颜色 / padding / radius / border）都真比对且通过**。实测样例：Input 条目 borderHex `#595959==#595959`、textFillHex `#7b7b7b==#7b7b7b`、padding `8/12/8/12` exact、radius `4==4`。这是 Vue gate 自带的保守标签约定，不是空心通过。
2. **4 个 FAIL 全是同 4 个 `button-url-link` 变体**（padding 期望 4 实际 16；链接文字色期望 #3892f3 实际 #cccccc）。**这 4 条在 Vue gate 里同样 FAIL**（已交叉核对 `render-verification.report.json`）——即它是 **Vue/React 共有的预存 schema-gap（B 类，非阻断）**，源于 canonical Button 的 url-link 变体当前对 Figma 的偏差，**不是 React pilot 引入的**。React 忠实继承了 Vue 的这条既有偏差。
3. **0 个 A_TRUE_DRIFT_CANDIDATE**：按项目自己的 CI 判据，React 三组件与 Vue **同等过闸**。闸现已加 `expect(A_TRUE_DRIFT===0)` 断言，未来真漂移会被拦（H1 修复）。

**覆盖局限（诚实）**：node-level（子元素）走查 434 个期望节点中仅 1 个被比对、433 个 gap——因为还没有 `data-figma-node-id` 标注（INFRA-F41 Task 4 未做，Vue 侧同样）。所以**保真证明在 root + text 元素的 computed style 层，不是逐子节点穷尽**。Button 的 manifest 覆盖也薄（仅 4 条，且都是 url-link schema-gap 变体）——Button 的非 url-link 变体未被 manifest 直接数值验证（但走了同一渲染管线）。

---

## 4. 为什么是单源继承（路线 A）而非双实现（路线 B）—— 给 owner 的论证

owner 在 pilot 中提过一个合理担忧：「如果 Vue 实现本身有偏差，再转 React，偏差会不会被放大？」这里把机制讲清：

1. **路线 A 不是"把 Vue 翻译成 React"**，而是**原样运行那个已编译的 Vue 组件**（包在 custom element 里）。底层渲染的就是同一个 Vue 组件，所以 **React 的保真度逐像素等于 Vue 的保真度**——不增不减，没有二次翻译，故**不叠加偏差**。本 pilot 的 gate 结果实证了这点：React 与 Vue **逐条 FAIL/PASS 完全一致**（同 4 个 button-url-link fail，其余同样表现）。
2. **两端各自对同一个 Figma 真源验证**，不是 React 去对 Vue 验证。Vue 早已被 render-verification（930 条 + drift-gate）约束到 Figma；React 现在跑**同一套 comparator + 同一 manifest 的 expectedFromFigma**。输入端（manifest `codeProps` = Figma 派生属性值）和输出端（期望视觉）都是 Figma。
3. **canonical props = 已登记的 Figma↔code 映射**：React wrapper 的 props 逐字镜像自 `src/canonical/*.vue` 的 `defineProps`（那本身就是 Figma 变体轴→code 的人工拍板 + 登记层）。这保证 **Vue 和 React 的 prop API 完全一致**——这正是"双框架一等公民"要的。绕开 canonical 自己从 Figma 原始导出再推一套，会重做已完成的翻译工作、且很可能让两框架 API 分叉。
4. **对比路线 B（手写原生 React）**：那才会引入**第二次独立翻译**、可能产生与 Vue 不同的新偏差、并要双倍保真维护。路线 A 用"单一实现"恰好消掉了 owner 担心的"放大偏差"风险。

> 若某个组件 Vue 当前对 Figma 有偏差（如 button-url-link），修 Vue 即可，React 自动跟上——这正是单一实现的红利。

---

## 5. 路线 A 的代价清单（已暴露的真实 quirks）

全量推进前 owner 应知道这些（pilot 实测得出，不是推测）：

| # | Quirk | 影响 / 现状 | 全量阶段处理 |
|---|---|---|---|
| Q1 | **`style` prop 命名冲突** | Button 有设计轴 prop 字面叫 `style`，与 React 的内联 CSS `style` 保留名冲突。当前 wrapper 用 element property 设值、加了 JSDoc 警示，能工作但是消费者 footgun。 | owner 决策：React 端是否重命名（如 `variant`/`buttonStyle`）？重命名会与 Vue 的 `style` prop 名分叉。 |
| Q2 | **token 必须在 document 根** | 组件自身不自带 token（shadow root 内 `:root` 不匹配）。消费者 / React 包必须在 document 级 `import variables.css`（= TVU 现有 `style.css` 用法）。 | 全量 React 包应自带 document 级 token 导入或文档强约束。 |
| Q3 | **prop 走 element property（非 attribute）** | wrapper 经 ref+effect 设 `el.<prop>`。 | 全量可考虑生成器自动产 wrapper，统一这套机制。 |
| Q4 | **v-model 事件名** | Vue CE 同时分发 `update:modelValue` + `update:model-value`；wrapper 监听前者。 | 已知，生成器统一处理。 |
| Q5 | **`onChange`/回调 memoize** | 非 memoized 内联回调会每渲染重挂监听（行为正确、性能 footgun）。 | wrapper JSDoc 建议 `useCallback`；可在生成器层用 ref 稳定回调。 |
| Q6 | **`readonly` vs React `readOnly`** | Vue prop 名 `readonly`；消费者误用 React 的 `readOnly` 无报错无效果。 | 文档化 / 生成器加别名。 |
| Q7 | **bundle 体积** | WC bundle ~1.5MB（含全图标系统）。 | 全量需 code-split / 按需图标。 |
| Q8 | **showcase iframe 主题不联动** | docs 站切到 light 时 React iframe 仍 dark。 | 给 React demo 加跨 iframe 主题同步（postMessage）。 |
| Q9 | **cold-start 依赖** | React gate / harness 依赖先跑 `pnpm build:wc`（`dist-wc/` 是 gitignore）。 | CI / 文档把 `build:wc` 列为前置步骤。 |
| Q10 | **comparator 重复** | React gate 的 `drift-compare.ts` 是 Vue spec 的逐字移植（已标注）。 | 生产化时抽成单一共享模块给两端用。 |
| Q11 | **node-level 覆盖薄** | 仅 root+text 层数值验证；子节点走查需 `data-figma-node-id` 标注（INFRA-F41 Task 4）。 | 与 Vue 侧同步推进节点标注后两端都受益。 |

---

## 6. 全量阶段估值（修正自 spec 假设）

pilot 实测后，每个组件的 React 化成本已知且**主要是机械工作**：① 从 canonical SFC 逐字镜像 prop union 类型 → ② 写薄 wrapper（property 映射 + 事件/v-model + slot children） → ③ 在 WC 入口注册 → ④ 接进 render-verification harness。简单组件（Button 类）约 0.5 天、含 v-model/组合的中等组件约 1 天。

**风险较高、需重点验证的剩余组件类别**：
- **复合 / slot 重**：Table（行）、Select / DropDownList（弹层 + 选项）、Tab（TabList/TabItem 组合 + provide/inject）、Steps —— slot 与 provide/inject 跨 shadow 边界需逐个验。
- **overlay / teleport**：Tooltip、PopupBox、Notification、Message —— `<Teleport to="body">` 在 custom element 里的行为需专门验（render-verification 已有 teleport 的 selector fallback 经验）。
- **canvas**：Chart（chart.js）—— canvas 在 shadow DOM 内的渲染与尺寸需验。
- 其余（Badge/Pill/Switch/Checkbox/Radio/Progress/Slider/Rating/Breadcrumb/Icon/Logo/TopBar/InputNumber/Pagination）多为 Button/Input 同型，低风险。

建议全量分批：先低风险一批跑通生成器化，再逐个攻 overlay/复合/canvas 三类高风险组件。

---

## 7. 给 owner 的待决策项

1. **是否扩到全量 26 组件**（建议：是，分批，先低风险批验证生成器化 ROI）。
2. **`style` prop（Q1）**：React 端重命名（更地道、与 Vue 分叉）vs 保持同名（API 一致、footgun）。
3. **React 包形态**：自带 document 级 token 导入 vs 文档强约束消费者导入（Q2）。
4. **生成器化**：全量前是否投入做一个"canonical SFC → WC 注册 + React wrapper + 类型"的代码生成器（pilot 证明这层高度机械，ROI 应为正）。
5. **gate 生产化**：抽共享 comparator（Q10）+ 推进 node 标注（Q11）让两端保真都更深。

### 决策（owner 拍板 2026-06-30 — 全部按推荐默认）

| # | 决策 | 落地形态 |
|---|---|---|
| 1 | **全量，分批**：先低风险批跑通生成器化验证 ROI，再逐个攻 overlay/teleport · 复合/slot · canvas 三类高风险 | 第一份计划 = Phase 0 生成器 + Phase 1 低风险批；高风险三类各起独立计划 |
| 2 | **`style` prop React 端重命名 `style`→`variant`**，做成**通用 React 保留名重映射机制**（单一真源登记，非 per-component 硬编码） | 实证：`ButtonBridge.vue:22` 轴字面叫 `style`（filling/ghost/rimless），遮蔽 React 标准 `style`=真 footgun；`variant` 全仓未占用、语义贴切。Vue 端保留 `style` 不动，分叉只在 React 适配层 + 登记 translation/ 为合法值形态映射 |
| 3 | **React 包导出可 import 的 `styles.css`（token），消费者 app 根 import 一次**——与 Vue `import variables.css` 对称；**不**用组件内副作用自动注入（SSR/重复注入/tree-shaking 脆） | Q2 落地 |
| 4 | **先做生成器**（canonical SFC → WC 注册 + wrapper + 类型） | 排期原则权重 3（自动化前置）；反模式 #5（扩展只改一处） |
| 5 | **抽共享 comparator（Q10）随生成器一起做**；node 标注（Q11）与 Vue 同步另起轨道，**不阻塞** React rollout | — |

---

## 8. 交付物索引

- WC 构建入口：`src/web-components/register.ts` + `vite.web-components.config.ts`（`pnpm build:wc` → `dist-wc/`）
- React 工作区：`react-pilot/`（隔离 React 19；wrappers 在 `src/wrappers/`）
- 保真度闸：`tests/render-verification-react/` + `playwright.render-verification-react.config.ts`；报告 `figma-data/normalized/react-render-verification.report.json` + `docs/internal/react-render-verification-report.md`
- 双框架展示页：`playground/docs/pages/DualFrameworkPilotPage.vue`（`pnpm build:playground` 后查看）
- 执行记录：commits `cead1745`(WC) → `c4d75899`(ES fix) → `e8318e72`/`473380b4`(React ws + token 架构) → `6775be17`/`0a06d8bf`(Button) → `9a60b222`(Input) → `13817170`(FormItem) → `38e54793`/`681185ac`/`4be8c4ac`(gate) → `72150df1`(showcase)

> **执行过程注记**：subagent-driven 执行中遇到 agent 偶发"spawn 嵌套 agent + 提前返回计划文本"导致一次并发冲突（两个 agent 同时做 Task 6）；已 kill 冗余 agent、reset 到干净提交态、controller 亲自核实闸有效性后收口。后续 dispatch 加了 anti-delegation 约束。
