@contentful/experience-design-system-generation 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.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-generation",
3
- "version": "2.27.5-dev-build-56846c8.0",
3
+ "version": "2.27.5-dev-build-347f1b0.0",
4
4
  "description": "Agent-invocation and skill-prompt engine for the Contentful Experience Design System SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,6 +89,18 @@ export interface ParsedToolCalls {
89
89
  }
90
90
  export declare function parseToolCallLines(stdout: string): ParsedToolCalls;
91
91
  export declare function parseTokenToolCallLines(stdout: string): ParsedTokenToolCalls;
92
+ export interface MapTokenPropCall {
93
+ tool: 'map_token_prop';
94
+ component: string;
95
+ prop: string;
96
+ /** Narrowed subset of tokens the prop may draw from. Required and non-empty. */
97
+ token_allowed: string[];
98
+ }
99
+ export interface ParsedMapTokenPropToolCalls {
100
+ calls: MapTokenPropCall[];
101
+ warnings: string[];
102
+ }
103
+ export declare function parseMapTokenPropToolCallLines(stdout: string): ParsedMapTokenPropToolCalls;
92
104
  export declare function resolveBinary(agent: AgentName): string;
93
105
  /**
94
106
  * Resolve the model for an agent. Explicit flag/creds value wins, then a
@@ -220,6 +220,51 @@ export function parseTokenToolCallLines(stdout) {
220
220
  }
221
221
  return { calls, warnings };
222
222
  }
223
+ function isStringArray(value) {
224
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
225
+ }
226
+ export function parseMapTokenPropToolCallLines(stdout) {
227
+ const calls = [];
228
+ const warnings = [];
229
+ for (const raw of stdout.split('\n')) {
230
+ const line = raw.trim();
231
+ if (!line.startsWith('{'))
232
+ continue;
233
+ let obj;
234
+ try {
235
+ obj = JSON.parse(line);
236
+ }
237
+ catch {
238
+ warnings.push(`unparseable line: ${line.slice(0, 120)}`);
239
+ continue;
240
+ }
241
+ if (typeof obj !== 'object' || obj === null || !('tool' in obj))
242
+ continue;
243
+ const rec = obj;
244
+ if (rec.tool !== 'map_token_prop')
245
+ continue; // not a map-tokens call — skip silently
246
+ if (typeof rec.component !== 'string' || !rec.component) {
247
+ warnings.push('map_token_prop missing component — skipped');
248
+ continue;
249
+ }
250
+ if (typeof rec.prop !== 'string' || !rec.prop) {
251
+ warnings.push(`map_token_prop '${rec.component}': missing prop — skipped`);
252
+ continue;
253
+ }
254
+ if (!isStringArray(rec.token_allowed) || rec.token_allowed.length === 0) {
255
+ warnings.push(`map_token_prop '${rec.component}.${String(rec.prop)}': missing or empty token_allowed — skipped`);
256
+ continue;
257
+ }
258
+ const call = {
259
+ tool: 'map_token_prop',
260
+ component: rec.component,
261
+ prop: rec.prop,
262
+ token_allowed: rec.token_allowed,
263
+ };
264
+ calls.push(call);
265
+ }
266
+ return { calls, warnings };
267
+ }
223
268
  // --- Agent invocation ---
224
269
  const AGENT_BINARIES = {
225
270
  claude: 'claude',
@@ -1,9 +1,9 @@
1
1
  export { AGENT_NAMES, DEFAULT_AGENT_NAME, isAgentName } from './agent-names.js';
2
2
  export type { AgentName } from './agent-names.js';
3
- export { buildArgs, checkAgentAuth, describeAgentFailure, extractSentinelOutput, parseSelectToolCallLines, parseTokenToolCallLines, parseToolCallLines, resolveAgentModel, resolveBinary, runAgent, } from './agent-runner.js';
4
- export type { AgentAuthStatus, AgentDebugEvent, AgentRunResult, ClassifyComponentCall, ClassifyPropCall, ClassifySlotCall, ExcludePropCall, ParsedSelectToolCalls, ParsedTokenToolCalls, ParsedToolCalls, RejectComponentCall, SelectComponentCall, SelectToolCall, SetGroupCall, SetTokenCall, ToolCall, TokenToolCall, } from './agent-runner.js';
3
+ export { buildArgs, checkAgentAuth, describeAgentFailure, extractSentinelOutput, parseMapTokenPropToolCallLines, parseSelectToolCallLines, parseTokenToolCallLines, parseToolCallLines, resolveAgentModel, resolveBinary, runAgent, } from './agent-runner.js';
4
+ export type { AgentAuthStatus, AgentDebugEvent, AgentRunResult, ClassifyComponentCall, ClassifyPropCall, ClassifySlotCall, ExcludePropCall, MapTokenPropCall, ParsedMapTokenPropToolCalls, ParsedSelectToolCalls, ParsedTokenToolCalls, ParsedToolCalls, RejectComponentCall, SelectComponentCall, SelectToolCall, SetGroupCall, SetTokenCall, ToolCall, TokenToolCall, } from './agent-runner.js';
5
5
  export { createLocalCliAgentInvoker } from './agent-invoker.js';
6
6
  export type { AgentInvoker, CreateLocalCliAgentInvokerOptions, InvokeAgentOptions } from './agent-invoker.js';
7
7
  export { buildPrompt, formatCustomPromptBanner, resolveSkillPath } from './prompt-builder.js';
8
- export type { Mode, PromptOptions, Skill } from './prompt-builder.js';
8
+ export type { ComponentSourceRef, GeneratedCdf, Mode, PromptOptions, Skill } from './prompt-builder.js';
9
9
  export { formatGenerateProgressLine } from './progress.js';
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Agent identity
2
2
  export { AGENT_NAMES, DEFAULT_AGENT_NAME, isAgentName } from './agent-names.js';
3
3
  // Agent invocation (low-level)
4
- export { buildArgs, checkAgentAuth, describeAgentFailure, extractSentinelOutput, parseSelectToolCallLines, parseTokenToolCallLines, parseToolCallLines, resolveAgentModel, resolveBinary, runAgent, } from './agent-runner.js';
4
+ export { buildArgs, checkAgentAuth, describeAgentFailure, extractSentinelOutput, parseMapTokenPropToolCallLines, parseSelectToolCallLines, parseTokenToolCallLines, parseToolCallLines, resolveAgentModel, resolveBinary, runAgent, } from './agent-runner.js';
5
5
  // Agent invocation (interface)
6
6
  export { createLocalCliAgentInvoker } from './agent-invoker.js';
7
7
  // Prompt building
@@ -1,6 +1,41 @@
1
- /** `components` — classify component props; `tokens` — classify design tokens; `select` — decide whether a component belongs in Contentful Experience Orchestration */
2
- export type Skill = 'components' | 'tokens' | 'select';
1
+ import { type CDFComponentEntry } from '@contentful/experience-design-system-types';
2
+ /** `components` — classify component props; `tokens` — classify design tokens; `select` — decide whether a component belongs in Contentful Experience Orchestration; `map-tokens` — suggest `$token.allowed` restrictions for design-category token props */
3
+ export type Skill = 'components' | 'tokens' | 'select' | 'map-tokens';
3
4
  export type Mode = 'autonomous';
5
+ /**
6
+ * A component name paired with the source file it was extracted from, for
7
+ * token-mapping evidence. `content` is the real file text (bounded, see
8
+ * MAX_COMPONENT_SOURCE_CHARS) — this pipeline is agent-fs-free by design (see
9
+ * generate-components.md's "you do not write any files"), so the caller must
10
+ * read the file itself and inline the text here rather than handing the
11
+ * agent a path and expecting it to open the file. `null` when the file
12
+ * couldn't be read (moved/deleted since extraction) — callers fall back to
13
+ * inferring from the prop name and $token.kind alone in that case.
14
+ * `siblingFiles` carries the content of files the source file relatively
15
+ * imports (e.g. a co-located `.styles.ts`) — token-resolution logic often
16
+ * lives one hop away from the component file itself. `truncatedSiblingCount`
17
+ * is set when the caller found more resolvable sibling files than it inlines
18
+ * (see MAX_SIBLING_FILES) — surfaced in the prompt so the classifier knows
19
+ * evidence was dropped rather than that the search came up empty.
20
+ */
21
+ export interface ComponentSourceRef {
22
+ component: string;
23
+ sourcePath: string;
24
+ content: string | null;
25
+ siblingFiles?: Array<{
26
+ path: string;
27
+ content: string;
28
+ }>;
29
+ truncatedSiblingCount?: number;
30
+ /**
31
+ * Properties with at least one use that fell outside the excerpt budget of
32
+ * the file it sits in. The reader cannot see that use, so its absence is a
33
+ * gap in the evidence, not evidence of absence.
34
+ */
35
+ usesNotShown?: string[];
36
+ }
37
+ /** Plain-data shape of the CDF generated so far — component name -> component entry. */
38
+ export type GeneratedCdf = Record<string, CDFComponentEntry>;
4
39
  /**
5
40
  * Render the warning banner shown when a custom skill prompt is active.
6
41
  * Always cites the bundled invariants that the override bypasses so the
@@ -19,6 +54,12 @@ export interface PromptOptions {
19
54
  outDir: string;
20
55
  /** For components skill only: the single component's name (used in error messages). */
21
56
  componentName?: string;
57
+ /** For map-tokens skill: the CDF generated so far. Filtered internally to design-category token-typed props only. */
58
+ generatedCdf?: GeneratedCdf;
59
+ /** For map-tokens skill: the full DTCG token tree. Flattened internally to a path+`$type` index, with `$value` stripped. */
60
+ tokenTree?: Record<string, unknown>;
61
+ /** Component source file references — consumption evidence for the components skill, explicit-restriction evidence (comments, allowlists) for map-tokens. */
62
+ componentSourceRefs?: ComponentSourceRef[];
22
63
  /**
23
64
  * Feature 8: custom prompt path override. When set, this absolute or relative
24
65
  * `.md` path is read in place of the bundled skill file. The bundled-prompt
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { flattenDTCG } from '@contentful/experience-design-system-types';
5
6
  /**
6
7
  * Render the warning banner shown when a custom skill prompt is active.
7
8
  * Always cites the bundled invariants that the override bypasses so the
@@ -16,6 +17,7 @@ const SKILL_FILES = {
16
17
  components: 'generate-components.md',
17
18
  tokens: 'generate-tokens.md',
18
19
  select: 'select-components.md',
20
+ 'map-tokens': 'map-tokens.md',
19
21
  };
20
22
  export async function buildPrompt(options) {
21
23
  const skillContent = await readSkillFile(options.skill, options.skillPathOverride);
@@ -68,6 +70,11 @@ function inferFenceLang(filename) {
68
70
  ts: 'ts',
69
71
  mts: 'ts',
70
72
  cts: 'ts',
73
+ tsx: 'tsx',
74
+ jsx: 'jsx',
75
+ vue: 'vue',
76
+ svelte: 'svelte',
77
+ astro: 'astro',
71
78
  scss: 'scss',
72
79
  sass: 'scss',
73
80
  css: 'css',
@@ -76,8 +83,53 @@ function inferFenceLang(filename) {
76
83
  };
77
84
  return map[ext] ?? 'text';
78
85
  }
86
+ /**
87
+ * Keeps only design-category, token-typed props per component (dropping
88
+ * components left with none), alongside the distinct `$token.kind` values
89
+ * seen among them and whether any such prop has no `$token.kind` at all —
90
+ * collected in the same pass rather than a second walk of the raw CDF.
91
+ */
92
+ function filterDesignTokenProps(cdf) {
93
+ const result = {};
94
+ const kinds = new Set();
95
+ let hasUnscoped = false;
96
+ for (const [componentName, component] of Object.entries(cdf)) {
97
+ const properties = component.$properties;
98
+ if (!properties)
99
+ continue;
100
+ const filteredProps = {};
101
+ for (const [propName, prop] of Object.entries(properties)) {
102
+ if (prop.$type === 'token' && prop.$category === 'design') {
103
+ const { '$token.allowed': _tokenAllowed, ...rest } = prop;
104
+ filteredProps[propName] = rest;
105
+ const kind = prop['$token.kind'];
106
+ if (typeof kind === 'string' && kind.length > 0) {
107
+ kinds.add(kind);
108
+ }
109
+ else {
110
+ hasUnscoped = true;
111
+ }
112
+ }
113
+ }
114
+ if (Object.keys(filteredProps).length > 0) {
115
+ result[componentName] = { ...component, $properties: filteredProps };
116
+ }
117
+ }
118
+ return { filtered: result, kinds: [...kinds].sort(), hasUnscoped };
119
+ }
120
+ /** One line per candidate, e.g. `colors.brand.primary · color` — legible and countable, unlike nested JSON. */
121
+ function formatTokenCandidateLines(entries) {
122
+ return entries.map((entry) => `${entry.path} · ${entry.$type}`).join('\n');
123
+ }
124
+ /** Renders one kind-scoped (or, for `kind: null`, full-tree) candidate section. Sections are cumulative, never merged, so each stays independently legible. */
125
+ function renderTokenCandidateSection(kind, entries) {
126
+ const label = kind
127
+ ? `Token path index — ${kind} candidates only`
128
+ : 'Token path index — full tree (no $token.kind to scope by)';
129
+ return `${label}, one leaf token per line as \`path · type\`, no \`$value\`:\n${formatTokenCandidateLines(entries)}`;
130
+ }
79
131
  function buildPreamble(options) {
80
- const { skill, rawComponentsInline, rawTokensInline, rawTokensFilename, tokensInline, tokenMapInline } = options;
132
+ const { skill, rawComponentsInline, rawTokensInline, rawTokensFilename, tokensInline, tokenMapInline, generatedCdf, tokenTree, componentSourceRefs, } = options;
81
133
  const sections = [];
82
134
  if (rawComponentsInline) {
83
135
  sections.push(`Raw component data (JSON):\n\`\`\`json\n${rawComponentsInline}\n\`\`\``);
@@ -93,6 +145,56 @@ function buildPreamble(options) {
93
145
  if (tokenMapInline) {
94
146
  sections.push(`Token-name sidecar (raw name → DTCG path):\n\`\`\`json\n${tokenMapInline}\n\`\`\``);
95
147
  }
148
+ const filteredTokenProps = generatedCdf ? filterDesignTokenProps(generatedCdf) : undefined;
149
+ if (filteredTokenProps && Object.keys(filteredTokenProps.filtered).length > 0) {
150
+ sections.push(`Generated CDF so far — design-category token props only (JSON):\n\`\`\`json\n${JSON.stringify(filteredTokenProps.filtered)}\n\`\`\``);
151
+ }
152
+ if (tokenTree) {
153
+ const index = flattenDTCG(tokenTree, '').sort((a, b) => a.path.localeCompare(b.path));
154
+ if (index.length > 0) {
155
+ if (filteredTokenProps) {
156
+ const { kinds, hasUnscoped } = filteredTokenProps;
157
+ for (const kind of kinds) {
158
+ const scoped = index.filter((entry) => entry.$type === kind);
159
+ if (scoped.length > 0)
160
+ sections.push(renderTokenCandidateSection(kind, scoped));
161
+ }
162
+ if (hasUnscoped)
163
+ sections.push(renderTokenCandidateSection(null, index));
164
+ }
165
+ else {
166
+ sections.push(renderTokenCandidateSection(null, index));
167
+ }
168
+ }
169
+ }
170
+ if (componentSourceRefs && componentSourceRefs.length > 0) {
171
+ const withContent = componentSourceRefs.filter((ref) => ref.content != null);
172
+ const withoutContent = componentSourceRefs.filter((ref) => ref.content == null);
173
+ if (withContent.length > 0) {
174
+ const blocks = withContent.map((ref) => {
175
+ const mainBlock = `#### ${ref.component} (\`${ref.sourcePath}\`)\n\`\`\`${inferFenceLang(ref.sourcePath)}\n${ref.content}\n\`\`\``;
176
+ const siblingBlocks = (ref.siblingFiles ?? []).map((sibling) => `##### ${ref.component} — imported file \`${sibling.path}\`\n\`\`\`${inferFenceLang(sibling.path)}\n${sibling.content}\n\`\`\``);
177
+ const truncationNote = ref.truncatedSiblingCount && ref.truncatedSiblingCount > 0
178
+ ? [
179
+ `##### ${ref.component} — +${ref.truncatedSiblingCount} more imported file${ref.truncatedSiblingCount === 1 ? '' : 's'} not shown (evidence may be incomplete)`,
180
+ ]
181
+ : [];
182
+ const notShownNote = ref.usesNotShown && ref.usesNotShown.length > 0
183
+ ? [
184
+ `##### ${ref.component} — uses not shown: ${ref.usesNotShown.join(', ')}\n\nThe files above are excerpts, and each of these properties has at least one use that fell outside the excerpt budget. Treat their consumption as unknown, not absent: do not classify them \`token\` on what is shown, and do not conclude they are unread.`,
185
+ ]
186
+ : [];
187
+ return [mainBlock, ...siblingBlocks, ...truncationNote, ...notShownNote].join('\n\n');
188
+ });
189
+ sections.push(`### Component source references\n\n${blocks.join('\n\n')}`);
190
+ }
191
+ if (withoutContent.length > 0) {
192
+ const guidance = skill === 'components'
193
+ ? 'there is no consumption evidence for these, so `token` cannot be earned for any of their props — classify from the prop signature alone, and any prop that would otherwise be enum-versus-token is `enum` (or `string`)'
194
+ : 'there is no source evidence to narrow from — emit nothing for their props';
195
+ sections.push(`Component source unavailable for (JSON) — ${guidance}:\n\`\`\`json\n${JSON.stringify(withoutContent.map((ref) => ({ component: ref.component, sourcePath: ref.sourcePath })))}\n\`\`\``);
196
+ }
197
+ }
96
198
  const inputBlock = sections.length > 0 ? `\n\n${sections.join('\n\n')}` : '';
97
199
  if (skill === 'components') {
98
200
  return buildComponentsAutonomousPreamble(inputBlock);
@@ -100,6 +202,9 @@ function buildPreamble(options) {
100
202
  if (skill === 'select') {
101
203
  return buildSelectAutonomousPreamble(inputBlock);
102
204
  }
205
+ if (skill === 'map-tokens') {
206
+ return buildMapTokensAutonomousPreamble(inputBlock);
207
+ }
103
208
  return buildTokensAutonomousPreamble(inputBlock);
104
209
  }
105
210
  function buildComponentsAutonomousPreamble(inputBlock) {
@@ -110,8 +215,6 @@ Context: You are classifying a React component for **Contentful Experience Orche
110
215
  - **content**: the data a content editor fills in (text, images, URLs, rich text)
111
216
  - **state**: runtime behavioral flags (disabled, loading, expanded, identifiers)
112
217
 
113
- For props with complex TypeScript types (named types, enums): reason from the prop name and type name to classify them. Do not automatically exclude a prop just because its type is a named reference — infer the likely values and classify it as enum if it controls appearance.
114
-
115
218
  Your task: classify every prop and slot in the component below. Apply all judgment calls yourself — do not pause to ask for confirmation. Include a "description" field on each tool call to document your reasoning so the developer can review it afterward.
116
219
 
117
220
  All input data is provided inline below — do not read any additional files.${inputBlock}
@@ -138,10 +241,12 @@ Rules:
138
241
  - Every slot in the input must have exactly one classify_slot call.
139
242
  - Valid cdf_type values: string, richtext, media, enum, token, boolean
140
243
  - Valid cdf_category values: content, design, state
141
- - For enum type, always include "values" (non-empty string array).
142
- - For token type, always include "token_kind" (DTCG \$type, e.g. "color").
143
- - href and URL props → cdf_type "string", cdf_category "content". Do NOT use cdf_type "link" — it is not valid.
144
- - Framework internals (ref, event handlers, test IDs) → exclude_prop.
244
+ - For enum type, always include \`values\` (non-empty string array of the variant names the prop accepts).
245
+ - For token type, always include \`token_kind\` (DTCG \$type, e.g. "color"). **Never emit \`values\` for \`cdf_type: "token"\` props** — an enum prop's list holds variant names; a token prop's list holds design token paths, produced separately and never by you. Emitting \`values\` on a token prop makes the definition invalid.
246
+ - Never emit both "values" and "token_kind" on one prop.
247
+ - \`enum\` vs \`token\` is decided by three questions answered in order from the source shown — never from the TypeScript type. Q1: does the component look the value up, switch on it, or have a vocabulary for it (\`tokens[x]\`, \`switch (variant)\`, \`allowedValues\`)? yes → \`enum\`, stop.
248
+ - Q2: is the value written straight into a style or attribute (\`rx={radius}\`, \`padding: \${padding}\`)? no → not \`token\`, apply the type rules.
249
+ - Q3: is there a design-token reference at that use — a \`tokens.*\` parameter default, a \`tokenReference\`, an inline \`tokens.*\` / \`var(--*)\`? yes → \`token\` (cite both lines in "reason"); no → \`string\`. "Ambiguity resolves to \`enum\`" applies only when Q1–Q3 cannot be answered from the source shown.
145
250
  - CSS design props (className, style, styles, positional/geometric props: top, bottom, left, right, rotation, offset, etc.) → classify_prop, cdf_type: "string", cdf_category: "design".
146
251
  - On classify_component, "rationale" fields are operator-facing (read-only) but may surface in customer-facing exports. The "rationale.description" field is subject to the description content rules in the skill prompt (no internal initiative names). "rationale.props" and "rationale.slots" describe your reasoning about scope; "classify_slot.rationale" explains why each slot was kept.
147
252
  - On classify_prop, "reason" is REQUIRED and is the LLM's internal rationale — shown to the developer reviewing the import, never to end-users. "description" is the customer-facing copy and is subject to the description content rules in the skill prompt. Keep them distinct: "description" is short and customer-facing; "reason" explains your reasoning in detail.
@@ -173,6 +278,31 @@ Rules:
173
278
  - Emit exactly one tool call per input component. The "name" field must match a component name from the input array exactly. Tool calls may appear in any order.
174
279
  - You may emit prose lines (not starting with {) to reason before each tool call — they are ignored by the parser.`;
175
280
  }
281
+ function buildMapTokensAutonomousPreamble(inputBlock) {
282
+ return `You are running as part of the experience-design-system-cli generate pipeline in AUTONOMOUS mode. The developer is not present to answer questions.
283
+
284
+ Context: The components below already have design-category, token-typed props (\`$token.kind\` set). Your task is to decide, from source evidence, whether to narrow each one to a restricted subset (\`token_allowed\`) of the tokens matching its \`$token.kind\`. Apply all judgment calls yourself — do not pause to ask for confirmation.
285
+
286
+ All input data is provided inline below — do not read any additional files.${inputBlock}
287
+
288
+ ## Output protocol
289
+
290
+ Do NOT write any files or emit any JSON blobs. Instead, emit one JSON object per line to stdout for each prop you narrow. The CLI reads your stdout line by line and writes each decision directly to the pipeline database.
291
+
292
+ The one tool call you may emit:
293
+
294
+ \`\`\`
295
+ {"tool":"map_token_prop","component":"<ComponentName>","prop":"<propName>","token_allowed":["colors.brand.primary","colors.brand.secondary"]}
296
+ \`\`\`
297
+
298
+ Rules:
299
+ - Emit exactly one JSON object per line. No multi-line JSON. No markdown fences around the lines.
300
+ - Only emit a call for a prop that appears in the "Generated CDF so far" section.
301
+ - Each "Token path index" section below is already scoped to one \`$token.kind\` — a prop only draws candidates from the section matching its own \`$token.kind\` (or the "full tree" section, for a prop with no \`$token.kind\`). Never cross sections, never emit a group/prefix path, and never invent a path — every entry in \`token_allowed\` must exist verbatim in the matching section.
302
+ - \`token_allowed\` is required and must be non-empty when the call is emitted.
303
+ - For the judgment call of whether a prop should be narrowed at all, follow the decision tree in the map-tokens skill file: it explains defaults vs. restrictions, misclassified variant-name props, and when to emit nothing.
304
+ - You may emit prose lines (not starting with \`{\`) anywhere — they are ignored by the parser and serve as your reasoning log.`;
305
+ }
176
306
  function buildTokensAutonomousPreamble(inputBlock) {
177
307
  return `You are running as part of the experience-design-system-cli generate pipeline in AUTONOMOUS mode. The developer is not present to answer questions.
178
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-generation",
3
- "version": "2.27.5-dev-build-56846c8.0",
3
+ "version": "2.27.5-dev-build-347f1b0.0",
4
4
  "description": "Agent-invocation and skill-prompt engine for the Contentful Experience Design System SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "skills/"
23
23
  ],
24
24
  "dependencies": {
25
- "@contentful/experience-design-system-types": "2.27.5-dev-build-56846c8.0"
25
+ "@contentful/experience-design-system-types": "2.27.5-dev-build-347f1b0.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@tsconfig/node24": "^24.0.4",
@@ -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.