# INFRA-F59 外观变体 prop 命名统一 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:** 把 8 个组件的外观变体 prop 统一命名（`fill`/`type`/`color`/`state`），消灭 Button `style` footgun，加一道 SoT 驱动的横向一致性 audit gate 防漂回。

**Architecture:** 只改 **canonical 公开层 + config + 派生物**，base 组件内部 prop 名不动（canonical→base 边界映射沿用现有 normalize 模式）。改名靠现有 pre-commit gate（binding-config-parity / component-affordances / demo-parity / vitest）保原子性；新 gate 在所有改名完成后最后接线（届时真实 config 已干净）。Figma 属性名保留、code 用规范名、差异登记 `prop-aliases.json` approved-alias。破坏性改名（无 runtime alias），走 MIGRATION + changeset。

**Tech Stack:** Vue 3.5 SFC (`defineCustomElement`) · Node 22+ 原生 TS stripping (audit 直接 `import` `.ts` config) · vitest · Gitea/GitHub CI + husky pre-commit + prepublishOnly。

## Global Constraints

- **Figma 属性名保留不变**：`data-figma-*` 属性 key 照旧（`data-figma-style`/`-tag`/`-property1`/`-property2`），value 改从新 prop 取。`prop-aliases.json` 记 code 新名 ↔ Figma 旧名 approved-alias。
- **枚举值不改**：所有值形态（`filling`/`Filled`/`Line`/大小写混用）原样保留，只改 prop 名。
- **base 组件内部 prop 名不改**：`BaseTab type` / `BaseInputNumber property1` / `BaseStepItem step-style` / Button base `canonical-style` 全不动；canonical 公开 prop 改名后在 `<BaseX :old-name="newProp">` 边界映射。
- **无 runtime deprecation alias**：旧 prop 名直接消失（破坏性）；靠 MIGRATION + changeset(minor/breaking) 传播。
- **规范名映射**：`fill`(填充处理) · `type`(种类) · `color`(颜色) · `state`(状态) · `size`(尺寸，仅守 casing)。
- **gate 零硬编码**：`audit-prop-naming.mjs` 读 `prop-naming-conventions.json` SoT + `import` config，不在脚本写死组件规则。
- **git**：owner 直 commit master、精确 `git add <file>`（禁 `-A`）、commit body 按 PR 模板字段、changeset 随实质改动同 commit。视觉/`.vue` 改动 commit 需 `VISUAL_COMMIT_APPROVED=1`（见 pre-commit hook line 74-89）。

---

### 全量改名映射（唯一真源表，所有 task 引用）

| 组件 | canonical 公开旧名 | 新名 | 值（不变）| base 边界目标 prop（不改）| Figma 属性名（alias 记）|
|---|---|---|---|---|---|
| Button | `style` | `fill` | filling/ghost/rimless | `:canonical-style` | `style` |
| Badge | `tag` | `fill` | Filled/Line | (Badge 无 base，自渲染) | `tag` |
| Tab | `type` | `fill` | Line/Text/Filled(+lc) | `:type`(normalized lc) | `type` |
| Tab | `property2` | `color` | White/Green(+lc) | `:active-color`(normalized) | `Property 2` |
| TabList | `type` | `fill` | Line/Text/Filled(+lc) | `:type`(normalized lc) | `type` |
| TabItem | `type` | `fill` | Line/Text/Filled(+lc) | `:type`(normalized lc) | `type` |
| TabItem | `property1` | `state` | Normal/Active(+lc) | `:state`(normalized) | `Property 1` |
| TabItem | `property2` | `color` | White/Green(+lc) | `:active-color`(normalized) | `Property 2` |
| Steps | `stepStyle` | `type` | number/icon | `:step-style` | `Property 1`(见 Task 4 step 1 核实) |
| StepItem | `stepStyle` | `type` | number/icon | `:step-style` | `Property 1`(同上) |
| StepItem | `style`(删除) | — | — | — | — |
| InputNumber | `property1` | `type` | Default/Only Add/Only Reduce/Readonly | `:property1` | `Property 1`(已有 alias entry 006) |
| Pagination | `type`(不改) | `type` | Classic/Simple/Small | — | 打 `axis:'kind'` |
| Badge | `type`(不改) | `type` | Circle/Rectangle | — | 打 `axis:'kind'` |

> **axis 标记**（Task 7 加进 config，供 gate 概念一致性校）：`fill` 组 → `axis:'fill'`；`type` 组（含不改的 Pagination/Badge `type`）→ `axis:'kind'`；`color` 组 → `axis:'color'`；TabItem `state` → `axis:'state'`。

---

### Task 1: Button `style` → `fill`（含 React remap 移除）

**Files:**
- Modify: `src/canonical/ButtonBridge.vue`（defineProps + default + data-figma + 转发）
- Modify: `src/web-components/components.config.ts:121-125`（Button `style` prop entry）
- Modify: `src/web-components/reserved-names.ts`（删 `style: 'variant'`）
- Modify: `docs/internal/component-affordances.json:1024`（Button `style` → `fill`）
- Modify: `scripts/generate-render-verification-manifest.mjs`（Button 块 codeProps `style` → `fill`）
- Modify: `playground/docs/pages/ButtonPage.vue`（props 对象 `style:` → `fill:` + 展示串）
- Modify: `react-pilot/src/wrappers/Button.test.tsx:28`（`variant="ghost"` → `fill="ghost"`）
- Modify: `tests/Button.test.ts`（**只改** canonical 公开 `style:` → `fill:`；`canonicalStyle:` 是 base prop **不动**）
- Regenerate: react bindings（`Button.tsx` / `types.ts` 派生）+ affordances `.md` + manifest

**Interfaces:**
- Produces: Button canonical 公开 prop `fill?: 'filling' | 'ghost' | 'rimless'`（default `'filling'`）；React 侧直接暴露 `fill`（不再 remap 为 `variant`）。

- [ ] **Step 1: 改 ButtonBridge.vue**

`src/canonical/ButtonBridge.vue` 改 4 处：type 别名重命名、defineProps、default、data-figma + 转发。

第 11 行 `type Style = ...` → 保留类型体、重命名：
```ts
type Fill = 'filling' | 'ghost' | 'rimless'
```
第 22 行 `style?: Style` → `fill?: Fill`
第 32 行 `style: 'filling',` → `fill: 'filling',`
第 48 行 `'data-figma-style': props.style,` → 保持 key、改取值：`'data-figma-style': props.fill,`
第 62 行 `:canonical-style="style"` → `:canonical-style="fill"`（base prop 名 `canonical-style` 不变，只改绑定源）

- [ ] **Step 2: 改 components.config.ts Button entry**

`src/web-components/components.config.ts` 第 106 行注释 `// defineProps: color, ..., style, theme` → `..., fill, theme`。
第 121-125 的 prop entry 整块替换（**不写 `axis` 字段**——`axis` 在 Task 7 才加进 `PropConfig` 接口，Task 1-6 提前写会 TS 报「对象字面量多余属性」）：
```ts
      {
        name: 'fill',
        tsType: `'filling' | 'ghost' | 'rimless'`,
        jsdoc: 'Appearance/fill variant enum (filling/ghost/rimless).',
      },
```

- [ ] **Step 3: 移除 reserved-names remap**

`src/web-components/reserved-names.ts` 第 6-8 行 `RESERVED_NAME_REMAP` 清空为空对象（保留文件 + 函数，见 spec §7 item 2）：
```ts
export const RESERVED_NAME_REMAP: Record<string, string> = {}
```
第 2-5 行注释更新：把「A canonical design axis literally named one of these...」保留，追加一句：`// Currently empty — Button's former `style` axis was renamed to `fill` (INFRA-F59), so no remap is needed. Kept as the extension point for any future reserved-name collision.`

- [ ] **Step 4: 更新 affordances SoT + manifest 生成器 + 派生物**

`docs/internal/component-affordances.json` 第 1024 行 `{ "name": "style", "type": "filling | ghost | rimless", "default": "filling" }` → `{ "name": "fill", ... }`。

`scripts/generate-render-verification-manifest.mjs` Button 块（`grep -n "figmaName: 'Button'"` 附近，非 url-link 那个）里所有 `style:` codeProps key → `fill:`。用 scoped 替换后 `grep -n "style:" ` 确认 Button 块内无残留（注意别动 url-link 块）。

`playground/docs/pages/ButtonPage.vue`：
- 第 74 行展示串 `'style=rimeless'`（注意原 typo）→ `'fill=rimless'`
- 第 121-138 等 props 对象里的 `style: '<v>'` → `fill: '<v>'`（`figmaVariantName` 字符串内的 `style=` 是 Figma 变体名**保留不动**，只改 JS `props: { ... style: ... }` 对象的 key）。命令：审 `grep -n "style:" playground/docs/pages/ButtonPage.vue`，逐条把对象 key 改 `fill:`。

`react-pilot/src/wrappers/Button.test.tsx` 第 28 行 `variant="ghost"` → `fill="ghost"`。

`tests/Button.test.ts`：第 85、290 行（及任何）canonical 公开 `style: '<v>'` → `fill: '<v>'`。**`canonicalStyle:`（base prop，line 16/50/71/104…）保持不变**——它是 `src/components/Button/Button.vue` 的内部 contract prop，本 plan 不动 base。

- [ ] **Step 5: 重生成派生物**

```bash
pnpm run gen:react-bindings                     # = node scripts/generate-react-bindings.mjs（重生成 react-pilot Button.tsx/types.ts：fill，不再 variant）
node scripts/generate-component-affordances.mjs # 重生成 affordances .md 视图
node scripts/generate-render-verification-manifest.mjs  # 重生成 manifest（Button codeProps fill）
```

- [ ] **Step 6: 跑测试验证**

Run: `pnpm exec vue-tsc --noEmit && pnpm test`
Expected: PASS（Button/Canonical/ButtonPage 相关全绿；`<Button fill="filling">` 静态写法可用，footgun 消）
Run: `node scripts/audit-binding-config-parity.mjs && node scripts/audit-component-affordances.mjs && node scripts/audit-demo-framework-parity.mjs && node scripts/audit-demo-slot-boolean-coverage.mjs`
Expected: 全 exit 0

- [ ] **Step 7: Commit**

```bash
git add src/canonical/ButtonBridge.vue src/web-components/components.config.ts src/web-components/reserved-names.ts docs/internal/component-affordances.json docs/internal/component-affordances.md scripts/generate-render-verification-manifest.mjs figma-data/render-verification-manifest.json playground/docs/pages/ButtonPage.vue react-pilot/src/wrappers/Button.test.tsx react-pilot/src/wrappers/Button.tsx react-pilot/src/wrappers/types.ts tests/Button.test.ts
VISUAL_COMMIT_APPROVED=1 git commit -m "$(cat <<'EOF'
refactor(button): rename appearance prop style→fill, drop React variant remap (INFRA-F59)

改动内容：
- ButtonBridge 公开 prop style→fill（消灭与 HTML style 冲突的 footgun；<Button fill="filling"> 静态写法可用）
- 删 reserved-names.ts style→variant remap，React 直接暴露 fill；重生成 react bindings
- components.config.ts / component-affordances / manifest 生成器 / ButtonPage / tests 同步；data-figma-style 属性名保留(值取自 fill)
改动类型：refactor (breaking)
测试：pnpm test ✅ · vue-tsc ✅ · binding-config-parity/affordances/demo-parity audits ✅
EOF
)"
```
（changeset 在 Task 8 统一写；本 task 是 breaking 但 changeset 汇总在收尾一条，避免每 task 一个 changeset 碎片。）

---

### Task 2: Badge `tag` → `fill`

**Files:**
- Modify: `src/canonical/Badge.vue`（type 别名 + defineProps + default + data-figma + class + badgeVars 里 `props.tag` 引用）
- Modify: `src/web-components/components.config.ts:217`（Badge `tag` entry）
- Modify: `docs/internal/component-affordances.json:888`（Badge `tag` → `fill`；**注意 line 875 `synonyms_en` 里的 "tag" 是搜索同义词，不动**）
- Modify: `scripts/generate-render-verification-manifest.mjs`（Badge 块 codeProps `tag` → `fill`，~40 条）
- Modify: `playground/docs/pages/BadgePage.vue`（`tag=`/`:tag=` 引用）
- Modify: `react-pilot/src/demos/Badge.tsx`（若存在 `tag=` 引用；先 `grep -n "tag" react-pilot/src/demos/Badge.tsx`）
- Modify: `tests/Canonical.test.ts:26`（`tag: 'Line'` → `fill: 'Line'`；`data-figma-tag` 断言若有则**保留 key**）
- Regenerate: react bindings + affordances .md + manifest

**Interfaces:**
- Produces: Badge 公开 `fill?: 'Filled' | 'Line'`（default `'Filled'`）；`type?: 'Circle' | 'Rectangle'` 不变。

- [ ] **Step 1: 改 Badge.vue**

`src/canonical/Badge.vue`：
- 第 5 行 `type Tag = 'Filled' | 'Line'` → `type Fill = 'Filled' | 'Line'`
- 第 10 行 `tag?: Tag` → `fill?: Fill`
- 第 14 行 `tag: 'Filled',` → `fill: 'Filled',`
- 第 45 行 `'data-figma-tag': props.tag,` → 保持 key：`'data-figma-tag': props.fill,`
- 第 53-55 行 `props.tag === 'Filled'` / `=== 'Line'` → `props.fill === ...`（3 处）
- 第 67 行 class `` `canonical-badge--${props.tag.toLowerCase()}` `` → `props.fill.toLowerCase()`

- [ ] **Step 2: 改 config + affordances + manifest 生成器 + demos/tests**

`components.config.ts:217` `{ name: 'tag', tsType: `'Filled' | 'Line'` },` → `{ name: 'fill', tsType: `'Filled' | 'Line'` },`
`component-affordances.json:888` name `tag`→`fill`（line 875 synonyms 不动）。
manifest 生成器 Badge 块 codeProps `tag:` → `fill:`。
BadgePage.vue / react demos / `tests/Canonical.test.ts:26`：canonical 公开 `tag` 用法 → `fill`；`data-figma-tag` 断言 key 保留。

- [ ] **Step 3: 重生成 + 测试**

```bash
pnpm run gen:react-bindings && node scripts/generate-component-affordances.mjs && node scripts/generate-render-verification-manifest.mjs
pnpm exec vue-tsc --noEmit && pnpm test
node scripts/audit-binding-config-parity.mjs && node scripts/audit-component-affordances.mjs
```
Expected: 全绿。

- [ ] **Step 4: Commit**（精确 `git add` 上述文件 + 派生物；`VISUAL_COMMIT_APPROVED=1`；message `refactor(badge): rename appearance prop tag→fill (INFRA-F59)`，body 同 Task 1 格式）

---

### Task 3: Tab 家族（Tab + TabList + TabItem）

外观 `type`→`fill` · 颜色 `property2`→`color` · 状态 `property1`→`state`。三组件共享 normalize 模式，一起做保 provide/inject 一致。

**Files:**
- Modify: `src/canonical/Tab.vue`（`type`→`fill`、`property2`→`color`；normalize computed 重命名）
- Modify: `src/canonical/TabList.vue`（`type`→`fill`）
- Modify: `src/canonical/TabItem.vue`（`type`→`fill`、`property1`→`state`、`property2`→`color`）
- Modify: `src/web-components/components.config.ts`（Tab 663-666 / TabList 693-696 / TabItem 725-731 三 entry）
- Modify: `docs/internal/component-affordances.json`（Tab `property2`:543 / TabItem `property1`:596 `property2`:597 + Tab/TabItem 的 `type` 条目）
- Modify: `scripts/generate-render-verification-manifest.mjs`（Tab List 块 + Tab/Item 块 codeProps：`type`→`fill`、`property1`→`state`、`property2`→`color`）
- Modify: `react-pilot/src/demos/Tabs.tsx`（35/179/182/183 行 `type=`/`property1=`/`property2=`）
- Modify: `playground/docs/pages/`（Tab 相关页；`grep -rln "property1\|property2" playground/docs/pages`）
- Modify: `tests/Tab.test.ts`（canonical 公开 prop 用法；`data-figma-*` 断言 key 保留）
- Regenerate: react bindings + affordances .md + manifest

**Interfaces:**
- Consumes: base `BaseTab`/`BaseTabList`/`BaseTabItem` 的 `:type`(lc)/`:active-color`/`:state` prop 名**不变**。
- Produces: Tab/TabList/TabItem 公开 `fill?: 'Line'|'Text'|'Filled'|'line'|'text'|'filled'`；Tab/TabItem `color?: 'White'|'Green'|'white'|'green'`；TabItem `state?: 'Normal'|'Active'|'normal'|'active'`。

- [ ] **Step 1: 改 Tab.vue**

- 第 5 行 `type TabType = ...` → `type Fill = 'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`
- 第 6 行 `type Property2 = ...` → `type Color = 'White' | 'Green' | 'white' | 'green'`
- 第 10 行 `type?: TabType` → `fill?: Fill`；第 11 行 `property2?: Property2` → `color?: Color`
- 第 14 行 `type: 'Line',` → `fill: 'Line',`；第 15 行 `property2: 'White',` → `color: 'White',`
- 第 20 行 `props.type.toLowerCase()` → `props.fill.toLowerCase()`（变量 `normalizedType` 保留名即可，或重命名 `normalizedFill`；保留 `normalizedType` 更省 diff，但为可读改 `normalizedFill`——**改 `normalizedFill`**，同步第 34 行 `:type="normalizedType"` → `:type="normalizedFill"`）
- 第 21 行 `props.property2.toLowerCase()` → `props.color.toLowerCase()`；变量 `normalizedProperty2`→`normalizedColor`，同步第 35 行 `:active-color="normalizedProperty2"` → `:active-color="normalizedColor"`
- 第 26-27 行 data-figma：key 保留 `'data-figma-type'`/`'data-figma-property2'`，值改 `props.fill`/`props.color`

- [ ] **Step 2: 改 TabList.vue**

- 第 5 行 `type TabListType = ...` → `type Fill = 'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`
- 第 10 行 `type?: TabListType` → `fill?: Fill`；第 19 行 `type: 'Line',` → `fill: 'Line',`
- 第 23 行 `props.type` → `props.fill`（`normalizedType`→`normalizedFill`）；第 27 行 data-figma-type 值 `props.fill`；第 36 行 `:type="normalizedType"`→`:type="normalizedFill"`

- [ ] **Step 3: 改 TabItem.vue**

- 第 5 行 `type Property1 = ...` → `type State = 'Normal' | 'Active' | 'normal' | 'active'`
- 第 6 行 `type Property2 = ...` → `type Color = 'White' | 'Green' | 'white' | 'green'`
- 第 7 行 `type TabType = ...` → `type Fill = 'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`
- 第 10-13 行 defineProps：`property1?: Property1`→`state?: State`、`property2?: Property2`→`color?: Color`、`type?: TabType`→`fill?: Fill`
- 第 16-19 行 default：`property1: 'Normal',`→`state: 'Normal',`、`property2: 'White',`→`color: 'White',`、`type: 'Line',`→`fill: 'Line',`
- 第 23-25 行 normalize：`props.type`→`props.fill`(`normalizedFill`)、`props.property1`→`props.state`(`normalizedState`)、`props.property2`→`props.color`(`normalizedColor`)
- 第 27-32 行 data-figma：key 全保留，值改新 prop
- 第 40-42 行转发：`:type="normalizedFill"`、`:state="normalizedState"`、`:active-color="normalizedColor"`（base prop 名 `:type`/`:state`/`:active-color` 不变）

- [ ] **Step 4: config + affordances + manifest 生成器 + demos/tests**

`components.config.ts`：
- Tab（663-666）：`{ name: 'type', ... }`→`{ name: 'fill', ... }`、`{ name: 'property2', ... }`→`{ name: 'color', ... }`（tsType 值不变）
- TabList（695）：`{ name: 'type', ... }`→`{ name: 'fill', ... }`
- TabItem（727-729）：`property1`→`state`、`property2`→`color`、`type`→`fill`

`component-affordances.json`：Tab `property2`(543)→`color`、TabItem `property1`(596)→`state`/`property2`(597)→`color`，以及 Tab/TabItem/TabList 的 `type` 条目→`fill`（`grep -n '"type"' docs/internal/component-affordances.json` 定位 Tab 家族块）。

manifest 生成器：`Tab List`(268) / `Tab/Item`(525) 块 codeProps：`type`→`fill`、`property1`→`state`、`property2`→`color`。**注意 line 213-222 的 DropDownListSelect `codeProps.type` 是 DropDown（out of scope）不动**。

`react-pilot/src/demos/Tabs.tsx`：35 行 `type="Line" property2="Green"`→`fill="Line" color="Green"`；179/182/183 `property2=`/`property1=`→`color=`/`state=`（含 `member.property1`/`member.property2` 数据字段——若 member 数据源也需改名，同步改数据定义；`grep -n "property1\|property2" react-pilot/src/demos/Tabs.tsx` 全覆盖）。

playground Tab 页 + `tests/Tab.test.ts`：canonical 公开 prop 用法改名；`data-figma-*` 断言 key 保留。

- [ ] **Step 5: 重生成 + 测试**

```bash
pnpm run gen:react-bindings && node scripts/generate-component-affordances.mjs && node scripts/generate-render-verification-manifest.mjs
pnpm exec vue-tsc --noEmit && pnpm test
node scripts/audit-binding-config-parity.mjs && node scripts/audit-component-affordances.mjs && node scripts/audit-demo-framework-parity.mjs
```
Expected: 全绿（Tab.test.ts 的 inject warn 是既有无害告警，不影响 pass）。

- [ ] **Step 6: Commit**（精确 add Tab 家族全部文件 + 派生物；`VISUAL_COMMIT_APPROVED=1`；message `refactor(tab): unify Tab family appearance props — type→fill, property2→color, property1→state (INFRA-F59)`）

---

### Task 4: Steps 家族（Steps + StepItem）

`stepStyle`→`type` · 删除 StepItem `style`（footgun workaround）。

**Files:**
- Modify: `src/canonical/Steps.vue`（`stepStyle`→`type` + 转发）
- Modify: `src/canonical/StepItem.vue`（`stepStyle`→`type`、**删 `style` prop** + `normalizedStepStyle` 逻辑简化）
- Modify: `src/web-components/components.config.ts`（Steps 757 / StepItem 790+796）
- Modify: `docs/internal/component-affordances.json`（Steps `stepStyle`:675 / StepItem `stepStyle`:705）
- Modify: `scripts/generate-render-verification-manifest.mjs`（`Step/Item` 块 codeProps `stepStyle`→`type`，**删 `style` codeProps**）
- Modify: `react-pilot/src/demos/Steps.tsx`（141/183/274 行 `stepStyle=`→`type=`）
- Modify: `playground/docs/pages/`（Steps 页）
- Modify: tests（Steps 相关；`data-figma-*` 断言保留）
- Regenerate: react bindings + affordances .md + manifest

**Interfaces:**
- Consumes: base `BaseSteps`/`BaseStepItem` 的 `:step-style` prop 名**不变**。
- Produces: Steps/StepItem 公开 `type?: 'number' | 'icon'`；StepItem 不再有 `style` prop。

- [ ] **Step 1: 先核实 Figma 属性名**

Run: `grep -rn "Step/Item\|stepStyle\|step-style" figma-data/normalized/components.manifest.json | head` 确认 Steps 外观轴的 Figma 属性名（映射表填 `Property 1` 是推断；以 manifest 实测为准）。记下用于 Task 7 的 prop-aliases 登记。

- [ ] **Step 2: 改 Steps.vue**

- 第 11 行 `stepStyle?: 'number' | 'icon'` → `type?: 'number' | 'icon'`
- 第 16 行 `stepStyle: 'number',` → `type: 'number',`
- 第 33 行 `:step-style="stepStyle"` → `:step-style="type"`（base prop `:step-style` 不变）

- [ ] **Step 3: 改 StepItem.vue（删 style prop）**

- 第 7 行 `type StepStyle = 'number' | 'icon'` 保留
- 第 17 行 `stepStyle?: StepStyle` → `type?: StepStyle`
- **删第 18 行** `style?: StepStyle | Record<string, unknown> | Array<unknown>`
- 第 28 行 default `stepStyle: undefined,` → `type: undefined,`；**删第 29 行** `style: undefined,`
- 第 33-38 行 `normalizedStepStyle` 简化（不再 fallback `props.style`）：
```ts
const normalizedStepStyle = computed<StepStyle | undefined>(() => props.type)
```
- 第 44 行 data-figma-style 值 `normalizedStepStyle.value`（key 保留 `data-figma-style`，逻辑不变）
- 第 59 行 `:step-style="normalizedStepStyle"` 不变

- [ ] **Step 4: config + affordances + manifest + demos/tests**

`components.config.ts`：Steps 757 `stepStyle`→`type`；StepItem 790 `stepStyle`→`type`，**删 796 行 `style` entry**（连同 791-795 注释块）。
`component-affordances.json`：675 `stepStyle`→`type`；705 `stepStyle`→`type`（若有 StepItem `style` 条目一并删）。
manifest 生成器 `Step/Item` 块：`stepStyle`→`type`，删 `style` codeProps（对应 manifest 输出 85034/85154… 的 `style` 条目会随重生成消失）。
`react-pilot/src/demos/Steps.tsx`：141/183 `stepStyle="icon"`→`type="icon"`；274 `stepStyle="number"`→`type="number"`。
playground Steps 页 + tests 同步。

- [ ] **Step 5: 重生成 + 测试 + demo-slot-boolean**

```bash
pnpm run gen:react-bindings && node scripts/generate-component-affordances.mjs && node scripts/generate-render-verification-manifest.mjs
pnpm exec vue-tsc --noEmit && pnpm test
node scripts/audit-binding-config-parity.mjs && node scripts/audit-component-affordances.mjs && node scripts/audit-demo-slot-boolean-coverage.mjs && node scripts/audit-demo-framework-parity.mjs
```
Expected: 全绿。特别确认删 `style` 后 binding-config-parity 不报 StepItem name-set 不匹配（SFC 与 config 都删了 `style`）。

- [ ] **Step 6: Commit**（`VISUAL_COMMIT_APPROVED=1`；message `refactor(steps): rename stepStyle→type, remove StepItem style footgun prop (INFRA-F59)`）

---

### Task 5: InputNumber `property1` → `type`

**Files:**
- Modify: `src/canonical/InputNumber.vue`（`property1`→`type` + 转发 + readonly 派生）
- Modify: `src/web-components/components.config.ts:542`
- Modify: `docs/internal/component-affordances.json:49`
- Modify: `scripts/generate-render-verification-manifest.mjs`（`InputNumber` 块 codeProps `property1`→`type`）
- Modify: `src/design-system/translation/prop-aliases.json`（**更新已有 entry `component-prop-006`**：`codeName` `property1`→`type`）
- Modify: `playground/docs/pages/InputNumberPage.vue:163`
- Modify: `tests/Canonical.test.ts:242`（`property1: 'Only Add'`→`type: 'Only Add'`；`data-figma-property1` 断言 251 行 key 保留）
- Regenerate: react bindings + affordances .md + manifest

**Interfaces:**
- Consumes: base `BaseInputNumber` `:property1` prop **不变**。
- Produces: InputNumber 公开 `type?: 'Default' | 'Only Add' | 'Only Reduce' | 'Readonly'`（default `'Default'`）。

- [ ] **Step 1: 改 InputNumber.vue**

- 第 5 行 `type Property1 = ...` → `type Type = 'Default' | 'Only Add' | 'Only Reduce' | 'Readonly'`
- 第 8 行 `property1?: Property1` → `type?: Type`
- 第 15 行 `property1: 'Default',` → `type: 'Default',`
- 第 27 行 data-figma-property1 值 `props.type`（key 保留 `'data-figma-property1'`）
- 第 33 行 `:property1="property1"` → `:property1="type"`（base prop `:property1` 不变）
- 第 40 行 `:readonly="property1 === 'Readonly'"` → `:readonly="type === 'Readonly'"`

- [ ] **Step 2: config + affordances + manifest + prop-aliases + demos/tests**

`components.config.ts:542` `{ name: 'property1', tsType: `...` }` → `{ name: 'type', tsType: `...` }`；531 行注释 `defineProps: modelValue (v-model number), property1, ...` → `..., type, ...`。
`component-affordances.json:49` name `property1`→`type`。
manifest 生成器 `InputNumber` 块（~390 行）codeProps `property1`→`type`。
`prop-aliases.json` entry `component-prop-006`：`"codeName": "property1"` → `"codeName": "type"`（`figmaName` 保持 `"Property 1"`，`status` 保持 `approved-alias`，notes 追加 `; INFRA-F59 code 侧统一为 type`）。
`InputNumberPage.vue:163` `property1="Default"`→`type="Default"`。
`tests/Canonical.test.ts:242` `property1: 'Only Add'`→`type: 'Only Add'`（251 行 `data-figma-property1` 断言 key 不动）。

- [ ] **Step 3: 重生成 + 测试**

```bash
pnpm run gen:react-bindings && node scripts/generate-component-affordances.mjs && node scripts/generate-render-verification-manifest.mjs
pnpm exec vue-tsc --noEmit && pnpm test
node scripts/audit-binding-config-parity.mjs && node scripts/audit-component-affordances.mjs && pnpm run audit:translation-completeness
```
Expected: 全绿（translation-completeness 认可更新后的 006 entry）。

- [ ] **Step 4: Commit**（`VISUAL_COMMIT_APPROVED=1`；message `refactor(input-number): rename property1→type (INFRA-F59)`）

---

### Task 6: 补齐其余 prop-aliases approved-alias 登记

Task 1-5 已改代码；本 task 把尚未登记的 code↔Figma 分歧补进 `prop-aliases.json`（InputNumber 006 已在 Task 5 更新；此处补 Button/Badge/Tab/TabList/TabItem/Steps/StepItem 的新 entry）。

**Files:**
- Modify: `src/design-system/translation/prop-aliases.json`（新增 entry）
- Modify: `src/design-system/translation/prop-aliases.md`（narrative 段补一句 F59 来源）

**Interfaces:**
- Consumes: 全量改名映射表的「Figma 属性名」列。
- Produces: 每处改名一条 `scope:"component-prop"`, `status:"approved-alias"` entry，供 `audit:translation-completeness` 认可。

- [ ] **Step 1: 追加 entries**

在 `prop-aliases.json` `entries` 数组的 `component-prop` 段后追加（`id` 续 `component-prop-014` 起，先 `grep -o 'component-prop-[0-9]*' src/design-system/translation/prop-aliases.json | sort -u | tail` 确认最大号）。每条形如（Button 示例，其余照映射表 Figma 名填）：
```json
{ "id": "component-prop-014", "scope": "component-prop", "component": "Button", "components": null, "codeName": "fill", "figmaName": "style", "canonicalAxis": null, "derivedValue": null, "figmaComponentSet": null, "diffType": null, "canonicalPropState": null, "canonical": null, "props": null, "aliasScope": null, "status": "approved-alias", "notes": "INFRA-F59 2026-07-09: code 统一为 fill（消灭与 HTML style 冲突的 footgun）；Figma 属性名保留 style。" }
```
需追加：Button `fill`↔`style` · Badge `fill`↔`tag` · Tab `fill`↔`type` · Tab `color`↔`Property 2` · TabList `fill`↔`type` · TabItem `fill`↔`type` · TabItem `color`↔`Property 2` · TabItem `state`↔`Property 1` · Steps `type`↔`<Task4-step1 核实名>` · StepItem `type`↔`<同>`。（Figma 名以映射表 + Task 4 step 1 核实为准。）

- [ ] **Step 2: prop-aliases.md 来源注**

在 `prop-aliases.md` 规则/narrative 段补一句：`> 2026-07-09 INFRA-F59：外观变体 prop code 侧统一命名（fill/type/color/state），Figma 属性名保留，逐条登记为 component-prop approved-alias（见 .json）。`

- [ ] **Step 3: 验证 + Commit**

Run: `pnpm run audit:translation-completeness`
Expected: exit 0。
Commit：`git add src/design-system/translation/prop-aliases.json src/design-system/translation/prop-aliases.md` + message `docs(translation): register F59 appearance-prop code↔figma approved-aliases`。

---

### Task 7: SoT + `axis` 字段 + 横向一致性 audit gate + 接线

所有改名已完成、真实 config 干净。现在建 SoT + audit + 接 L4/L5。

**Files:**
- Create: `src/design-system/translation/prop-naming-conventions.json`
- Create: `src/design-system/translation/prop-naming-conventions.md`
- Modify: `src/web-components/components.config.ts`（`PropConfig` 接口加 `axis?`；给 in-scope props 加 `axis` 值）
- Create: `scripts/audit-prop-naming.mjs`
- Create: `tests/audit-prop-naming.test.ts`
- Modify: `package.json`（加 `audit:prop-naming` script + prepublishOnly 追加）
- Modify: `.husky/pre-commit`（加条件 block）

**Interfaces:**
- Consumes: `COMPONENT_CONFIGS`（`import` from config.ts）+ conventions SoT。
- Produces: `audit:prop-naming` gate（exit 1 违例）。

- [ ] **Step 1: 建 SoT json**

`src/design-system/translation/prop-naming-conventions.json`：
```json
{
  "$comment": "INFRA-F59 SoT for cross-component appearance-variant prop naming. Read by scripts/audit-prop-naming.mjs. Figma property names are NOT governed here (see prop-aliases.json); this governs code-side public prop names in components.config.ts.",
  "version": 1,
  "conventions": {
    "fill": "fill",
    "kind": "type",
    "color": "color",
    "state": "state",
    "size": "size"
  },
  "blocklist": ["style", "variant", "canonicalStyle"],
  "blocklistPatterns": ["^property\\d+$"],
  "sizeCasing": { "allowed": ["XS", "S", "M", "L", "XL"] }
}
```

- [ ] **Step 2: 建 rationale md**

`src/design-system/translation/prop-naming-conventions.md`：写命名规则（同概念同名 / 一眼看懂 / 拿不准对标 EP / fallback type|tag）+ 每个 convention 的依据（见 spec §1、§2.1 命名依据）+ blocklist 理由（`style` footgun / `propertyN` Figma 垃圾名 / `variant`+`canonicalStyle` 防旧 remap 名回归）+ enforcement L4+L5 声明 + 来源（INFRA-F59 2026-07-09）。

- [ ] **Step 3: config 加 `axis` 字段**

`src/web-components/components.config.ts` `PropConfig` 接口（第 9-20 行）加：
```ts
  /** INFRA-F59 appearance-axis tag: 'fill'|'kind'|'color'|'state'|'size'. When set,
   *  audit-prop-naming asserts `name` === prop-naming-conventions.json conventions[axis]. */
  axis?: 'fill' | 'kind' | 'color' | 'state' | 'size'
```
给 in-scope props 加 `axis`：Button `fill`→`axis:'fill'`；Badge `fill`→`'fill'` + Badge `type`(Circle/Rectangle)→`axis:'kind'`；Tab/TabList/TabItem `fill`→`'fill'`；Tab/TabItem `color`→`'color'`；TabItem `state`→`'state'`；Steps/StepItem/InputNumber `type`→`'kind'`；Pagination `type`→`'kind'`。（size 不打 axis——由 audit 按 name==='size' 直接校 casing。）

- [ ] **Step 4: 写 audit 失败测试（TDD）**

`tests/audit-prop-naming.test.ts`：用 fixture configs（内联假 PropConfig 数组）+ 抽出的纯校验函数断言：
- blocklist 命中（`{name:'style'}` / `{name:'property1'}`）→ 返回 violation
- axis 不一致（`{name:'variant', axis:'fill'}`）→ violation
- size 小写值（`{name:'size', tsType:"'m'|'l'"}`）→ violation
- 干净 fixture → 0 violation

为可测，`audit-prop-naming.mjs` 把校验逻辑导出为纯函数 `findViolations(configs, conventions)`（不直接 side-effect），main 部分 import 真实 config + conventions 跑它。

先写测试 + 断言函数签名，run 使其 fail（函数未实现）。
Run: `pnpm exec vitest run tests/audit-prop-naming.test.ts`
Expected: FAIL（`findViolations is not a function`）

- [ ] **Step 5: 实现 audit 脚本**

`scripts/audit-prop-naming.mjs`（模式参考 `audit-binding-config-parity.mjs` 的 config import + `audit-icon-canonical-names.mjs` 的 findings 输出）：
```js
#!/usr/bin/env node
// scripts/audit-prop-naming.mjs
// INFRA-F59 横向一致性 gate：校验 components.config.ts 公开 prop 名符合
// prop-naming-conventions.json（黑名单 + 概念一致性 + size casing）。零硬编码：规则读 SoT。
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
if (nodeMajor < 22) { console.error('[audit-prop-naming] requires Node >= 22'); process.exit(1) }
import { COMPONENT_CONFIGS } from '../src/web-components/components.config.ts'
const __dirname = dirname(fileURLToPath(import.meta.url))
const SOT = JSON.parse(readFileSync(resolve(__dirname, '..', 'src/design-system/translation/prop-naming-conventions.json'), 'utf8'))

export function findViolations(configs, sot) {
  const out = []
  const patterns = (sot.blocklistPatterns ?? []).map((p) => new RegExp(p))
  const sizeAllowed = new Set(sot.sizeCasing?.allowed ?? [])
  for (const c of configs) {
    for (const p of c.props ?? []) {
      // 1. blocklist
      if (sot.blocklist.includes(p.name) || patterns.some((re) => re.test(p.name))) {
        out.push({ component: c.name, prop: p.name, kind: 'blocklist', msg: `prop 名 "${p.name}" 在黑名单/垃圾名模式` })
      }
      // 2. concept consistency
      if (p.axis && sot.conventions[p.axis] && p.name !== sot.conventions[p.axis]) {
        out.push({ component: c.name, prop: p.name, kind: 'concept', msg: `axis='${p.axis}' 应命名 "${sot.conventions[p.axis]}"，实为 "${p.name}"` })
      }
      // 3. size casing
      if (p.name === 'size' && typeof p.tsType === 'string' && p.tsType.includes('|')) {
        const members = p.tsType.split('|').map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
        const bad = members.filter((m) => !sizeAllowed.has(m))
        if (bad.length) out.push({ component: c.name, prop: p.name, kind: 'size-casing', msg: `size 值非大写规范子集: ${bad.join(', ')}` })
      }
    }
  }
  return out
}

// main
if (import.meta.url === `file://${process.argv[1]}`) {
  const violations = findViolations(COMPONENT_CONFIGS, SOT)
  console.log(`audit-prop-naming: ${COMPONENT_CONFIGS.length} components scanned`)
  if (violations.length === 0) { console.log('OK — 所有公开 prop 名符合命名规范。'); process.exit(0) }
  console.log(`\n${violations.length} 违例：\n`)
  for (const v of violations) console.log(`   [${v.kind}] ${v.component}.${v.prop} — ${v.msg}`)
  console.log('\nFix: 按 prop-naming-conventions.json 改名 / 打正确 axis。')
  process.exit(1)
}
```
测试从脚本 import `findViolations`。
Run: `pnpm exec vitest run tests/audit-prop-naming.test.ts`
Expected: PASS

- [ ] **Step 6: 跑真实 gate（应 0 违例）**

Run: `node scripts/audit-prop-naming.mjs`
Expected: `OK — 所有公开 prop 名符合命名规范。` exit 0（Task 1-6 已清干净）。若报违例 → 回对应 task 修。

- [ ] **Step 7: 接线 package.json + pre-commit**

`package.json` scripts 加：`"audit:prop-naming": "node scripts/audit-prop-naming.mjs",`（放在 audit:binding-config-parity 附近）。prepublishOnly 末尾 ` && pnpm run audit:prop-naming`（追加在 line 51 串尾）。
`.husky/pre-commit` 在 binding-config-parity block（line 236-244）后插入：
```sh
echo "▶ pre-commit: prop-naming (appearance-variant 命名统一 + 黑名单 — INFRA-F59)"
if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/web-components/components\.config\.ts|src/canonical/.*\.vue|src/design-system/translation/prop-naming-conventions\.json|scripts/audit-prop-naming\.mjs)'; then
  node scripts/audit-prop-naming.mjs
else
  echo "   ✓ no prop-naming-relevant files staged, skipped"
fi
```

- [ ] **Step 8: 验证 + Commit**

Run: `pnpm exec vue-tsc --noEmit && pnpm test && node scripts/audit-prop-naming.mjs`
Expected: 全绿。
Commit：`git add src/design-system/translation/prop-naming-conventions.json src/design-system/translation/prop-naming-conventions.md src/web-components/components.config.ts scripts/audit-prop-naming.mjs tests/audit-prop-naming.test.ts package.json .husky/pre-commit react-pilot/src/wrappers/types.ts`（config 加 axis 会重生成 types）+ message `feat(audit): add prop-naming horizontal-consistency gate (INFRA-F59)`。

---

### Task 8: MIGRATION + changeset

**Files:**
- Modify: `docs/internal/migration-protocol.md`（或 `docs/MIGRATION_TO_V1.md`——先 `grep -rln "MIGRATION\|migration" docs | grep -i migrat` 定位 consumer-facing 迁移文档）
- Create: `.changeset/infra-f59-appearance-variant-prop-naming.md`
- Modify: `README.md` / `docs/GETTING_STARTED.md`（删 F58 加的 Button `style` 临时 workaround 注——`grep -rn "style" README.md docs/GETTING_STARTED.md | grep -i "bind\|filling\|footgun\|:style"`）

- [ ] **Step 1: 写 changeset**

`.changeset/infra-f59-appearance-variant-prop-naming.md`：
```md
---
"@nancyzeng0210/tvu-design-system": minor
---

BREAKING (INFRA-F59): 外观变体 prop 全组件统一命名，消灭 Button `style` footgun。

改名对照（旧 → 新）：
- Button `style` → `fill`（`<Button style="filling">` 静态写法此前静默失效，现用 `fill`；React 侧此前的 `variant` 也统一为 `fill`）
- Badge `tag` → `fill`
- Tab / TabList / TabItem `type` → `fill`
- Tab / TabItem `property2` → `color`
- TabItem `property1` → `state`
- Steps / StepItem `stepStyle` → `type`；StepItem 移除 `style` prop
- InputNumber `property1` → `type`

枚举值不变。Figma 属性名不变（本次仅 code 侧公开 prop 命名统一）。
```

- [ ] **Step 2: MIGRATION 段 + 删临时注**

migration 文档加 F59 段（同上对照表 + 一句「values 不变，仅 prop 名」）。删 F58 在 README/GETTING_STARTED 里给 Button `style` 的临时 workaround 注（改用 `fill` 的正常示例）。

- [ ] **Step 3: 验证 + Commit**

Run: `pnpm run changeset:status && pnpm exec vue-tsc --noEmit && pnpm test`
Expected: changeset 待发布项含本条；测试全绿。
Commit：精确 add 上述文件 + message `docs(migration): F59 appearance-prop rename migration guide + changeset`。

---

### Task 9: 全量验证 + Sprint 收尾 Self-Audit

**Files:** 无（纯验证）

- [ ] **Step 1: 全 gate 跑一遍**

```bash
pnpm exec vue-tsc --noEmit
pnpm test
node scripts/audit-prop-naming.mjs
pnpm run audit:binding-config-parity
node scripts/audit-component-affordances.mjs
node scripts/audit-demo-framework-parity.mjs
node scripts/audit-demo-slot-boolean-coverage.mjs
pnpm run audit:translation-completeness
pnpm run build
```
Expected: 全 exit 0 / build 绿。

- [ ] **Step 2: render 验证（改名后 codeProps 生效）**

Run: `pnpm run test:render-verification` （或至少 `node scripts/generate-render-verification-manifest.mjs && pnpm run audit:render-drift-gate`）
Expected: A_TRUE_DRIFT=0（改名后 manifest codeProps 与组件公开 API 对齐）。若 Gitea/本地无 chromium，记录「render 验证需 CI」并在 commit body 如实标未跑。

- [ ] **Step 3: Sprint 收尾 Self-Audit（AGENTS §Sprint 收尾）A–K**

跑 D/E/F/G/I/K 机械 audit（`pnpm run audit:self-audit-phase2`）+ 人工过 A（spec gap：11 改名+1 删除全落？）/ B（实现 bug：有无 `catch{}` 吞错 / base 边界漏改）/ C（doc lag：STATUS/backlog F59 标 shipped）。输出 finding 分级。
Expected: 全绿或 finding 有对应 backlog entry。

- [ ] **Step 4: 收尾 doc + backlog**

更新 `docs/STATUS.md`（Last updated 今天 + F59 shipped 摘要）+ `docs/internal/backlog.md`（删 INFRA-F59 entry，改动留 git）。Commit：`docs(status,backlog): INFRA-F59 shipped`。

---

## Self-Review

**Spec coverage**（对 spec §2.1/§3/§4/§5/§6/§8/§9 逐项核）：
- §2.1 11 改名+1 删除 → Task 1-5 全覆盖（Button/Badge/Tab×3/Steps×2/InputNumber）✓；Pagination/Badge `type` axis 标记 → Task 7 step 3 ✓
- §3 Figma↔code approved-alias 登记 → Task 5(006 更新)+Task 6 ✓
- §4 破坏性改名 + MIGRATION + changeset → Task 8 ✓
- §5 连带清理（reserved-names / config / SFC / base 边界 / React 重生成 / docs）→ Task 1 step 3 + 各 task ✓
- §6 gate（SoT + axis + 三断言 + L4+L5）→ Task 7 ✓
- §8 testing → Task 9 ✓
- §9 交付物 10 项 → 全映射到 task ✓

**Placeholder scan**：react 重生成入口已坐实 = `pnpm run gen:react-bindings`（= `node scripts/generate-react-bindings.mjs`，package.json:139），全 task 统一。唯一保留的 executor-核实项 = Steps/StepItem 外观轴的 Figma 属性名（Task 4 step 1 给 grep 命令核实后填 Task 6 alias）——非占位（有确定的核实动作 + 默认值）。

**Type consistency**：`fill`/`type`/`color`/`state` 规范名跨 config/SFC/SoT/audit 一致；base 边界 prop 名（`canonical-style`/`type`/`active-color`/`state`/`step-style`/`property1`）全程不改，与 Global Constraints 一致；`findViolations(configs, sot)` 签名在 Task 7 step 4/5 一致。

**风险备注**：Steps Figma 属性名（Task 4 step 1 核实）+ render-verification 本地可跑性（Task 9 step 2，Gitea 无 chromium 则标 CI 验）= executor 需留意的两点，已在对应 step 给处理。
