# Draft — generator 输出 schema 表达层

> Round 1 draft only. This file proposes the expression layer for `figma-data/normalized/docs-figma-members/<X>.ts`; it is not an implementation spec until plan owner review.

## TS 类型定义

```ts
export type DocsFigmaEvidenceLevel = 'direct' | 'heuristic' | 'semantic-inference'

export interface DocsFigmaSource {
  figmaFileKey: string
  figmaNodeId: string
  figmaPage: string
  figmaPageId?: string
  sourceFile: string
  sourceLayer?: string
  rawSourceFile?: string
  rawSha256?: string
}

export interface DocsFigmaAxis {
  figmaAxis: string
  codeProp: string
  values: readonly string[]
  valueCase: 'figma-original'
}

export interface DocsFigmaVariant<TProps extends Record<string, string> = Record<string, string>> {
  variantId: string
  figmaName: string
  props: TProps
  /**
   * Conditional S5 field:
   * - absent when the component has no theme axis, e.g. Badge
   * - required per variant when a theme axis exists, e.g. Select/Tooltip extension rounds
   */
  theme?: 'dark' | 'light'
}

export interface DocsFigmaMembers<TProps extends Record<string, string> = Record<string, string>> {
  schemaVersion: 't2-sample-v0.1'
  component: string
  source: DocsFigmaSource
  axes: readonly DocsFigmaAxis[]
  variants: readonly DocsFigmaVariant<TProps>[]
  meta: {
    generatedAt: string
    generatorVersion: 't2-sample-v0.1'
    sourceFile: string
    variantCount: number
    evidenceLevel: DocsFigmaEvidenceLevel
    evidenceSource: readonly ('components-tokenized-json' | 'variant-name-parser')[]
  }
}
```

### S5 theme 字段的两种表达思路

- Option A: `variant.theme?: 'dark' | 'light'`
  - Pros: grid/filter 直接读每个 variant；Badge 无 theme 时字段缺省，和 plan §2.3 "无 S5 -> 全渲染 / α=N/A" 对齐。
  - Cons: theme 是否存在需要从 `variants.some(v => v.theme)` 推断，axes 列表里不会显式展示 theme axis。
- Option B: 在 `axes` 里保留 theme axis，且 `variant.props.theme` 或 `variant.theme` 同步出现
  - Pros: β audit 统一消费全部 axes；theme axis 也能显示为枚举。
  - Cons: 容易把 site theme filter 误当普通 prop 渲染，Badge 无 theme 时也要定义更多空态。

Draft 建议倾向 Option A，但待 plan owner 拍板。

## Badge 实例预览

真源读取自 `figma-data/normalized/components-tokenized/badge__4821_1665.json`：

- `figmaName`: `Badge`
- `nodeId`: `4821:1665`
- `figmaFileKey`: `YbsPRUVmNdsbN40NNwh1Gn`
- `pageName`: `— — Notifications & Pop box`
- `pageId`: `1379:4191`
- `variants.length`: `20`

```ts
import type { DocsFigmaMembers } from './types'

export type BadgeFigmaProps = {
  type: 'Circle' | 'Rectangle'
  color: 'Green' | 'Blue' | 'Orange' | 'Red' | 'Black'
  tag: 'Filled' | 'Line'
}

export const badgeFigmaMembers = {
  schemaVersion: 't2-sample-v0.1',
  component: 'Badge',
  source: {
    figmaFileKey: 'YbsPRUVmNdsbN40NNwh1Gn',
    figmaNodeId: '4821:1665',
    figmaPage: '— — Notifications & Pop box',
    figmaPageId: '1379:4191',
    sourceFile: 'figma-data/normalized/components-tokenized/badge__4821_1665.json',
    sourceLayer: 'figma-data/raw/components',
    rawSourceFile: 'figma-data/raw/components/badge__4821_1665.json',
    rawSha256: 'f2d2c993e2a90b7ce41acc62ed6ed6b28a7311dfc6e95d62a5c32fe850d22974',
  },
  axes: [
    { figmaAxis: 'Type', codeProp: 'type', values: ['Circle', 'Rectangle'], valueCase: 'figma-original' },
    { figmaAxis: 'Color', codeProp: 'color', values: ['Green', 'Blue', 'Orange', 'Red', 'Black'], valueCase: 'figma-original' },
    { figmaAxis: 'Tag', codeProp: 'tag', values: ['Filled', 'Line'], valueCase: 'figma-original' },
  ],
  variants: [
    { variantId: '4821:1662', figmaName: 'Type=Circle, Color=Green, Tag=Filled', props: { type: 'Circle', color: 'Green', tag: 'Filled' } },
    { variantId: '4895:1363', figmaName: 'Type=Circle, Color=Green, Tag=Line', props: { type: 'Circle', color: 'Green', tag: 'Line' } },
    { variantId: '4821:1663', figmaName: 'Type=Circle, Color=Blue, Tag=Filled', props: { type: 'Circle', color: 'Blue', tag: 'Filled' } },
    { variantId: '4895:1365', figmaName: 'Type=Circle, Color=Blue, Tag=Line', props: { type: 'Circle', color: 'Blue', tag: 'Line' } },
    { variantId: '4821:1661', figmaName: 'Type=Circle, Color=Orange, Tag=Filled', props: { type: 'Circle', color: 'Orange', tag: 'Filled' } },
    { variantId: '4895:1367', figmaName: 'Type=Circle, Color=Orange, Tag=Line', props: { type: 'Circle', color: 'Orange', tag: 'Line' } },
    { variantId: '4821:1660', figmaName: 'Type=Circle, Color=Red, Tag=Filled', props: { type: 'Circle', color: 'Red', tag: 'Filled' } },
    { variantId: '4895:1369', figmaName: 'Type=Circle, Color=Red, Tag=Line', props: { type: 'Circle', color: 'Red', tag: 'Line' } },
    { variantId: '4993:1547', figmaName: 'Type=Circle, Color=Black, Tag=Line', props: { type: 'Circle', color: 'Black', tag: 'Line' } },
    { variantId: '4993:1549', figmaName: 'Type=Circle, Color=Black, Tag=Filled', props: { type: 'Circle', color: 'Black', tag: 'Filled' } },
    { variantId: '4821:1668', figmaName: 'Type=Rectangle, Color=Green, Tag=Filled', props: { type: 'Rectangle', color: 'Green', tag: 'Filled' } },
    { variantId: '4895:1371', figmaName: 'Type=Rectangle, Color=Green, Tag=Line', props: { type: 'Rectangle', color: 'Green', tag: 'Line' } },
    { variantId: '4821:1669', figmaName: 'Type=Rectangle, Color=Blue, Tag=Filled', props: { type: 'Rectangle', color: 'Blue', tag: 'Filled' } },
    { variantId: '4895:1373', figmaName: 'Type=Rectangle, Color=Blue, Tag=Line', props: { type: 'Rectangle', color: 'Blue', tag: 'Line' } },
    { variantId: '4821:1667', figmaName: 'Type=Rectangle, Color=Orange, Tag=Filled', props: { type: 'Rectangle', color: 'Orange', tag: 'Filled' } },
    { variantId: '4895:1375', figmaName: 'Type=Rectangle, Color=Orange, Tag=Line', props: { type: 'Rectangle', color: 'Orange', tag: 'Line' } },
    { variantId: '4821:1666', figmaName: 'Type=Rectangle, Color=Red, Tag=Filled', props: { type: 'Rectangle', color: 'Red', tag: 'Filled' } },
    { variantId: '4895:1377', figmaName: 'Type=Rectangle, Color=Red, Tag=Line', props: { type: 'Rectangle', color: 'Red', tag: 'Line' } },
    { variantId: '4993:1553', figmaName: 'Type=Rectangle, Color=Black, Tag=Line', props: { type: 'Rectangle', color: 'Black', tag: 'Line' } },
    { variantId: '4993:1551', figmaName: 'Type=Rectangle, Color=Black, Tag=Filled', props: { type: 'Rectangle', color: 'Black', tag: 'Filled' } },
  ],
  meta: {
    generatedAt: '<ISO timestamp generated at runtime>',
    generatorVersion: 't2-sample-v0.1',
    sourceFile: 'figma-data/normalized/components-tokenized/badge__4821_1665.json',
    variantCount: 20,
    evidenceLevel: 'direct',
    evidenceSource: ['components-tokenized-json', 'variant-name-parser'],
  },
} satisfies DocsFigmaMembers<BadgeFigmaProps>
```

Note: `props` tuple values are parsed from `variant.name` because this normalized Badge JSON does not carry a separate `componentProperties` tuple per variant.

## 关键决策点（plan owner 拍板）

- [ ] 决策 1: 输出文件名
  - Option A: `badge.ts`
    - Pros: matches plan "lowercase" direction and existing normalized file style.
    - Cons: less self-descriptive when imported beside other generated files.
  - Option B: `Badge.ts`
    - Pros: matches component name.
    - Cons: weaker alignment with normalized data file naming.
  - Option C: `badge.figma-members.ts`
    - Pros: explicit purpose, easier grep.
    - Cons: longer import path and not yet a repo pattern.
- [ ] 决策 2: `axes` shape
  - Option A: `Array<{ figmaAxis; codeProp; values }>`
    - Pros: stable order for grid rendering; supports labels/metadata per axis.
    - Cons: β audit needs a small lookup map.
  - Option B: `Record<axisName, values[]>`
    - Pros: direct membership checks.
    - Cons: order and code prop mapping need extra fields elsewhere.
- [ ] 决策 3: `variants` shape
  - Option A: `Array<{ variantId; figmaName; props }>`
    - Pros: preserves Figma order; easiest for grid "one cell per variant".
    - Cons: lookup by variant id is O(n) unless audit builds an index.
  - Option B: `Record<variantId, propTuple>`
    - Pros: direct lookup.
    - Cons: loses obvious render order unless another ordered id list is added.
- [ ] 决策 4: `figmaPage` / `figmaFileKey` 是否保留
  - Option A: keep both in `source`
    - Pros: satisfies plan S2, helps generator/audit debugging and cross-file traceability.
    - Cons: grid meta currently only needs node id and count.
  - Option B: keep only `figmaNodeId`
    - Pros: smaller output.
    - Cons: weakens traceability and violates plan S2 unless plan is amended.
- [ ] 决策 5: prop value case
  - Option A: keep Figma original values, e.g. `'Circle'`
    - Pros: matches canonical Badge prop types and avoids unregistered aliases.
    - Cons: page code must keep PascalCase literals.
  - Option B: normalize to lowercase
    - Pros: familiar runtime style in some Vue components.
    - Cons: would require explicit translation registration and conflicts with current canonical Badge.
- [ ] 决策 6: S5 theme expression
  - Option A: `variant.theme?: 'dark' | 'light'`
    - Pros: direct filter; clean N/A state for Badge.
    - Cons: theme axis less visible in generic axis list.
  - Option B: `theme` as a normal axis plus variant prop
    - Pros: uniform axis coverage.
    - Cons: higher risk of rendering theme as component prop.

## 自检

| 自检项 | 结果 |
|---|---|
| 覆盖 plan §1.2 S1 Component identity | `component` |
| 覆盖 plan §1.2 S2 Figma 来源溯源 | `source.figmaNodeId` / `source.figmaPage` / `source.figmaFileKey` |
| 覆盖 plan §1.2 S3 Axes 枚举 | `axes[]` |
| 覆盖 plan §1.2 S4 Variant tuple 集合 | `variants[].variantId` + `variants[].props` |
| 覆盖 plan §1.2 S5 Theme 条件必填 | `variants[].theme?` proposal + decision point |
| 覆盖 plan §1.2 S6 Generator 元数据 | `meta.generatedAt` / `meta.generatorVersion` / `meta.sourceFile` |
| 不含 plan §1.3 排除项 | ✓ no fills/strokes/text token refs, no canonical path field, no Code Connect mapping |
| 不写 generator 脚本本身 | ✓ |
| 没自决 | ✓ all expression choices listed as decision points |
| 没扩范围 | ✓ Badge-only schema draft; no `src/`, `figma-sync/`, or `playground/` implementation |
