# T1a Fix v2 Plan — CSS Indirection + Vue Palette Resolution

> Round 1 plan only. This document defines the next implementation step for
> `figma-sync/audit-component-token-fidelity.mjs`. It does not change code.

## 1. 问题诊断

v1 audit 已经能把 Figma tokenized JSON 和 Vue SFC 静态 CSS 放到同一张表里，但颜色类 finding 仍有 3 个盲区。

### 盲区 1：CSS 间接变量链被当作 token-mismatch

Button 的 code 端实际写法是 `background: var(--btn-bg)`，而 `--btn-bg` 由 `buttonVars` 从 `palette.value.bg` 注入。v1 audit 只看到表层字符串 `var(--btn-bg)`，看不到 `palette.value.bg` 最终会变成 `var(--brand-hover)` / `var(--red)` / `var(--orange)` 等 token。

具体证据：

- `docs/internal/component-token-fidelity-report.md:204`：Figma `--brand-hover` vs code `var(--btn-bg)` 被标 `⚠️ token-mismatch | direct`。
- `docs/internal/component-token-fidelity-report.md:234`：Figma `--red-hover` vs code `var(--btn-bg)` 同样被标 mismatch。
- `src/components/Button/Button.vue:262-335`：`palette = computed(() => specMap[style][color][state])`。
- `src/components/Button/Button.vue:365-368`：`buttonVars` 把 `--btn-bg` / `--btn-color` / `--btn-border` 指到 palette 字段。
- `src/components/Button/Button.vue:457-459`：CSS 消费 `var(--btn-border)` / `var(--btn-bg)` / `var(--btn-color)`。

这类 finding 的问题不是 code 错，而是 audit 没有把 Vue computed palette 和 CSS 自定义属性链连起来。

### 盲区 2：命名不同但 hex 相同无法被识别

Badge 的 canonical wrapper 使用 `--badge-bg` / `--badge-border` 作为语义层变量，再由 `palette.value[props.color]` 解析到真实 token。v1 audit 只看到 `var(--badge-bg)` / `var(--badge-border)`，因此把可解析的间接变量也标成 mismatch。

具体证据：

- `docs/internal/component-token-fidelity-report.md:37`：Figma `--brand` vs code `var(--badge-bg)` 被标 `⚠️ token-mismatch | direct`。
- `docs/internal/component-token-fidelity-report.md:43`：Figma `--brand` vs code `var(--badge-border)` 被标 `⚠️ token-mismatch | direct`。
- `src/canonical/Badge.vue:18-24`：`palette` 把 `Green -> var(--brand)`、`Black -> var(--line-deep)`。
- `src/canonical/Badge.vue:38-43`：`badgeVars` 把 `--badge-bg` / `--badge-border` 指到 `palette.value[props.color]`。

v2 需要区分两种结果：解析后 token 一致时标 `✅ token-match-via-indirection`；解析后 token 名不同但 hex 一样时标 `⚠️ hex-equal-different-token`，这是命名债，不是视觉 drift。

### 盲区 3：纯 CSS 自定义属性链也被当作 direct mismatch

Input / Tooltip 不是 Vue computed palette，但它们也有 CSS 自定义属性链：例如 `background: var(--input-filled-bg)` 或 `color: var(--tooltip-text)`，真实值在 theme selector 里定义。

具体证据：

- `docs/internal/component-token-fidelity-report.md:531`：Input Figma `--line-deep` vs code `var(--input-filled-bg)` 被标 `⚠️ token-mismatch | direct`。
- `docs/internal/component-token-fidelity-report.md:1457`：Tooltip Figma `--color-grey-2` vs code `var(--tooltip-text)` 被标 `⚠️ token-mismatch | direct`。
- `src/components/Input/Input.vue:68-70`：注释明确 `--input-filled-bg` 会按 dark/light 解析。
- `src/components/Tooltip/Tooltip.vue:94-102`：`.tooltip-box--light/dark` 分别设置 `--tooltip-bg` / `--tooltip-text` / `--tooltip-border`。

v2 不应只实现 Vue AST。它还需要先补静态 CSS custom property resolver；Vue palette resolver 是第二层。

## 2. 受影响组件清单（grep 全库）

本轮按 prompt 跑了两条 grep：

```bash
grep -rln 'computed.*=>.*({' src/components/ | head -50
grep -rln "'--[a-z]*-bg'\\|'--[a-z]*-color'" src/components/
```

结果：

```text
src/components/FormItem/FormItem.vue
src/components/Progress/Progress.vue
src/components/Alert/Alert.vue
src/components/Slider/Slider.vue
src/components/Button/Button.vue

src/components/PromptMessage/PromptMessage.vue
src/components/Button/Button.vue
```

这个 grep 只覆盖 `src/components/`，没有覆盖 `src/canonical/`，因此漏掉了 canonical Badge。补充 `rg` 后确认还有：

```text
src/canonical/Badge.vue
```

严格的 "Vue computed palette + inline indirect vars" 清单如下：

| 组件 | palette 文件路径 | computed 函数行号 | 涉及的间接变量名 |
|---|---|---:|---|
| Button | `src/components/Button/Button.vue` | `palette` 262, `buttonVars` 365 | `--btn-bg`, `--btn-color`, `--btn-border`, `--btn-border-width` |
| PromptMessage | `src/components/PromptMessage/PromptMessage.vue` | `palette` 67, inline `:style` 75 | `--prompt-bg`, `--prompt-text` |
| Badge | `src/canonical/Badge.vue` | `palette` 18, `badgeVars` 38 | `--badge-accent`, `--badge-bg`, `--badge-border` |
| Progress | `src/components/Progress/Progress.vue` | `fillColor` 25, `progressVars` 36 | `--progress-fill-color`, `--progress-track-height` |
| FormItem | `src/components/FormItem/FormItem.vue` | `rootStyle` 42 | `--fi-*` layout/style vars, not primarily color palette |

Pure CSS custom property chain, no Vue palette required:

| 组件 | 文件路径 | 定义/消费行号 | 涉及的间接变量名 |
|---|---|---:|---|
| Input | `src/components/Input/Input.vue` | comment 68, consume 70 | `--input-filled-bg` |
| Tooltip | `src/components/Tooltip/Tooltip.vue` | consume 76-78, define 94-102 | `--tooltip-bg`, `--tooltip-text`, `--tooltip-border` |
| Slider | `src/components/Slider/Slider.vue` | computed trackStyle 38, CSS 83-102 | `--slider-fill-color`, `--slider-track-bg`, `--slider-value-color` |
| Checkbox | `src/components/Checkbox/Checkbox.vue` | CSS 59-112 | `--control-disabled-bg`, `--control-disabled-border`, etc. |
| Radio | `src/components/Radio/Radio.vue` | CSS 44-84 | `--control-disabled-bg`, `--radio-disabled-selected-bg`, etc. |
| Switch | `src/components/Switch/Switch.vue` | CSS 46, 60 | `--line-border` direct chain |
| Select | `src/components/Select/Select.vue` | CSS 53 | `--line-border` direct chain; canonical wrapper has state mapping |

Round 2 impact scope should therefore be 10 components for color-token indirection: Button, PromptMessage, Badge, Progress, FormItem, Input, Tooltip, Slider, Checkbox, Radio. Switch and Select are lower-priority direct/custom-property chain cases and should be covered opportunistically by the generic CSS resolver.

### 2.X prop-axis 候选（vue-prop 实现层级）

补遗 grep:

```bash
grep -rln 'defineProps' src/canonical/ src/components/ | xargs grep -lE "type\s+\w+\s*=\s*'[^']+'\s*\|\s*'"
```

该 grep 命中面很广。Round 2 不应把所有 union prop 都自动视为 Figma axis；应优先纳入已经能在 `canonical-components.json` 或 `axis-implementation-map.md` 找到 Figma component set 的候选。

| 组件 | 文件 | prop 名 | 枚举值 | figma 对应 axis（推测） |
|---|---|---|---|---|
| SelectBoxLine | `src/canonical/SelectBoxLine.vue` | `feature` | `default | time | date` | Select `feature` |
| SelectBoxFilled | `src/canonical/SelectBoxFilled.vue` | `feature` | `default | time | date` | Select `feature` |
| SelectBoxBase | `src/canonical/SelectBoxBase.vue` | `feature` | `default | time | date` | Select `feature` |
| SelectBoxBase | `src/canonical/SelectBoxBase.vue` | `status` | `default | normal | multi select` | Select `status` |
| SelectBoxBase | `src/canonical/SelectBoxBase.vue` | `ux` | `default | error | hover | click | editable` | Select `UX` |
| FormItem | `src/components/FormItem/FormItem.vue` | `theme` | `Dark | Light` | Form Item `Theme` |
| FormItem | `src/components/FormItem/FormItem.vue` | `status` | `Error | Normal` | Form Item `Status` |
| FormItem | `src/components/FormItem/FormItem.vue` | `layout` | `1 line | 1 line & Right | 2 lines` | Form Item `Layout` |
| Progress | `src/components/Progress/Progress.vue` | `status` | `default | success | error | warning` | Progress `status` |
| Progress | `src/components/Progress/Progress.vue` | `theme` | `dark | light` | Progress `theme` |
| Tooltip | `src/canonical/Tooltip.vue` | `darkTheme` | `on | off` | Tooltips `dark theme` |
| StepItem | `src/canonical/StepItem.vue` | `status/style/direction` | union props | Step/Item axes, exact mapping needs owner review |
| TabItem | `src/canonical/TabItem.vue` | `property1/property2/type` | union props | Tab/Item axes, exact mapping needs owner review |
| BreadcrumbItem | `src/canonical/BreadcrumbItem.vue` | `state` | union prop | Breadcrumb/Item `state` |

### 2.Y 拓扑映射候选（cross-component-topology 实现层级）

| code 端实体组合 | figma 端推测组件集 | 证据 |
|---|---|---|
| SelectBoxLine + SelectBoxFilled + DateTimePage | Select / select box filled+line, `feature=date/time` | Figma set IDs `1436:32816`, `1804:26921`; `divergences.md` 已登记 DateTime ↔ Select.feature=date/time |
| Steps + StepItem + canonical StepItem | Step/Item | Figma set ID `4943:7310`; code has container `src/components/Steps/Steps.vue` + item `src/components/Steps/StepItem.vue` |
| Tab + TabList + TabItem + canonical Tab* | Tab List + Tab/Item | Figma set IDs `4452:7148`, `4265:16082`; code exposes `Tab`, `TabList`, `TabItem` multi-file topology |
| Breadcrumb + BreadcrumbItem + canonical BreadcrumbItem | Breadcrumb/Item | Figma set ID `5003:7495`; code adds container wrapper around item-level Figma source |
| SelectPage + DateTimePage docs split | Select feature families | `playground/docs/figmaFirstRegistry.ts` lists Select and DateTime both using `SelectBoxLine` / `SelectBoxFilled`; topology is docs-layer N:1 |

## 3. 新 verdict 类型 + evidenceLevel 映射

Add these 5 verdicts without deleting the old verdicts immediately. v2 should emit the new verdict only when it has enough evidence; otherwise keep the old verdict or use the failure verdict.

| New verdict | Purpose | evidenceLevel | evidenceSource |
|---|---|---|---|
| `✅ token-match-via-indirection` | A 类：code 表层是 indirect var, resolved token equals figma cssVar | `direct` | `["figma-tokenized-json", "vue-static-css", "vue-computed-palette"]` or `["figma-tokenized-json", "vue-static-css", "css-custom-property-chain"]` |
| `⚠️ hex-equal-different-token` | B 类：resolved code cssVar != figma cssVar, but resolved hex equal | `direct` | `["figma-tokenized-json", "variables-json", "variables-css", "resolved-token-hex-compare"]` |
| `⚠️ true-token-mismatch` | C 类：both sides resolve to tokens, token and hex differ | `direct` | `["figma-tokenized-json", "variables-json", "variables-css", "resolved-token-hex-compare"]` |
| `⚠️ true-visual-drift` | C 类：both sides are literal/resolved hex and hex differ | `direct` | `["figma-tokenized-json", "vue-static-css", "resolved-hex-compare"]` |
| `❌ palette-resolution-failed` | Tool boundary: indirect var found, but resolver cannot prove final token | `heuristic` | `["vue-static-css", "vue-computed-palette", "resolution-failed"]` |

Supplement rows from `axis-implementation-map.md` §5:

| New verdict | Purpose | evidenceLevel | evidenceSource |
|---|---|---|---|
| `✅ token-match-via-prop` | prop-axis 实现验证通过 | `direct` | `[figma-tokenized-json, vue-static-css, vue-defineProps]` |
| `✅ token-match-via-topology` | 跨组件拓扑映射验证通过 | `direct` | `[figma-tokenized-json, axis-implementation-map.md, multi-file-resolution]` |

Trigger pseudocode:

```js
if codeValue is var(--semantic-indirect) {
  resolved = resolveCodeVar(varName, axisTuple, codeModel)
  if resolved.failed return palette-resolution-failed
  if resolved.cssVar === figma.cssVar return token-match-via-indirection
  if hex(resolved.cssVar) === hex(figma.cssVar) return hex-equal-different-token
  return true-token-mismatch
}

if codeValue is var(--direct-token) && figma.cssVar exists {
  if codeVar === figma.cssVar return token-match
  if hex(codeVar) === hex(figma.cssVar) return hex-equal-different-token
  return true-token-mismatch
}

if codeValue has hex literal && figma.hex exists {
  if codeHex === figmaHex return literal-but-equal
  return true-visual-drift
}

if axisImplementationMap.find(match => match.codeImplementationLayer === 'vue-prop') {
  // Verify codeFile defineProps contains the mapped prop and enum includes figmaValue.
  if (verifyVueProp(match, axisTuple).success) return token-match-via-prop
  return implementation-mismatch
}

if axisImplementationMap.find(match => match.codeImplementationLayer === 'cross-component-topology') {
  // Recursively inspect codeAnchor/codeFile list; each file delegates to its layer-specific verifier.
  if (verifyCrossComponentTopology(match, axisTuple).success) return token-match-via-topology
  return implementation-mismatch
}
```

Old verdict migration table:

| v1 verdict | v2 target |
|---|---|
| `✅ token-match` | Keep as-is for direct `var(--X) === figma --X` |
| `⚠️ token-mismatch | direct` with code `var(--btn-bg)` / `var(--badge-bg)` / `var(--tooltip-text)` | Try resolver; mostly `✅ token-match-via-indirection`, `⚠️ hex-equal-different-token`, or `⚠️ true-token-mismatch` |
| `⚠️ literal-but-equal` | Keep as-is unless literal came from resolved var; then use `hex-equal-different-token` |
| `⚠️ visual-drift` for color | Rename to `⚠️ true-visual-drift` after hex resolution |
| `⚠️ visual-drift` for dimensions | Keep old verdict + `heuristic`; do not migrate in this v2 |
| `⚠️ axis-branch-missing` | Keep as-is |
| `❌ figma-data-missing` | Keep as-is |
| `❌ token-broken` | Keep as-is |

## 4. Vue AST 解析方案

Use `@vue/compiler-sfc`:

- `pnpm list @vue/compiler-sfc` was read-only and produced no top-level output.
- `pnpm-lock.yaml` contains `@vue/compiler-sfc@3.5.32`, so the package is available transitively through Vue tooling. Round 2 should confirm `node -e "require.resolve('@vue/compiler-sfc')"` before implementation. If unavailable, do not install in-place unless the Round 2 prompt permits it.

Parsing steps:

```js
import { parse, compileScript } from '@vue/compiler-sfc'
import { parse as babelParse } from '@babel/parser'
import traverse from '@babel/traverse'

function extractPalette(vueFilePath) {
  const source = readFileSync(vueFilePath, 'utf8')
  const { descriptor } = parse(source)
  const script = compileScript(descriptor, { id: hash(vueFilePath) })
  const ast = babelParse(script.content, {
    sourceType: 'module',
    plugins: ['typescript'],
  })

  return {
    computedVars: findComputedObjectExpressions(ast),
    styleBindings: findTemplateStyleBindings(descriptor.template),
    constMaps: findConstObjectMaps(ast),
  }
}
```

Extraction contract:

```ts
type PaletteExtraction = {
  filePath: string
  cssVars: Record<string, ExpressionRef>
  computedMaps: Record<string, ComputedRefModel>
  constMaps: Record<string, ObjectMapModel>
  failures: Array<{ reason: string; loc?: string }>
}

extractPalette(vueFilePath) -> PaletteExtraction
```

Concrete cases to support in Round 2:

- `const buttonVars = computed<Record<string, string>>(() => ({ '--btn-bg': palette.value.bg }))`
- `const badgeVars = computed<Record<string, string>>(() => ({ '--badge-bg': props.tag === 'Filled' ? palette.value[props.color] : 'transparent' }))`
- Inline template style object:
  `:style="{ '--prompt-bg': palette.background, '--prompt-text': palette.text }"`
- Plain object maps:
  `const paletteMap = { info: { background: 'var(--blue-bg)' } }`
- Computed lookup:
  `const palette = computed(() => paletteMap[props.status])`

Fallback strategy:

- `ref(...)`, `reactive(...)`, and direct const object are parsed as object maps if the initializer is static.
- Imported constants are marked unresolved in Round 2 unless the import is from the same file; finding becomes `❌ palette-resolution-failed`.
- Function calls are not executed. They become `❌ palette-resolution-failed`.

## 5. Palette 求值算法

The resolver should evaluate a small, safe subset of JS AST. It must not eval arbitrary code.

Input/output contract:

```ts
resolvePaletteVar(
  cssVar: '--btn-bg',
  axisTuple: { color: 'green', status: 'hover', style: 'filling', theme: 'dark', size: 'L' },
  extraction: PaletteExtraction
) -> { cssVar?: '--brand-hover'; literal?: 'transparent' | '#fff'; failed?: string }
```

Algorithm:

```js
function resolvePaletteVar(cssVar, axisTuple, extraction) {
  expr = extraction.cssVars[cssVar]
  return resolveExpression(expr, axisTuple, extraction)
}

function resolveExpression(expr, tuple, model) {
  switch expr.type:
    case StringLiteral:
      return parseTokenOrLiteral(expr.value)
    case ConditionalExpression:
      return evalTest(expr.test, tuple)
        ? resolveExpression(expr.consequent, tuple, model)
        : resolveExpression(expr.alternate, tuple, model)
    case MemberExpression:
      return resolveMember(expr, tuple, model)
    case Identifier:
      return resolveIdentifier(expr.name, tuple, model)
    default:
      return failed('unsupported expression')
}
```

Member resolution examples:

- `palette.value.bg`:
  1. Resolve `palette`.
  2. If `palette = computed(() => specMap[style][color][state])`, resolve `style`, `color`, `state`.
  3. `style` and `color` come from `resolvedCanonical.value`.
  4. Map `resolvedCanonical.value.color` to `axisTuple.color`.
  5. `state` is derived by `status === 'hover' ? 'hover' : status === 'disable' || status === 'loading' ? 'disable' : 'default'`.
  6. Return `specMap[style][color][state].bg`.

- `palette.value[props.color]`:
  1. Resolve `props.color` from `axisTuple.color`.
  2. Lookup the static object map.

- `palette.background`:
  1. Resolve `palette = computed(() => paletteMap[props.status])`.
  2. Lookup `paletteMap[axisTuple.status].background`.

Exact match vs enumeration:

- Prefer exact matching for the current axis tuple.
- Do not globally enumerate every prop combination in Round 2.
- If an axis tuple is missing a required key, mark `❌ palette-resolution-failed`.

Tool boundary:

- Dynamic calls like `getColor(props)` fail.
- Runtime-only values like user-provided `color` fail.
- CSS `color-mix()` fails unless its inner var resolves and the finding is explicitly heuristic.

## 6. 双端 hex 比对算法

Resolution order:

1. Figma side:
   - Primary: `finding.figma.cssVar`.
   - Resolve cssVar to hex using `figma-data/normalized/variables.json` by `cssVar`.
   - Fallback: `finding.figma.hex`.

2. Code side:
   - If code is direct `var(--X)`, resolve `--X` through CSS custom property chain.
   - If code is indirect `var(--btn-bg)`, first try Vue palette resolver, then CSS chain resolver.
   - Resolve final token through `src/tokens/variables.css`.
   - If token is not in `variables.css`, fallback to `figma-data/normalized/variables.json`.

3. Compare:

```js
if code.cssVar === figma.cssVar:
  token-match-via-indirection
else if code.hex && figma.hex && code.hex.toLowerCase() === figma.hex.toLowerCase():
  hex-equal-different-token
else if code.cssVar || figma.cssVar:
  true-token-mismatch
else:
  true-visual-drift
```

Variables source preference:

- `src/tokens/variables.css` should be code truth.
- `figma-data/normalized/variables.json` should be Figma truth and fallback for external variable IDs.
- `src/styles/variables.css` is referenced by the older prompt, but current repo has `src/tokens/variables.css`; resolver should support both candidates like v1 already does.

## 6.5 audit 读真源算法的实现伪代码

This section translates `src/design-system/translation/axis-implementation-map.md` §5 into implementation contracts. Round 2 should implement these as helpers inside the audit script or a local module imported by it.

```js
function auditAxisValue(figmaComponent, figmaAxis, figmaValue, axisTuple) {
  const instances = parseAxisImplementationMap()
  const match = instances.find(instance =>
    instance.figmaComponent === figmaComponent &&
    instance.figmaAxis === figmaAxis &&
    (instance.figmaValue === figmaValue || instance.figmaValue === '*')
  )

  if (match) {
    switch (match.codeImplementationLayer) {
      case 'css-class':
        return verifyCssClass(match, axisTuple)
      case 'vue-prop':
        return verifyVueProp(match, axisTuple)
      case 'vue-computed-palette':
        return verifyComputedPalette(match, axisTuple)
      case 'css-custom-property-chain':
        return verifyCssCustomPropertyChain(match, axisTuple)
      case 'cross-component-topology':
        return verifyCrossComponentTopology(match, axisTuple)
      default:
        return {
          success: false,
          verdict: '⚠️ implementation-mismatch',
          evidenceLevel: 'direct',
          evidenceSource: ['axis-implementation-map.md'],
          reason: `Unknown codeImplementationLayer: ${match.codeImplementationLayer}`,
        }
    }
  }

  const fallbackResult = verifyCssClass({
    figmaComponent,
    figmaAxis,
    figmaValue,
    codeImplementationLayer: 'css-class',
  }, axisTuple)

  if (fallbackResult.success) return fallbackResult

  return {
    success: false,
    verdict: '⚠️ unmapped-axis-value',
    evidenceLevel: 'heuristic',
    evidenceSource: ['figma-tokenized-json', 'axis-implementation-map.md'],
    reason: `No axis-implementation-map instance for ${figmaComponent}.${figmaAxis}=${figmaValue}`,
  }
}
```

Verifier contracts:

```ts
type VerifyInput = {
  figmaComponent: string
  figmaComponentSetIds: string[]
  figmaAxis: string
  figmaValue: string | '*'
  codeImplementationLayer: string
  codeFile: string | string[]
  codeAnchor: string
  verifyHint?: string
  notes?: string
}

type VerifyResult = {
  success: boolean
  verdict:
    | '✅ token-match-via-css-class'
    | '✅ token-match-via-prop'
    | '✅ token-match-via-indirection'
    | '✅ token-match-via-custom-property-chain'
    | '✅ token-match-via-topology'
    | '⚠️ implementation-mismatch'
    | '⚠️ unmapped-axis-value'
  evidenceLevel: 'direct' | 'heuristic' | 'semantic-inference'
  evidenceSource: string[]
  code?: { filePath?: string; selector?: string; prop?: string; value?: string; line?: number }
  reason?: string
}
```

`verifyCssClass(match, axisTuple)`:

- Input: one instance plus normalized axis tuple.
- Behavior: search `match.codeFile` for selector/class matching `figmaAxis/figmaValue`, using v1 selector logic.
- Success: return `✅ token-match-via-css-class` with `[figma-tokenized-json, vue-static-css]`.
- Failure: return `⚠️ implementation-mismatch` with reason `css-class-not-found`.

`verifyVueProp(match, axisTuple)`:

- Input: instance with `codeImplementationLayer='vue-prop'`.
- Behavior: parse `defineProps` in each `codeFile`; verify prop exists and literal union includes `figmaValue`, or `figmaValue='*'` and prop exists.
- Success: return `✅ token-match-via-prop` with `[figma-tokenized-json, vue-static-css, vue-defineProps]`.
- Failure: return `⚠️ implementation-mismatch` with missing prop/value evidence.

`verifyComputedPalette(match, axisTuple)`:

- Input: instance with computed palette anchors.
- Behavior: call `extractPalette(codeFile)`, then `resolvePaletteVar()` for relevant indirect vars and current axis tuple.
- Success: return `✅ token-match-via-indirection` when resolved code token matches figma token/hex.
- Failure: return `⚠️ implementation-mismatch` for resolved-but-wrong token, or `❌ palette-resolution-failed` if extraction cannot prove a value.

`verifyCssCustomPropertyChain(match, axisTuple)`:

- Input: instance with selector/custom-property chain anchors.
- Behavior: build custom property map from matching selectors, resolve `var(--x)` chains to final token/literal.
- Success: return `✅ token-match-via-custom-property-chain`.
- Failure: return `⚠️ implementation-mismatch` for resolved mismatch; return `⚠️ unmapped-axis-value` if source variable has no definition in reachable selectors.

`verifyCrossComponentTopology(match, axisTuple)`:

- Input: instance with `codeFile` array.
- Behavior: recursively inspect each file listed in `codeFile`; delegate to `verifyVueProp`, `verifyCssClass`, `verifyComputedPalette`, or `verifyCssCustomPropertyChain` based on anchors and component role.
- Success: if any required topology branch validates, return `✅ token-match-via-topology` with `[figma-tokenized-json, axis-implementation-map.md, multi-file-resolution]`.
- Failure: return `⚠️ implementation-mismatch` with list of failed child verifiers.

## 7. 边界 case 列表

1. `palette` is `ref(...)` instead of `computed(...)`.
   - Parse if initializer is a static object; otherwise `palette-resolution-failed`.

2. `palette` is direct const object.
   - Parse as `constMaps[name]`; support string literals and nested object literals.

3. palette references constants from another module.
   - Round 2 does not chase imports; mark failed with `imported-constant-unsupported`.

4. nested ternary, e.g. `color === 'red' ? (status === 'hover' ? ... ) : ...`.
   - Support `===`, `!==`, `||`, `&&`, and nested `ConditionalExpression`.

5. switch / Map lookup.
   - Support object-map lookup first. Switch support can be a separate helper if encountered; otherwise fail explicitly.

6. function call `getColor(props)`.
   - Do not eval. Mark `palette-resolution-failed`.

7. axis tuple not covered, e.g. Figma has `color=cyan`.
   - Mark `palette-resolution-failed` with missing branch evidence.

8. same indirect variable has different fallback under different selectors.
   - Selector-specific CSS chain wins first, component base fallback second, Vue inline style third if bound on root.

9. code value is `transparent`.
   - Treat as literal semantic value. If Figma fill is transparent/opacity 0, ignore or mark match; otherwise true mismatch.

10. CSS fallback syntax `var(--brand, #2fb54e)`.
   - Resolve primary var first. If undefined, use fallback literal.

11. `rgb(from var(--line-border) ...)` / `color-mix(...)`.
   - Mark heuristic unless the compared finding is not a direct color fill.

12. dimensions still compare as heuristic.
   - Do not include dimensions in token indirection reclassification in this v2.

## 8. 自验 sanity check（带预期数字）

Use v1 baseline:

- Total findings: `36089`.
- Button findings: `31977`.
- Button `⚠️ token-mismatch`: `6377`.
- Button `⚠️ visual-drift`: `9600`.
- Button `✅ token-match`: `3200`.

Expected v2 checks:

```bash
node figma-sync/audit-component-token-fidelity.mjs
node -e "const j=require('./figma-data/normalized/component-token-fidelity.audit.json'); const b=j.components.find(c=>c.componentName==='Button'); const fs=b.variants.flatMap(v=>v.findings); console.log(fs.filter(f=>f.verdict==='✅ token-match-via-indirection').length)"
```

Expected:

- Button `✅ token-match-via-indirection` should be `> 2500`.
- Button `⚠️ true-token-mismatch` should be `< 1000`; if higher, inspect resolver misses before trusting it.
- Button `❌ palette-resolution-failed` should be `< 500`; higher means the AST subset is too weak.
- Badge `✅ token-match-via-indirection` should be `>= 10` because `--badge-bg` / `--badge-border` should resolve.
- Tooltip `⚠️ token-mismatch` should drop or split into `hex-equal-different-token` / `true-token-mismatch`; exact count needs main session review because current `--tooltip-text` maps to semantic text token rather than primitive Figma grey token.
- Total finding count should remain exactly `36089` unless Round 2 also changes duplicate generation. This v2 should redistribute verdicts, not add/remove findings.
- `direct + heuristic + semantic-inference` should still sum to `36089`.
- Recommended action for Button should change from generic "复核组件 CSS token" to "多数经 indirection 解析；仅处理 true-token-mismatch / palette-resolution-failed".

Grep checks:

```bash
grep -c 'token-match-via-indirection' figma-data/normalized/component-token-fidelity.audit.json
grep -c 'hex-equal-different-token' figma-data/normalized/component-token-fidelity.audit.json
grep -c 'true-token-mismatch' figma-data/normalized/component-token-fidelity.audit.json
grep -c 'palette-resolution-failed' figma-data/normalized/component-token-fidelity.audit.json
grep -n 'Component: Button' docs/internal/component-token-fidelity-report.md
```

If exact counts are disputed, the Round 2 implementation should print per-component verdict deltas against v1 so the main session can review.

## 9. 工程量分解 + 风险点

| sub-task | 估时 | 风险 | 降级方案 |
|---|---:|---|---|
| Confirm `@vue/compiler-sfc` runtime availability | 5min | low | If not resolvable, use regex extraction for Round 2 and leave AST for Round 3 |
| Add token → hex resolver for `variables.json` + `variables.css` | 30min | low | Keep old verdict if hex missing |
| Add CSS custom property chain resolver | 45min | medium | Resolve only same-file style rules and same selector/base selector |
| Add `extractPalette(vueFilePath)` for computed object / inline style | 1h | medium | Support Button/Badge/PromptMessage first |
| Add safe expression evaluator subset | 1.5h | high | If unsupported expression appears, emit `palette-resolution-failed` instead of guessing |
| Integrate reclassification into `classifyColorFinding` | 45min | medium | Run only when existing verdict is `token-mismatch` and code has `var(--*)` |
| Add JSON/report schema for new verdicts | 30min | low | Keep old summary keys plus new keys |
| Add self-check script snippets / console deltas | 30min | low | Manual node one-liners if no test harness |
| Review Button/Badge/Tooltip samples manually | 45min | medium | If Button too noisy, gate v2 to Button only and leave other components old verdict |

Total estimate: 5.0 hours.

High-risk sub-task count: 1.

The high-risk part is the JS expression evaluator. The safe default is never to eval and never to guess: unresolved dynamic expressions become `❌ palette-resolution-failed` with `heuristic` evidence. That keeps the audit useful without turning tool limits into false code defects.
