# INFRA-F45 — 「组件 demo 必演示 Slot + Boolean」全量回填 + audit gate 设计

> **状态**：设计已 owner 批准（A1 + B1，2026-07-03）。
> **规则真源**：[`AGENTS.md`](../../../AGENTS.md) 硬规则 #8。
> **backlog**：[`docs/internal/backlog.md`](../../internal/backlog.md) INFRA-F45。
> **exemplar**：PopupBox（React `App.tsx` overlay 段已达标；Vue `PopupBoxPage.vue` 缺 live boolean 切换，本 sprint 补齐）。

---

## 1. 目标

把硬规则 #8 从 L1（AI 自律）升到 L4/L5（pre-commit / CI gate）：

- **全量回填**：每个「有 slot 或 boolean prop」的组件，在 **Vue playground 页** 与 **React pilot demo** 两侧各演示
  - (a) slot 投影**真实内容**，
  - (b) ≥1 个 boolean prop 的 **live 可交互切换**（控件驱动 state 翻转 prop，非静态字面量）。
- **确定性 audit**：`scripts/audit-demo-slot-boolean-coverage.mjs`，从 `components.config.ts` 派生 in-scope 清单，静态解析两侧 demo 源码断言达标。
- **pre-commit gate**：回填**完成后**升为阻塞；回填期间 report-only。

**动机**：slot 投影 + boolean 属性是双框架 CE 最易 silent 破的两条链（INFRA-F43 light-DOM slot 恒空 body 数版本未被发现即实证）。static demo 只证"声明了"，live 切换 + 真 slot 才证"真的通"。

---

## 2. In-scope 判定（audit 与回填共用同一 SoT）

**SoT = `src/web-components/components.config.ts`**。一个组件 in-scope ⟺ 满足任一：
- `hasDefaultSlot === true`，或
- `namedSlots.length > 0`，或
- 存在 `props[].tsType === 'boolean'`（精确等于字面 `boolean`；`'yes' | 'no'` / `'on' | 'off'` 等枚举**不算** boolean）。

audit 必须**运行时从 config 派生**该集合，不写死清单——config 加组件后 audit 自动纳入（防清单 drift）。

### 2.1 当前 in-scope = 28 个（35 − 7）

**排除（既无 slot 又无 boolean）**：Switch、Pagination、DropDownListSelect、Chart、Logo、MenuList、UserMenu。

每个 in-scope 组件的 slot 清单 + 选定的 boolean toggle prop：

| # | 组件 | slot(s) | 选定 boolean toggle | Vue 页 |
|---|------|---------|--------------------|--------|
| 1 | Button | default | —（无 boolean）| ButtonPage |
| 2 | Input (InputBoxLine) | — | `readonly` | InputPage |
| 3 | FormItem | default, `label` | `required` | FormItemPage |
| 4 | Badge | default | —（无 boolean）| BadgePage |
| 5 | PillStatus | default, `count` | `active` | PillPage |
| 6 | Progress | — | `showLabel` | ProgressPage |
| 7 | Rating | — | `readonly` | RatingPage |
| 8 | BreadcrumbItem | default | `showSeparator` | BreadcrumbPage |
| 9 | TopBar | logo/left/search/menu/right-content | `showMenu` | TopBarPage |
| 10 | CheckBox | default | `readonly` | CheckboxPage |
| 11 | Radio | default | —（无 boolean）| RadioPage |
| 12 | PillCounter | default | —（无 boolean）| PillPage |
| 13 | Breadcrumb | default | —（无 boolean）| BreadcrumbPage |
| 14 | InputBoxFilled | — | `readonly` | InputPage |
| 15 | InputNumber | — | `disabled` | InputNumberPage |
| 16 | Slider | — | `disabled` | SliderPage |
| 17 | Tab | default | —（无 boolean）| TabsPage |
| 18 | TabList | default | —（无 boolean）| TabsPage |
| 19 | TabItem | default | `disabled` | TabsPage |
| 20 | Steps | default | —（无 boolean）| StepsPage |
| 21 | StepItem | — | `showLeadingConnector` | StepsPage |
| 22 | **PopupBox** | default, `footer` | `closable` | PopupBoxPage（Vue 补 live 切换）|
| 23 | SelectBoxLine | — | `multiple` | SelectPage |
| 24 | SelectBoxFilled | — | `multiple` | SelectPage |
| 25 | Tooltip | default, `content` | `disabled` | TooltipPage |
| 26 | Notification | — | `closable` | NotificationPage |
| 27 | Message | — | `closable` | MessagePage |
| 28 | Table | — | `striped` | TablePage |

> **per-component 要求**：有 slot 的必须演示 slot 投影；有 boolean 的必须演示 live 切换。**两者独立**——只有 slot 没 boolean（如 Button）只需 slot；只有 boolean 没 slot（如 Notification）只需 boolean live。选定 toggle prop 可在实现时按最能体现语义换（如 Slider 用 `showValue`、Tooltip 用 `open`），audit 只要求"≥1 个该组件的 boolean prop live 绑定"。

### 2.2 canonical 事实（已核实，无需改 canonical）

- **Badge** `<slot>{{ content }}</slot>` — slot 真实（有 prop 兜底），投影真内容有意义。
- **TopBar** 具名 slot 条件渲染真实（`v-if="$slots.menu"`）；React wrapper 已暴露 `logo/left/search/menu/rightContent` 具名 slot props（META-02）。
- 无需任何 canonical/base SFC 改动——**本 sprint 只改 demo + 新增 audit**。

---

## 3. Demo 标准（A1 严格字面）

每侧、每 in-scope 组件：

**slot 投影**：组件元素内投影**真实可见内容**（非空、非纯空白）。
- 默认 slot：Vue `<Comp>真内容</Comp>` / React `<Comp>真内容</Comp>`。
- 具名 slot：Vue `<template #name>` / React 具名 slot prop（`menu={<…>}`）。TopBar 至少投影 `menu`（放一个真实 MenuList / 链接组）。

**boolean live 切换**：把选定 boolean prop 绑到一个响应式变量，并在同一 demo 块提供一个可交互控件翻转它。
- Vue：`const foo = ref(true)` → `:the-bool="foo"` + 一个 `<Switch>/<CheckBox>` 或按钮 `@click`/`v-model` 改 `foo`。
- React：`const [foo,setFoo]=useState(true)` → `theBool={foo}` + `<CheckBox onStatusChange=… >` 或 button `onClick={()=>setFoo(v=>!v)}`。
- 复用 PopupBox exemplar 的写法（React App.tsx L199-205 + L215 的 `modalClosable`/CheckBox）。

**exemplar 补口**：PopupBoxPage.vue 增一个 `closable` 的 live 切换（ref + Switch/CheckBox），使 Vue 侧也字面达标。

### 3.1 UX 约束（owner UX review 2026-07-03 补）

从 UX 设计师视角审 plan 后补三条，防止"为过 audit 把 demo 做成丑陋补丁"：

1. **统一「Interactive-props」控件范式（硬约束）**：28 个页面的 live 切换控件**必须复用同一 UI pattern**，不允许各页随手 bolt 一个没对齐的 checkbox。范式定义：
   - **位置**：demo 区上方一个固定的 "Interactive props" 控件条（Vue 页）/ demo Card 内顶部（React）。
   - **控件**：优先用 DS 自己的 `Switch` / `CheckBox`（dogfooding），带**清晰标签 + 当前值可见**（如 `closable ✓`）。
   - **双语**：Vue 页控件标签走 `t(en, zh)`（React pilot 英文即可）。
   - plan 里给一段统一模板片段，所有页/Card 套用。

2. **A1 的取舍诚实登记**：live toggle 强在"手感"、弱在"扫读对比"；对 `striped`/`showSeparator` 这类布尔，"两态并排"其实参考性更强。**本 spec 统一 A1 是为了 audit 确定性（混合 A1/A2 会让静态 gate 难判定），并非 A1 就是 UX 全局最优**——这是有意识的 enforcement-over-pedagogy 取舍。允许在 live toggle 之外**额外**加静态两态展示（不强制、不影响 audit）。

3. **两侧投入不对等（定位诚实）**：**Vue playground 页 = 主 docs**（开发者日常查用法的地方）→ UX 打磨预算投这里；**React pilot = 双框架回归证明面 / QA surface**（单文件 gallery，开发者不逐组件查）→ React 侧 toggle 做到"能证明绑定活着"即可，不追求教学美观、不与 Vue 页等价投入。
   > **React 逐组件 docs 体验是独立缺口**，不在 F45 内解决：owner 2026-07-03 决定另立 **INFRA-F46「docs 全局 Vue↔React 框架开关（逐组件 React 视图）」**——在同一 Vue docs 外壳上加全局框架开关，拨到 React 时所有组件页 demo 区统一切 React 版本（复用 shell / TOC / API 表，非独立站）。F46 走自己的 baseline + 设计（关键架构 fork = React islands vs scoped iframe），F45 只按 scope ① 收（React 维持 gallery + 只加 demo）。

---

## 4. Audit 设计（B1 静态结构解析）

**文件**：`scripts/audit-demo-slot-boolean-coverage.mjs`（ESM，对齐现有 `scripts/audit-*.mjs` 风格）。

### 4.1 输入
- `src/web-components/components.config.ts` → 派生 in-scope 集合 + 每组件 slot/boolean 元信息。
- React demo：`react-pilot/src/App.tsx`。
- Vue demo：`playground/docs/pages/*.vue`（组件→页映射见 §2.1 表；映射表内置于 audit，缺页即 FAIL）。

### 4.2 每组件、每侧断言
1. **used**：该组件在对应 demo 源码中被使用（React 按 wrapper import 名 / JSX tag；Vue 按 import 名 / template tag）。
2. **slot（若组件有 slot）**：至少一处使用带**非空子内容**（默认 slot 有 children，或具名 slot 有对应 `<template #x>` / 具名 slot prop）。
3. **boolean live（若组件有 boolean prop）**：至少一处使用把某 boolean prop 绑到**非字面量表达式**（引用一个变量），即 live 绑定的结构签名：
   - React：`theBool={identifier}`（排除 `{true}`/`{false}`、排除静态 attr `theBool`/`theBool={/* literal */}`）。
   - Vue：`:the-bool="identifier"`（排除 `:the-bool="true"`/`"false"` 与静态 `the-bool`）。
   - **加强**：该 identifier 必须在同文件被"写"过（React 有 `setIdentifier` 或对应 setter 调用；Vue 有对该 ref 的赋值 / `v-model` / 事件 handler），证明有翻转路径，非只读常量。

### 4.3 解析实现
- 优先用真解析器而非脆弱 regex：Vue 用 `@vue/compiler-sfc`（仓库已依赖）解析 `<template>` AST；React/TSX 用 TypeScript compiler API（`typescript` 已依赖）解析 JSX AST。
- 实现前先看现有 `scripts/audit-binding-config-parity.mjs` / `audit-canonical-compliance.mjs` 的解析范式，复用同风格。
- 若某组件多处使用，规则是"**至少一处**满足 slot + 至少一处满足 boolean-live"（不要求同一处同时满足）。

### 4.4 输出
- 逐组件、逐侧 PASS/FAIL 表 + 缺口原因（`missing-slot` / `missing-boolean-live` / `not-used` / `page-not-found`）。
- 汇总计数。
- exit code：**report-only 阶段 exit 0**（打印 FAIL 但不阻塞）；**回填完成后 exit 1 on any FAIL**。

### 4.5 report-only → blocking 切换
- audit 内置 `REPORT_ONLY` 常量（或 `--report-only` flag）。
- 回填全绿实测后，一次性把默认改为 blocking + 挂 pre-commit。切换 commit 独立、message 注明。

---

## 5. pre-commit gate（memory `feedback_audit-pre-commit-gate`）

- 挂进 `.husky/pre-commit`（或现有 hook 脚本），**条件触发**：staged 命中 `react-pilot/src/App.tsx` / `playground/docs/pages/*.vue` / `src/web-components/components.config.ts` / 该 audit 自身时才跑（对齐现有 conditional gate 如 icon-canonical-names）。
- 纳入 `audit:self-audit-phase2` 链（若合适）。
- `package.json` 加 `"audit:demo-slot-boolean": "node scripts/audit-demo-slot-boolean-coverage.mjs"`。

---

## 6. 执行策略

1. **audit 先行（report-only）**：先写 audit，跑出**确定性 baseline 红名单**（替代人工 baseline，消除误差）。
2. **回填**：
   - React 侧（`App.tsx` 单文件）由主线串行改（单文件不可并行）。
   - Vue 侧（各 `*Page.vue` 独立）可派并行 subagent 分批回填（每 subagent 一组页，互不重叠），主线复审 diff。
   - 每组件回填后本地跑 audit 看该行转绿。
3. **build 产物**（committed demo 实物）：
   - React demo 改动后重建 `react-pilot/dist`（git-tracked，部署实物）：`pnpm build:react-pilot`。
   - Vue playground：`pnpm build:playground`（best-effort 链 react-pilot）。
   - **本 sprint 不改 canonical/base SFC → 无需 `build:wc` 重生成 wrapper**（demo 仅消费既有 wrapper）。若回填中发现某 wrapper 缺 named-slot prop（如 TopBar React menu），才触发 `build:wc` + 重生成 bindings，另记。
4. **升 gate**：全绿后切 blocking + 挂 pre-commit。
5. **收尾**：跑 sprint 收尾 self-audit（D/E/F/G）+ 更新 STATUS/backlog（INFRA-F45 移出 Active）+ commit（含三端 push；React demo 视觉改动需 `VISUAL_COMMIT_APPROVED=1`）。

---

## 7. 验证（gates）

- `node scripts/audit-demo-slot-boolean-coverage.mjs` → 全 PASS（blocking 模式 exit 0）。
- `pnpm exec vue-tsc --noEmit` / `pnpm vitest run`（demo 改动不应破类型/测试）。
- `pnpm build:react-pilot` 成功 + `react-pilot/dist` 更新已 stage。
- React demo 若视觉变 → playwright 截图核实渲染完整。
- sprint 收尾 self-audit D/E/F/G exit 0。

---

## 8. 非目标（YAGNI）

- 不改任何 canonical/base SFC 的 props/slots（除非回填暴露 wrapper 缺 named-slot prop，另记 backlog）。
- 不引入 runtime playwright toggle 断言（slot 投影 runtime 证明已由现有 render-verification gate 覆盖）。
- 不给"既无 slot 又无 boolean"的 7 个组件加任何东西。
- 不做 docs 站视觉大改版——只在既有 demo 结构里加最小 live 控件 + slot 内容。

---

## 9. 风险 / 注意

- **多处使用歧义**：组件在 demo 里多次出现时 audit 取"任一处满足"——避免误报，但也意味着一处达标即过。可接受（规则是"演示过"）。
- **静态解析局限**：证明"绑到可写变量"≠证明"UI 上真能点到"。用真解析器 + code review 兜；runtime 强证明留给 render-verification（slot 侧）。
- **并行 subagent 一致性**：各 Vue 页回填风格须统一（ref 命名、控件选型），plan 里给统一模板片段。
- **build 环境**：`build:react-pilot` 含 `pnpm install --frozen-lockfile`，本地跑；部署侧 best-effort 回退 committed dist（META-02 已处理）。
