@contentful/experience-design-system-cli 2.27.5-dev-build-56846c8.0 → 2.27.5-dev-build-347f1b0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export declare function die(message: string): never;
2
+ export declare function assertBinaryInPath(binary: string): Promise<boolean>;
@@ -0,0 +1,12 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { MapTokenPropCall } from '@contentful/experience-design-system-generation';
3
+ export interface ApplyMapTokenPropCallsResult {
4
+ applied: number;
5
+ warnings: string[];
6
+ }
7
+ /**
8
+ * Applies parsed `map_token_prop` calls to the session database. Each call is
9
+ * validated against session state — unlike parseMapTokenPropToolCallLines,
10
+ * which only checks the call's own shape — before any write happens.
11
+ */
12
+ export declare function applyMapTokenPropCalls(db: DatabaseSync, sessionId: string, calls: MapTokenPropCall[], incomingWarnings: string[]): ApplyMapTokenPropCallsResult;
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMapTokensCommand(program: Command): void;
@@ -0,0 +1,31 @@
1
+ /** A token default extracted from a design-category token property. */
2
+ export interface RawDesignTokenDefault {
3
+ /** The source default, retained verbatim for persistence and review. */
4
+ rawDefault: string;
5
+ /** The property's expected DTCG token type. */
6
+ tokenKind: string | null;
7
+ }
8
+ /** The path and type of one DTCG token leaf. Values are intentionally absent. */
9
+ export interface DTCGTokenLeaf {
10
+ path: string;
11
+ type: string;
12
+ }
13
+ export type UnresolvedTokenDefaultReason = 'no_match' | 'ambiguous' | 'type_mismatch';
14
+ export interface UnresolvedTokenDefault {
15
+ rawDefault: string;
16
+ tokenKind: string | null;
17
+ reason: UnresolvedTokenDefaultReason;
18
+ candidatePaths: string[];
19
+ message: string;
20
+ }
21
+ export interface ResolveTokenDefaultsResult {
22
+ /** Exact source defaults mapped to their one compatible DTCG path. */
23
+ mappings: Record<string, string>;
24
+ diagnostics: UnresolvedTokenDefault[];
25
+ }
26
+ /**
27
+ * Resolves extracted source defaults without inspecting token values or changing
28
+ * the source defaults. A normalised leaf-key match is accepted only when it
29
+ * identifies exactly one leaf compatible with the property's token kind.
30
+ */
31
+ export declare function resolveTokenDefaults(defaults: readonly RawDesignTokenDefault[], leaves: readonly DTCGTokenLeaf[]): ResolveTokenDefaultsResult;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface MapTokensViewResult {
3
+ agent: string;
4
+ sessionId: string;
5
+ applied: number;
6
+ cached: boolean;
7
+ }
8
+ interface MapTokensViewProps {
9
+ result: MapTokensViewResult;
10
+ onExit: () => void;
11
+ }
12
+ export declare function MapTokensView({ result, onExit }: MapTokensViewProps): React.ReactElement;
13
+ export {};
@@ -1,2 +1,11 @@
1
1
  import type { Command } from 'commander';
2
+ export declare function rebuildDTCGTree(groups: Array<{
3
+ path: string;
4
+ $description?: string;
5
+ }>, tokens: Array<{
6
+ path: string;
7
+ $type: string;
8
+ $value: unknown;
9
+ $description?: string;
10
+ }>): Record<string, unknown>;
2
11
  export declare function registerPrintCommand(program: Command): void;
@@ -10,6 +10,8 @@ export type ModifyLauncherInput = {
10
10
  * generated. Forwarded to the wizard so the modify entry pre-loads
11
11
  * tokens alongside the extract/generate sessions. */
12
12
  tokenSessionId?: string | null;
13
+ /** Absolute path to the complete DTCG token catalog written for this run. */
14
+ tokensPath?: string | null;
13
15
  projectPath: string;
14
16
  savePath: string;
15
17
  entryStep: 'scope-gate' | 'final-review';
@@ -1,11 +1,11 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { RawComponentDefinition } from '../types.js';
3
3
  import type { CDFComponentEntry, DTCGTokenEntry, DTCGTokenGroup } from '@contentful/experience-design-system-types';
4
- import type { ToolCall, TokenToolCall } from '@contentful/experience-design-system-generation';
4
+ import type { ToolCall, TokenToolCall, ComponentSourceRef } from '@contentful/experience-design-system-generation';
5
5
  import type { ComponentTypeSummary } from '@contentful/experience-design-system-types';
6
6
  import type { SlotCycle, SlotEdge } from '../analyze/cycle-detection.js';
7
7
  export type StepStatus = 'pending' | 'complete' | 'failed' | 'interrupted';
8
- export type CommandName = 'analyze extract' | 'analyze select' | 'generate components' | 'generate tokens' | 'generate edit' | 'apply preview' | 'apply select' | 'apply push' | 'print components' | 'print tokens' | 'import';
8
+ export type CommandName = 'analyze extract' | 'analyze select' | 'generate components' | 'generate tokens' | 'generate edit' | 'apply preview' | 'apply select' | 'apply push' | 'print components' | 'print tokens' | 'map tokens' | 'import';
9
9
  export interface SessionRow {
10
10
  id: string;
11
11
  name: string | null;
@@ -26,6 +26,30 @@ export interface StepRow {
26
26
  }
27
27
  export declare function getPipelineDbPath(): string;
28
28
  export declare function openPipelineDb(dbPath?: string): DatabaseSync;
29
+ /** An exact source token reference paired with its canonical DTCG path. */
30
+ export type RawTokenNamePaths = Record<string, string>;
31
+ export type RawTokenNamePathSource = 'automatic' | 'manual';
32
+ export interface RawTokenNamePath {
33
+ rawName: string;
34
+ path: string;
35
+ source: RawTokenNamePathSource;
36
+ }
37
+ /**
38
+ * Replaces the session's exact source-reference-to-DTCG-path sidecar.
39
+ * Source references are retained verbatim; normalisation belongs to the
40
+ * deterministic resolver, not to persistence or CDF emission.
41
+ */
42
+ export declare function replaceRawTokenNamePaths(db: DatabaseSync, sessionId: string, tokenNamePaths: RawTokenNamePaths, source?: RawTokenNamePathSource): void;
43
+ /** Loads the exact source-reference-to-DTCG-path sidecar for a session. */
44
+ export declare function loadRawTokenNamePaths(db: DatabaseSync, sessionId: string): RawTokenNamePaths;
45
+ export declare function loadRawTokenNamePathRows(db: DatabaseSync, sessionId: string): RawTokenNamePath[];
46
+ /**
47
+ * Where a prop's token-path list came from. 'agent' is a map-tokens suggestion,
48
+ * which a later run of that step may revise. 'review' is a person's decision in
49
+ * the review editor, which a re-run must leave alone.
50
+ */
51
+ export type RawPropTokenPathSource = 'agent' | 'review';
52
+ export declare function replaceRawPropTokenPaths(db: DatabaseSync, sessionId: string, componentId: string, propName: string, paths: string[], source: RawPropTokenPathSource): void;
29
53
  export interface ApplyToolCallsResult {
30
54
  classified: number;
31
55
  excluded: number;
@@ -83,6 +107,8 @@ export type RawComponentWithId = RawComponentDefinition & {
83
107
  component_id: string;
84
108
  };
85
109
  export declare function loadRawComponents(db: DatabaseSync, sessionId: string, allowedNames?: Set<string>): RawComponentWithId[];
110
+ export declare function loadComponentSourceRef(name: string, sourcePath: string, propNames?: string[]): Promise<ComponentSourceRef>;
111
+ export declare function loadComponentSourceRefs(db: DatabaseSync, sessionId: string): Promise<ComponentSourceRef[]>;
86
112
  export declare function renameEmptySlots(db: DatabaseSync, sessionId: string, componentId: string, componentName: string, slotCount: number): {
87
113
  renames: Array<{
88
114
  oldName: string;
@@ -133,9 +159,10 @@ export declare function seedDefaultsFromChangedItems(db: DatabaseSync, sessionId
133
159
  proposed: object;
134
160
  }>): number;
135
161
  export declare function backfillUnclassifiedProps(db: DatabaseSync, sessionId: string): number;
162
+ export type CacheEntityType = 'component' | 'token_set' | 'token_mapping';
136
163
  export interface CacheEntry {
137
164
  inputHash: string;
138
- entityType: 'component' | 'token_set';
165
+ entityType: CacheEntityType;
139
166
  entityId: string;
140
167
  sourceSessionId: string;
141
168
  humanEdited: boolean;
@@ -145,9 +172,10 @@ export interface CacheEntry {
145
172
  }
146
173
  export declare function computeComponentInputHash(component: RawComponentWithId): string;
147
174
  export declare function computeTokenInputHash(rawTokenContent: string): string;
148
- export declare function lookupCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, promptHash?: string): CacheEntry | null;
149
- export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): CacheEntry | null;
150
- export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
175
+ export declare function computeMapTokensInputHash(db: DatabaseSync, sessionId: string, componentSourceRefs?: readonly ComponentSourceRef[]): string;
176
+ export declare function lookupCache(db: DatabaseSync, inputHash: string, entityType: CacheEntityType, entityId: string, promptHash?: string): CacheEntry | null;
177
+ export declare function lookupCacheByEntity(db: DatabaseSync, entityType: CacheEntityType, entityId: string): CacheEntry | null;
178
+ export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: CacheEntityType, entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
151
179
  export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
152
180
  export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
153
181
  export declare function storeSlotCycles(db: DatabaseSync, sessionId: string, cycles: Array<SlotCycle & {
@@ -161,6 +189,7 @@ export declare function clearSlotCycles(db: DatabaseSync, sessionId: string): vo
161
189
  export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
162
190
  export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
163
191
  export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;
192
+ export declare function copyMapTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): number;
164
193
  export declare function getCliCacheVersion(): Promise<string>;
165
194
  export interface ExtractCacheEntry {
166
195
  filePath: string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Bounded source excerpts that keep the lines that matter.
3
+ *
4
+ * Head truncation of a styles module keeps its imports and top-level
5
+ * constants and drops the one line that decides a property's classification —
6
+ * the place the value is interpolated into a style. An excerpt built from
7
+ * windows around each property name's occurrences spends the same byte budget
8
+ * on that line instead.
9
+ */
10
+ export interface SourceExcerpt {
11
+ content: string;
12
+ /**
13
+ * Names with at least one occurrence that did not fit in `content`. The
14
+ * reader cannot see that use, so its absence from the excerpt is a gap, not
15
+ * evidence. Empty when every occurrence is shown.
16
+ */
17
+ usesNotShown: string[];
18
+ }
19
+ /**
20
+ * Returns `text` whole when it fits `maxChars`. Otherwise returns windows of
21
+ * `contextLines` lines around every line that mentions one of `names`, merged
22
+ * where they overlap, in file order, until the budget is spent. With no
23
+ * occurrences at all it falls back to the head of the file.
24
+ */
25
+ export declare function excerptAroundNames(text: string, names: string[], maxChars: number, contextLines?: number): SourceExcerpt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.27.5-dev-build-56846c8.0",
3
+ "version": "2.27.5-dev-build-347f1b0.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "svelte": "^5.56.4",
40
40
  "ts-morph": "^27.0.2",
41
41
  "typescript": "^5.9.3",
42
- "@contentful/experience-design-system-types": "2.27.5-dev-build-56846c8.0"
42
+ "@contentful/experience-design-system-types": "2.27.5-dev-build-347f1b0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@tsconfig/node24": "^24.0.4",
@@ -51,9 +51,9 @@
51
51
  "ink-testing-library": "^4.0.0",
52
52
  "typescript-eslint": "^8.67.0",
53
53
  "vitest": "^4.0.16",
54
- "@contentful/experience-design-system-client": "2.27.5-dev-build-56846c8.0",
55
- "@contentful/experience-design-system-extraction": "2.27.5-dev-build-56846c8.0",
56
- "@contentful/experience-design-system-generation": "2.27.5-dev-build-56846c8.0"
54
+ "@contentful/experience-design-system-client": "2.27.5-dev-build-347f1b0.0",
55
+ "@contentful/experience-design-system-extraction": "2.27.5-dev-build-347f1b0.0",
56
+ "@contentful/experience-design-system-generation": "2.27.5-dev-build-347f1b0.0"
57
57
  },
58
58
  "repository": {
59
59
  "type": "git",
@@ -29,6 +29,8 @@ All input is embedded inline in the prompt before this file:
29
29
  - **Raw component data** — `RawComponentDefinition[]` (one component for this run)
30
30
  - **DTCG token data** — full token tree, if provided
31
31
  - **Token-name sidecar** — raw CSS custom property name → DTCG dot-notation path, if provided
32
+ - **Component source references** — the real file text for the component's own source, plus up to 5 sibling files (e.g. a co-located `.styles.ts` or `utils.ts`, or a file reached transitively through another sibling — such as a re-exported component's own styles module), if provided. **You have no filesystem access and no tools — `sourcePath` is a citation label only, never something to open.**
33
+ - A **"uses not shown"** line may accompany these files. The files are excerpts windowed around the property names, and this line names the properties with at least one use that fell outside the excerpt budget. Treat those properties' consumption as unknown: do not classify them `token` on what is shown, and do not conclude they are unread. Unknown resolves to `enum` (or `string`) per "Ambiguity resolves to `enum`".
32
34
 
33
35
  ```typescript
34
36
  interface RawPropDefinition {
@@ -39,7 +41,7 @@ interface RawPropDefinition {
39
41
  allowedValues?: string[];
40
42
  defaultValue?: string;
41
43
  description?: string;
42
- tokenReference?: string; // raw token name, e.g. "--brand-primary"
44
+ tokenReference?: string; // raw token name — shape varies by design system, e.g. "--brand-primary" (CSS custom property) or "tokens.blue500" (flat/dotted JS reference)
43
45
  }
44
46
 
45
47
  interface RawSlotDefinition {
@@ -89,7 +91,8 @@ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not
89
91
  - Every prop in the input must produce exactly one call: `classify_prop` OR `exclude_prop`.
90
92
  - Every slot must produce exactly one `classify_slot` call.
91
93
  - Emit `classify_component` once at the start (required). The `description` field is **required** — always provide a brief description of the component's purpose.
92
- - `values` is required for `cdf_type: "enum"` — must be a non-empty string array.
94
+ - `values` is required for `cdf_type: "enum"` — must be a non-empty string array of the variant names the prop accepts.
95
+ - **Do NOT include `values` for `cdf_type: "token"`.** The two property types carry different kinds of list. An `enum` prop's list holds *variant names* the component accepts (`"primary"`, `"secondary"`). A `token` prop's list holds *design token paths* (`"color.brand.primary"`), produced by the separate token-mapping step (`$token.allowed`) — never by you. Emitting `values` on a token prop makes the definition invalid.
93
96
  - `token_kind` is required for `cdf_type: "token"` — must be a DTCG `$type` string, e.g. `"color"`.
94
97
  - `required` must be a JSON boolean (`true`/`false`), not a string.
95
98
  - `description` on `classify_prop` is customer-facing — keep it short and subject to the description content rules below.
@@ -129,6 +132,99 @@ Exactly **6** valid types:
129
132
 
130
133
  ---
131
134
 
135
+ ### `enum` versus `token`
136
+
137
+ Both describe a closed set of choices, but the *contents* differ, and that is the whole distinction:
138
+
139
+ | | What the list holds | Example |
140
+ |---|---|---|
141
+ | `enum` | Variant names the component accepts | `["primary", "secondary", "ghost"]` |
142
+ | `token` | Design token paths the prop may bind to | `["color.brand.primary", "color.brand.neutral"]` |
143
+
144
+ The distinction is **what the component does with the value it receives**. That
145
+ is decidable from source; it is not inferred from the property's name, from the
146
+ component's role, or from the property's TypeScript type.
147
+
148
+ An `enum` design property is delivered as a plain string, so the component
149
+ receives `variant="primary"` — the value its own code branches on. A `token`
150
+ property is resolved to the token's value *before* it reaches the component, so
151
+ that same component would receive `variant="#0059c8"` and match no branch.
152
+
153
+ #### The three-question procedure (closed, ordered)
154
+
155
+ For every design prop that carries a value (not a boolean), answer these three
156
+ questions **in this order**, from the source shown, and stop at the first answer
157
+ that decides. There is no fourth question.
158
+
159
+ - **Q1 — Does the component look the value up, switch on it, or compare it against names?**
160
+ An indexed access (`tokens[fontColor]`, `SPACING[padding]`, a `Record<Variant, Token>`),
161
+ a `switch (variant)` / `if (variant === 'x')` chain, a `styles[variant]` class lookup,
162
+ or a vocabulary of its own (`allowedValues`, a literal union) — any of these means
163
+ the component needs the *name*. A presence check (`if (width)`) or arithmetic on
164
+ the value is not a lookup.
165
+ **Yes → `enum`, names in `values`, stop.** No → Q2.
166
+ - **Q2 — Is the value written straight into a style or attribute?**
167
+ `rx={radius}`, `padding: ${padding}`, `style={{ color }}`, `background: var(${bg})` —
168
+ the value reaches the renderer without the component resolving it.
169
+ **No → not `token`; leave this procedure and continue with the type rules.** Yes → Q3.
170
+ - **Q3 — Is there a design-token reference at that use?** Any one of:
171
+ the parameter default is a token (`radius = tokens.borderRadiusSmall`), the raw prop
172
+ carries a `tokenReference`, or an inline `tokens.*` / `var(--*)` sits in the same
173
+ expression. The reference must belong to *this* prop — a `tokens.*` on some other
174
+ line of the file, or in a sibling's style object, is not one.
175
+ **Yes → `token`, stop; resolve `token_kind` from that reference. No → `string`, stop.**
176
+
177
+ **The raw TypeScript type is not an input to Q1–Q3.** `string`, `number`,
178
+ `string | number`, `stringOrNumber` — none of these answers any question above;
179
+ they say what the compiler accepts, not what the component does with the value.
180
+ "The type accepts any value, the default just happens to be a token reference" is
181
+ a Q3-yes described as a no: a token parameter default *is* the design-token
182
+ reference Q3 asks for, and the prop is `token`. Equally, a prop named `color` with
183
+ no lookup and no token reference is `string`, however token-like the name.
184
+
185
+ **Two worked examples — same shape, opposite answers:**
186
+
187
+ ```tsx
188
+ // Divider.tsx
189
+ export const Divider = ({ thickness = tokens.borderWidthDefault, inset = 0 }: DividerProps) => (
190
+ <hr style={{ borderTopWidth: thickness, marginLeft: inset }} />
191
+ );
192
+ ```
193
+
194
+ - `thickness` — Q1: no lookup. Q2: yes, `borderTopWidth: thickness`. Q3: yes, the
195
+ parameter default is `tokens.borderWidthDefault`. → **`token`**, `token_kind` from
196
+ `borderWidthDefault` via the sidecar. Its `string | number` annotation changes nothing.
197
+ - `inset` — Q1: no lookup. Q2: yes, `marginLeft: inset`. Q3: no, the default is the
198
+ bare number `0` and nothing at this use is a token. → **`string`**, `default: "0"`.
199
+
200
+ A lookup map from variant names to tokens is evidence **for** `enum`, not against
201
+ it — that is Q1 answering yes. The map exists precisely because the component was
202
+ handed a name it had to resolve itself.
203
+
204
+ **`token` is earned by Q2 and Q3 together, and only that way.** Cite both lines
205
+ in `reason`: the interpolation and the token reference. Once both are cited, the
206
+ prop is `token` — do not then downgrade it because of its type annotation, because
207
+ the default "could be overridden" (every default can), or because the tie-break
208
+ below exists. Nothing downstream re-derives, corrects, or second-guesses this —
209
+ what you emit is what ships.
210
+
211
+ **Ambiguity resolves to `enum` — but only when Q1–Q3 cannot be answered.** The
212
+ tie-break applies solely when the source shown does not let you answer the three
213
+ questions: the property is interpolated in one place and looked up in another,
214
+ the use sits in a file you cannot see, or the source is truncated at the point
215
+ of use (the "uses not shown" line). In those cases emit `enum` (or `string` when
216
+ it has no value set) and record the ambiguity in `reason`. When the three
217
+ questions *can* be answered from the source shown, they are answered and the
218
+ tie-break does not apply: a clear Q1-yes is `enum`, a clear Q2-yes + Q3-yes is
219
+ `token`, and a type annotation does not create ambiguity. An `enum` is delivered
220
+ as the plain string the component was written to receive and remains reachable
221
+ from a content-type field; a wrong `token` delivers a resolved value the
222
+ component cannot branch on and is permanently excluded from content-type field
223
+ mapping. That asymmetric cost is why a genuine tie goes to `enum` — it is not a
224
+ reason to call a decided case a tie.
225
+
226
+ ---
227
+
132
228
  ## Valid cdf_category values
133
229
 
134
230
  | cdf_category | Use case |
@@ -145,14 +241,18 @@ The pre-classified `category` in the raw input is a starting point — correct i
145
241
 
146
242
  For each `RawPropDefinition`, apply in order:
147
243
 
148
- 1. **Framework / DOM / accessibility pass-through?** → `exclude_prop`. These are escape hatches for developers, not configurable surfaces for marketers. Exposing them in the ExO editor adds noise that obscures the props that actually carry intent. Always exclude:
244
+ 1. **Framework / DOM / accessibility pass-through?** → `exclude_prop`. These are escape hatches for developers, not configurable surfaces for marketers. Exposing them in the ExO editor adds noise that obscures the props that actually carry intent.
245
+
246
+ **The governing principle:** a prop is pass-through if it is inherited from a base interface the component extends (`React.HTMLAttributes<...>`, `AllHTMLAttributes`, a native element's attribute interface, an ARIA-attributes interface, etc.) or is one of a small set of universal framework hooks, **and the component does not redeclare or give it component-specific meaning**. Test this from the type declaration, not from name-matching against a list — a component that `extends React.HTMLAttributes<HTMLDivElement>` inherits `title`, `lang`, `translate`, `slot`, and dozens more that a fixed list will never enumerate; a component that redeclares one of these with its own type or JSDoc (e.g. its own `title: string` documented as "card heading") has given it new meaning and it is no longer pass-through.
247
+
248
+ Always exclude under this principle:
149
249
  - Framework internals: `ref`, `innerRef`, event handlers (any `onSomething`), `testId`, `data-testid`, `key`
150
- - DOM pass-through: `className`, `class`, `classes`, `classNames`, `rootClassName`, `prefixCls`, `style`, `styles`, `id`, `role`, `tabIndex`, `htmlFor`, `for`, `slot`, `is`, `lang`, `dir`, `hidden`, `draggable`, `spellCheck`, `contentEditable`, `inputMode`, `autoComplete`, `autoFocus`, `translate`, `part`, `exportparts`
250
+ - Any prop inherited from an HTML-attributes base interface and not redeclared by the component — this replaces any fixed name list. Common examples include `className`, `class`, `classes`, `classNames`, `rootClassName`, `prefixCls`, `style`, `styles`, `id`, `role`, `tabIndex`, `htmlFor`, `for`, `slot`, `is`, `lang`, `dir`, `hidden`, `draggable`, `spellCheck`, `contentEditable`, `inputMode`, `autoComplete`, `autoFocus`, `translate`, `part`, `exportparts` — but these are examples of the principle, not an exhaustive list; apply the inheritance test to any prop the examples don't cover.
151
251
  - Accessibility pass-through: any `aria-*` or `ariaSomething` prop (including bare `aria` as an aria-attributes object), `aria-label`, `aria-hidden`, `aria-describedby`, `aria-controls`
152
252
  - Data attributes: any `data-*` prop
153
253
  - **Polymorphic component props**: `as`, `element`, `component` (when typed as an HTML tag string or component reference) — these change rendered HTML, not marketer-visible behavior
154
254
  - **Framework theming / pass-through escape hatches**: PrimeVue's `dt` / `pt` / `ptOptions` / `unstyled`, MUI/Chakra-style `sx`, anything explicitly typed as a developer "override" / "passthrough" object
155
- - **Important caveat**: only exclude when the prop is one of these *as the bare HTML attribute or framework-internal pass-through*. Compound names like `fileName`, `displayName`, `dataset`, `dataSource`, `roleDescription`, `idLabel` are not pass-through — classify them normally.
255
+ - **Important caveat**: only exclude when the prop is inherited/universal *and unredeclared*. Compound names like `fileName`, `displayName`, `dataset`, `dataSource`, `roleDescription`, `idLabel` are not pass-through — classify them normally. Likewise, a `title` (or any other inherited-attribute name) that the component redeclares with its own type or doc comment is component-specific content, not pass-through — classify it per rule 2 onward, do not exclude it and do not route it through the `title` → `content` correction below.
156
256
  2. **Common semantic props — DO classify, do not exclude.** The LLM has been over-excluding these because they sound like framework internals; they are not. Classify each per the rest of this tree:
157
257
  - `icon` / `leftIcon` / `rightIcon` / `prefixIcon` / `suffixIcon` — slot or `string` (icon name); see slot guidance below
158
258
  - `items` / `options` / `actions` / `links` — usually array content; if the element shape is simple, classify as `string` (comma-separated names/IDs) and note in `description`. Only exclude when elements are deep nested objects with no flat representation.
@@ -163,10 +263,25 @@ For each `RawPropDefinition`, apply in order:
163
263
  - `accessibleNameRef` / `accessibleDescriptionRef` (web components) — these are ID references for a11y wiring; classify as `string`, `cdf_category: "state"` (behavioral wiring, not design or content).
164
264
  - `eventDetails` / similar telemetry props — `cdf_category: "state"`.
165
265
  3. **Positional/geometric design prop?** (`top`, `bottom`, `left`, `right`, `rotation`, `offset`, `zIndex`) → `classify_prop`, `cdf_type: "string"`, `cdf_category: "design"`.
166
- 4. **Has `tokenReference`?** → `cdf_type: "token"`, resolve `token_kind` via sidecar lookup (see below). This overrides all other heuristics.
167
- 5. **Union of string literals** (e.g. `'a' | 'b' | 'c'`)? → `cdf_type: "enum"`, extract literals into `values`.
266
+ 4. **`enum` or `token`? — run the three-question procedure first.** Before any type-based rule, for every design prop that carries a value, answer Q1–Q3 from "`enum` versus `token`" above, in order, from the source shown:
267
+
268
+ * **Q1** looked up / switched on / compared / has its own vocabulary → `cdf_type: "enum"`, names in `values`, no `token_kind`. This holds even when every name maps 1:1 to a design token; the map is the component resolving a name it was given.
269
+ * **Q2** not written straight into a style or attribute → not `token`; continue to rules 5–7.
270
+ * **Q3** written straight into a style or attribute **and** a token reference at that use (a `tokens.*` parameter default, a `tokenReference`, an inline `tokens.*` / `var(--*)`) → `cdf_type: "token"`, `token_kind` from that reference via the sidecar, no `values`. Interpolated with no token reference at that use → `cdf_type: "string"`.
271
+ * Q1–Q3 **cannot be answered** from the source shown (unread, truncated, in a file you cannot see, evidence pointing both ways) → `cdf_type: "enum"` (or `string`). See "Ambiguity resolves to `enum`" above.
272
+
273
+ **Do not classify on the component's role.** A layout primitive is not automatically a token consumer. Design systems routinely key their primitives on token *names*, and those props are `enum` — the component needs the name to perform its own lookup:
274
+
275
+ ```ts
276
+ // Box.tsx — padding is an enum, despite Box being a layout primitive
277
+ const { padding } = props; // "spacingXs"
278
+ getSpacingStyles({ padding }); // → SpacingTable["spacingXs"] → "0.5rem"
279
+ ```
280
+
281
+ Classify that `padding` as `token` and the platform delivers `"0.5rem"`, so `SpacingTable["0.5rem"]` is `undefined` and the spacing silently disappears. A value that is spelled like a token name is still a lookup key (Q1 yes). Curated components with named variant APIs (`Button`, `Tag`, `Badge`, `Avatar`, `Notification`) are `enum` for the same reason. A design system may legitimately produce **zero** token-typed props.
282
+ 5. **Union of string literals** (e.g. `'a' | 'b' | 'c'`)? → `cdf_type: "enum"`, extract literals into `values`. (Rule 4's Q1 will normally have decided this already — a literal union is the prop's own vocabulary.) This applies even when Component source references show each literal resolving to a design token internally — a resolved token map is evidence about the component's internals, not its interface.
168
283
  6. **Raw type is `string`** and prop name is `href`, `url`, or clearly a URL? → `cdf_type: "string"`, `cdf_category: "content"`.
169
- 7. **Raw type is `string` / `number` / `boolean`?** → For `boolean`, use `cdf_type: "boolean"` with `default: true` or `false` (native boolean). For `number`, use `cdf_type: "string"` with `default` as the numeric value as a string (e.g. `"0"`). For `string`, use `cdf_type: "string"`.
284
+ 7. **Raw type is `string` / `number` / `boolean`?** → For `boolean`, use `cdf_type: "boolean"` with `default: true` or `false` (native boolean). For `number`, use `cdf_type: "string"` with `default` as the numeric value as a string (e.g. `"0"`). For `string`, use `cdf_type: "string"`. **A design prop reaches this rule only after leaving rule 4 at Q2 (not written into a style) or Q3 (written into a style with no token reference at that use).** A prop that answered Q3 yes is already `token` and never arrives here — a `string`, `number`, or `string | number` annotation on it does not bring it back to this rule.
170
285
  8. **Media/image type** (`ImageProps`, `MediaSource`, asset types)? → `cdf_type: "media"`.
171
286
  9. **Rich text / markup** (`ReactNode` used as content, HTML string)? → `cdf_type: "richtext"`.
172
287
  10. **Complex type — resolve before excluding** (see below).
@@ -182,8 +297,8 @@ A prop with a complex TypeScript type is **not automatically excluded**. Many pr
182
297
  | Raw type pattern | How to resolve |
183
298
  |---|---|
184
299
  | `'primary' \| 'secondary' \| 'ghost'` (union of literals) | → `enum`, extract `values` |
185
- | `HeadingSize` / `ButtonVariant` / any named type that is clearly a finite set of visual options | → `enum`, infer likely values from the prop name and context (e.g. `['sm', 'md', 'lg']` for size, `['primary', 'secondary']` for variant). Document your inference in `description`. |
186
- | `Variant` / `variant` prop | Usually a visual design variant. → `enum`, `cdf_category: "design"`. Infer values from context. |
300
+ | `HeadingSize` / `ButtonVariant` / any named type that is clearly a finite set of visual options | → `enum`. **Never invent a value list.** Use only values you can cite from the source shown (the type's own declaration, a lookup table, a default). If the source shown does not enumerate the values, this is the "cannot be answered" case — apply "Ambiguity resolves to `enum`": emit `enum` (or `string` if it has no discoverable value set) and record the missing evidence in `description` (not `reason` — `description` ships to the CDF, `reason` does not), e.g. `"WARNING: values not visible in source — enum values unconfirmed"`. Do not default to a generic guess like `['sm', 'md', 'lg']` or `['primary', 'secondary']` — those are one design system's vocabulary, not a fallback for missing evidence. |
301
+ | `Variant` / `variant` prop | Usually a visual design variant. → `enum`, `cdf_category: "design"`. Same evidence rule as above — cite values from the source shown; do not invent them. |
187
302
  | `Section[]` / array of custom items where the structure is unclear | → `exclude_prop` only if the array elements are complex objects with no obvious flat representation. If items are simple (title, label, id), consider representing as `string` (a comma-separated IDs or keys) or note in `description` why. |
188
303
  | `ExperienceConfiguration<Variant>` / deep generic | Personalization config — → `exclude_prop`, reason: `"personalization configuration — framework internal"` |
189
304
  | `React.Dispatch<...>` / setter | State setter — → `exclude_prop`, reason: `"React state setter — framework internal"` |
@@ -233,26 +348,20 @@ Rules for nested objects:
233
348
 
234
349
  ---
235
350
 
236
- ## Token-aware mapping
351
+ ## Resolving `token_kind`
237
352
 
238
- When `tokenReference` is present, classify with `cdf_type: "token"`. The `token_kind` field becomes `$token.kind` in the CDF output (a DTCG `$type` string, e.g. `"color"`).
353
+ `token_kind` becomes `$token.kind` in the CDF output — the DTCG `$type` of the token the prop binds to. For a prop that earned `cdf_type: "token"` (Q2 yes, Q3 yes), take the token name cited at Q3 (`tokens.borderRadiusSmall`, `var(--brand-primary)`, a raw `tokenReference`) and resolve it against the inline token data: through the token-name sidecar (raw name → DTCG dot-notation path) when one is provided, otherwise by matching the name to the DTCG tree by spelling (`tokens.borderRadiusSmall` → `border-radius.border-radius-small`); the leaf's `$type` (`"dimension"`) is the `token_kind`. If the name cannot be resolved to a leaf, keep `cdf_type: "token"`, omit `token_kind`, and say so in `description` (`"WARNING: token_kind unknown — <name> not found in token data"`).
239
354
 
240
- 1. Look up `tokenReference` in the inline token-name sidecar → get the DTCG dot-notation path
241
- 2. Traverse that path in the inline DTCG token data to reach the leaf token
242
- 3. Use the leaf's `$type` (e.g. `"color"`) as `token_kind`
355
+ **Canonical example — a named variant backed by tokens is still `enum`:**
243
356
 
244
- Example:
245
357
  ```
246
- tokenReference: "--brand-primary"
247
- → sidecar["--brand-primary"] → "colors.brand.primary"
248
- → token data: colors.brand.primary.$type → "color"
249
- → tool call: {"tool":"classify_prop","prop":"bgColor","cdf_type":"token","cdf_category":"design","token_kind":"color","description":"..."}
358
+ Tag.variant has allowedValues ["primary","secondary","warning","negative"] and PillNext.styles.ts
359
+ maps each one to a token (primary→tokens.blue100, warning→tokens.orange100, ...). The mapping is
360
+ internal to the design system — Tag's prop signature accepts the name, not the token. Classifying
361
+ as enum so the author picks "primary" and the component receives "primary".
362
+ {"tool":"classify_prop","prop":"variant","cdf_type":"enum","cdf_category":"design","required":false,"values":["primary","secondary","warning","negative"],"default":"primary","description":"Visual variant","reason":"Fully token-backed internally, but the component consumes the variant name. Token classification would deliver a resolved color the component cannot branch on."}
250
363
  ```
251
364
 
252
- If `tokenReference` is not found in the sidecar → `cdf_type: "token"`, omit `token_kind`, add `description: "WARNING: tokenReference not found in sidecar — token_kind unknown"`.
253
-
254
- If token data was not provided and `tokenReference` is present → `cdf_type: "token"`, omit `token_kind`, add `description: "WARNING: no token data supplied — token_kind unknown"`.
255
-
256
365
  ---
257
366
 
258
367
  ## Category correction rules
@@ -261,7 +370,7 @@ The pre-classified `category` is wrong in predictable ways. Correct silently (do
261
370
 
262
371
  - Visual style props (`color`, `size`, `padding`, `spacing`, `variant`, `theme`, `bgColor`, `imageOnLeft`, `enableXxx`) classified as `content` or `state` → `design`
263
372
  - Interactive/behavioral state props (`disabled`, `loading`, `expanded`, `selected`, `checked`, `active`, `isOpen`, `isSearchVisible`, `showXxx`, `hideXxx`, `preview`, `componentId`, `sectionKey`, `variantIndex`) classified as `design` or `content` → `state`
264
- - Text/label/data props (`title`, `label`, `description`, `caption`, `text`, `boldText`, `labelText`, `richText`, `backgroundImage`, `link`, `placeholder`, `searchValue`, `total`, `slug`) classified as `design` → `content`
373
+ - Text/label/data props (`label`, `description`, `caption`, `text`, `boldText`, `labelText`, `richText`, `backgroundImage`, `link`, `placeholder`, `searchValue`, `total`, `slug`) classified as `design` → `content`. `title` is excluded from this auto-correction — it is one of the most common HTML-inherited attribute names (see the pass-through principle above). Only apply this correction to a `title` prop the component redeclares with its own type or documented, component-specific meaning; a `title` that is bare inherited pass-through was already excluded in rule 1 and never reaches this step.
265
374
  - Locale classified as `content` → `state` (it is a behavioral routing value, not editor-filled text)
266
375
 
267
376
  > **Key question for category**: "Who fills this in?" — A content editor fills in `content`. A designer configures `design`. Neither fills in `state` — it comes from routing, runtime behavior, or component infrastructure.
@@ -363,13 +472,6 @@ inferring likely values: ["h1", "h2", "h3", "h4", "h5", "h6"] — documenting in
363
472
  {"tool":"classify_prop","prop":"titleSize","cdf_type":"enum","cdf_category":"design","required":false,"values":["h1","h2","h3","h4","h5","h6"],"description":"Heading level — inferred from HeadingSize type name; actual values may be h1–h6 or sm/md/lg"}
364
473
  ```
365
474
 
366
- ### Token-linked prop
367
-
368
- ```
369
- bgColor has tokenReference "--bg-primary" — looking up sidecar
370
- {"tool":"classify_prop","prop":"bgColor","cdf_type":"token","cdf_category":"design","token_kind":"color","description":"Background color token linked via --bg-primary → colors.bg.primary"}
371
- ```
372
-
373
475
  ### href prop
374
476
 
375
477
  ```
@@ -383,7 +485,6 @@ href is a URL string — cdf_type string (not link), category content
383
485
 
384
486
  - **Prop with unresolvable type** (generics, intersection, callback) → `exclude_prop` with reason `"complex type — not representable in CDF"`.
385
487
  - **Component with zero classified props after exclusions** → still emit `classify_component`. The DB entry will have an empty `$properties` object.
386
- - **tokenReference present but not in sidecar** → `cdf_type: "token"`, omit `token_kind`, add `description` warning.
387
488
  - **Slot not in DB** → skipped with a warning; does not abort the run.
388
489
  - **Prop not in DB** → skipped with a warning; does not abort the run.
389
490
 
@@ -395,15 +496,18 @@ Before emitting any tool calls, verify:
395
496
  2. Every slot has exactly one `classify_slot` call
396
497
  3. `classify_component` is emitted exactly once
397
498
  4. Every `cdf_type: "enum"` has a non-empty `values` array
398
- 5. Every `cdf_type: "token"` has `token_kind` (or a warning in `description` if lookup failed)
499
+ 5. Every `cdf_type: "token"` has `token_kind` (or a warning in `description` if lookup failed) and does **not** have `values` set
399
500
  6. No `cdf_type: "link"` — all href/url props use `string`
400
501
  7. `required` values are JSON booleans, not strings
401
- 8. Framework, DOM, accessibility, and data-* pass-through props are excluded — `className`/`classes`/`classNames`/`rootClassName`/`prefixCls`, `style`, `id`, `role`, `tabIndex`, `aria-*` (and bare `aria`), `data-*`, polymorphic `as`/`element`/`component`, framework theming `dt`/`pt`/`ptOptions`/`unstyled`/`sx`. Discrete positional/geometric props (`top`, `bottom`, `left`, `right`, `rotation`, etc.) ARE classified as `string` design props. Common semantic props (`icon`, `items`, `actions`, `options`, `value`, `name`, `form`, `inputId`, `componentId`) are NOT excluded — classify them per their content/design/state nature.
502
+ 8. Framework, DOM, accessibility, and data-* pass-through props are excluded per the inheritance principle (rule 1) — any prop inherited from an HTML-attributes base interface and not redeclared by the component, plus `aria-*` (and bare `aria`), `data-*`, polymorphic `as`/`element`/`component`, framework theming `dt`/`pt`/`ptOptions`/`unstyled`/`sx`. A prop matching an inherited-attribute name (including `title`) that the component redeclares with its own type or meaning is NOT excluded. Discrete positional/geometric props (`top`, `bottom`, `left`, `right`, `rotation`, etc.) ARE classified as `string` design props. Common semantic props (`icon`, `items`, `actions`, `options`, `value`, `name`, `form`, `inputId`, `componentId`) are NOT excluded — classify them per their content/design/state nature.
402
503
  9. No `cdf_type: "link"` used — `link` is reserved and rejected by the CLI parser
403
504
  10. No `cdf_type: "number"` used — this is not a supported type; use `"string"` with numeric defaults. `cdf_type: "boolean"` IS valid — use it for boolean toggle props.
404
505
  11. `classify_component` includes a `rationale` object with all three sub-fields (`rationale.description`, `rationale.props`, `rationale.slots`) populated as non-empty strings.
405
506
  12. Every `classify_slot` includes a non-empty `rationale` string.
406
507
  13. `rationale.description` follows the same "Description content rules" as `description` — no internal initiative names (`INTEG-*`, `EDSI`, `DSI`, `M1`, `M2`, wave/phase references, etc.).
508
+ 14. Every `cdf_type: "token"` prop cites a line that interpolates its value into a style **and** a token reference at that use (Q2 yes, Q3 yes), and carries no `values`. If you cannot cite both, it is `enum` or `string`; if you can, it is `token` — its type annotation is not a reason to emit `string`.
509
+ 15. Every prop whose values are friendly names is `enum` with a non-empty `values` array, even when each name resolves to a design token internally; every prop for which Q1–Q3 could not be answered was emitted as `enum` (or `string`) with the ambiguity stated in `reason`, and no answerable prop was called ambiguous.
510
+ 16. Every `enum`'s `values` array is copied from evidence in the source shown (a type declaration, lookup table, or default) — none of it was invented. Where no evidence exists for the values, the `description` states so (e.g. `"WARNING: values not visible in source"`), not just `reason`.
407
511
 
408
512
  After the run completes, the developer can validate the pipeline output with:
409
513
 
@@ -0,0 +1,90 @@
1
+ # Map Tokens — Token Restriction Narrowing Skill
2
+
3
+ ## Purpose
4
+
5
+ For every design-category, token-typed prop already present in the generated CDF, decide whether there is concrete evidence to narrow it to a restricted subset (`$token.allowed`). Output one JSON tool call per line to stdout — the CLI reads it and writes each decision directly to the pipeline database. You do not write any files.
6
+
7
+ This is a narrowing fallback, not a classification step: it does not re-classify props or touch anything that isn't already a design-category `token` prop. It also does not name a candidate universe — the universe is implicit, every token in the library whose type matches the prop's `$token.kind`. Your only job is to decide, from source evidence, whether that universe should be narrowed further.
8
+
9
+ Default-path resolution is deterministic and happens in the CLI before this skill runs. A resolved `$default` is not model input for a mapping decision and never asks you to emit a mapping — a default alone does not restrict the allowed list.
10
+
11
+ ---
12
+
13
+ ## Input
14
+
15
+ All input is embedded inline in the prompt before this file:
16
+
17
+ - **Generated CDF so far** — design-category, token-typed props only, grouped by component. Every prop shown here already has `$type: "token"` and `$category: "design"`; you do not need to re-verify either.
18
+ - **Token path index** — one or more sections, each a flat list of `path · type` lines (one leaf token per line, `$value` omitted). Every section is pre-scoped: a section titled with a `$token.kind` contains every leaf token of that type and no others; a prop with that `$token.kind` draws its candidates only from the matching section. The "full tree" section — present only when at least one prop has no `$token.kind` — lists every leaf token, unscoped. Never mix candidates across sections.
19
+ - **Component source references** — the real file text for each component (bounded/truncated), rendered inline as a fenced code block, so you can look for an explicit restriction: a comment naming the valid tokens, or code that validates the prop against a fixed list of token paths. Content from files the component's own file imports (siblings) is inlined the same way, one hop deep — token-resolution logic often lives one file away from the component itself. When more sibling files were found than could be inlined, or a property has a use that fell outside the excerpt shown, the prompt says so explicitly; treat that as unknown evidence, not absence. **You have no filesystem access and no tools — a citation path is a label only, never something to open.** When a component's source couldn't be read (moved/deleted since extraction), there is no source evidence to narrow from — skip the prop.
20
+
21
+ If no token path index is provided, there is nothing to narrow against — do not emit any tool calls.
22
+
23
+ ---
24
+
25
+ ## Decision tree
26
+
27
+ For each design-category, token-typed prop shown in the "Generated CDF so far" section:
28
+
29
+ 1. **Prop type is a union of variant names** (e.g. `'primary' | 'secondary'`)? That prop should not be a `token` at all — the component receives a name and resolves it, which is the `enum` case in the classification step. Do not narrow it; narrowing cannot repair a wrong type. Emit nothing, and note in a prose line that it looks misclassified.
30
+ 2. **A default value or `tokenReference` is the prop's default, not a restriction.** `padding = tokens.spacingM` or a structured `tokenReference` tells you which token the prop *starts* on — it does not say the author may choose no other. Narrowing to that single path would leave the marketer one option, which is worse than no list. On its own, this evidence yields no tool call. If you narrow on other grounds (step 3), the default's path must be in the list.
31
+ 3. **Look for an explicit restriction** in the component source or its siblings: a comment naming the valid tokens, or code that validates/maps the prop against a fixed list of token paths (an allowlist array, a `satisfies` over specific token keys). If found, emit `$token.allowed` as the evidenced subset, scoped to tokens of the prop's `$token.kind`.
32
+ 4. **No explicit restriction?** Emit nothing. Omitting the list means "any token of this kind," which is correct and live — a guessed list is worse than none because it freezes the author's choices.
33
+ 5. **No plausible candidates at all** (no index entries of the prop's `$token.kind`)? Skip the prop.
34
+
35
+ ---
36
+
37
+ ## Output protocol
38
+
39
+ Emit one JSON object per line. The CLI parses lines starting with `{`; anything else is prose, ignored by the parser — use it freely for reasoning.
40
+
41
+ ```
42
+ {"tool":"map_token_prop","component":"<ComponentName>","prop":"<propName>","token_allowed":["colors.brand.primary","colors.brand.secondary"]}
43
+ ```
44
+
45
+ Rules:
46
+
47
+ - Emit exactly one JSON object per line. No multi-line JSON.
48
+ - Only emit a call for a prop that appears in the "Generated CDF so far" section.
49
+ - `token_allowed` is required and must be non-empty. If there's nothing to narrow to, emit no call at all.
50
+ - Every path in `token_allowed` must be an individual **leaf** path that exists verbatim in the token path index section matching the prop's `$token.kind` (or the "full tree" section, for a prop with no `$token.kind`). Never emit a group/prefix path (e.g. `colors.brand`) — the index has no entry for groups, only leaves. Never invent a path, and never substitute a variant/enum name for a real token path; if you can't find a matching path, omit that entry.
51
+ - No `$value` is provided in this step — reason from paths and `$type` only.
52
+
53
+ ---
54
+
55
+ ## Examples
56
+
57
+ ### Explicit restriction — narrow
58
+
59
+ ```ts
60
+ interface ButtonProps {
61
+ /** Accepts only the brand colour tokens: colors.brand.primary or colors.brand.secondary. */
62
+ accentColor?: string;
63
+ }
64
+ const ACCENT_TOKENS = ['colors.brand.primary', 'colors.brand.secondary'] as const;
65
+ ```
66
+
67
+ ```
68
+ accentColor is restricted to two named tokens by the doc comment and the ACCENT_TOKENS allowlist
69
+ {"tool":"map_token_prop","component":"Button","prop":"accentColor","token_allowed":["colors.brand.primary","colors.brand.secondary"]}
70
+ ```
71
+
72
+ ### No restriction evidence — emit nothing
73
+
74
+ `bgColor` is interpolated generically, with no comment or allowlist naming specific tokens, and no default. Any color token remains valid:
75
+
76
+ ```
77
+ bgColor has no restriction evidence in source — any color token remains valid, so no tool call
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Checklist
83
+
84
+ Before emitting any tool calls, verify:
85
+
86
+ 1. Every `map_token_prop` call targets a prop that appears in the "Generated CDF so far" section.
87
+ 2. Every path in `token_allowed` is a leaf path that exists verbatim in the token path index, and matches the prop's `$token.kind` — no group/prefix paths, no variant names, nothing invented.
88
+ 3. `token_allowed` is never empty.
89
+ 4. Every emitted list includes the prop's own default token when it has one.
90
+ 5. No list was emitted on the strength of a default alone, and none targets a prop whose type is a union of variant names.