# CANONICAL-009 — Design-system audit 升 strict + Tab.vue token 化

> **目标**：把 `pnpm audit:design-system` 从 warn-only 升 strict 全链路就位。修 audit 脚本（识别 component-scoped CSS vars + carrier 组件白名单 + exit code 纳入所有 violation）+ Tab.vue hex 替换为 token + workflow/RELEASING/changeset 同步。
>
> **一步到位 prompt**（user 2026-05-11 拍板 D1+D2+D3+D4）。Executor 严格按本 prompt 改，**禁止扩展 scope**（详见 §5）。

---

## §0 决策 baked-in（user 拍板 2026-05-11）

### D1 — audit 脚本识别 component-scoped CSS vars

**问题**：当前 `figma-sync/audit-design-system.mjs` 只扫 `src/tokens/variables.css` 当 defined token set。8 个 component（Button/FormItem/Progress/PromptMessage/Rating/Slider/Switch/Tooltip）内部使用的 scoped variables（`--btn-bg` / `--fi-border` / `--progress-fill-color` 等）被误报为 `undefinedTokens`，触发 exit 1。

这些是 Vue scoped style 标准范式（每 component 内部 `:host` / `.cls` 自赋值 + JS `style` binding `{ '--btn-bg': palette.value.bg }`），不是 global token。**audit 脚本逻辑 bug，不是 component bug**。

**修法**：每个 .vue 文件先 collect 自己内部所有 `--xxx:` 定义（regex 同现有 `collectDefinedTokens`，整文件 match 含 `<style>` scope + JS binding object key），加入 file-local defined set。`var(--xxx)` 合法性判定改用 `globalDefinedTokens ∪ fileLocalDefinedTokens` union。

### D2 — Tab.vue hex 替换 grey scale token（1:1 hex 已登记）

- `#1f1f1f` → `var(--color-grey-12)` (`src/design-system/translation/token-aliases.ts:24` 已登记 `UX/Grey/grey-12 #1F1F1F`)
- `#353535` → `var(--color-grey-10)` (`src/design-system/translation/token-aliases.ts:22` 已登记 `UX/Grey/grey-10 #353535`)

### D3 — carrier 组件白名单（合并的 Icon + Logo）

Icon.vue 和 Logo.vue 都是 **carrier / dispatcher 组件**——按外部 SOT 来源派发 SVG，**inline render 是设计意图**：

- **Icon** 派发自 `src/icons/catalog/generated/`（auto-sync from figma）
- **Logo** 派发自 `src/icons/raw.ts`（manually curated SOT；`raw.ts:4` 注释明示 `Logo icons retain brand color (#56AF31 / #50B848)`）

audit 应该 pass 它们，不是 fight。**新加 carrier 时必须 update `CARRIER_COMPONENT_PATHS` 常量 + 注明 SOT 来源**——机制可扩展，改一处。

### D4 — audit exit code 纳入所有 violation 类

修完 D1+D2+D3 后，`process.exitCode = 1` 判定要纳入全 3 类：
- `undefinedTokenFiles > 0`
- `hardcodedColorFiles > 0`
- `rawSvgInjectionFiles > 0`

否则升 strict 形同虚设。

### Anti-decisions（已拍板排除，executor 不可发明）

- ❌ 不为 Logo refactor 装 `vite-svg-loader` / 不抽 `.svg` 文件 / 不动 `src/icons/raw.ts`——tail wagging the dog（raw.ts 是 curated SOT，不是实现细节）
- ❌ 不删 `src/icons/generated/brand.ts` 的 `brand/logo-tvu` / `brand/logo-ts` registry
- ❌ 不动 `src/icons/index.ts` 的 `LogoTVU` / `LogoTS` re-export
- ❌ 不动 `playground/docs/pages/designAssetData.ts` 的 logo block
- ❌ 不动 `Icon.vue` / `Logo.vue` 文件本身
- ❌ 不引入 `vite-svg-loader` 或类似新 dep
- ❌ 不用 Tab.vue semantic token（`--bg-layer4` / `--line-deep` 等）——hex 1:1 grey scale 已登记最稳

---

## §1 改动顺序

按本顺序做，每步跑验证：

```
1. 改 figma-sync/audit-design-system.mjs（D1 + D3，不含 D4 exit code 升级）
2. 改 src/components/Tab/Tab.vue（D2 hex → token）
3. 跑 pnpm audit:design-system → 验证 exit 0（D1+D2+D3 已生效）
4. 改 figma-sync/audit-design-system.mjs（D4 exit code 纳入 hardcoded + rawSvg）
5. 跑 pnpm audit:design-system → 验证仍 exit 0（D4 升级后无回归）
6. 改 .github/workflows/publish.yml（删 warn-only step）
7. 改 package.json prepublishOnly chain（追加 audit:design-system）
8. 改 docs/RELEASING.md（gate 表 + checklist + troubleshooting）
9. 跑 pnpm changeset 生成 0.1.1 patch changeset
10. 跑 §3 全部验证命令
```

---

## §2 改动清单

### 2.1 `figma-sync/audit-design-system.mjs`

**改动 A — 文件顶部加模块注释**（替换 line 1 之前的空白，或加到 import 块之前）：

```js
/**
 * Design-system audit
 *
 * 检测 3 类 violation 并阻 exit code：
 * 1. undefinedTokens — var(--xxx) 引用了 `src/tokens/variables.css` global tokens
 *    ∪ 该 .vue 文件内部 `--xxx:` 定义之外的 token
 * 2. hardcodedColors — 组件 .vue 内出现 hex 字面量（违 AGENTS.md 硬规则 #4）
 * 3. rawSvgInjection — 业务组件不可 inline SVG (`v-html="..."`)；carrier 组件
 *    按 SOT 来源派发是设计意图，免审
 *
 * carrier 组件白名单：见 CARRIER_COMPONENT_PATHS。新增 carrier 需 update 该常量
 * + 注明 SOT 来源。
 */
```

**改动 B — 新增 carrier 白名单常量**（紧跟现有 const 段如 `COMPONENTS_DIR` 之后）：

```js
const CARRIER_COMPONENT_PATHS = [
  'src/components/Icon/',  // dispatches from src/icons/catalog/generated/ (auto-sync from figma)
  'src/components/Logo/',  // dispatches from src/icons/raw.ts (manually curated SOT; see raw.ts:4 "Logo icons retain brand color")
]
```

**改动 C — `collectFileFindings` 内识别 file-local scoped tokens（D1）+ carrier skip rawSvg（D3）**：

- 在 `const source = readFileSync(...)` 之后，先 collect file-local defined tokens：
  ```js
  const fileLocalDefinedTokens = new Set(
    [...source.matchAll(/(--[a-z0-9-]+)\s*:/gi)].map(match => match[1])
  )
  const allDefinedTokens = new Set([...definedTokens, ...fileLocalDefinedTokens])
  ```
- `undefinedTokens` 计算时把 filter 改成 `!allDefinedTokens.has(token)`
- `rawSvgInjection` 计算前先判 carrier：
  ```js
  const relativeFilePath = relative(ROOT, filePath)
  const isCarrier = CARRIER_COMPONENT_PATHS.some(p => relativeFilePath.startsWith(p))
  const rawSvgInjection = isCarrier ? [] : unique(
    [...source.matchAll(/v-html\s*=\s*"([^"]+)"/g)].map(match => match[1])
  ).sort()
  ```

**改动 D — exit code 判定（D4）**（line 92-94 区域）：

替换：
```js
if (report.totals.undefinedTokenFiles > 0) {
  process.exitCode = 1
}
```

为：
```js
if (
  report.totals.undefinedTokenFiles > 0 ||
  report.totals.hardcodedColorFiles > 0 ||
  report.totals.rawSvgInjectionFiles > 0
) {
  process.exitCode = 1
}
```

### 2.2 `src/components/Tab/Tab.vue`

- line 45 `border-bottom: 1px solid #353535;` → `border-bottom: 1px solid var(--color-grey-10);`
- line 53 `background: #1f1f1f;` → `background: var(--color-grey-12);`
- line 54 `border: 1px solid #353535;` → `border: 1px solid var(--color-grey-10);`

不动其余代码 / template / script。

### 2.3 `.github/workflows/publish.yml`

删除 line 35-38（warn-only step + 前面注释行 35）：

```yaml
      # Warn-only audit gates — failures logged as workflow warnings, do not block publish
      - name: audit:design-system (warn-only)
        continue-on-error: true
        run: pnpm audit:design-system
```

修正注释 line 35（如保留则把 `Warn-only audit gates` → `Warn-only audit gate`，因为只剩 published-vs-code 一个）。

不动 `audit:published-vs-code` 那个 step（line 40-42）——本任务 scope 不含 BRIDGE-MOCKUP-004。

### 2.4 `package.json`

line 40 `prepublishOnly` chain 末尾追加 `&& pnpm run audit:design-system`：

```json
"prepublishOnly": "pnpm run audit:icon-fill-currentcolor && pnpm run audit:no-hardcoded-design-tokens && pnpm run audit:component-no-inline-svg && pnpm run audit:figma-conformance && pnpm run audit:docs-site && pnpm run audit:component-tokens && pnpm run audit:tokenized-diff && pnpm run audit:design-system",
```

### 2.5 `docs/RELEASING.md`

**line 19** `7 strict audits` → `8 strict audits`

**line 27 之后** 在 `pnpm run audit:tokenized-diff` 行后追加：
```
  pnpm run audit:design-system
```
（保持 4 字符缩进与其它行一致）

**line 113** 把 `audit:design-system` 行从：
```
| `audit:design-system` | warn-only | Logs warning; does not block |
```
改为：
```
| `audit:design-system` | **strict** | Blocks publish |
```
**并把该行移到 line 112 后**（紧跟 `audit:tokenized-diff` 之后，与其它 strict audits 同段）。

**line 157** `7 strict audits` → `8 strict audits`

**line 158** 把：
```
| `audit:design-system` / `audit:published-vs-code` always warns in CI | Known baseline FAIL items (see `CANONICAL-009` / `BRIDGE-MOCKUP-004` backlog) | Track in backlog; do not block publish |
```
改为：
```
| `audit:published-vs-code` always warns in CI | Known baseline FAIL items (see `BRIDGE-MOCKUP-004` backlog) | Track in backlog; do not block publish |
```

### 2.6 `.changeset/<auto-generated-name>.md`

跑 `pnpm changeset` 交互式生成。选 bump level `patch`（0.1.0 → 0.1.1）。Description 填：

```
CANONICAL-009: audit:design-system 升 strict

- 修 audit 脚本 scoped CSS vars false-positive（每 .vue 内部定义的 --xxx 现纳入 defined set）
- 加 carrier components 白名单（Icon + Logo carrier 模式 by design，按 SOT 来源派发 SVG）
- Tab.vue hex 替换为 grey scale token (--color-grey-10 / --color-grey-12)
- exit code 纳入 hardcoded color + raw SVG injection（不只 undefined token）
```

Frontmatter 应该是：
```
---
'@tvu/design-system': patch
---
```

---

## §3 验证（机械化命令，按顺序）

每条 exit code 必须满足"预期 exit"列：

| # | 命令 | 预期 exit | 关键期望 |
|---|---|---|---|
| 1 | `pnpm typecheck` | 0 | — |
| 2 | `pnpm build` | 0 | — |
| 3 | `pnpm audit:design-system` | **0** | JSON `totals.undefinedTokenFiles == 0` |
| 4 | `pnpm audit:design-system` | **0** | JSON `totals.hardcodedColorFiles == 0` |
| 5 | `pnpm audit:design-system` | **0** | JSON `totals.rawSvgInjectionFiles == 0`（Icon + Logo 被 carrier 白名单过滤） |
| 6 | `pnpm audit:design-system \| jq '.undefinedTokenFiles \| length'` | stdout=`0` | Button/FormItem 等 8 file 不再误报 |
| 7 | `pnpm audit:design-system \| jq '.rawSvgInjectionFiles \| length'` | stdout=`0` | carrier 白名单生效 |
| 8 | `pnpm audit:icon-fill-currentcolor` | 0 | 不破坏其它 audit |
| 9 | `pnpm audit:no-hardcoded-design-tokens` | 0 | 不破坏其它 audit |
| 10 | `pnpm audit:component-no-inline-svg` | 0 | 不破坏其它 audit |
| 11 | `pnpm audit:figma-conformance` | 0 | 不破坏其它 audit |
| 12 | `pnpm audit:docs-site` | 0 | 不破坏其它 audit |
| 13 | `pnpm audit:component-tokens` | 0 | 不破坏其它 audit |
| 14 | `pnpm audit:tokenized-diff` | 0 | 不破坏其它 audit |
| 15 | `pnpm changeset:status` | 0 | 列出 1 个 pending changeset (0.1.1 patch) |
| 16 | `grep -c "audit:design-system" .github/workflows/publish.yml` | stdout=`0` | warn-only step 已删除 |
| 17 | `grep -c "audit:design-system" package.json` | stdout `>=2` | prepublishOnly 含 + scripts 段定义 |
| 18 | `grep -c "8 strict audits" docs/RELEASING.md` | stdout `>=2` | line 19 + line 157 都更新 |
| 19 | `grep "audit:design-system" docs/RELEASING.md \| grep -c "strict"` | stdout `>=1` | gate 表里已 strict |
| 20 | `grep -c "audit:design-system.*warn-only" docs/RELEASING.md` | stdout=`0` | 文档无残留 warn-only 引用 |

---

## §4 报告 schema (STOP 后输出)

按本 schema 填值后 STOP，**不要 commit / push / tag**：

```
### CANONICAL-009 执行报告

#### 改动文件清单
- figma-sync/audit-design-system.mjs (modified: +XX/-XX lines, 含模块注释 + CARRIER_COMPONENT_PATHS + scoped vars 识别 + carrier rawSvg skip + exit code 升级)
- src/components/Tab/Tab.vue (modified: 3 处 hex → token)
- .github/workflows/publish.yml (modified: 删 audit:design-system warn-only step)
- package.json (modified: prepublishOnly chain 追加 audit:design-system)
- docs/RELEASING.md (modified: 4 处 — 7→8 strict / chain 列表 / gate 表移段 / troubleshooting)
- .changeset/<auto-name>.md (new: 0.1.1 patch)

#### 验证结果（§3 顺序，全 20 条）

| # | 命令 | 实际 exit / stdout | 预期 | 状态 |
|---|---|---|---|---|
| 1 | pnpm typecheck | 0 | 0 | ✅ |
| 2 | pnpm build | 0 | 0 | ✅ |
| 3 | pnpm audit:design-system | 0 / undefinedTokenFiles=0 | 0 / 0 | ✅ |
| ... | ... | ... | ... | ... |

（填全 20 行）

#### 未解决项 / blocker
（如无写"无"。如有任一 §3 验证不过，STOP 报告具体哪条，不要发明 fix）

#### 自检
- [ ] 没自创新规则（未扩展 audit 检测器到 §0 列举之外的类型）
- [ ] 没改 §5 禁止列表内文件
- [ ] §3 全 20 条命令实际跑过（不是猜测）
- [ ] §0 决策按 D1→D2→D3→D4 顺序落地，未合并 / 未跳步
```

---

## §5 不允许的扩展（防 executor 加料）

**严格禁止**：

| 类别 | 禁止动作 |
|---|---|
| Logo SOT | 不动 `src/icons/raw.ts`；不抽 `.svg` 文件；不引入 `vite-svg-loader` |
| Icon registry | 不动 `src/icons/generated/brand.ts`；不动 `src/icons/index.ts`；不删 `brand/logo-tvu` / `brand/logo-ts` |
| Docs | 不动 `playground/docs/pages/designAssetData.ts` |
| Carrier 组件 | 不动 `src/components/Icon/Icon.vue`；不动 `src/components/Logo/Logo.vue` 本身 |
| Tab 范围 | 只改 line 45/53/54 3 处 hex；不动 `<script>` / `<template>` |
| 其它 component | 不"顺手"修 Button/FormItem 等的 scoped variables 命名 / 重构 — D1 让 audit 识别它们，**不要去 fix 那 8 个 component** |
| 其它 audit 脚本 | 只改 `audit-design-system.mjs`；不动 `audit-figma-conformance` / `audit-docs-site` / 任何其它 audit |
| dep / config | 不加新 npm dependency；不动 `vite.config.ts` / `tsconfig.json` / `pnpm-lock.yaml`（除 changeset 之外） |
| changeset | 只生成 1 个 changeset，bump `patch`，描述按 §2.6 填；不动 `.changeset/config.json` |
| commit / push | **不要 commit**；**不要 push**；**不要 tag**；STOP 等 plan owner 复审 |

**如发现 §3 验证不过**：STOP，报告具体哪条 + 实际输出 + 诊断假设，**不要发明 fix 路线**。

---

## §6 plan-owner 自检（meta-rules.md §3 反模式 5 条）

| # | 反模式 | 本 prompt 是否避免 | 实证 |
|---|---|---|---|
| 1 | 硬编码项目级规则到工具 | ✅ | `CARRIER_COMPONENT_PATHS` 在 audit 脚本注释明示"新增 carrier 需 update + 注明 SOT 来源"——扩展机制显式 |
| 2 | 打补丁方案 | ✅ | D1 (scoped vars 识别) 是结构性升级；D3 (carrier 白名单) 是机制化扩展点——不是为这 8 file / 这 2 carrier 一次性 hack |
| 3 | to-do list 思维 | ✅ | §4 报告 schema 字段化可机械比对；§3 全 20 条验证 exit code / stdout 数字 |
| 4 | 没问下游怎么消费 | ✅ | audit 升 strict 下游是 publish workflow `prepublishOnly` chain；§2.3-2.5 同步改 workflow + package.json + RELEASING |
| 5 | 没问扩展时改哪里 | ✅ | 新增 carrier 只改 `CARRIER_COMPONENT_PATHS` 一处；新增 component-scoped CSS var 自动识别（无需改 audit） |

---

## §7 STOP 协议

完成 §3 全部 20 条验证 + 写完 §4 报告后 **立即 STOP**。

**禁止**：
- 自行 `git add` / `git commit`
- 自行 `git push`
- 自行 `git tag`
- 自行 fire 下一个 task（如 BRIDGE-MOCKUP-004）

等 plan owner 复审 diff → user 拍板 → 由 user 操作 commit + push + tag 触发 CI。

---

## §8 元说明

- **本 prompt 生命周期**：commit 入仓库 → executor fire → executor STOP 后 plan owner 复审 → user 拍板 commit。完成后不删除（file-based prompt 范式，保留追溯）
- **关联 backlog**：[`docs/internal/backlog.md`](../backlog.md) CANONICAL-009 entry
- **关联 pickup**：[`docs/internal/_plans/next-session-pickup-2026-05-12.md`](../_plans/next-session-pickup-2026-05-12.md) §3 第 1 步
- **关联复盘**：[`docs/internal/retrospection/2026-05-11-v0-1-publish-flow.md`](../retrospection/2026-05-11-v0-1-publish-flow.md) §Anti-pattern 3 (one-shot prompts) — 本 prompt 即一步到位实证
- **下一步**：CANONICAL-009 落地后接 BRIDGE-MOCKUP-004 baseline（pickup §3 第 2 步），最后 0.1.1 release cycle 演练（pickup §3 第 3 步）
