# TS/JSON Token Export 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:** 把 `src/tokens/variables.css` 转成随包发布的 DTCG JSON + TS token 出口，让非 web 工具消费结构化 token。

**Architecture:** 新增单一零依赖 emitter `figma-sync/generate-token-exports.mjs`（四纯函数 parse→classify→buildTree→emit），读已生成的 `variables.css`，写 `dist/tokens/{tokens.json,tokens.ts→.js/.d.ts}`。不碰现有 `generate-tokens.mjs`。新增 `audit-token-exports.mjs` gate 挂 prepublishOnly 防漂移。

**Tech Stack:** Node.js ESM（纯 stdlib：`node:fs` / `node:path` / `node:url`）、vitest（`vitest run`）、vite build。

**Spec:** [docs/superpowers/specs/2026-07-10-tsjson-token-export-design.md](../specs/2026-07-10-tsjson-token-export-design.md)

## Global Constraints

- **零新增依赖**：脚本只 import `node:*` stdlib（`audit:scripts-stdlib` gate 强制）。禁 Style Dictionary / 任何 npm token 引擎。
- **不碰生成链**：不修改 `figma-sync/generate-tokens.mjs` 或 Figma sync 逻辑；变源真源 = 已生成的 `src/tokens/variables.css`。
- **DTCG 格式**：`$type` / `$value` / `$extensions`；alias 保留 reference `{path}`；主题走 `$extensions["tvu.mode"]`。
- **全量三层**：palette / semantic / nonColor 全导出。
- **alias 规则**：整值恰为 `^var\(\s*--[\w-]+\s*\)$` → reference；复合值 → resolve 成字面。
- **未知 reference 目标 → FAIL（非零 exit），不脑补。**
- **测试**：vitest，`.test.ts` 放 `tests/`，`vitest run` 跑。
- **提交纪律**：精确 `git add` 只碰本 feature 文件；owner 直推 master + push 双 remote（commit 含 push）。**并行 session 警告**：另有 session 在动 React parity / F54，提交前 `git status` 核实未误触其文件。

---

### Task 1: CSS Parser (`parseCss`)

**Files:**
- Create: `figma-sync/generate-token-exports.mjs`
- Test: `tests/token-exports-parse.test.ts`

**Interfaces:**
- Produces: `export function parseCss(cssText: string): RawToken[]` where `RawToken = { name: string, value: string, scope: 'dark'|'light', section: string }`. `name` 去 `--` 前缀；`value` 去尾分号 trim；`scope` = `light` 若在 `[data-theme="light"]` 块内否则 `dark`；`section` = 该 token 之前最近的分组注释（normalized，如 `Neutrals (grey scale, light → dark)`）。`@font-face` 块内声明跳过。

- [ ] **Step 1: Write the failing test**

```ts
// tests/token-exports-parse.test.ts
import { describe, it, expect } from 'vitest'
import { parseCss } from '../figma-sync/generate-token-exports.mjs'

const CSS = `
/* ===== Neutrals ===== */
:root {
  --color-grey-8: #595959;
  --input-line-border: var(--color-grey-8);
}
@font-face { font-family: 'X'; src: url('x.woff'); }
/* ── Spacing ── */
:root { --sp-m: 16px; }
[data-theme="light"] { --color-grey-8: #595959; --bg-layer1: #ffffff; }
`

describe('parseCss', () => {
  it('extracts name/value/scope/section and skips @font-face', () => {
    const toks = parseCss(CSS)
    const grey = toks.find(t => t.name === 'color-grey-8' && t.scope === 'dark')
    expect(grey).toEqual({ name: 'color-grey-8', value: '#595959', scope: 'dark', section: 'Neutrals' })
    const sp = toks.find(t => t.name === 'sp-m')
    expect(sp.section).toBe('Spacing')
    expect(sp.value).toBe('16px')
    const light = toks.find(t => t.name === 'bg-layer1' && t.scope === 'light')
    expect(light.value).toBe('#ffffff')
    // no @font-face internals leaked as tokens
    expect(toks.some(t => t.name === 'font-family' || t.value.includes('url('))).toBe(false)
  })
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-parse.test.ts`
Expected: FAIL — `parseCss` is not a function / module has no export.

- [ ] **Step 3: Write minimal implementation**

```js
// figma-sync/generate-token-exports.mjs
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const OPENER = /^(:root|\[data-theme[^\]]*\])\s*\{/
const LIGHT = /^\[data-theme="light"\]/
const TOKEN = /^--([A-Za-z0-9-]+)\s*:\s*(.+?);?\s*$/

function normalizeLabel(text) {
  return text.replace(/^[\s=─\-]+/, '').replace(/[\s=─\-]+$/, '').trim()
}

/** @returns {{name,value,scope,section}[]} */
export function parseCss(cssText) {
  const lines = cssText.split('\n')
  const out = []
  let inComment = false, inTracked = false, inFontFace = false
  let scope = 'dark', section = ''
  for (const raw of lines) {
    const s = raw.trim()
    if (inComment) { if (s.includes('*/')) inComment = false; continue }
    if (s.startsWith('/*')) {
      const close = s.indexOf('*/', 2)
      const text = close !== -1 ? s.slice(2, close) : s.slice(2)
      if (close === -1) inComment = true
      const label = normalizeLabel(text)
      if (label) section = label.split(/\s*[—=]{2,}|=====/)[0].trim() || label
      // keep only leading words up to first delimiter noise
      section = normalizeLabel(section).replace(/\s*\(.*$/, '').trim() || section
      continue
    }
    if (s.startsWith('@font-face')) { inFontFace = true; continue }
    if (inFontFace) { if (s.includes('}')) inFontFace = false; continue }
    if (OPENER.test(s)) { inTracked = true; scope = LIGHT.test(s) ? 'light' : 'dark'; continue }
    if (inTracked && s.startsWith('}')) { inTracked = false; continue }
    if (inTracked) {
      const m = s.match(TOKEN)
      if (m) out.push({ name: m[1], value: m[2].trim(), scope, section })
    }
  }
  return out
}
```

> Note: `section` 归一取分组注释首段（去掉 `(...)` 补充与 `===`/`——` 装饰）。测试里 `Neutrals`/`Spacing` 即验证此归一。

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run tests/token-exports-parse.test.ts`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add figma-sync/generate-token-exports.mjs tests/token-exports-parse.test.ts
git commit -m "feat(tokens): parseCss for token export emitter (INFRA-F55 支柱①)"
```

---

### Task 2: Classifier (`classify`)

**Files:**
- Modify: `figma-sync/generate-token-exports.mjs` (add `classify`)
- Test: `tests/token-exports-classify.test.ts`

**Interfaces:**
- Consumes: `RawToken` from Task 1.
- Produces: `export function classify(tok: RawToken): { layer: 'palette'|'semantic'|'nonColor', $type: 'color'|'dimension'|'fontFamily'|'shadow'|'other' }`.

**Classification rules (from spec §4.1/§4.2):**
- layer: name 前缀 `color-`/`brand`/`red`/`orange`/`blue`（原始色板）→ `palette`；`text-`/`icon-`/`bg-`/`line-`/`input-`/`select-`/`notification-`/…语义色 → `semantic`；`sp-`/`r-`/`font-`/尺寸/shadow → `nonColor`。
- $type: 值 `#`/`rgb(`/`hsl(` 或 whole-value `var()` 指向 color → `color`；结尾 `px`/`rem`/`em` → `dimension`；name 前缀 `font-family-` → `fontFamily`；section 含 `Effect`/`shadow` → `shadow`；否则 `other`（emit 阶段 log 警告）。

- [ ] **Step 1: Write the failing test**

```ts
import { describe, it, expect } from 'vitest'
import { classify } from '../figma-sync/generate-token-exports.mjs'

const t = (name, value, section = '') => ({ name, value, scope: 'dark', section })

describe('classify', () => {
  it('palette color', () => expect(classify(t('color-grey-8', '#595959'))).toEqual({ layer: 'palette', $type: 'color' }))
  it('semantic color literal', () => expect(classify(t('bg-layer1', '#141414'))).toEqual({ layer: 'semantic', $type: 'color' }))
  it('semantic color via var', () => expect(classify(t('input-line-border', 'var(--color-grey-8)'))).toEqual({ layer: 'semantic', $type: 'color' }))
  it('spacing dimension', () => expect(classify(t('sp-m', '16px'))).toEqual({ layer: 'nonColor', $type: 'dimension' }))
  it('font family', () => expect(classify(t('font-family-en', '-apple-system, sans-serif'))).toEqual({ layer: 'nonColor', $type: 'fontFamily' }))
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-classify.test.ts`
Expected: FAIL — `classify` is not a function.

- [ ] **Step 3: Write minimal implementation**

```js
// append to figma-sync/generate-token-exports.mjs
const PALETTE_PREFIX = /^(color-|brand|red|orange|blue|green|yellow|chart-)/
const SEMANTIC_PREFIX = /^(text-|icon-|bg-|line-|input-|select-|notification-|tooltip-|progress-|switch-)/
const isColorVal = v => /^#|^rgb\(|^rgba\(|^hsl\(/.test(v)
const WHOLE_VAR = /^var\(\s*--([\w-]+)\s*\)$/

export function classify(tok) {
  const { name, value, section } = tok
  let layer = 'nonColor'
  if (PALETTE_PREFIX.test(name)) layer = 'palette'
  else if (SEMANTIC_PREFIX.test(name)) layer = 'semantic'

  let $type = 'other'
  if (name.startsWith('font-family-')) $type = 'fontFamily'
  else if (/effect|shadow/i.test(section)) $type = 'shadow'
  else if (isColorVal(value)) $type = 'color'
  else if (/(px|rem|em)$/.test(value.replace(/;?$/, ''))) $type = 'dimension'
  else if (WHOLE_VAR.test(value) && (layer === 'palette' || layer === 'semantic')) $type = 'color'
  return { layer, $type }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run tests/token-exports-classify.test.ts`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add figma-sync/generate-token-exports.mjs tests/token-exports-classify.test.ts
git commit -m "feat(tokens): classify layer + \$type for token export"
```

---

### Task 3: Tree Builder (`buildTree`)

**Files:**
- Modify: `figma-sync/generate-token-exports.mjs` (add `buildTree`)
- Test: `tests/token-exports-tree.test.ts`

**Interfaces:**
- Consumes: `parseCss` + `classify`.
- Produces: `export function buildTree(tokens: RawToken[]): { dtcg: object, nameToPath: Map<string,string> }`. `dtcg` 是 DTCG 嵌套对象（顶层 `palette`/`semantic`/`nonColor`，叶子 `{ $type, $value, $extensions? }`）。`nameToPath` = cssName → dotted path（`<layer>.<name>`）。whole-value var → `$value = "{targetPath}"`；未知 target → **throw Error**。light scope 覆盖挂 `$extensions["tvu.mode"] = { dark, light }`。

- [ ] **Step 1: Write the failing test**

```ts
import { describe, it, expect } from 'vitest'
import { buildTree } from '../figma-sync/generate-token-exports.mjs'

const toks = [
  { name: 'color-grey-8', value: '#595959', scope: 'dark', section: 'Neutrals' },
  { name: 'input-line-border', value: 'var(--color-grey-8)', scope: 'dark', section: 'Input' },
  { name: 'bg-layer1', value: '#141414', scope: 'dark', section: 'Surface' },
  { name: 'bg-layer1', value: '#ffffff', scope: 'light', section: 'Surface' },
]

describe('buildTree', () => {
  it('keeps whole-value var as reference and resolves path', () => {
    const { dtcg } = buildTree(toks)
    expect(dtcg.semantic['input-line-border']).toEqual({ $type: 'color', $value: '{palette.color-grey-8}' })
  })
  it('attaches $extensions modes for light overrides', () => {
    const { dtcg } = buildTree(toks)
    expect(dtcg.semantic['bg-layer1']).toEqual({
      $type: 'color', $value: '#141414',
      $extensions: { 'tvu.mode': { dark: '#141414', light: '#ffffff' } },
    })
  })
  it('throws on unknown reference target', () => {
    expect(() => buildTree([{ name: 'x', value: 'var(--nope)', scope: 'dark', section: 'Input' }])).toThrow(/nope/)
  })
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-tree.test.ts`
Expected: FAIL — `buildTree` is not a function.

- [ ] **Step 3: Write minimal implementation**

```js
// append to figma-sync/generate-token-exports.mjs
export function buildTree(tokens) {
  const dark = tokens.filter(t => t.scope === 'dark')
  const light = new Map(tokens.filter(t => t.scope === 'light').map(t => [t.name, t.value]))
  const nameToPath = new Map()
  for (const t of dark) {
    const { layer } = classify(t)
    nameToPath.set(t.name, `${layer}.${t.name}`)
  }
  const resolveVal = (value) => {
    const m = value.match(WHOLE_VAR)
    if (!m) return { literal: value }
    const target = nameToPath.get(m[1])
    if (!target) throw new Error(`token-export: unresolved reference var(--${m[1]})`)
    return { ref: `{${target}}` }
  }
  const dtcg = { palette: {}, semantic: {}, nonColor: {} }
  for (const t of dark) {
    const { layer, $type } = classify(t)
    if ($type === 'other') console.warn(`[token-export] WARN unknown $type for --${t.name}`)
    const rv = resolveVal(t.value)
    const node = { $type, $value: rv.ref ?? rv.literal }
    if (light.has(t.name)) {
      const lv = resolveVal(light.get(t.name))
      node.$extensions = { 'tvu.mode': { dark: rv.ref ?? rv.literal, light: lv.ref ?? lv.literal } }
    }
    dtcg[layer][t.name] = node
  }
  return { dtcg, nameToPath }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run tests/token-exports-tree.test.ts`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add figma-sync/generate-token-exports.mjs tests/token-exports-tree.test.ts
git commit -m "feat(tokens): buildTree with reference resolution + \$extensions modes"
```

---

### Task 4: Emitters + `main()` (JSON & TS writers)

**Files:**
- Modify: `figma-sync/generate-token-exports.mjs` (add `emitJson`, `emitTs`, `main`)
- Test: `tests/token-exports-emit.test.ts`

**Interfaces:**
- Consumes: `buildTree`.
- Produces: `export function emitJson(dtcg): string`；`export function emitTs(dtcg): string`（含 `export const tokens`, `export const tokensLight`, `export type TokenName`）；`export function main()` 读 `src/tokens/variables.css` 写 `dist/tokens/tokens.json` + `tokens.ts`。

- [ ] **Step 1: Write the failing test**

```ts
import { describe, it, expect } from 'vitest'
import { buildTree, emitJson, emitTs } from '../figma-sync/generate-token-exports.mjs'

const toks = [
  { name: 'color-grey-8', value: '#595959', scope: 'dark', section: 'Neutrals' },
  { name: 'bg-layer1', value: '#141414', scope: 'dark', section: 'Surface' },
  { name: 'bg-layer1', value: '#ffffff', scope: 'light', section: 'Surface' },
]

describe('emit', () => {
  const { dtcg } = buildTree(toks)
  it('emitJson is valid parseable DTCG', () => {
    const parsed = JSON.parse(emitJson(dtcg))
    expect(parsed.palette['color-grey-8'].$value).toBe('#595959')
  })
  it('emitTs exposes tokens, tokensLight, TokenName', () => {
    const ts = emitTs(dtcg)
    expect(ts).toContain("'color-grey-8': '#595959'")
    expect(ts).toContain('export const tokensLight')
    expect(ts).toContain("export type TokenName =")
    expect(ts).toContain("'bg-layer1'")
  })
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-emit.test.ts`
Expected: FAIL — `emitJson`/`emitTs` not functions.

- [ ] **Step 3: Write minimal implementation**

```js
// append to figma-sync/generate-token-exports.mjs
export function emitJson(dtcg) {
  return JSON.stringify(dtcg, null, 2) + '\n'
}

// TS 侧一律 resolved（引用给最终值）；这里用 dark $value（若是 ref 保留字符串则跳过 resolve —— ref 已经是 {path}，TS 侧应解析回值）
function resolvedValue(node, dtcg) {
  const v = node.$value
  const m = typeof v === 'string' && v.match(/^\{(.+)\}$/)
  if (!m) return v
  const [layer, name] = m[1].split('.')
  return dtcg[layer]?.[name]?.$value ?? v
}
export function emitTs(dtcg) {
  const layers = ['palette', 'semantic', 'nonColor']
  const names = []
  const body = layers.map(layer => {
    const entries = Object.entries(dtcg[layer]).map(([name, node]) => {
      names.push(name)
      return `    '${name}': '${resolvedValue(node, dtcg)}',`
    }).join('\n')
    return `  ${layer}: {\n${entries}\n  },`
  }).join('\n')
  const lightEntries = layers.flatMap(layer =>
    Object.entries(dtcg[layer])
      .filter(([, n]) => n.$extensions?.['tvu.mode'])
      .map(([name, n]) => `  '${name}': '${n.$extensions['tvu.mode'].light}',`)
  ).join('\n')
  const nameUnion = names.map(n => `'${n}'`).join(' | ')
  return `// AUTO-GENERATED by figma-sync/generate-token-exports.mjs — DO NOT EDIT.
export const tokens = {
${body}
} as const

export const tokensLight: Record<string, string> = {
${lightEntries}
}

export type TokenName = ${nameUnion}
`
}

export function main() {
  const here = dirname(fileURLToPath(import.meta.url))
  const cssPath = join(here, '..', 'src', 'tokens', 'variables.css')
  const outDir = join(here, '..', 'dist', 'tokens')
  const { dtcg } = buildTree(parseCss(readFileSync(cssPath, 'utf8')))
  mkdirSync(outDir, { recursive: true })
  writeFileSync(join(outDir, 'tokens.json'), emitJson(dtcg))
  writeFileSync(join(outDir, 'tokens.ts'), emitTs(dtcg))
  console.log('[token-export] wrote dist/tokens/{tokens.json,tokens.ts}')
}

if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
  try { main() } catch (e) { console.error(e.message); process.exit(1) }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run tests/token-exports-emit.test.ts`
Expected: PASS

- [ ] **Step 5: Smoke-run the emitter against real CSS**

Run: `node figma-sync/generate-token-exports.mjs && node -e "const t=require('./dist/tokens/tokens.json'); console.log(Object.keys(t.palette).length, Object.keys(t.semantic).length, Object.keys(t.nonColor).length)"`
Expected: 三个非零计数打印，无 unresolved-reference throw。

- [ ] **Step 6: Commit**

```bash
git add figma-sync/generate-token-exports.mjs tests/token-exports-emit.test.ts
git commit -m "feat(tokens): emitJson/emitTs + main() writing dist/tokens"
```

---

### Task 5: Build wiring + package.json exports + generate script

**Files:**
- Modify: `package.json` (scripts + exports)
- Test: `tests/token-exports-package.test.ts`

**Interfaces:**
- Consumes: `main()` from Task 4.
- Produces: `generate:token-exports` script；`./tokens` + `./tokens/js` exports；`build` 在 `vite build` 之后调 emitter。

- [ ] **Step 1: Write the failing test**

```ts
import { describe, it, expect } from 'vitest'
import pkg from '../package.json'

describe('package.json token export wiring', () => {
  it('exposes ./tokens exports', () => {
    expect(pkg.exports['./tokens']).toBe('./dist/tokens/tokens.json')
    expect(pkg.exports['./tokens/js'].types).toBe('./dist/tokens/tokens.d.ts')
  })
  it('build runs emitter after vite build', () => {
    expect(pkg.scripts.build).toContain('generate-token-exports.mjs')
    const bi = pkg.scripts.build.indexOf('vite build')
    const ei = pkg.scripts.build.indexOf('generate-token-exports.mjs')
    expect(ei).toBeGreaterThan(bi)
  })
  it('has generate:token-exports script', () => {
    expect(pkg.scripts['generate:token-exports']).toContain('generate-token-exports.mjs')
  })
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-package.test.ts`
Expected: FAIL — exports/scripts absent.

- [ ] **Step 3: Edit package.json**

`scripts` 加：
```json
"generate:token-exports": "node figma-sync/generate-token-exports.mjs",
```
`build` 改为（在 `vite build` 后插 emitter，因 vite `emptyOutDir` 会清 dist，必须后置）：
```json
"build": "vue-tsc --noEmit && vite build && node figma-sync/generate-token-exports.mjs && node figma-sync/build-icon-dist.mjs && pnpm build:playground",
```
`exports` 加：
```json
"./tokens": "./dist/tokens/tokens.json",
"./tokens/js": { "types": "./dist/tokens/tokens.d.ts", "import": "./dist/tokens/tokens.js" }
```
> `files` 已含 `"dist"` → `dist/tokens` 自动纳入，无需改。

- [ ] **Step 4: Run test + verify TS compiles to js/d.ts**

Run: `pnpm vitest run tests/token-exports-package.test.ts`
Expected: PASS

> ⚠️ `./tokens/js` 指向 `tokens.js` + `tokens.d.ts`，但 emitter 只写 `tokens.ts`。在此 step 决定编译方式：build 中 `vue-tsc` 是 `--noEmit`，不产出。**在 Step 3 的 build 里，emitter 之后追加一步 `tsc dist/tokens/tokens.ts --declaration --emitDeclarationOnly --allowJs` 不可行（.ts 需真编译）。改用**：emitter 直接同时写 `tokens.js`（ESM，`export const` 原样）+ `tokens.d.ts`（类型）——回到 Task 4 `main()` 增写两文件。修正 Task 4 `main()`：额外 `writeFileSync('tokens.js', tsBodyAsJs)` + `writeFileSync('tokens.d.ts', declOnly)`。此处补测：`expect(existsSync('dist/tokens/tokens.js')).toBe(true)` 经 Step 5 smoke 覆盖。

- [ ] **Step 5: Commit**

```bash
git add package.json figma-sync/generate-token-exports.mjs tests/token-exports-package.test.ts
git commit -m "feat(tokens): wire emitter into build + ./tokens exports"
```

---

### Task 6: Drift Gate (`audit-token-exports.mjs`) + prepublishOnly

**Files:**
- Create: `scripts/audit-token-exports.mjs`
- Modify: `package.json` (scripts + prepublishOnly)
- Test: `tests/token-exports-audit.test.ts`

**Interfaces:**
- Consumes: `parseCss`/`buildTree`/`emitJson` from emitter (import, 不复制逻辑)。
- Produces: `audit:token-exports` script；挂进 `prepublishOnly`。gate：重新生成 in-memory JSON，与 `dist/tokens/tokens.json` 深比对；不一致或 dist 缺失 → 非零 exit；reference 目标完整性校验（buildTree 已 throw 覆盖）。

- [ ] **Step 1: Write the failing test**

```ts
import { describe, it, expect } from 'vitest'
import { checkExportsFresh } from '../scripts/audit-token-exports.mjs'

describe('audit-token-exports', () => {
  it('passes when on-disk JSON matches freshly-built', () => {
    const fresh = '{"palette":{"a":{"$type":"color","$value":"#000"}}}'
    expect(checkExportsFresh(fresh, JSON.parse(fresh)).ok).toBe(true)
  })
  it('fails when on-disk drifts from source', () => {
    const built = { palette: { a: { $type: 'color', $value: '#000' } } }
    const stale = { palette: { a: { $type: 'color', $value: '#fff' } } }
    const r = checkExportsFresh(JSON.stringify(built), stale)
    expect(r.ok).toBe(false)
    expect(r.message).toMatch(/drift|mismatch/i)
  })
})
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run tests/token-exports-audit.test.ts`
Expected: FAIL — module/function absent.

- [ ] **Step 3: Write minimal implementation**

```js
// scripts/audit-token-exports.mjs
import { readFileSync, existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { parseCss, buildTree, emitJson } from '../figma-sync/generate-token-exports.mjs'

/** @returns {{ok:boolean,message?:string}} */
export function checkExportsFresh(freshJsonStr, onDiskObj) {
  const fresh = typeof freshJsonStr === 'string' ? JSON.parse(freshJsonStr) : freshJsonStr
  const a = JSON.stringify(fresh), b = JSON.stringify(onDiskObj)
  if (a === b) return { ok: true }
  return { ok: false, message: `token-export drift: dist/tokens/tokens.json 与 variables.css 不一致，请重跑 pnpm generate:token-exports` }
}

function run() {
  const here = dirname(fileURLToPath(import.meta.url))
  const cssPath = join(here, '..', 'src', 'tokens', 'variables.css')
  const jsonPath = join(here, '..', 'dist', 'tokens', 'tokens.json')
  if (!existsSync(jsonPath)) {
    console.error('[audit:token-exports] FAIL dist/tokens/tokens.json 不存在，请先 pnpm build 或 pnpm generate:token-exports')
    process.exit(1)
  }
  const { dtcg } = buildTree(parseCss(readFileSync(cssPath, 'utf8')))
  const r = checkExportsFresh(emitJson(dtcg), JSON.parse(readFileSync(jsonPath, 'utf8')))
  if (!r.ok) { console.error('[audit:token-exports] FAIL', r.message); process.exit(1) }
  console.log('[audit:token-exports] PASS')
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) run()
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run tests/token-exports-audit.test.ts`
Expected: PASS

- [ ] **Step 5: Wire into package.json**

`scripts` 加 `"audit:token-exports": "node scripts/audit-token-exports.mjs"`；`prepublishOnly` 末尾追加 `&& pnpm run audit:token-exports`。

- [ ] **Step 6: Full verification**

Run: `pnpm build && pnpm run audit:token-exports && pnpm run audit:scripts-stdlib && pnpm vitest run tests/token-exports-*.test.ts`
Expected: build 绿、gate PASS、scripts-stdlib PASS（新脚本纯 stdlib）、全测试 PASS。

- [ ] **Step 7: Commit**

```bash
git add scripts/audit-token-exports.mjs package.json tests/token-exports-audit.test.ts
git commit -m "feat(tokens): audit:token-exports drift gate + prepublishOnly"
```

---

### Task 7: Consumer docs + STATUS/backlog closeout

**Files:**
- Modify: `docs/GETTING_STARTED.md` (add token-import usage section)
- Modify: `docs/STATUS.md` (能力×成熟度表 能力1 token 状态；Last updated；Active 计数)
- Modify: `docs/internal/backlog.md` (F55 支柱① 首块进度)

- [ ] **Step 1: Add usage doc**

在 GETTING_STARTED 加：
```md
### 消费设计 token（JSON / TS）

\`\`\`ts
import tokens from '@nancyzeng0210/tvu-design-system/tokens' assert { type: 'json' }
import { tokens as tsTokens, type TokenName } from '@nancyzeng0210/tvu-design-system/tokens/js'
\`\`\`
JSON 为 DTCG 格式（alias 保留 `{path}` reference，双主题在 `$extensions["tvu.mode"]`）；TS 为 resolved dark 值 + `tokensLight` 覆盖。
```

- [ ] **Step 2: Update STATUS 能力×成熟度表**

能力 1 行 token 描述由「token 仅 CSS 变量（无 TS/JSON 出口）」改为「token = CSS + DTCG JSON + TS 出口（`./tokens` / `./tokens/js`）」。更新 Last updated 到当天 + 摘要。

- [ ] **Step 3: Run doc gates**

Run: `pnpm run audit:doc-sync && pnpm run audit:status-consistency && pnpm run audit:doc-de-mirror`
Expected: PASS（未引入字面版本号 / 跨文件漂移）。

- [ ] **Step 4: Commit**

```bash
git add docs/GETTING_STARTED.md docs/STATUS.md docs/internal/backlog.md
git commit -m "docs(tokens): consumer usage + STATUS 能力1 token 出口 shipped (INFRA-F55)"
```

---

## Self-Review

**1. Spec coverage:**
- §3 parse/classify/buildTree/emit 四单元 → Task 1-4 ✅
- §4 DTCG（path/type/alias/modes）→ Task 2-4 ✅
- §5 TS（tokens/tokensLight/TokenName）→ Task 4 ✅
- §6 exports + dist 布局 → Task 5 ✅（`files` 已含 dist，验证过免改）
- §7 audit gate + prepublishOnly → Task 6 ✅
- §8 测试（四单元 + round-trip 抽样）→ Task 1-4 测试 + Task 6 smoke ✅
- §10 交付物 6 项（含消费文档）→ Task 5 wiring + Task 7 docs ✅

**2. Placeholder scan:** Task 5 Step 4 原本埋了 `./tokens/js` 指向 `.js/.d.ts` 但 emitter 只写 `.ts` 的矛盾 → 已在该 step 内标出修正（emitter 同时写 `tokens.js` + `tokens.d.ts`），回填 Task 4 `main()`。**执行 Task 4 时务必按此写三文件（.json/.js/.d.ts），tokens.ts 作可读源保留可选。**

**3. Type consistency:** `RawToken` 字段（name/value/scope/section）跨 Task 1-4 一致；`buildTree` 返回 `{dtcg, nameToPath}` 一致；`checkExportsFresh` 签名 Task 6 一致；`WHOLE_VAR` 正则 Task 2 定义、Task 3 复用（同文件内，无重名冲突）。

**已知执行注意**：Task 4 需最终产出 `.json` + `.js` + `.d.ts` 三文件（Task 5 exports 依赖 `.js`/`.d.ts`）。`.ts` 源可不发。执行者按 Task 5 Step 4 的修正落实。
