# TS/JSON Token Export — Design Spec

> **Date**: 2026-07-10
> **Backlog**: INFRA-F55 支柱① (工具无关机器可读契约) — 首块「TS/JSON token 出口」
> **Status**: design (brainstorming 产出，待 owner review → writing-plans)
> **Owner scope 决策（本 session 直接指令）**: DTCG 格式 · 无 Style Dictionary 依赖 · 全量三层 · 方案 A（parse `variables.css` 单一真源 + 独立 emitter + 不碰现有生成链）

---

## 1. 问题与目标

**缺口（实证）**：设计 token 当前**仅** CSS 变量出口（`dist/style.css`）。`package.json` `exports` 只有 `.` / `./style.css` / `./icons/*` / `./eslint-plugin`，**无 TS/JSON token 出口**。非 web 工具（原生 / RN / 设计工具 / AI agent）拿不到结构化 token。

**真源事实**（2026-07-10 live 核实，md5 `e94a602…`）：
- [src/tokens/variables.css](../../../src/tokens/variables.css)，453 行，头注释标 `AUTO-GENERATED — Figma-synced + code-authored PRESERVED`，由 `figma-sync/generate-tokens.mjs` 产出。
- **293** 条 custom-property 声明：dark 默认散布在多个 `:root` 块（~201 unique token），light 在单个 `[data-theme="light"]` 块覆盖 **92** 条。
- **220 字面值 / 73 `var()` alias**。
- 三层（对齐 PROJECT_GOAL §Token 架构）：① 原始色板 ② 语义色 ③ 非颜色（spacing / radius / typography / effect-shadow / 组件变体尺寸）。

**目标**：新增确定性 emitter，把 `variables.css` 转成 **DTCG JSON** + **TS 模块**，随包发布，consumer 可 `import`。**全量三层**，零新增运行时/构建依赖。

**非目标（YAGNI）**：
- ❌ 不改 `generate-tokens.mjs` / Figma sync 链。
- ❌ 不引 Style Dictionary 或任何 npm token 引擎。
- ❌ 不做 token 文档站页面（后续可选，不在本 spec）。
- ❌ 不改 CSS 变量本身的命名/值。

---

## 2. 方案选择

| | 做法 | 结论 |
|---|---|---|
| **A ✅ 采用** | parse `variables.css` 作单一真源 → 新增独立 emitter → 不碰 `generate-tokens.mjs` | variables.css 已合并 Figma-synced+code-authored 全三层，一次 parse 全覆盖；零依赖；emitter 纯后处理，风险隔离在新文件 |
| B 弃 | 改造 `generate-tokens.mjs` 同时吐 CSS+TS+JSON | 侵入现有链、回归面大、违「不碰生成链」 |
| C 弃 | 引 Style Dictionary 统一 token 引擎 | 重依赖、违 `audit:scripts-stdlib` gate、真源被换成 SD 中间层 |

**约束吻合**：项目已有 `audit:scripts-stdlib` gate 强制脚本纯 Node stdlib → 零依赖是既有硬纪律，天然排除 Style Dictionary。现有 `audit:sort-tokens.mjs` 已 parse variables.css → 有解析范式可复用。

---

## 3. 架构（单元 + 边界）

新增单一脚本 `figma-sync/generate-token-exports.mjs`，纯 Node stdlib，内部四个纯函数单元：

```
variables.css
   │  (fs.readFileSync)
   ▼
┌─────────────────────────────────────────────────────────────┐
│ 1. parseCss(cssText) → RawToken[]                             │
│    每条 = { name, value, scope('dark'|'light'), section }     │
│    单元职责：纯文本 → 声明列表；不解释语义                       │
├─────────────────────────────────────────────────────────────┤
│ 2. classify(RawToken) → { layer, $type }                      │
│    layer ∈ {palette, semantic, nonColor}                      │
│    $type ∈ {color, dimension, fontFamily, shadow, ...}        │
│    单元职责：按 section+prefix+值形态 确定性归类                 │
├─────────────────────────────────────────────────────────────┤
│ 3. buildTree(tokens) → { dtcg, nameToPath, modes }            │
│    先建 name→path 查找表 → 组 DTCG 树 → 挂 $extensions modes    │
│    单元职责：确定性寻址 + reference 解析 + 双主题合并            │
├─────────────────────────────────────────────────────────────┤
│ 4. emit(tree) → 写 dist/tokens/{tokens.json, tokens.ts}       │
│    单元职责：序列化；不含分类/解析逻辑                           │
└─────────────────────────────────────────────────────────────┘
```

**边界清晰性**：每个单元输入/输出是纯数据结构，可独立 vitest。parse 不懂 DTCG，classify 不碰文件，emit 不含业务逻辑。

**接线（不碰生成链）**：`build` 脚本在 `generate-tokens`（若在 build 内）与 `vite build` 之间插一步；实际挂点在 writing-plans 阶段按 `package.json` `build`/`generate` 现状精确定位。**读** `src/tokens/variables.css`，**写** `dist/tokens/`，两端都不与现有 generator 共享可变状态。

---

## 4. DTCG 输出结构

### 4.1 命名 → 路径（确定性寻址）

CSS 名去 `--` 前缀后按**分隔约定**映射到 DTCG 嵌套路径，保证 reference 可解析：

- 规则：token 名按 `layer` 分组作顶层 group，token 名本身（去前缀、`-`→`.` 不做，保留原名作单一 leaf）作叶子。即 **path = `<layer>.<name-no-prefix>`**。
  - `--color-grey-8` → `palette.color-grey-8`
  - `--text-heading` → `semantic.text-heading`
  - `--sp-m` → `nonColor.sp-m`
- **先建 `nameToPath: Map<cssName, dottedPath>`**（全 token 一遍扫），再组树。
- reference 解析查此表；**查不到的 `var()` 目标 → 脚本 FAIL（非零 exit），不脑补**（对齐项目「工具无真实返回禁编造」纪律）。

> 采用 `<layer>.<原名>` 单叶而非深层拆分（`color.grey.8`），因为原 CSS 名是唯一稳定 key，深拆需二次约定易与 alias 目标失配。reference 用完整 dotted path。

### 4.2 `$type` 推断（确定性）

| 判据（优先级从上到下） | `$type` |
|---|---|
| section 属颜色层 / 值匹配 `#hex`/`rgb(`/`hsl(` | `color` |
| 值以 `px`/`rem`/`em` 结尾（数字尺寸） | `dimension` |
| name 前缀 `--font-family-` | `fontFamily` |
| section = Effect Style（drop shadow） | `shadow` |
| 其余（无法判定） | 归 `other` 并在 emit 时 `log` 警告（不静默） |

### 4.3 alias 表示（owner 定：保留 reference）

- **仅当**整值**恰好**为单个 `var(--x)`（whole-value 单引用）→ 输出 DTCG reference `{<path-of-x>}`。
- **复合值**（多个 `var()`、`var()`+字面混合、font 栈如 `--font-family-base: var(--a), var(--b)`）→ **解析成最终字面字符串**（不做 reference），保证输出可用。
- 判据用正则精确匹配 `^var\(\s*--[\w-]+\s*\)$`。

```jsonc
"semantic": {
  "input-line-border": { "$type": "color", "$value": "{palette.color-grey-8}" }
}
```

### 4.4 主题 modes（owner 定：`$extensions` 单文件双值）

- `$value` = **dark**（默认主题）。
- 被 light 覆盖的 92 条额外挂 `$extensions["tvu.mode"]`：

```jsonc
"semantic": {
  "bg-layer1": {
    "$type": "color",
    "$value": "#141414",
    "$extensions": { "tvu.mode": { "dark": "#141414", "light": "#ffffff" } }
  }
}
```

- 未被 light 覆盖的 ~201 条只有 `$value`（theme-stable，无 `$extensions`）。
- `tvu.mode` 内的值：若 dark/light 任一侧是单 `var()`，同 §4.3 规则决定 reference vs 字面。

---

## 5. TS 输出

`dist/tokens/tokens.ts`（编译出 `tokens.js` + `tokens.d.ts`）导出：

```ts
// resolved dark-mode 值（最常用：非 web consumer 要实际值）
export const tokens = {
  palette: { 'color-grey-8': '#595959', /* … */ },
  semantic: { 'bg-layer1': '#141414', /* … */ },
  nonColor: { 'sp-m': '16px', /* … */ },
} as const;

// light-mode 覆盖（仅 92 条），consumer 按需 merge
export const tokensLight: Partial<typeof tokens> /* 结构镜像 */;

// 全 token 名联合类型
export type TokenName = 'color-grey-8' | 'bg-layer1' | 'sp-m' | /* … */;
```

- TS 侧 alias **一律 resolved**（给最终值），与 JSON 保留 reference 互补——正是 owner 选 reference 时「TS 侧同时给 resolved 值」的落点。
- `as const` 保证字面量类型 + `TokenName` 类型可供 consumer 类型约束。

---

## 6. package.json exports & dist 布局

```jsonc
"exports": {
  // … 现有 …
  "./tokens": "./dist/tokens/tokens.json",
  "./tokens/js": { "types": "./dist/tokens/tokens.d.ts", "import": "./dist/tokens/tokens.js" }
}
```

- `files` 数组加 `dist/tokens`（现 `files` = `["dist", …]`，`dist` 已含 → 实际免改，writing-plans 阶段核实 `dist` 通配是否已覆盖，避免多余改动）。
- dist 布局：`dist/tokens/tokens.json` · `dist/tokens/tokens.js` · `dist/tokens/tokens.d.ts`。

---

## 7. 验证 gate（owner 定：audit gate + prepublishOnly）

新增 `scripts/audit-token-exports.mjs`（纯 stdlib）：
1. 重新 `parseCss` + `buildTree`（复用 emitter 的纯函数，import 而非复制逻辑）。
2. 读已生成的 `dist/tokens/tokens.json`，**深比对**。
3. 不一致 → 打印 diff + 非零 exit（FAIL）。
4. reference 完整性校验：每个 `{…}` reference 的目标 path 必须存在于树。
5. `dist/tokens` 不存在 → 明确 FAIL 提示先 `pnpm generate:token-exports`（非静默 pass）。

- `package.json` scripts 加 `generate:token-exports` + `audit:token-exports`。
- 挂进 `prepublishOnly` 现有 gate 序列（22 gate）末尾。
- **对齐纪律**：项目「新增 generator 必挂 gate」（[[feedback_audit-pre-commit-gate]]）——防 stale 产物 push、防 908/930 类跨文件漂移。

---

## 8. 测试（vitest — 与项目范式一致）

emitter 四单元各测（纯函数好测）：
- `parseCss`：多 `:root` 块 + light 块 + `@font-face`（应跳过）+ 注释 → 正确 RawToken[]（含 scope/section）。
- `classify`：各层样本 → 正确 `layer`/`$type`；px/hex/font-family/shadow 边界。
- `buildTree`：单 `var()` → reference；复合值 → 字面；light 覆盖 → `$extensions`；**未知 reference 目标 → throw**（FAIL 路径）。
- `emit` + round-trip：parse→emit→re-parse 幂等；抽样 token（`--bg-layer1` dark `#141414`/light `#ffffff`、`--sp-m` `16px`、`--input-line-border` → reference）值断言。

---

## 9. 风险与决策记录

| 风险 | 缓解 |
|---|---|
| variables.css 格式变化破坏 parser | parser 只依赖 `--name: value;` + 块归属 + section 注释；`@font-face` 显式跳过；未知 `$type` 不静默（log 警告） |
| reference 目标失配 | name→path 表先建；查不到 FAIL 不脑补 |
| `dist/tokens` 未 rebuild 导致 stale 发布 | audit gate 挂 prepublishOnly，比对不过阻断发布 |
| 复合 var 值歧义 | 精确正则区分 whole-value 单引用 vs 复合；复合一律 resolve |
| 与并行 session 冲突 | 全新文件 + 仅追加 `package.json` scripts/exports；不碰组件源码；提交前 `git status` 核实只碰本 feature 文件 |

**已定决策**（owner 本 session）：DTCG · 无 SD · 全量三层 · 方案 A · alias 保留 reference · 主题 `$extensions` modes · gate 挂 prepublishOnly。

---

## 10. 交付物清单

1. `figma-sync/generate-token-exports.mjs`（emitter，四纯函数 + main）
2. `scripts/audit-token-exports.mjs`（防漂移 gate）
3. `package.json`：`exports`（`./tokens` + `./tokens/js`）+ scripts（`generate:token-exports` / `audit:token-exports`）+ `prepublishOnly` 追加
4. vitest 测试文件
5. build 接线（emitter 挂进构建，precise 定位不碰 generator）
6. 文档：token 消费用法（README 或 GETTING_STARTED 补一段 —— writing-plans 定落点）
