# 两端 demo 单一来源（D2）Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 让 Vue 文档页 demo 与 react-pilot demo 从一份框架中立 descriptor 渲染，使结构漂移物理上不可能——PoC 覆盖 Progress→Button→TopBar 三组件 + migration-aware audit。

**Architecture:** 一份纯 TS descriptor（`react-pilot/src/demos/descriptors/<comp>.ts`）被两端 import；两个薄 `DemoRenderer`（Vue `h()` 渲染函数 / React tsx）各读 descriptor、经 per-framework 组件注册表把 `component:'X'` 解析成本框架 DS 组件渲染。每组件的两个 demo 文件收缩为薄委托器。

**Tech Stack:** Vue 3.5（`h()` 渲染函数 + `<component :is>`）、React 18（`createElement`）、TypeScript（Node ≥22 原生 type-strip）、Vitest（单测）、`@vue/compiler-sfc` + `typescript` AST（audit）。

## Global Constraints（每个 task 隐含遵守，值逐字取自 spec）

- **descriptor 必须纯 TS 无 JSX**——否则 Vue 构建图无法安全静态 import（spec §3.2）。JSX / 框架特定内容只能在 renderer 侧或逃生舱模块（React-only 动态 import 路径）。
- **descriptor 存放** `react-pilot/src/demos/descriptors/`：Vue 用现成 `@demos/descriptors/<x>` alias（零新配置），React 用相对 `./descriptors/<x>`。
- **双语文案统一建模为 `LocalizedText = [en, zh]` tuple**（spec §3.3）。
- **渲染契约 = `DemoProps { theme:'dark'|'light', locale:'en-US'|'zh-CN' }`**（spec §3.4）。
- **Figma 真源不可越界**：descriptor 只声明"用哪个组件 + 什么 props/slot"，**不含任何视觉数值**；组件实例视觉 1:1 Figma（spec §3.5）。
- **canonical 结构 = 丰富版**（owner 2026-07-14）：`figma-coverage`/`status-matrix` 复用 FigmaMembersGrid/StatusMatrix 的丰富结构，样式单一来源到共享 CSS。
- **两端消费同一 DS 组件**：Vue = `@/src/canonical/*.vue` 或 `@/src/index` 导出；React = `../wrappers/*`。
- **提交纪律**：只 `git add` 具体文件、绝不 `-A`；跑 gate 再生的 `figma-data/normalized/*.report.json` + `docs/internal/*-report.md` 提交前 `git checkout --` restore；executor 只报 diff+stdout，controller 复审 + gate 绿后才提交（[[feedback_executor-no-self-commit]]）。
- **视觉门**：改 `.vue`/`.tsx`/`.css`（真 docs demo，用户可见）的提交，**owner 亲审视觉效果 approve 后**才带 `VISUAL_COMMIT_APPROVED=1`（Task 3/4/5/6/7/8 命中；Task 1/2/9 不命中，正常提交）。
- **push**：`git push origin master`（双推 Gitea+GitHub），`git ls-remote origin -h refs/heads/master` 权威核 SHA。

---

## File Structure（decomposition 锁定）

**新建：**
- `react-pilot/src/demos/descriptors/schema.ts` — 框架中立 descriptor 类型 + `validateDescriptor()`。单一职责：契约。
- `react-pilot/src/demos/descriptors/progress.ts` / `button.ts` / `topbar.ts` — 三个 PoC 的 descriptor 数据。
- `playground/docs/components/DemoRenderer.vue` — Vue 通用 renderer（`h()`）+ Vue 组件注册表 + `resolveIcon`。
- `react-pilot/src/demos/DemoRenderer.tsx` — React 通用 renderer + React 组件注册表 + `resolveIcon`。
- `react-pilot/src/demos/demo-figma-members.css` — 从 FigmaMembersGrid + StatusMatrix `<style scoped>` 单一来源出来的共享样式（两框架加载）。
- `tests/demo-renderer-vue.test.ts` / `tests/demo-renderer-react.test.ts` — renderer 单测。
- `react-pilot/src/demos/descriptors/schema.test.ts`（或 tests/ 下）— validator 单测。

**修改：**
- `playground/docs/components/FigmaMembersGrid.vue` + `StatusMatrix.vue` — 去 `scoped`、样式移入共享 CSS（组件本身保留、行为不变；另 24 页无回归）。
- `playground/docs/pages/ProgressPage.vue` / `ButtonPage.vue` / `TopBarPage.vue` — demo 区收缩为 `<DemoRenderer :descriptor>`（API 表 region 外保持不变）。
- `react-pilot/src/demos/Progress.tsx` / `Button.tsx` / `TopBar.tsx` — 收缩为薄委托器。
- `scripts/audit-demo-framework-parity.mjs` — migration-aware 双模。
- `tests/audit-demo-framework-parity.test.ts` — 新增迁移感知用例。

---

## Task 1: Descriptor schema + validator

**Files:**
- Create: `react-pilot/src/demos/descriptors/schema.ts`
- Test: `tests/demo-descriptor-schema.test.ts`

**Interfaces:**
- Produces（后续所有 task 依赖这些类型名与签名）：
  - `type LocalizedText = [en: string, zh: string]`
  - `type ContentNode`（union: `text` / `component` / `icon` / `el`）
  - `type Control`（union: `range` / `switch`）
  - `type Card`（union，见下）
  - `type Section = { title: LocalizedText; summary?: LocalizedText; layout?: 'grid'|'stack'; cards: Card[] }`
  - `type ComponentDescriptor = { component: string; sections: Section[] }`
  - `function validateDescriptor(d: unknown): string[]` — 返回问题列表，空数组 = 合法。

- [ ] **Step 1: 写 schema 类型 + validator（这是纯类型 + 一个校验函数，先写实现再写测试——类型无法 TDD，validator 用 TDD）**

`react-pilot/src/demos/descriptors/schema.ts`：
```ts
// Framework-neutral demo descriptor schema. Pure TS — NO JSX, NO framework imports.
import type { DocsFigmaMembers } from '../../../../figma-data/normalized/docs-figma-members/types'

export type LocalizedText = [en: string, zh: string]

/** Prop values may reference runtime/loop vars via '$'-prefixed strings:
 *  '$theme' (DemoProps.theme) | '$row' | '$col' (matrix axis value) | '$<stateVar>' (interactive). */
export type PropValue = unknown

export type ContentNode =
  | { kind: 'text'; text: LocalizedText }
  | { kind: 'component'; component: string; props?: Record<string, PropValue>; slots?: Record<string, ContentNode[]>; children?: ContentNode[] }
  | { kind: 'icon'; name: string; size?: number; class?: string }
  | { kind: 'el'; tag: string; class?: string; props?: Record<string, unknown>; children?: ContentNode[] }

export type Control =
  | { type: 'range'; boundTo: string; min: number; max: number; label?: LocalizedText }
  | { type: 'switch'; boundTo: string; label: LocalizedText }

export type Card =
  | { type: 'figma-coverage'; component: string; members: DocsFigmaMembers<any> }
  | { type: 'status-matrix'; component: string; members: DocsFigmaMembers<any>; baseProps?: Record<string, unknown> }
  | { type: 'component-matrix'; title?: LocalizedText; component: string; members: DocsFigmaMembers<any>; rowAxis: string; colAxis: string; cellProps: Record<string, PropValue>; hoverPreview?: { prop: string; value: string } }
  | { type: 'instance-list'; title?: LocalizedText; gridClass?: string; component: string; items: { label: LocalizedText; props: Record<string, PropValue> }[] }
  | { type: 'slotted-instance'; component: string; props?: Record<string, PropValue>; slots: Record<string, ContentNode[]> }
  | { type: 'code-block'; title?: LocalizedText; code: string }
  | { type: 'prose'; title?: LocalizedText; summary: LocalizedText }
  | { type: 'interactive'; title?: LocalizedText; summary?: LocalizedText; state: Record<string, unknown>; controls: Control[]; target: { component: string; props: Record<string, PropValue> }; readout?: (v: Record<string, unknown>) => LocalizedText }
  | { type: 'custom'; module: string } // escape-hatch seam — NOT implemented in D2 (spec §4 D-4)

export type Section = { title: LocalizedText; summary?: LocalizedText; layout?: 'grid' | 'stack'; cards: Card[] }
export type ComponentDescriptor = { component: string; sections: Section[] }

const CARD_TYPES = new Set([
  'figma-coverage', 'status-matrix', 'component-matrix', 'instance-list',
  'slotted-instance', 'code-block', 'prose', 'interactive', 'custom',
])

/** Structural validation used by audit + tests. Returns problems ([] = valid). */
export function validateDescriptor(d: unknown): string[] {
  const errs: string[] = []
  const desc = d as ComponentDescriptor
  if (!desc || typeof desc.component !== 'string') return ['missing component name']
  if (!Array.isArray(desc.sections) || desc.sections.length === 0) errs.push('sections must be a non-empty array')
  desc.sections?.forEach((s, i) => {
    if (!isLocalized(s.title)) errs.push(`section[${i}].title must be [en,zh]`)
    if (!Array.isArray(s.cards) || s.cards.length === 0) errs.push(`section[${i}].cards must be non-empty`)
    s.cards?.forEach((c, j) => {
      if (!CARD_TYPES.has((c as Card).type)) errs.push(`section[${i}].card[${j}].type "${(c as any).type}" unknown`)
    })
  })
  return errs
}

function isLocalized(v: unknown): v is LocalizedText {
  return Array.isArray(v) && v.length === 2 && typeof v[0] === 'string' && typeof v[1] === 'string'
}
```

- [ ] **Step 2: 写 validator 失败测试**

`tests/demo-descriptor-schema.test.ts`：
```ts
import { describe, it, expect } from 'vitest'
import { validateDescriptor } from '../react-pilot/src/demos/descriptors/schema'

describe('validateDescriptor', () => {
  it('flags missing component name', () => {
    expect(validateDescriptor({})).toContain('missing component name')
  })
  it('flags empty sections', () => {
    expect(validateDescriptor({ component: 'X', sections: [] })).toContain('sections must be a non-empty array')
  })
  it('flags bad title tuple', () => {
    const errs = validateDescriptor({ component: 'X', sections: [{ title: 'nope', cards: [{ type: 'prose', summary: ['a', 'b'] }] }] })
    expect(errs.some((e) => e.includes('title must be [en,zh]'))).toBe(true)
  })
  it('flags unknown card type', () => {
    const errs = validateDescriptor({ component: 'X', sections: [{ title: ['A', '甲'], cards: [{ type: 'bogus' }] }] })
    expect(errs.some((e) => e.includes('unknown'))).toBe(true)
  })
  it('accepts a minimal valid descriptor', () => {
    expect(validateDescriptor({ component: 'X', sections: [{ title: ['A', '甲'], cards: [{ type: 'prose', summary: ['a', 'b'] }] }] })).toEqual([])
  })
})
```

- [ ] **Step 3: 跑测试确认通过**

Run: `pnpm vitest run tests/demo-descriptor-schema.test.ts`
Expected: 5 passed。若 fail 先修 validator。

- [ ] **Step 4: 提交（.ts only，不命中视觉门）**

```bash
git add react-pilot/src/demos/descriptors/schema.ts tests/demo-descriptor-schema.test.ts
git commit -F <msgfile>   # feat(D2): descriptor schema + validator
```

---

## Task 2: Progress descriptor（数据）

**Files:**
- Create: `react-pilot/src/demos/descriptors/progress.ts`
- Test: 扩 `tests/demo-descriptor-schema.test.ts`（或新 `tests/demo-descriptor-progress.test.ts`）

**Interfaces:**
- Consumes: Task 1 `ComponentDescriptor` / `Card` 类型 + `progressFigmaMembers`。
- Produces: `export const progressDescriptor: ComponentDescriptor`（Task 4/5 renderer 消费、Task 6 委托器 import）。

**Data source**：逐段对照现有 [`ProgressPage.vue`](../../../playground/docs/pages/ProgressPage.vue) 的 6 个 section（canonical-rich：§1 用 figma-coverage、§5 用 status-matrix）。

- [ ] **Step 1: 写 progress descriptor**

`react-pilot/src/demos/descriptors/progress.ts`：
```ts
import type { ComponentDescriptor } from './schema'
import { progressFigmaMembers } from '../../../../figma-data/normalized/docs-figma-members/progress'

const usageCode = `import Progress from '@/src/canonical/Progress.vue'

<Progress
  theme="dark"
  status="success"
  size="M"
  :value="72"
/>`

export const progressDescriptor: ComponentDescriptor = {
  component: 'Progress',
  sections: [
    { // §1 Figma Coverage — canonical rich (FigmaMembersGrid)
      title: ['Figma Coverage', 'Figma Coverage Figma 变体覆盖'],
      summary: ['Progress is a 16-member Figma family. This page starts with the exact exported axes and real members before discussing runtime value changes.',
                'Progress 是一个 16 成员的 Figma family。这一页先展示真实导出轴和真实成员，再补充运行时数值变化。'],
      cards: [{ type: 'figma-coverage', component: 'Progress', members: progressFigmaMembers }],
    },
    { // §2 Family Matrix — component-matrix over status × size
      title: ['Family Matrix', 'Family Matrix 主题矩阵'],
      summary: ['This matrix renders the eight Figma members for the current theme: four statuses by two sizes on the canonical 60% baseline.',
                '此矩阵渲染当前主题下 Figma 中的 8 个成员：4 个状态乘 2 个尺寸，并维持 canonical 60% 基线。'],
      layout: 'stack',
      cards: [{ type: 'component-matrix', title: ['theme axis member', 'theme 轴成员'], component: 'Progress',
        members: progressFigmaMembers, rowAxis: 'status', colAxis: 'size',
        cellProps: { theme: '$theme', status: '$row', size: '$col', value: 60 } }],
    },
    { // §3 Runtime Value — instance-list + interactive Try-it
      title: ['Runtime Value', 'Runtime Value 运行时数值'],
      summary: ['`value` is still useful in product code, but it now sits after the family matrix as a runtime width override rather than pretending to be a Figma axis.',
                '`value` 在产品代码里仍然有用，但现在放在 family 矩阵之后，作为运行时宽度覆盖，而不是伪装成 Figma 轴。'],
      cards: [
        { type: 'instance-list', title: ['Value progression on one canonical shell', '单一 canonical 外壳上的数值变化'], gridClass: 'value-grid', component: 'Progress',
          items: [0, 35, 60, 100].map((v) => ({ label: [`${v}%`, `${v}%`] as [string, string], props: { theme: '$theme', status: 'default', size: 'M', value: v } })) },
        { type: 'interactive', title: ['Try it', 'Try it 真交互试用'],
          summary: ['Use the controls below to see Progress respond in real time.', '用下方控件实时驱动 Progress 状态变化。'],
          state: { value: 45 },
          controls: [{ type: 'range', boundTo: 'value', min: 0, max: 100, label: ['progress value demo control', 'progress value demo control'] }],
          target: { component: 'Progress', props: { theme: '$theme', status: 'default', size: 'M', value: '$value' } },
          readout: (v) => [`value = ${v.value}%`, `value = ${v.value}%`] },
      ],
    },
    { // §4 Development Usage — code-block
      title: ['Development Usage', 'Development Usage 开发引用'],
      cards: [{ type: 'code-block', title: ['Current canonical reference', '当前 canonical 引用方式'], code: usageCode }],
    },
    { // §5 Status Matrix — canonical rich (StatusMatrix)
      title: ['Status Matrix', 'Status Matrix 状态矩阵'],
      summary: ['QA view: the progress bar across every status Figma exports (size held constant), live and following the global theme toggle.',
                'QA 视图：把进度条在 Figma 导出的每个状态下渲染（尺寸固定为代表值），跟随全局主题切换。'],
      cards: [{ type: 'status-matrix', component: 'Progress', members: progressFigmaMembers }],
    },
    { // §6 Interactive: showLabel — interactive switch
      title: ['Interactive: showLabel', '交互演示：showLabel'],
      summary: ['Toggle `showLabel` to show or hide the percentage readout inside the progress bar.', '切换 `showLabel` 显示或隐藏进度条内的百分比标注。'],
      cards: [{ type: 'interactive',
        state: { showLabel: true },
        controls: [{ type: 'switch', boundTo: 'showLabel', label: ['showLabel', '显示标签'] }],
        target: { component: 'Progress', props: { theme: '$theme', status: 'default', size: 'M', value: 45, showLabel: '$showLabel' } } }],
    },
  ],
}
```

- [ ] **Step 2: 写测试（validator 通过 + 结构断言）**

追加到 schema 测试文件：
```ts
import { progressDescriptor } from '../react-pilot/src/demos/descriptors/progress'
it('progress descriptor is valid + has 6 sections', () => {
  expect(validateDescriptor(progressDescriptor)).toEqual([])
  expect(progressDescriptor.sections.map((s) => s.title[0])).toEqual([
    'Figma Coverage', 'Family Matrix', 'Runtime Value', 'Development Usage', 'Status Matrix', 'Interactive: showLabel',
  ])
})
```

- [ ] **Step 3: 跑测试确认通过**

Run: `pnpm vitest run tests/demo-descriptor-schema.test.ts`
Expected: 全 passed（含新 2 断言）。

- [ ] **Step 4: 提交（.ts only，不命中视觉门）**

```bash
git add react-pilot/src/demos/descriptors/progress.ts tests/demo-descriptor-schema.test.ts
git commit -F <msgfile>   # feat(D2): Progress descriptor
```

---

## Task 3: 单一来源化 FigmaMembersGrid/StatusMatrix（视图逻辑抽纯 TS + 样式抽共享 CSS）

> **owner 2026-07-14 拍 Option A**（取代原"un-scope verbatim"——那会撞全局 `.coverage-chip`（FigmaMembersGrid 用 `--text-2 #cccccc`、全局 `.coverage-chip` 用 `--text-body #f8f8f8`，两者不等）+ 逼 React 重抄 ~300 行视图逻辑=双份漂移）。**把这俩共享组件的视图逻辑 + 样式都单一来源化**——逻辑抽框架中立纯 TS（组件 + 两 renderer 共用）、样式抽共享 CSS（chip 改独有类避冲突）。这是把 D2 descriptor/renderer 哲学应用到组件本身；真零漂移。

**Files:**
- Create: `react-pilot/src/demos/lib/figma-members-view.ts` — 框架中立纯 TS 视图逻辑。
- Create: `react-pilot/src/demos/demo-figma-members.css` — un-scoped 样式。
- Modify: `playground/docs/components/FigmaMembersGrid.vue` — 用共享逻辑替内联 computed；模板 `coverage-chip`→`figma-members-grid__chip`；删 `<style scoped>` + import 共享 CSS。
- Modify: `playground/docs/components/StatusMatrix.vue` — 用共享逻辑替内联 computed；删 `<style scoped>` + import 共享 CSS。
- Test: `tests/figma-members-view.test.ts`（纯逻辑单测）+ `tests/figma-members-grid.test.ts`（组件回归）。

**Interfaces（Produces — Task 5 React renderer 复用）：**
```ts
// figma-members-view.ts — 从 FigmaMembersGrid.vue / StatusMatrix.vue 的 computed 抽取，逻辑严格等价
import type { DocsFigmaMembers, DocsFigmaVariant, DocsFigmaAxis } from '<rel>/figma-data/normalized/docs-figma-members/types'
export interface CoverageView {
  axes: DocsFigmaAxis[]                                   // 轴 chips 数据
  variants: { variantId: string; props: Record<string,unknown>; label: string }[] // theme 过滤后的 live 预览
  meta: string                                            // `source=X · nodeId=… · variant count=N`
}
export function computeCoverageView(members: DocsFigmaMembers<any>, theme: 'dark'|'light'): CoverageView
export interface StatusMatrixView {
  hasStateAxis: boolean
  stateAxes: DocsFigmaAxis[]                              // 状态轴 chips
  heldConstant: string[]                                  // ['size=M', …]
  variants: { variantId: string; cellProps: Record<string,unknown>; label: string }[]
  meta: string
  noStateNote?: string
}
export function computeStatusMatrixView(members: DocsFigmaMembers<any>, theme: 'dark'|'light', baseProps?: Record<string,unknown>): StatusMatrixView
```

- [ ] **Step 1: 抽视图逻辑到 figma-members-view.ts（TDD）**

先读 [`FigmaMembersGrid.vue`](../../../playground/docs/components/FigmaMembersGrid.vue)（`visibleVariants`/`variantLabel`/`metaText` computed）+ [`StatusMatrix.vue`](../../../playground/docs/components/StatusMatrix.vue)（`stateAxesList`/`presentationalAxes`/`representative`/`visibleVariants`/`stateLabel`/`heldConstant`/`metaText` computed + `DEFAULT_STATE_VOCAB`/`THEME_PROPS` 常量）。把这些**逻辑严格等价**搬进两个纯函数（`inject('docsTheme')` 换成 `theme` 参数；其余算法一字不差）。先写 `tests/figma-members-view.test.ts`：用 `progressFigmaMembers` 断言 `computeCoverageView('dark')` 出 3 轴 + 8 个 dark variants + meta 串；`computeStatusMatrixView('dark')` 出 stateAxes=[status] + heldConstant=['size=M'] + 4 个 variants（default/success/error/warning）+ meta。跑测试确认 FAIL→实现→PASS。

- [ ] **Step 2: 抽样式到 demo-figma-members.css（chip 改名避冲突）**

把 FigmaMembersGrid.vue `<style scoped>`（`.figma-members-grid*` + `.coverage-chip` + `.figma-members-grid__preview`）+ StatusMatrix.vue `<style scoped>`（`.status-matrix*`）合并进 `demo-figma-members.css`，**去 scoped、值一字不改**，唯一改动：FigmaMembersGrid 的 `.coverage-chip` 规则**改名为 `.figma-members-grid__chip`**（保留其 `--text-2` 等原值），避开 demo-shared.css 的全局 `.coverage-chip`（`--text-body`）冲突。StatusMatrix 的类全部组件专属、无冲突、原名保留。

- [ ] **Step 3: 重构两组件用共享逻辑 + import 共享 CSS**

`FigmaMembersGrid.vue`：`<script setup>` 用 `computeCoverageView(props.members, injectedTheme.value)` 替 `visibleVariants`/`metaText`/`variantLabel`（computed 包一层）；模板里 `class="coverage-chip"`→`class="figma-members-grid__chip"`；删 `<style scoped>`、加 `import '@demos/demo-figma-members.css'`。`StatusMatrix.vue`：用 `computeStatusMatrixView(props.members, injectedTheme.value, props.baseProps)` 替各 computed；`#cell` scoped slot 契约不变；删 `<style scoped>`、import 共享 CSS。**渲染出的 DOM class 与改前完全一致**（除 FigmaMembersGrid chip 的类名，值不变）。

- [ ] **Step 4: 跑测试 + 全 vitest 无回归**

`tests/figma-members-grid.test.ts`：mount 两组件断言关键 class（`.figma-members-grid__axes` / `.figma-members-grid__chip` / `.figma-members-grid__cell` / `.status-matrix__cell` / `.status-matrix__chip`）存在 + variant 数正确。
Run: `pnpm vitest run tests/figma-members-view.test.ts tests/figma-members-grid.test.ts && pnpm vitest run`
Expected: 新测试 pass + 全绿（含 RemainingCanonicalPages / FormControlPages 等覆盖 12 个用这俩组件的页，零回归）。

- [ ] **Step 5: 视觉核（owner 亲审 — 12 页 blast radius 确认）**

启 `pnpm dev`（Vue 态），走查 ≥3 个用 FigmaMembersGrid/StatusMatrix 的页（Progress + 另 2 个如 Radio/Message），确认 chip / 网格 / 状态矩阵视觉与改前一致（尤其 chip 改名后颜色仍 `--text-2`）。**owner approve 后**才提交。

- [ ] **Step 6: 提交（命中视觉门，owner approve 后）**

restore 掉 gate 再生的 report 产物后（只 add 具体文件）：
```bash
git add react-pilot/src/demos/lib/figma-members-view.ts react-pilot/src/demos/demo-figma-members.css playground/docs/components/FigmaMembersGrid.vue playground/docs/components/StatusMatrix.vue tests/figma-members-view.test.ts tests/figma-members-grid.test.ts
VISUAL_COMMIT_APPROVED=1 git commit -F <msgfile>   # refactor(D2): single-source FigmaMembersGrid/StatusMatrix view-logic + styles (Task 3)
```

---

## Task 4: Vue DemoRenderer + 注册表（渲 Progress 全部 card 类型）

**Files:**
- Create: `playground/docs/components/DemoRenderer.vue`
- Test: `tests/demo-renderer-vue.test.ts`

**Interfaces:**
- Consumes: Task 1 schema 类型；Task 2 `progressDescriptor`；`DemoProps`。
- Produces: `DemoRenderer.vue`（default export SFC，props `{ descriptor: ComponentDescriptor; theme: 'dark'|'light'; locale: 'en-US'|'zh-CN' }`）——Task 6 委托器消费。

**实现要点**：用 `h()` 渲染函数（避开模板动态组件限制）。Vue 组件注册表 `VUE_REGISTRY: Record<string, Component>`；`resolveIcon(name,size)` → `h(Icon, {name, size})`；`$占位符`求值 `resolveProps(props, {theme,row,col,state})`。card 分派 `renderCard(card, ctx)`。

- [ ] **Step 1: 写 DemoRenderer.vue（render 函数 + 注册表 + card 分派）**

`playground/docs/components/DemoRenderer.vue`：
```vue
<script lang="ts">
import { defineComponent, h, inject, reactive, ref, type Component, type PropType, type Ref } from 'vue'
import Progress from '@/src/canonical/Progress.vue'
import { Switch, Icon } from '@/src/index'
import FigmaMembersGrid from './FigmaMembersGrid.vue'
import StatusMatrix from './StatusMatrix.vue'
import type { Card, ComponentDescriptor, ContentNode, LocalizedText } from '@demos/descriptors/schema'

const VUE_REGISTRY: Record<string, Component> = { Progress, Switch }

function resolveIcon(name: string, size?: number, cls?: string) {
  return h(Icon, { name, size: size ?? 24, class: cls })
}

/** Replace '$theme'/'$row'/'$col'/'$<var>' placeholders in a props object. */
function resolveProps(props: Record<string, unknown> = {}, ctx: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {}
  for (const [k, v] of Object.entries(props)) {
    out[k] = typeof v === 'string' && v.startsWith('$') ? ctx[v.slice(1)] : v
  }
  return out
}

export default defineComponent({
  name: 'DemoRenderer',
  props: {
    descriptor: { type: Object as PropType<ComponentDescriptor>, required: true },
    theme: { type: String as PropType<'dark' | 'light'>, default: 'dark' },
    locale: { type: String as PropType<'en-US' | 'zh-CN'>, default: 'en-US' },
  },
  setup(props) {
    const t = (txt: LocalizedText) => (props.locale === 'zh-CN' ? txt[1] : txt[0])
    // interactive state buckets keyed by section+card index (stable across renders)
    const states = reactive<Record<string, Record<string, unknown>>>({})

    function comp(name: string): Component {
      const c = VUE_REGISTRY[name]
      if (!c) throw new Error(`DemoRenderer(vue): unknown component "${name}"`)
      return c
    }

    function renderNode(node: ContentNode, ctx: Record<string, unknown>): any {
      if (node.kind === 'text') return t(node.text)
      if (node.kind === 'icon') return resolveIcon(node.name, node.size, node.class)
      if (node.kind === 'el') return h(node.tag, { class: node.class, ...(node.props ?? {}) }, (node.children ?? []).map((c) => renderNode(c, ctx)))
      // component
      const slots: Record<string, () => any> = {}
      for (const [sname, nodes] of Object.entries(node.slots ?? {})) slots[sname] = () => nodes.map((n) => renderNode(n, ctx))
      const children = node.children ? { default: () => node.children!.map((n) => renderNode(n, ctx)) } : slots
      return h(comp(node.component), resolveProps(node.props, ctx), Object.keys(children).length ? children : undefined)
    }

    function renderCard(card: Card, key: string): any {
      switch (card.type) {
        case 'figma-coverage':
          return h(FigmaMembersGrid, { members: card.members, component: comp(card.component) })
        case 'status-matrix':
          return h(StatusMatrix, { members: card.members, component: comp(card.component), baseProps: card.baseProps })
        case 'component-matrix': {
          const rows = card.members.axes.find((a) => a.codeProp === card.rowAxis)?.values ?? []
          const cols = card.members.axes.find((a) => a.codeProp === card.colAxis)?.values ?? []
          const header = [h('div', { class: 'family-matrix__header family-matrix__header--axis' }),
            ...cols.map((c) => h('div', { class: 'family-matrix__header' }, c))]
          const body = rows.flatMap((r) => [
            h('div', { class: 'family-matrix__axis' }, r),
            ...cols.map((c) => h('div', { class: 'family-matrix__cell' },
              h(comp(card.component), resolveProps(card.cellProps, { theme: props.theme, row: r, col: c })))),
          ])
          return h('article', { class: 'docs-demo-card docs-demo-card--wide' }, [
            card.title ? h('p', { class: 'docs-demo-card__summary' }, t(card.title)) : null,
            h('div', { class: 'family-matrix' }, [...header, ...body]),
          ])
        }
        case 'instance-list':
          return h('article', { class: 'docs-demo-card docs-demo-card--wide' }, [
            card.title ? h('span', { class: 'docs-demo-card__title' }, t(card.title)) : null,
            h('div', { class: card.gridClass ?? 'value-grid' }, card.items.map((it) =>
              h('div', { class: `${card.gridClass ?? 'value-grid'}__item` }, [
                h('span', { class: `${card.gridClass ?? 'value-grid'}__label` }, t(it.label)),
                h(comp(card.component), resolveProps(it.props, { theme: props.theme })),
              ]))),
          ])
        case 'slotted-instance': {
          const slots: Record<string, () => any> = {}
          for (const [sname, nodes] of Object.entries(card.slots)) slots[sname] = () => nodes.map((n) => renderNode(n, { theme: props.theme }))
          return h(comp(card.component), resolveProps(card.props, { theme: props.theme }), slots)
        }
        case 'code-block':
          return h('article', { class: 'docs-demo-card docs-demo-card--wide' }, [
            card.title ? h('span', { class: 'docs-demo-card__title' }, t(card.title)) : null,
            h('pre', { class: 'code-block' }, h('code', card.code)),
          ])
        case 'prose':
          return h('article', { class: 'docs-demo-card' }, [
            card.title ? h('span', { class: 'docs-demo-card__title' }, t(card.title)) : null,
            h('p', { class: 'docs-demo-card__summary' }, t(card.summary)),
          ])
        case 'interactive': {
          const st = states[key] ?? (states[key] = { ...card.state })
          const ctx = { theme: props.theme, ...st }
          const controls = card.controls.map((ctl) => {
            if (ctl.type === 'range') return h('input', { class: 'try-range', type: 'range', min: ctl.min, max: ctl.max, value: st[ctl.boundTo],
              'aria-label': ctl.label ? t(ctl.label) : undefined, onInput: (e: any) => { st[ctl.boundTo] = Number(e.target.value) } })
            // switch → DS Switch (fixes React native-checkbox divergence)
            return h('label', { class: 'sb-control' }, [
              h(Switch, { darkTheme: props.theme === 'dark' ? 'on' : 'off', status: st[ctl.boundTo] ? 'on' : 'off', enable: 'yes', loading: 'no',
                'onUpdate:status': (s: string) => { st[ctl.boundTo] = s === 'on' } }),
              h('span', `${t(ctl.label)}: ${String(st[ctl.boundTo])}`),
            ])
          })
          return h('article', { class: 'docs-demo-card docs-demo-card--wide' }, [
            card.title ? h('span', { class: 'docs-demo-card__title' }, t(card.title)) : null,
            card.summary ? h('p', { class: 'docs-demo-card__summary' }, t(card.summary)) : null,
            h('div', { class: 'try-stage' }, h(comp(card.target.component), resolveProps(card.target.props, ctx))),
            h('div', { class: 'sb-controls' }, controls),
            card.readout ? h('p', { class: 'try-readout' }, t(card.readout(st))) : null,
          ])
        }
        case 'custom':
          throw new Error(`DemoRenderer(vue): custom card "${card.module}" — escape hatch not implemented in D2`)
      }
    }

    return () => h('div', { class: 'docs-page' }, props.descriptor.sections.map((sec, si) =>
      h('section', { class: 'docs-section' }, [
        h('h2', { class: 'docs-section__title' }, t(sec.title)),
        sec.summary ? h('p', { class: 'docs-section__summary' }, t(sec.summary)) : null,
        h('div', { class: sec.layout === 'stack' ? 'docs-demo docs-demo--stack' : 'docs-demo' },
          h('div', { class: 'docs-demo-grid' }, sec.cards.map((card, ci) => renderCard(card, `${si}:${ci}`)))),
      ])))
  },
})
</script>
```

> 注：card 分派里 `family-matrix` / `value-grid` / `try-*` / `sb-*` / `code-block` / `docs-demo-card*` 等 class **必须与现有 demo 一字不差**（CSS 单一来源在 `*-demo.css`）。逐 class 对照 [`ProgressPage.vue`](../../../playground/docs/pages/ProgressPage.vue) 模板 + [`Progress.tsx`](../../../react-pilot/src/demos/Progress.tsx)。

- [ ] **Step 2: 写 renderer 单测（失败）**

`tests/demo-renderer-vue.test.ts`：
```ts
import { mount } from '@vue/test-utils'
import DemoRenderer from '../playground/docs/components/DemoRenderer.vue'
import { progressDescriptor } from '../react-pilot/src/demos/descriptors/progress'
import { describe, it, expect } from 'vitest'

describe('DemoRenderer (vue)', () => {
  it('renders all 6 progress sections with correct titles', () => {
    const w = mount(DemoRenderer, { props: { descriptor: progressDescriptor, theme: 'dark', locale: 'en-US' }, global: { provide: { docsTheme: { value: 'dark' } } } })
    const titles = w.findAll('.docs-section__title').map((n) => n.text())
    expect(titles).toEqual(['Figma Coverage', 'Family Matrix', 'Runtime Value', 'Development Usage', 'Status Matrix', 'Interactive: showLabel'])
  })
  it('renders figma-coverage via FigmaMembersGrid (axes chips present)', () => {
    const w = mount(DemoRenderer, { props: { descriptor: progressDescriptor, theme: 'dark', locale: 'en-US' }, global: { provide: { docsTheme: { value: 'dark' } } } })
    expect(w.find('.figma-members-grid').exists()).toBe(true)
  })
  it('renders family-matrix cells + code-block + try-range', () => {
    const w = mount(DemoRenderer, { props: { descriptor: progressDescriptor, theme: 'dark', locale: 'en-US' }, global: { provide: { docsTheme: { value: 'dark' } } } })
    expect(w.findAll('.family-matrix__cell').length).toBeGreaterThan(0)
    expect(w.find('.code-block').exists()).toBe(true)
    expect(w.find('input.try-range').exists()).toBe(true)
  })
})
```

- [ ] **Step 3: 跑测试确认 fail → 实现补齐 → pass**

Run: `pnpm vitest run tests/demo-renderer-vue.test.ts`
Expected: 先 fail（组件缺失/class 不符）→ 修 renderer → 3 passed。

- [ ] **Step 4: 提交（命中视觉门——含 .vue）**

```bash
git add playground/docs/components/DemoRenderer.vue tests/demo-renderer-vue.test.ts
VISUAL_COMMIT_APPROVED=1 git commit -F <msgfile>   # feat(D2): Vue DemoRenderer + registry
```
> 本 task 只加新组件、不改真页面，视觉零改动；VISUAL_COMMIT_APPROVED 因 pre-commit 对 .vue 一律拦而需要，但**无 owner 视觉审必要**（新文件未接入任何页面）。controller 判定后带 flag。

---

## Task 5: React DemoRenderer + 注册表 + resolveIcon

**Files:**
- Create: `react-pilot/src/demos/DemoRenderer.tsx`
- Test: `tests/demo-renderer-react.test.ts`

**Interfaces:**
- Consumes: Task 1 schema；Task 2 `progressDescriptor`；`DemoProps`。
- Produces: `export function DemoRenderer({ descriptor, theme, locale }: { descriptor: ComponentDescriptor } & DemoProps)`（Task 6 委托器消费）。

**实现要点**：与 Vue renderer **同结构、同输出 class**，用 `createElement`/JSX。React 组件注册表 `REACT_REGISTRY`；`resolveIcon(name)` 读 `src/icons/raw.ts`（D-2）；interactive 用 `useState`；`$占位符`同 `resolveProps`。

- [ ] **Step 1: 核实 icon 数据源**

Run: `grep -n "app-launcher\|IconNavigationAppLauncher\|navigation/app-launcher" src/icons/raw.ts | head` — 确认 name→SVG 映射结构（Task 8 TopBar 要用；Progress 用不到 icon，但 resolveIcon 先建好）。记录 raw.ts 导出形态（是 map 还是 named exports）供 resolveIcon 实现。

- [ ] **Step 2: 写 DemoRenderer.tsx（与 Vue 同 class 输出）**

`react-pilot/src/demos/DemoRenderer.tsx`：
```tsx
import { Fragment, useState, type ReactNode } from 'react'
import '@tvu/wc'
import { Progress } from '../wrappers/Progress'
import { Switch } from '../wrappers/Switch'
import type { Card, ComponentDescriptor, ContentNode, LocalizedText } from './descriptors/schema'
import type { DemoProps } from './types'
// resolveIcon reads the DS raw icon source (D-2 demo-layer; no CE/wrapper yet)
import { resolveIcon } from './resolve-icon' // small helper created in this task

const REACT_REGISTRY: Record<string, any> = { Progress, Switch }
function comp(name: string) { const c = REACT_REGISTRY[name]; if (!c) throw new Error(`DemoRenderer(react): unknown component "${name}"`); return c }

function resolveProps(props: Record<string, unknown> = {}, ctx: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {}
  for (const [k, v] of Object.entries(props)) out[k] = typeof v === 'string' && v.startsWith('$') ? ctx[v.slice(1)] : v
  return out
}

export function DemoRenderer({ descriptor, theme, locale }: { descriptor: ComponentDescriptor } & DemoProps) {
  const t = (txt: LocalizedText) => (locale === 'zh-CN' ? txt[1] : txt[0])
  return (
    <div className="docs-page">
      {descriptor.sections.map((sec, si) => (
        <section className="docs-section" key={si}>
          <h2 className="docs-section__title">{t(sec.title)}</h2>
          {sec.summary && <p className="docs-section__summary">{t(sec.summary)}</p>}
          <div className={sec.layout === 'stack' ? 'docs-demo docs-demo--stack' : 'docs-demo'}>
            <div className="docs-demo-grid">
              {sec.cards.map((card, ci) => <CardView key={ci} card={card} ck={`${si}:${ci}`} theme={theme} t={t} />)}
            </div>
          </div>
        </section>
      ))}
    </div>
  )
}
```
`CardView`（同一文件内）用 `switch(card.type)` 逐类渲染，**输出 class 与 Vue 组件/renderer 逐字一致**：
- `figma-coverage`/`status-matrix`：**用 Task 3 的共享纯 TS 逻辑** `computeCoverageView(card.members, theme)` / `computeStatusMatrixView(card.members, theme, card.baseProps)` 拿到 view 数据，再复现 FigmaMembersGrid/StatusMatrix 的 DOM（`.figma-members-grid__*` + chip 用 `.figma-members-grid__chip` / `.status-matrix__*`）。live 预览 = `createElement(comp(card.component), variant.props)`。文件顶 `import './demo-figma-members.css'`（React 侧加载共享样式）。**逻辑不重抄**——与 Vue 组件共用同一份 compute 函数（Task 3 已单一来源）。
- `component-matrix` 复现 `.family-matrix`（读 `card.members.axes` 取 rowAxis/colAxis 值）；`instance-list` 复现 `.value-grid`；`interactive` 用 `useState(card.state)` + `.try-range`/DS `<Switch>`/`.try-readout`；`slotted-instance` 递归 `renderNode`（Task 8 用）；`prose`/`code-block` 同 Vue；`custom` throw。`renderNode` 对 `icon` 调 `resolveIcon(name,size)`。

> 关键约束（spec §3.2 + owner canonical-rich, Task 3 Option A）：React 侧 `figma-coverage`/`status-matrix` 渲染的**完整** DOM（axes chips + live 变体预览 + meta/held-constant）+ 视图逻辑，都来自 Task 3 单一来源的 `figma-members-view.ts` + `demo-figma-members.css`，与 Vue 组件共用——**不是**旧的简化 coverage-grid，也**不重抄逻辑**。

- [ ] **Step 3: 写 renderer 单测（React Testing Library / jsdom）**

`tests/demo-renderer-react.test.ts`（对齐现有 `tests/react-island.test.ts` 的 RTL 用法）：
```ts
import { render } from '@testing-library/react'
import { createElement } from 'react'
import { DemoRenderer } from '../react-pilot/src/demos/DemoRenderer'
import { progressDescriptor } from '../react-pilot/src/demos/descriptors/progress'
import { describe, it, expect } from 'vitest'

describe('DemoRenderer (react)', () => {
  it('renders 6 sections + figma-members-grid + family cells + code + range', () => {
    const { container } = render(createElement(DemoRenderer, { descriptor: progressDescriptor, theme: 'dark', locale: 'en-US' }))
    expect([...container.querySelectorAll('.docs-section__title')].map((n) => n.textContent)).toEqual([
      'Figma Coverage', 'Family Matrix', 'Runtime Value', 'Development Usage', 'Status Matrix', 'Interactive: showLabel'])
    expect(container.querySelector('.figma-members-grid')).toBeTruthy()
    expect(container.querySelectorAll('.family-matrix__cell').length).toBeGreaterThan(0)
    expect(container.querySelector('.code-block')).toBeTruthy()
    expect(container.querySelector('input.try-range')).toBeTruthy()
  })
})
```

- [ ] **Step 4: 跑测试确认 fail → 实现 → pass**

Run: `pnpm vitest run tests/demo-renderer-react.test.ts`
Expected: 先 fail → 修 → passed。

- [ ] **Step 5: 提交（命中视觉门——含 .tsx，同 Task 4 判定）**

```bash
git add react-pilot/src/demos/DemoRenderer.tsx react-pilot/src/demos/resolve-icon.ts tests/demo-renderer-react.test.ts
VISUAL_COMMIT_APPROVED=1 git commit -F <msgfile>   # feat(D2): React DemoRenderer + registry + resolveIcon
```

---

## Task 6: 接线 Progress 委托器 + 视觉对齐验证（owner gate）

**Files:**
- Modify: `playground/docs/pages/ProgressPage.vue`（demo 区 → `<DemoRenderer :descriptor>`；API 表 region 外保留）
- Modify: `react-pilot/src/demos/Progress.tsx`（→ 薄委托器）

**Interfaces:**
- Consumes: Task 2 `progressDescriptor`；Task 4 Vue `DemoRenderer`；Task 5 React `DemoRenderer`。

- [ ] **Step 1: 改 ProgressPage.vue demo 区为委托**

把 [`ProgressPage.vue`](../../../playground/docs/pages/ProgressPage.vue) 的 `<FrameworkDemoRegion>...6 段手写 demo...</FrameworkDemoRegion>` 替换为：
```vue
<FrameworkDemoRegion :loader="() => import('@demos/Progress')" :locale="(props.locale as 'en-US' | 'zh-CN')">
  <DemoRenderer :descriptor="progressDescriptor" :theme="docsTheme" :locale="(props.locale as 'en-US' | 'zh-CN')" />
</FrameworkDemoRegion>
```
`<script setup>` 删掉现已由 descriptor/renderer 承担的常量与 computed（figmaMembers 直接用/statusList/sizeList/usageCode/progressShowLabel/progressValue 等），新增 `import DemoRenderer from '../components/DemoRenderer.vue'` + `import { progressDescriptor } from '@demos/descriptors/progress'`。**API 表 section（region 外）+ progressApiRows/progressSlotRows/progressEventRows 保留不动。**

- [ ] **Step 2: 改 Progress.tsx 为薄委托器**

`react-pilot/src/demos/Progress.tsx` 整体替换为：
```tsx
import { DemoRenderer } from './DemoRenderer'
import { progressDescriptor } from './descriptors/progress'
import type { DemoProps } from './types'
export default function ProgressDemo(props: DemoProps) {
  return <DemoRenderer descriptor={progressDescriptor} {...props} />
}
```

- [ ] **Step 3: 跑既有 gate 确认零回归**

Run: `pnpm audit:demo-framework-parity && pnpm vitest run tests/ButtonPage.test.ts tests/RemainingCanonicalPages.test.ts && pnpm vitest run`
Expected: demo-framework-parity 仍绿（标题集不变）+ 全 vitest 绿。

- [ ] **Step 4: 视觉对齐验证（owner gate — 治本核心证据）**

启 `pnpm dev`（Vue 态）+ react-pilot 或 docs React 态切换，**owner 亲审** Progress 页两态：
- 六段结构一致；
- ①Figma Coverage React 侧现在**也有** 16 变体 live 预览（canonical-rich 生效）；
- ⑤Status Matrix 两态同结构（轴 chips + held-constant + 预览）；
- Try-it range + showLabel Switch 两态都工作。
截图归档进 SDD ledger（[[feedback_verify-subagent-artifacts-exist]]：亲核，不信报告文字）。

- [ ] **Step 5: 提交（命中视觉门，owner approve 后）**

restore report 产物后：
```bash
git add playground/docs/pages/ProgressPage.vue react-pilot/src/demos/Progress.tsx
VISUAL_COMMIT_APPROVED=1 git commit -F <msgfile>   # refactor(D2): Progress demo → single-source descriptor (PoC 1)
```

---

## Task 7: Button 迁移（hoverPreview + prose + 默认文字 slot）

**Files:**
- Create: `react-pilot/src/demos/descriptors/button.ts`
- Modify: `playground/docs/components/DemoRenderer.vue` + `react-pilot/src/demos/DemoRenderer.tsx`（若需新 card 特性）
- Modify: `playground/docs/pages/ButtonPage.vue` + `react-pilot/src/demos/Button.tsx`（委托器）
- Test: 扩 `tests/demo-renderer-vue.test.ts` / `-react.test.ts`（button descriptor 用例）

**前置亲读**（implementer 起手 mandatory）：完整读 [`ButtonPage.vue`](../../../playground/docs/pages/ButtonPage.vue) demo 区 + [`Button.tsx`](../../../react-pilot/src/demos/Button.tsx)，逐段记录：9 个 axis section 的 `row.rows[].samples[]` 结构、2 个信息 section、Loading 40-成员矩阵、hover 预览逻辑（`hoveredKey`/`resolvedStatus`）、默认文字 slot（"Button"）。

**新增 schema/renderer 能力**（写进 Task 1 schema + 两 renderer）：
- `component-matrix.hoverPreview?: { prop, value }`（已在 schema）：renderer 对每个 cell 绑 `mouseenter/leave/focusin/focusout`，hover 时把 `cellProps[prop]` 换成 `value`（Button = `{prop:'status', value:'hover'}`）。Vue 用局部 reactive hover key、React 用 `useState`。
- 默认文字 slot：`component` content-node 的 `children:[{kind:'text',...}]` → 组件默认 slot 内容（Button 文字）。
- Button 的 axis section 若 `row.rows[].samples[]` 不吻合 `component-matrix`（Button 无 Figma axis rows/cols 那种规整），用 `instance-list`（每 sample 一 item）或新增轻量 `sample-grid` card——**遇到再定，先按 instance-list 尝试**，不吻合则扩 schema 并记录。

- [ ] **Step 1: 亲读两端 Button demo + 写 button descriptor**（数据，对照源码逐段）
- [ ] **Step 2: 扩 renderer 支持 hoverPreview + 默认文字 slot（若 Step 1 需要新 card 一并加）**
- [ ] **Step 3: 写 button renderer 单测**（断言 9 axis section 标题 + hover 切 status + 默认文字 "Button" 渲出）
- [ ] **Step 4: 跑测试 fail→实现→pass；接委托器；跑 `pnpm audit:demo-framework-parity` + 全 vitest**
- [ ] **Step 5: 视觉对齐验证（owner gate）**——Button 页两态：9 axis 矩阵、hover 预览、Loading 矩阵、Dev Usage code-block 一致
- [ ] **Step 6: 提交（视觉门，owner approve 后；分别 add button.ts / renderer / ButtonPage.vue / Button.tsx 具体文件）**

---

## Task 8: TopBar 迁移（打穿治本三难点）

**Files:**
- Create: `react-pilot/src/demos/descriptors/topbar.ts`
- Modify: 两 renderer（`slotted-instance` + `content-tree` 递归 + icon-by-name + switch-toggle + 多 var interactive 已在 schema/renderer，验证补齐）
- Modify: `playground/docs/pages/TopBarPage.vue` + `react-pilot/src/demos/TopBar.tsx`（委托器）
- Test: 扩 renderer 单测（topbar 用例）

**前置亲核**（implementer 起手 mandatory — stale 纪律）：live 读当前 [`TopBarPage.vue`](../../../playground/docs/pages/TopBarPage.vue) + [`TopBar.tsx`](../../../react-pilot/src/demos/TopBar.tsx)，**亲自确认** subagent 报的"Logo/九宫格/search 已手工补齐 + Icon 内联 + 原生 checkbox toggle"三条现状（[[feedback_verify-subagent-artifacts-exist]]）；记录具名 slot（logo/menu/right-content/search）确切内容 + Icon name（`navigation/app-launcher`）+ toggle 绑定。

**治本验证点**：
- `slotted-instance` + `content-tree`：TopBar 主实例的 logo/menu/right-content/search 从**同一 content-tree** 生成（Vue `<template #x>` ↔ React `x={}` prop 都由 renderer 从 content-tree 产出）。
- icon-by-name（D-2）：content-tree `{kind:'icon', name:'navigation/app-launcher', size:28, class:'topbar-grid-icon'}` → Vue `<Icon>` / React `resolveIcon`。**React 不再手抄 SVG。**
- toggle 统一：`interactive.controls[{type:'switch'}]` → 两端都 DS `<Switch>`。**React 不再用原生 checkbox。**

- [ ] **Step 1: 亲核 TopBar 现状 + 写 topbar descriptor**（含 slotted-instance slots content-tree + 多 var interactive showMenu/showSearchBox）
- [ ] **Step 2: 验证/补齐 renderer 的 slotted-instance + content-tree(component/el/icon) 递归 + resolveIcon**（Progress 未触发这些分支，此处首次真跑）
- [ ] **Step 3: 写 topbar renderer 单测**（断言：Vue 侧 logo slot 渲出 `<tvu-logo>`/Logo；icon 由 Icon 组件渲非内联 svg；React 侧 rightContent prop 有内容；toggle 是 Switch 非原生 checkbox）
- [ ] **Step 4: fail→实现→pass；接委托器；跑 `pnpm audit:demo-framework-parity` + 全 vitest**
- [ ] **Step 5: 视觉对齐验证（owner gate — 漂移原点治本）**——TopBar 两态：九宫格图标一致、Logo 一致、search/menu slot 一致、showMenu/showSearchBox toggle 都是 Switch 且都工作
- [ ] **Step 6: 提交（视觉门，owner approve 后）**

---

## Task 9: Audit 升级（migration-aware）+ negative self-証

**Files:**
- Modify: `scripts/audit-demo-framework-parity.mjs`（`main()` per-component 循环 + gap 块，现 L638 / L657-668）
- Modify: `tests/audit-demo-framework-parity.test.ts`（新增迁移感知用例）

**Interfaces:**
- 新增 export `isMigrated(vueSrc: string, reactSrc: string): boolean` + `collectDelegatedDescriptor(src, framework): string | null`（返回 import 的 descriptor 模块路径）。

**逻辑**：per-component 探测——两端源都 (a) import 了 `DemoRenderer` 且 (b) import 同一 `descriptors/<x>` 模块 且 (c) FrameworkDemoRegion 内无手写 `<section class="docs-section">`（Vue）/无 `<h2 className="docs-section__title">`（React）→ **已迁移**：跑委托契约检查（两端引同一 descriptor 路径 + descriptor `validateDescriptor` 通过）。否则 → 旧标题集比对（未迁移 22 个零回归）。输出"已迁移 X/25"，未迁移列为已知债、不 fail。

- [ ] **Step 1: 写 isMigrated + collectDelegatedDescriptor（失败测试先行）**

`tests/audit-demo-framework-parity.test.ts` 新增：
```ts
import { isMigrated, collectDelegatedDescriptor } from '../scripts/audit-demo-framework-parity.mjs'
const vueMig = `<template><FrameworkDemoRegion :loader="l"><DemoRenderer :descriptor="progressDescriptor" /></FrameworkDemoRegion></template>
<script setup>import DemoRenderer from '../components/DemoRenderer.vue'; import { progressDescriptor } from '@demos/descriptors/progress'</script>`
const reactMig = `import { DemoRenderer } from './DemoRenderer'; import { progressDescriptor } from './descriptors/progress'
export default (p) => <DemoRenderer descriptor={progressDescriptor} {...p} />`
it('detects a migrated component pair', () => {
  expect(isMigrated(vueMig, reactMig)).toBe(true)
  expect(collectDelegatedDescriptor(vueMig, 'vue')).toContain('descriptors/progress')
  expect(collectDelegatedDescriptor(reactMig, 'react')).toContain('descriptors/progress')
})
it('detects a non-migrated (legacy) pair', () => {
  expect(isMigrated('<template><FrameworkDemoRegion><section><h2 class="docs-section__title">{{t(\\'A\\',\\'甲\\')}}</h2></section></FrameworkDemoRegion></template>', 'export default ()=><h2 className="docs-section__title">{t("A","甲")}</h2>')).toBe(false)
})
```

- [ ] **Step 2: 跑测试确认 fail**

Run: `pnpm vitest run tests/audit-demo-framework-parity.test.ts`
Expected: FAIL（`isMigrated` 未定义）。

- [ ] **Step 3: 实现 isMigrated + collectDelegatedDescriptor + main() 双模分派**

在 `audit-demo-framework-parity.mjs` 加两 export；`main()` per-component 循环里：`isMigrated` → 委托契约检查（两端 descriptor 路径相等 + import 后 `validateDescriptor` 通过；不等/不合法 push gap）；else → 现有标题集比对。汇总时 log `migrated X/25`。

- [ ] **Step 4: 跑测试 + 全量 audit 确认**

Run: `pnpm vitest run tests/audit-demo-framework-parity.test.ts && pnpm audit:demo-framework-parity`
Expected: 单测全 passed；audit 报 "migrated 3/25"（Progress/Button/TopBar）+ 其余 22 标题闸绿 + exit 0。

- [ ] **Step 5: negative self-証（证 audit 真能抓绕过）**

临时在 `ProgressPage.vue` 的 `<DemoRenderer>` 旁手写一个 `<section class="docs-section"><h2 class="docs-section__title">{{ t('Sneak', '偷') }}</h2></section>` → 跑 `pnpm audit:demo-framework-parity` → **断言 FAIL**（migrated 组件不该有手写 section）→ `git checkout -- playground/docs/pages/ProgressPage.vue` 恢复 → 复跑 → PASS。三态 stdout 记入 SDD ledger。

- [ ] **Step 6: 提交（.mjs + .test.ts，不命中视觉门）**

```bash
git checkout -- playground/docs/pages/ProgressPage.vue   # 确保 negative test 残留已清
git add scripts/audit-demo-framework-parity.mjs tests/audit-demo-framework-parity.test.ts
git commit -F <msgfile>   # feat(D2): migration-aware demo-framework-parity audit + self-verification
```

---

## Self-Review

**1. Spec coverage**（逐 spec 章核）：
- §4 D-1 runtime interpreter → Task 4/5 renderer ✅
- §4 D-2 Icon demo 层 → Task 5 resolveIcon + Task 8 icon-by-name ✅
- §4 D-3 机制+3 PoC → Task 1-8 ✅；停下复盘 = Task 8 后 handoff（执行时）
- §4 D-4 混合交互 / 逃生舱 seam only → schema `custom` throw（Task 1/4/5）✅；PoC 无复杂档 ✅
- §5 架构三单元 → schema(T1)/renderer(T4,T5)/registry(T4,T5) ✅
- §6 card 类型表 → Task 1 schema 全覆盖 ✅
- §6 canonical-rich 精化 → Task 3 CSS 单一来源 + T4/T5 复用 FigmaMembersGrid/StatusMatrix ✅
- §6 三分叉治好（Icon/toggle/硬编码）→ Task 8 icon+switch / Task 2 members 数据驱动 ✅
- §7 PoC 序 → Task 6(Progress)/7(Button)/8(TopBar) ✅
- §8 audit migration-aware → Task 9 ✅
- §11 验证（视觉门/audit/回归/negative test）→ Task 3/6/7/8 视觉门 + Task 9 negative ✅
- §13 开放项：figma-data 可 import(已核实 T)/`h()` 渲染函数(T4)/占位符 resolveProps(T4/5)/audit 探测信号(T9)/negative test(T9 纳入)/提交分 PoC(T6/7/8 各一)/restore 纪律(global) ✅

**2. Placeholder scan**：Task 7/8 的 descriptor 数据步骤要求 implementer 亲读源码后填——这是**数据转写**（非逻辑占位），给了确切文件 + 逐段结构要求 + 新增 card 的处理策略，非 "TODO"。renderer 关键分支（figma-coverage/matrix/interactive/content-tree/hoverPreview/resolveIcon/audit 探测）均给真代码或明确算法。

**3. Type consistency**：`ComponentDescriptor`/`Card`/`ContentNode`/`Control`/`LocalizedText`/`validateDescriptor`/`resolveProps`/`isMigrated`/`collectDelegatedDescriptor` 全 task 一致；两 renderer props 签名对齐 `DemoProps`。

---

## 执行方式

- **SDD in-session subagent**（横切两端 + 视觉敏感）：controller（我）dispatch per-task implementer + task-reviewer 复核；implementer 不自 commit，只报 diff+stdout；controller gate 绿 + owner 视觉审后才提交（[[feedback_executor-no-self-commit]]）。
- **owner gate**：Task 6/7/8 视觉对齐要 owner 亲审 demo 效果 approve（Task 8 后停下复盘全 D2 效果）。
- **发版 owner-gated**：D1+D2（机制+3 PoC）全完成 + owner 查看效果后才定 v0.11.0。
