# INFRA-F59 — 外观变体 prop 全组件命名统一 + 消灭 `style` footgun（设计 spec）

> **日期**：2026-07-09
> **归属**：INFRA-F55 支柱① API 一致性 P0 首任务（assessment §4「API 跨组件命名散乱」）
> **性质**：设计 spec（brainstorming 产出，owner 已逐节 ack）。实现 plan 见后续 writing-plans 产物。
> **前置读**：`docs/FIGMA_AS_SOURCE_OF_TRUTH.md`（硬规则 #1）· `docs/internal/backlog.md` INFRA-F59 · `docs/internal/full-lifecycle-assessment-2026-07-08.md` §4 支柱①

---

## 0. 问题 / 为什么做

治 assessment §4 支柱① 标 **P0 的「API 跨组件命名散乱」**，三层痛：

1. **vitest 已证的真 bug**：Button 公开 prop 字面叫 `style`，撞原生 HTML/Vue `style` 属性。`<Button style="filling">` 静态写法**静默坏**（编译器当成内联 CSS 对象，prop 收到 `[object Object]`，类型检查失败），只有 `:style="'filling'"` 绑定形式能用。React 端已被迫 remap 成 `variant`（`reserved-names.ts` `style→variant`）。两个框架都在绕同一颗地雷。
2. **跨组件命名发散**：同样是「选外观变体」，consumer 要记 Button=`style` / Badge=`tag` / Tab·Pagination=`type` / InputNumber=`property1` / Steps=`stepStyle`。其中 `property1`/`property2`/`tag` 是 **Figma 自动生成的默认属性名**从没改就泄漏进公开 API。直接砸 P0 受众（开发）的 npm-install-即用体验（能力 1）。
3. **零闸 = 修完还漂**：实证确认当前**无横向一致性 audit**、**无 deprecation/alias 基础设施**。修一次不加闸，下一个组件照样引入新发散。

**一句话目标**：让「选外观变体」在所有组件里用同一套可预测、可读的 prop 名，消灭 `style` 地雷，并加机械闸锁死不再漂。也是支柱③ 适配器的前置（契约不稳则适配器无从对齐）。

---

## 1. 命名规则（owner 拍定）

1. **按概念归名**：同一「外观概念」跨组件用**同一个名**；一个组件有多个外观轴就给多个各自说得通的名。**不**强行全部塞进一个万能名（`variant` 已被 owner 排除）。
2. **一眼能看懂优先**。
3. **拿捏不准 → 对标 Element Plus**；EP 也无干净枚举可抄时回落到「一眼看懂」；**仍难分类的回落 `type` 或 `tag`**。

---

## 2. Scope

### 2.1 In scope（8 组件 · 11 改名 + 1 删除）

> 2026-07-09 writing-plans 落地时按 config 实证订正（owner ack）：补 TabItem `type` / StepItem `stepStyle`+`style` / TabItem `property1` 改判 `state`。所有值形态不变。

| 概念 | 组件·当前公开名（config 行）| 值（不改） | 规范名 |
|---|---|---|---|
| **填充处理** | Button `style`（config 122）| filling/ghost/rimless | **`fill`** |
| **填充处理** | Badge `tag`（217）| Filled/Line | **`fill`** |
| **填充处理** | Tab `type`（664）| Line/Text/Filled(+lc) | **`fill`** |
| **填充处理** | TabList `type`（695）| Line/Text/Filled(+lc) | **`fill`** |
| **填充处理** | TabItem `type`（729）| Line/Text/Filled(+lc) | **`fill`** |
| **种类** | Steps `stepStyle`（757）| number/icon | **`type`** |
| **种类** | StepItem `stepStyle`（790）| number/icon | **`type`** |
| **种类** | InputNumber `property1`（542）| Default/Only Add/Only Reduce/Readonly | **`type`** |
| **颜色** | Tab `property2`（665）| White/Green(+lc) | **`color`** |
| **颜色** | TabItem `property2`（728）| White/Green(+lc) | **`color`** |
| **状态（垃圾名必除）** | TabItem `property1`（727）| Normal/Active(+lc) | **`state`** |
| **删除（footgun workaround）** | StepItem `style`（796）| — | **删除该 prop** |

**不改、仅打 `axis` 标记供 gate 校**：Pagination `type`(Classic/Simple/Small, axis=kind, 已是好名) · Badge `type`(Circle/Rectangle=形状, axis=kind) —— Badge 因此同时有 `fill`(填充)+`type`(形状) 双轴 = owner 认可的「多轴多名」。

**命名依据**：
- `fill`：填充处理簇（实心/描边/无框）EP 无干净枚举（EP 用布尔 `plain`/`text` 或 Tag `effect`）→ 回落「一眼看懂」，`fill` 最直白。
- `type`：EP 用 `type` 表「种类/形态」的通用惯例（Progress `type`=line/circle/dashboard 同款）；`stepStyle`/`property1` 归此。
- `color`：对齐 Button 既有 `color`（gray 1/green/orange/red）。
- `state`：TabItem `property1`(Normal/Active) 本质是激活态、映射到 base `:state`；对齐 BreadcrumbItem 既有 `state`。state 轴本在「统一 out-of-scope」，但垃圾名 `property1` 会被本 plan 的 gate 黑名单拦，故必须除名到规范的 `state`（不拉整个 state 轴进统一范围）。
- **StepItem `style` 删除**：它是「写 `style="number"` 被 Vue 当 CSS 对象」这个 footgun 的 workaround（`normalizedStepStyle` 从 `props.style` string 分支兜底）；有了干净的 `type` prop 后该逃生 prop 无存在意义，且 `style` 名踩黑名单。

### 2.2 Out of scope（明确排除，防误伤）

| 类别 | 组件 | 为何不动 |
|---|---|---|
| **语义状态/色轴** | Notification `type`(warning/danger/…) + `form` · Message `status` · Progress `status` | 语义色/状态轴，非外观变体；Notification 还是 `type` 语义过载案 → **独立 scope**（不并进本 plan）|
| **主题轴** | 各组件 `darkTheme`/`theme` | 主题轴另论；prop-aliases.json 已登记 global-axis-alias |
| **交互状态轴** | InputBox/Select `ux` · BreadcrumbItem `state` · TopBar `tag`(登录态) | state/mode，非外观变体 |
| **本质不同轴** | Chart `type`(图种) · Table `type`(结构 Header/Tbody) · DropDownListSelect `type`(选择模式) · Logo `type`(tvu/ts) · FormItem `type`(组合哪个控件) · MenuList `orientation` · UserMenu `color`(字符串色值) | 各是本质不同的轴，改名会误导 |

**同样不含（防 scope 蔓延）**：
- **枚举值的大小写/集合归一**（Button `filling` vs Badge `Filled` 值形态不同、size `XS|S|M|L` vs `M|S` 值集合不同）——本 plan 只统一 **prop 名**，值不动。（实证：size 公开 API casing 已全大写一致，backlog「4 casings」claim 已证伪；小写只出现在 base 组件内部映射。）
- **语义 `type` 过载拆分**——独立 scope。

---

## 3. Figma ↔ code 登记（硬规则 #1 合规）

这些名多源自 **Figma 变体属性名**。owner 拍定方向 = **保留 Figma 名不动，code 用规范名，差异登记为 approved-alias**（合法值形态映射 / 视觉等价）。

- 每处改名在 `src/design-system/translation/prop-aliases.json` 增 entry：
  - `scope`: `component-prop`
  - 组件名 · Figma 属性名（如 Button `style`、Badge `tag`、InputNumber `property1`）· code 规范名（`fill`/`type`/`color`）
  - `status`: `approved-alias`
  - 来源注：`INFRA-F59 owner-approved code-side rename 2026-07-09`
- `prop-aliases.md` narrative 段补一句规则来源（浓缩，不写项目复盘——meta-rules 回流边界）。
- 验证：`audit:translation-completeness`（L5）应放行这些已登记 alias；`audit:figma-vs-sot` / render-drift-gate 保持绿。

---

## 4. 迁移机制（owner 拍定 = 方案 ①：直接破坏性改名）

**不建 runtime deprecation alias**。理由：项目 pre-1.0，破坏性走 0.x.0 minor 是既定惯例（PromptMessage→Message 先例）；consumer 少且内部（react-pilot / MicroApps / tvu-saas-dashboard）；避免给 10 处各写「双 prop + 优先级 computed + dev 告警 + 日后拆除」的临时 cruft。Button `style` 静态写法本就是坏的，「保留它」无实际非破坏价值。

**产出**：
- MIGRATION 文档增 F59 段（旧名→新名对照表 + 一句迁移说明），走 `docs/internal/migration-protocol.md` 惯例。
- changeset 标 **minor（breaking）**，描述面向 consumer 的改名对照。
- CHANGELOG 由 changeset 自动汇总。

---

## 5. 连带清理（随改名一并做）

1. **删 `src/web-components/reserved-names.ts` 的 `style→variant` remap**：`fill` 不撞任何保留字 → React 直接暴露 `fill`，Vue↔React 真同名，remap 层消失。若 remap 表清空后无其它成员，评估是否整文件删除 / 保留空壳（实现时定）。
2. **`src/web-components/components.config.ts`**：10 处 `PropConfig.name` 改；相关 `jsdoc` 更新（Button 那条「exposed to React as `variant`」注释删掉）；加 `axis` 字段（见 §6）。
3. **canonical SFC**：各 `src/canonical/*.vue` 的 `defineProps` 名 + 默认值 + 内部转发（如 ButtonBridge `:canonical-style` → 对齐 `fill`）同步。
4. **base 组件**：`src/components/Button/Button.vue` 等内部 `canonicalStyle` / `CanonicalContract` 对齐 `fill`（内部名也一并去掉 `style`/`canonicalStyle`，避免黑名单误伤 + 保持一致）。
5. **React 产物重生成**：`react-pilot` wrappers / types 由 `components.config.ts` 派生，改完重跑生成器；`audit:demo-framework-parity` / `audit:binding-config-parity` 复核。
6. **docs / playground demo**：用到旧名处更新（F58 在 GETTING_STARTED/README 加的 `style` 临时 workaround 注一并删）。
7. **data-figma-\* 溯源属性**：Button 现有 `data-figma-style` 等——评估是否随改名调整（保持指向 Figma 真源属性名，不因 code 改名而改；实现时按溯源语义定）。

---

## 6. 横向一致性 audit gate（耐久件 · 防漂回）

**真源驱动、零硬编码**（meta-rules 反模式 #1 + 触发器 K）。

### 6.1 SoT

`src/design-system/translation/prop-naming-conventions.json`（机器读）+ 同层 `.md` 记 rationale：
- **概念→规范名映射**：`fill→'fill'` · `kind→'type'` · `color→'color'` · `size→'size'`（可扩展）。
- **禁用名黑名单**：`style`（HTML footgun）· `property1`/`property2`/`property3`/…（Figma 垃圾默认名，正则 `^property\d+$`）· `variant` / `canonicalStyle`（防重新引入旧 remap 名）。

### 6.2 config 侧

`PropConfig` 加可选字段 `axis?: string`（如 `axis: 'fill'`）。标了 `axis` 的 prop，其 `name` 必须等于 SoT 里该 concept 的规范名。

### 6.3 `scripts/audit-prop-naming.mjs` 三条断言（全确定性字面比对）

1. **黑名单**：任何公开 prop 名（`components.config.ts` 的 `PropConfig.name`）命中黑名单 → exit 1。（最高杠杆最便宜，单独堵住绝大多数漂移源。）
2. **概念一致性**：凡 `axis` 已标的 prop，`name` ≠ SoT 规范名 → exit 1。
3. **size casing**：名为 `size` 的 prop，枚举值必须是 `{XS,S,M,L,XL}` 的大写子集 → exit 1（守已一致的 casing，不强改值集合）。

### 6.4 Enforcement 层级（触发器 K 声明）

- **L4（pre-commit 条件 gate）**：staged 命中 `components.config.ts` / `src/canonical/*.vue` / `prop-naming-conventions.json` / 该 audit 自身时自动跑，exit 1 拦 commit（对齐 `audit:canonical-slot-guard` / `audit:icon-canonical-names` 范式）。
- **L5（prepublishOnly）**：并入发布前 audit 集，consumer 产物保证零违例。
- **为何不更低**：属「跨人协作 + 防错代码 merge」类（触发器 K 类别表 → 最低 L5），且完全客观可测（prop 名字面比对）→ 必须机械闸，不靠 AI 自律。
- `package.json` 注册 `audit:prop-naming`；接入 `audit:self-audit-phase2` 链（若适用）。

---

## 7. Open items

1. ~~TabItem `property1` 是否公开 prop~~ **已解（config 727 = 公开 prop，映射 base `:state`）→ 改 `state`**（见 §2.1）。
2. **`reserved-names.ts` 清空后**整文件删 vs 留空壳：`RESERVED_NAME_REMAP` 移除 `style` 后为空对象；`applyReservedRemap` 仍被生成器调用 → **保留文件 + 空 map**（幂等无害，未来若有新保留字直接加），不删文件。实现时确认无其它成员。
3. **`data-figma-*` 溯源属性**：保持指向 Figma 真源属性名不变（key 如 `data-figma-style`/`data-figma-type`/`data-figma-property1` 照旧），value 改为从新 prop 取（如 `'data-figma-style': props.fill`）。已确认 canonical SFC 均走此模式。
4. **canonical→base 边界**：base 组件内部 prop 名（BaseTab `type` / BaseInputNumber `property1` / BaseStepItem `step-style` / Button base `canonical-style`）**不改**——canonical 公开 prop 改名后在边界映射到 base 既有名（沿用现有 normalize 模式）。base 内部名不在 config 公开面、不撞 HTML、不触 gate 黑名单，故不动可控 ripple。

---

## 8. Testing / 验证

- **vitest**：改名后全绿；补 case 断言新名生效 + 旧名不再存在（防遗漏）；复现「`<Button fill="filling">` 静态写法可用」（footgun 已消）。
- **audit gate 自测**：故意引入 `property1` / `style` / 小写 size 值 → 断言 gate exit 1；合规态 exit 0。
- **框架 parity**：`audit:demo-framework-parity` + `audit:binding-config-parity` 绿。
- **Figma SoT**：`audit:translation-completeness` + `audit:figma-vs-sot` + render-drift-gate 绿。
- **build**：`vue-tsc` + `pnpm build` 绿。
- **sprint 收尾 self-audit**：AGENTS §Sprint 收尾 Self-Audit A–K 全跑。

---

## 9. 交付物清单（供 writing-plans 拆步）

1. `prop-naming-conventions.json` + rationale `.md`（SoT，plan owner 写）
2. `components.config.ts`：10 处改名 + `axis` 字段 + jsdoc
3. canonical SFC × N + base 组件内部名对齐
4. `reserved-names.ts` 清理
5. `prop-aliases.json` × 10 approved-alias entry + `prop-aliases.md` 来源注
6. `scripts/audit-prop-naming.mjs` + `package.json` 注册 + pre-commit 条件 gate 接线 + prepublishOnly 接入
7. React 产物重生成
8. docs/playground demo 更新 + 删 F58 临时 workaround 注
9. MIGRATION F59 段 + changeset（minor/breaking）
10. vitest case 增补
