@contentful/experience-design-system-generation 2.27.5-dev-build-6ae590e.0 → 2.27.5-dev-build-67ce1cd.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 +2 -2
- package/dist/src/agent-runner.d.ts +12 -0
- package/dist/src/agent-runner.js +45 -0
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/prompt-builder.d.ts +43 -2
- package/dist/src/prompt-builder.js +137 -7
- package/package.json +3 -3
- package/skills/generate-components.md +139 -35
- package/skills/map-tokens.md +90 -0
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-
|
|
3
|
+
"version": "2.27.5-dev-build-67ce1cd.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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@contentful/experience-design-system-types": "workspace:*"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tsconfig/node24": "^24.0.
|
|
36
|
+
"@tsconfig/node24": "^24.0.4",
|
|
37
37
|
"@types/node": "^24.0.3",
|
|
38
38
|
"eslint": "^9.39.5",
|
|
39
39
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -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
|
package/dist/src/agent-runner.js
CHANGED
|
@@ -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',
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
|
142
|
-
- For token type, always include
|
|
143
|
-
-
|
|
144
|
-
-
|
|
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-
|
|
3
|
+
"version": "2.27.5-dev-build-67ce1cd.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,10 +22,10 @@
|
|
|
22
22
|
"skills/"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@contentful/experience-design-system-types": "2.27.5-dev-build-
|
|
25
|
+
"@contentful/experience-design-system-types": "2.27.5-dev-build-67ce1cd.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@tsconfig/node24": "^24.0.
|
|
28
|
+
"@tsconfig/node24": "^24.0.4",
|
|
29
29
|
"@types/node": "^24.0.3",
|
|
30
30
|
"eslint": "^9.39.5",
|
|
31
31
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -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.
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
167
|
-
|
|
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
|
|
186
|
-
| `Variant` / `variant` prop | Usually a visual design variant. → `enum`, `cdf_category: "design"`.
|
|
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
|
-
##
|
|
351
|
+
## Resolving `token_kind`
|
|
237
352
|
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 (`
|
|
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 —
|
|
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.
|