# Docs Site DX & Codex Discipline Spec

## 一句话方向

**TVU 设计系统的 docs 站，在"开发者拿到一个组件页能做什么"这件事上，必须和 [Element Plus 组件文档](https://element-plus.org/en-US/component/overview) 对齐。视觉风格、分类划分、Figma 相关的额外内容可以不同，但"读完一页就知道怎么用"这个体验必须一样。**

并且：本 spec 不只是"页面长什么样"——它还**钉死 Codex 在过去几次踩过的具体坑**，让 AI 协作者没有发挥空间。

---

## 0. 这份 spec 解决的真实失败

每一条规则下面都列了它对应的真实 Codex 失败案例。**这不是抽象规范，是从 5 个 in-review 页（`breadcrumb` / `form-item` / `prompt-message` / `steps` / `tabs`）的实际事故中提炼**。

| # | Codex 实际犯的错（真实记录） | 拦下它的规则 |
|---|---|---|
| 1 | 用手写假控件 (`<input class="fake-input">`) 冒充组件 | §3 真组件 |
| 2 | Select 选错文件：用退化的 native `<select>`（`src/components/Select/Select.vue`）而不是 canonical `SelectBoxLine.vue` | §3 文件选择 |
| 3 | 用 `:deep()` 给单页打视觉补丁 | §5 修复层级判定 |
| 4 | 改完声称完成但浏览器仍是旧版（HMR 缓存） | §6 验证纪律 |
| 5 | 跑全量 `pnpm test` 把不相关失败混报 | §6 验证纪律 |
| 6 | section 顺序错（旧 Figma member 太重，把 demo 挤后面） | §1 五段固定顺序 |
| 7 | FormItem 组件本体 layout 错（`align-items: flex-start` + label `padding: 4px 0`），但 Codex 在 docs page 用 `:deep()` 补 | §5 修复层级判定 |
| 8 | 测试只验 render/value，从不覆盖视觉对齐 | §6 视觉验收 |
| 9 | Codex 反复在 docs 层打补丁，从不主动改组件本体 | §5 修复层级判定 + §7 Codex 工作纪律 |
| 10 | 一个 demo card 里同时换 size/status/theme 三件事 | §2 demo 单一职责 |
| 11 | demo 把状态用 prop 锁死摆拍（`<Tooltip :open="true">` 把所有气泡同时展开） | §4 真交互 |
| 12 | Codex 自己识别出根因后给"建议下一步"而不是直接动手 | §7 Codex 工作纪律 |

---

## 1. 页面骨架：5 段固定顺序

### 规则

每个组件页（`kind: "component"`）必须有这 5 段，**顺序不能调**：

1. **Figma Coverage** — Figma 变体覆盖（轴 / 值 / 数量）
2. **Real Figma Members** — Figma 真实成员展示
3. **`<Component>` Capability** — 组件能力演示（按真实 Figma 轴分 demo）
4. **Development Usage** — 开发引用方式（import / 模板 / 常见组合）
5. **`<Component>` API** — 组件 API 表格

> **样板**：[playground/docs/pages/BreadcrumbPage.vue](../playground/docs/pages/BreadcrumbPage.vue)。

### 对应失败 #6

Codex 改 StepsPage 时把 demo 提前但**没有**把旧 Figma member 段删/缩——结果旧段视觉上盖过新 demo。

### 反例 ❌

```vue
<template>
  <div class="docs-page">
    <!-- 新加的 demo -->
    <section class="docs-section">
      <h2>Capability</h2>
      <Steps>...</Steps>
    </section>

    <!-- 旧的 Figma member 大段还原封不动留着，视觉上盖过 capability -->
    <section class="docs-section">
      <h2>Horizontal Members</h2>
      <!-- 200 行密集 Figma 轴展示 -->
    </section>
  </div>
</template>
```

### 正例 ✅

按 Breadcrumb 5 段顺序：Coverage → Real Members（精简到一行/卡片）→ Capability（demo 主体）→ Development Usage → API。

### 自检

- 页面一打开看到的第一个 H2 是不是 "Figma Coverage"？
- 第三个 H2 是不是 "<Component> Capability"？
- 最后一个 H2 是不是 "<Component> API"？

任一 No = fail。

---

## 2. Demo 单一职责：一个 card 一个轴

### 规则

每个 `.docs-demo-card` **只演示一个轴**的变化。其它轴在 demo card 标题里**显式锁定**说明。

### 对应失败 #10

ButtonPage 早期版本一个 demo 同时换 color × size × status 三件事，无法判断单轴变化效果。

### 反例 ❌

```vue
<article class="docs-demo-card">
  <span class="docs-demo-card__title">Buttons</span>
  <div style="display: flex; gap: 8px">
    <Button color="green" size="L" status="default">A</Button>
    <Button color="red" size="M" status="hover">B</Button>
    <Button color="orange" size="S" status="disable">C</Button>
  </div>
</article>
```

A → B 同时变 color、size、status。读者无法从这个 demo 学到任何单轴信息。

### 正例 ✅

```vue
<article class="docs-demo-card docs-demo-card--wide">
  <span class="docs-demo-card__title">Color axis（size=M, status=default）</span>
  <div style="display: flex; gap: 8px">
    <Button color="green" size="M">Green</Button>
    <Button color="red" size="M">Red</Button>
    <Button color="orange" size="M">Orange</Button>
    <Button color="gray" size="M">Gray</Button>
  </div>
</article>

<article class="docs-demo-card docs-demo-card--wide">
  <span class="docs-demo-card__title">Size axis（color=green, status=default）</span>
  <div style="display: flex; gap: 8px; align-items: center">
    <Button color="green" size="XS">XS</Button>
    <Button color="green" size="S">S</Button>
    <Button color="green" size="M">M</Button>
    <Button color="green" size="L">L</Button>
  </div>
</article>
```

### 自检

每个 demo card 标题里写明"× 轴（其它轴 = 具体值）"。看不出哪个轴在变 = fail。

---

## 3. 真组件，不假控件，不选错文件

### 规则

3.1. demo 里**不允许**用 `<input>` / `<select>` / 手写 div 冒充组件。

3.2. **要用 canonical 层组件，不用退化 runtime**。具体清单：

| 组件 | ✅ 用这个 | ❌ 不用这个 |
|---|---|---|
| Input | [src/canonical/InputBoxLine.vue](../src/canonical/InputBoxLine.vue) / [InputBoxFilled.vue](../src/canonical/InputBoxFilled.vue) | [src/components/Input/Input.vue](../src/components/Input/Input.vue)（runtime） |
| Select | [src/canonical/SelectBoxLine.vue](../src/canonical/SelectBoxLine.vue) / [SelectBoxFilled.vue](../src/canonical/SelectBoxFilled.vue) | [src/components/Select/Select.vue](../src/components/Select/Select.vue)（**这是退化的 native `<select>`，不是真 dropdown**） |
| Button | [src/components/Button/Button.vue](../src/components/Button/Button.vue)（当前唯一公开） | — |
| 其它 canonical | 直接用 `src/canonical/*.vue` | 不要用 `src/components/*` 除非 canonical 不存在 |

### 对应失败 #1, #2

FormItemPage 早期：手写 `<input class="fake-input">` 当 Input；后来 Codex 又用 [src/components/Select/Select.vue](../src/components/Select/Select.vue)（native `<select>`）当 "本地 Select 组件"——**Codex 自己知道选错了**还是没换。

### 反例 ❌

```vue
<FormItem label="Name">
  <input class="fake-input" placeholder="Type something" />
</FormItem>

<FormItem label="Country">
  <!-- src/components/Select/Select.vue 是 native <select>，不是 canonical -->
  <Select v-model="country" :options="countries" />
</FormItem>
```

### 正例 ✅

```vue
<script setup lang="ts">
import InputBoxLine from '../../../src/canonical/InputBoxLine.vue'
import SelectBoxLine from '../../../src/canonical/SelectBoxLine.vue'
</script>

<template>
  <FormItem label="Name">
    <InputBoxLine v-model="name" placeholder="Type something" />
  </FormItem>
  <FormItem label="Country">
    <SelectBoxLine v-model="country" :options="countries" />
  </FormItem>
</template>
```

### 自检

```bash
# docs page 不应直接 import src/components/* 除非 canonical 不存在
grep -E "from.*src/components/(Input|Select|Switch|Radio|CheckBox)" playground/docs/pages/<X>Page.vue
# 结果应为空（这些组件 canonical 都已存在）
```

---

## 4. 真交互：能点能输入能 v-model

### 规则

每个 component page 在 §3 Capability 段里**至少一个 demo 是真交互**：组件绑了 `v-model` 或事件，用户点击/输入能看到值变化。

### 对应失败 #11

TooltipPage 把所有气泡用 `:open="true"` 一起强行展开"全可见"——开发者看到的是设计态，不是 hover 触发的真实状态。

### 反例 ❌

```vue
<!-- 摆拍：所有 tooltip 同时打开 -->
<Tooltip :open="true" placement="top" content="Top">Anchor</Tooltip>
<Tooltip :open="true" placement="bottom" content="Bottom">Anchor</Tooltip>
<Tooltip :open="true" placement="left" content="Left">Anchor</Tooltip>
```

```vue
<!-- 假交互：只 prop 没事件 -->
<Switch :model-value="true" />
```

### 正例 ✅

```vue
<script setup lang="ts">
import { ref } from 'vue'
const enabled = ref(false)
const inputValue = ref('')
</script>

<template>
  <article class="docs-demo-card docs-demo-card--wide">
    <span class="docs-demo-card__title">Try it（真交互）</span>
    <Switch v-model="enabled" />
    <p style="margin-top: 8px; font-size: 12px">enabled = {{ enabled }}</p>
  </article>

  <article class="docs-demo-card docs-demo-card--wide">
    <span class="docs-demo-card__title">Tooltip hover 真触发</span>
    <Tooltip placement="top" content="Top tip">
      <Button>Hover me</Button>
    </Tooltip>
  </article>
</template>
```

### 自检

页面里至少有一个 `v-model="..."` + 一个实时回显当前值的 `<p>`。任意状态都是 hover/click 真触发，不是 prop 强开。

---

## 5. 修复层级判定：永远问"应该改哪一层"

### 规则（**这是 Codex 最常违反的一条**）

5.1. 看到 docs page 视觉不对时，**第一反应不是"改 docs page CSS"**，而是问：

> "如果只能在 docs page 改才能让它看对，那 99% 是组件本体的契约问题，不是 docs 问题。"

5.2. **`:deep()` 用法 = 自动判定 fail**

任何在 docs page `<style scoped>` 里出现的 `:deep(.xxx)` 选择器，都视为"修错层级"的标志。正确做法是：

- 找到那个被 `:deep()` 选中的真实组件文件
- 在该组件本体修复布局/样式
- 删掉 docs page 的 `:deep()` 补丁

5.3. **同样的补丁需要复制到第二个 docs page → 必须改组件**

如果你发现自己想把 `:deep(.form-item__main { align-items: center })` 这种规则同时加到 FormItemPage 和某另一个用 FormItem 的页——立刻停下，去改 [src/components/FormItem/FormItem.vue](../src/components/FormItem/FormItem.vue) 本体。

### 对应失败 #3, #7, #9（**最痛的一组**）

实录：用户问"为什么左右还是没对齐"，Codex 自己诊断出 `FormItem.vue:172` 的 `align-items: flex-start` + label `padding: 4px 0` 是元凶——**然后给的修复是 [FormItemPage.vue:458](../playground/docs/pages/FormItemPage.vue#L458) 加 `:deep()` 补丁**。Codex 知道根因在哪一层，但行为上选错了层。

### 反例 ❌

```vue
<!-- FormItemPage.vue 末尾 -->
<style scoped>
:deep(.form-item__main) {
  align-items: center !important;
}
:deep(.form-item__label) {
  padding: 0 !important;
}
</style>
```

补丁会让**这一页**看起来对了，但：
- 任何其它用 FormItem 的页（PromptMessagePage、Composition demo）依然错
- FormItem 本体仍是错的
- 下一个 Codex Session 看到这一页"看起来对了"会以为没问题
- 视觉一致性靠手抄补丁维持，而不是契约

### 正例 ✅

去改组件本体 [src/components/FormItem/FormItem.vue:172](../src/components/FormItem/FormItem.vue#L172)：

```css
/* 改前 */
.form-item__main {
  align-items: flex-start;
}
.form-item__label {
  padding: 4px 0;
}

/* 改后 */
.form-item__main {
  align-items: center;
  min-height: 32px;
}
.form-item__label {
  padding: 0;
  line-height: 32px;
}
```

然后 FormItemPage 不需要任何 `:deep()` 补丁。

### 自检

```bash
# 任何 docs page 出现 :deep() = 必须解释为什么不能改组件本体
grep -rn ":deep(" playground/docs/pages/
# 结果应该尽可能为空。如果有，必须在 PR 描述里说明为什么本轮无法改组件
```

---

## 6. 验证纪律：测试 + curl + 视觉

### 规则

6.1. **测试必须 focused**：改 X 组件就跑 X 测试。

```bash
# ✅ 正确
pnpm exec vitest run tests/FormItem.test.ts

# ❌ 错误（混入不相关失败）
pnpm test
```

6.2. **改完必须 curl 验证服务端真返回新内容**：

```bash
curl -s "http://127.0.0.1:4173/docs/pages/FormItemPage.vue?t=$(date +%s)" | head -50
# 必须看到本轮新加的 section / class，不能只看本地文件
```

6.3. **视觉对齐不能靠测试自动化判定，必须人眼/截图确认**：

- 单元测试通过 ≠ 视觉通过
- 任何涉及 layout / 对齐 / 间距 的改动，PR 验收必须包含手工浏览器确认
- 如果 layout 改动的是组件本体，要确认**所有用到该组件的 docs page** 都目视通过

### 对应失败 #4, #5, #8

#4：Codex 改完文件后浏览器还显示旧版（HMR 缓存），它没 curl 验证，宣称完成。
#5：Codex 跑 `pnpm test` 看到一堆失败，分不清哪些是本轮引入的、哪些是历史遗留。
#8：单元测试只断言 render 和 v-model，FormItem 视觉对齐错了一个月没人发现。

### 反例 ❌

> Codex："我改完了 FormItem.vue:172。`pnpm test` 跑了，105 / 105 通过。"

漏洞：
- 没说测试是 focused 还是全量
- 没 curl 验证服务端返回
- 没说视觉是否对齐

### 正例 ✅

> "改完 FormItem.vue:172 + 删 FormItemPage `:deep()` 补丁。
> - `pnpm exec vitest run tests/FormItem.test.ts`：8 / 8 通过（focused）
> - `curl http://127.0.0.1:4173/docs/pages/FormItemPage.vue` 返回新内容，不含 `:deep` 补丁
> - 浏览器手工确认：FormItem label 与 Input/Switch/Radio/CheckBox 视觉中心线对齐
> - 同时确认 PromptMessagePage（也用 FormItem）视觉未回归
> - 截图见 [path]（如有）"

### 自检

PR 描述里有没有以下三件事的明确证据？
- focused test 结果
- curl 返回内容
- 视觉确认（手工 / 截图 / 至少一句"已浏览器人工目视"）

任一缺失 = fail。

---

## 7. Codex 工作纪律

### 规则

7.1. **诊断后直接动手，不留"建议下一步"出口**

Codex 一旦自己诊断出根因，应**立即在同一轮内执行修复**，而不是输出"我建议下一步改 X"。"建议"是托词。

7.2. **修错层级时主动回退，不层层叠补丁**

如果 Codex 第一轮选了错层（如 docs page `:deep()`），第二轮被指出后**先撤掉旧补丁**，再去正确层修。不允许在补丁上再加补丁。

7.3. **不主动声称"全部完成"**

每轮汇报必须区分：
- 本轮**已动手**改了什么
- 本轮**只是诊断**没动手的项
- 已知**未解决**的项

不允许把"我建议下一步"包装成"已完成"。

7.4. **引用本 spec 时按章节号引**

后续 Codex prompt 写"按 spec §5 修复层级判定"，不写"参考 EP"或"按用户偏好"。引用 EP URL 视为无效（Codex 不 fetch）。

### 对应失败 #9, #12

#9：Codex 在 :deep() 补丁上反复加补丁，从不撤回。
#12：Codex 给"我建议下一步直接改 src/components/FormItem/FormItem.vue"——这种话是**它知道答案但要用户再点一次头的拖延**。

### 反例 ❌

> Codex："我建议下一步直接改 src/components/FormItem/FormItem.vue：给真实 slot 组合模式建立统一 32px row 布局，让 label 和 slot 内容在组件层对齐。"

——给了诊断和方案，**没有动手**。下一轮用户还得说"那你就改"，Codex 才动。这一轮等于零产出。

### 正例 ✅

> Codex："已改 src/components/FormItem/FormItem.vue:172（diff 见下）。
> - `align-items: flex-start` → `align-items: center`
> - label `padding: 4px 0` → `padding: 0; line-height: 32px`
> - 已删 FormItemPage.vue:458 的 `:deep()` 补丁
> - focused test：tests/FormItem.test.ts 8 / 8 通过
> - curl 验证：FormItemPage 返回新内容
> - **未解决**：tests/RemainingCanonicalPages.test.ts 还有 1 个失败，但与本轮无关（既有 unrelated）"

### 自检

Codex 这一轮的 last sentence 是"我建议..."、"下一步可以..."、"是否需要我..."？是 = fail。
应是"已改/已删/已验证"动词开头。

---

## 8. 全局类规范

### 规则

8.1. 所有 docs page 必须用全局 `.docs-section / .docs-demo / .docs-demo-card` 体系（定义在 [playground/docs/docs.css](../playground/docs/docs.css)）。

8.2. **禁止**：
- 在单页 `<style scoped>` 里发明新视觉类盖全局类
- 用 `:deep()` 给单页打视觉补丁（见 §5）
- 起一套并行命名（`docs-block` / `docs-card-v2` 之类）
- 扩散使用 [DocsExampleBlock.vue](../playground/docs/components/DocsExampleBlock.vue)（历史并行实现，已仅限 Checkbox / Radio / Icon 三页存量使用）

### 对应失败（Codex 反思第 3 条 ❌ 误诊）

Codex 曾认为"全局 .docs-section / .docs-demo 类干扰，应避免使用"。**这是误诊**：Breadcrumb 用的就是这套，并被认可为样板。Codex 当时"避免使用"的应对方式反而让结果更不一致。

### 反例 ❌

```vue
<template>
  <!-- Codex 起了平行命名，绕开全局类 -->
  <div class="my-page">
    <div class="my-page__section">
      <div class="my-page__demo-block">...</div>
    </div>
  </div>
</template>
```

### 正例 ✅

```vue
<template>
  <div class="docs-page">
    <section class="docs-section">
      <h2 class="docs-section__title">...</h2>
      <p class="docs-section__summary">...</p>
      <div class="docs-demo">
        <article class="docs-demo-card docs-demo-card--wide">...</article>
      </div>
    </section>
  </div>
</template>
```

---

## 9. API 段必须三表齐全

### 规则

§5 段必须按 EP 模式分三表，每表列名固定：

#### Attributes
| Name | Description | Type | Default |

#### Slots
| Name | Description |

#### Events
| Name | Description | Parameters |

如果组件确实没有 Slots 或 Events，**显式写 "None" / "暂无"**——读者不能区分"省略"和"没有"。

### 反例 ❌

```vue
<!-- 只有 Attributes，省略 Slots / Events -->
<section class="docs-section">
  <h2>API</h2>
  <table>...attributes only...</table>
</section>
```

### 正例 ✅

```vue
<section class="docs-section">
  <h2>Breadcrumb API</h2>

  <h3>Attributes</h3>
  <table>...4 列 Name/Description/Type/Default...</table>

  <h3>Slots</h3>
  <p>None — Breadcrumb 通过子组件 BreadcrumbItem 组合，不暴露 slot</p>

  <h3>Events</h3>
  <p>None — 无运行时事件</p>
</section>
```

---

## 10. 30 秒开发者验收（成品 checklist）

一个**没见过 TVU**的 Vue 开发者，从打开一个组件页开始计时 30 秒，必须能回答：

| # | 问题 | 在哪一段找答案 |
|---|---|---|
| Q1 | 这个组件叫什么？做什么？ | §1 Figma Coverage 标题 + 摘要 |
| Q2 | 它有哪些视觉变化轴？ | §1 Coverage 列表 + §3 Capability demo card 标题 |
| Q3 | 状态有哪些？ | §3 Capability 里 status / state demo |
| Q4 | 怎么 import？ | §4 Development Usage 代码块 |
| Q5 | 有哪些 props？类型？默认值？ | §5 Attributes 表 |
| Q6 | 支持 v-model？emit 哪些事件？ | §5 Events 表（**没有就显式写 None**） |
| Q7 | 能现场点一下试试？ | §3 demo 必须真交互（v-model / 真 hover） |

任何一个 No = 页面 fail spec。

### Breadcrumb 当前合规扫描

| Q | 通过？ | 备注 |
|---|---|---|
| Q1 | ✅ | |
| Q2 | ✅ | |
| Q3 | ✅ | |
| Q4 | ✅ | |
| Q5 | ✅ | |
| Q6 | ⚠️ | 没显式写 "None"——按 §9 应补 |
| Q7 | ⚠️ | href 是 dummy 链接，悬停有反馈但点击不导航——勉强 |

**结论**：Breadcrumb 通过 5 / 7，剩 2 项 minor。其它 4 个 in-review 页（form-item / prompt-message / steps / tabs）当前估计通过 ≤ 3 / 7。

---

## 11. Codex prompt 引用本 spec 的方式

每个涉及 docs page 的 Codex prompt 必读前置**必须**包含：

```markdown
- /Users/nancy/Documents/AICoding/VS_Code/tvu-design-system/docs/docs-site-dx-parity-spec.md
- /Users/nancy/Documents/AICoding/VS_Code/tvu-design-system/playground/docs/pages/BreadcrumbPage.vue
```

prompt 主体里**禁止**说"参考 [EP URL]"——Codex 不会 fetch URL，凭印象会漂移。改说"按本 spec §N"。

每个 prompt 末尾**禁令清单**应至少包含本 spec 第 0 节失败列表里和本任务相关的项。

---

## 12. 本 spec 的演进规则

- 本文件由主 Session 维护，是 docs 站 DX + Codex 行为约束的**单一真源**
- 修改本文件需要：(a) 主 Session 确认，(b) 同步更新 [docs/session-handoff.md](session-handoff.md) 引用
- 子 Session / Codex / 任何 AI 不得直接修改本文件
- 新发现的 Codex 失败模式，应作为新条目追加到第 0 节失败列表，并在对应规则段补反例
- 历史决策记录在 [docs/conformance-issue-log.md](conformance-issue-log.md)
