@contentful/experience-design-system-generation 2.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,427 @@
1
+ # Generate Components — Classification Skill
2
+
3
+ ## Purpose
4
+
5
+ Classify every prop and slot in the component definition provided inline for use in **Contentful Experience Orchestration**. Output one JSON tool call per line to stdout. The CLI reads your stdout and writes each decision directly to the pipeline database — you do not write any files.
6
+
7
+ ---
8
+
9
+ ## What is Contentful Experience Orchestration?
10
+
11
+ Contentful Experience Orchestration is a Contentful product that enables **marketers** to manage both the content and visual presentation of digital experiences — web pages, multi-channel — entirely within Contentful. The entity being defined here is a **Component Type**: the schema that tells Contentful what a marketer can configure for this UI component.
12
+
13
+ A Component Type has two kinds of configurable properties:
14
+
15
+ - **Design Properties** — values that control *how the component looks*: color scheme, visual variant (primary/secondary/ghost), size (sm/md/lg), spacing, layout orientation, background color, font style, border style, any visual toggle that changes appearance. These are the values a designer sets once and a marketer may override. Think: "what would a designer put in a design token or a style guide?"
16
+ - **Content Properties** — values that are *data the component displays*: labels, headings, body text, rich text, images, media, URLs, counts, IDs used for CMS lookups, locales. These are filled by editors with real CMS content. Think: "what does a copywriter or content editor fill in?"
17
+
18
+ The third category:
19
+ - **State Properties** — values that control *interactive or behavioral state*: disabled, loading, expanded, isOpen, isSearchVisible. These are runtime behavioral flags, not visual design nor content.
20
+
21
+ Getting this right matters: Contentful uses the category to decide where a property appears in the editor UI. Design properties appear in the design panel, content properties appear in the content panel.
22
+
23
+ ---
24
+
25
+ ## Prerequisites — Input
26
+
27
+ All input is embedded inline in the prompt before this file:
28
+
29
+ - **Raw component data** — `RawComponentDefinition[]` (one component for this run)
30
+ - **DTCG token data** — full token tree, if provided
31
+ - **Token-name sidecar** — raw CSS custom property name → DTCG dot-notation path, if provided
32
+
33
+ ```typescript
34
+ interface RawPropDefinition {
35
+ name: string;
36
+ type: string; // raw TypeScript type, e.g. "'primary' | 'secondary'"
37
+ required: boolean;
38
+ category?: 'content' | 'design' | 'state'; // pre-classified — verify, do not blindly trust
39
+ allowedValues?: string[];
40
+ defaultValue?: string;
41
+ description?: string;
42
+ tokenReference?: string; // raw token name, e.g. "--brand-primary"
43
+ }
44
+
45
+ interface RawSlotDefinition {
46
+ name: string;
47
+ description?: string;
48
+ allowedComponents?: string[];
49
+ }
50
+
51
+ interface RawComponentDefinition {
52
+ name: string;
53
+ source: string;
54
+ props: RawPropDefinition[];
55
+ slots: RawSlotDefinition[];
56
+ }
57
+ ```
58
+
59
+ The `category` field on each prop is a pre-classification hint from static analysis heuristics.
60
+ It is correct approximately 80% of the time for simple props. You should:
61
+ - Trust it for obvious cases (event handlers excluded, text labels as content)
62
+ - Override it when your domain knowledge indicates otherwise
63
+ - NEVER produce zero output — if you disagree with all hints, explain why in descriptions
64
+
65
+ ---
66
+
67
+ ## Target schema
68
+
69
+ The CLI assembles your output into CDF (Component Definition Format), a JSON schema with `$schema: "https://contentful.com/schemas/cdf/v1"`. Each component you classify produces a CDF component entry (`$type: "component"`) in the pipeline database. Properties carry `$category` (`content`, `design`, or `state`) and a `$type`. You do not produce this JSON directly — emit tool calls and the CLI writes the DB columns.
70
+
71
+ ## Output protocol
72
+
73
+ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not starting with `{` are treated as prose and ignored by the parser — use them freely for reasoning.
74
+
75
+ **Four tool calls:**
76
+
77
+ ```
78
+ {"tool":"classify_component","description":"<required: one-sentence description of the component>","rationale":{"description":"<why this component is classified this way>","props":"<why these props were chosen / excluded>","slots":"<why these slots were chosen / excluded>"}}
79
+
80
+ {"tool":"classify_prop","prop":"<propName>","cdf_type":"<type>","cdf_category":"<category>","required":<bool>,"description":"<short customer-facing description>","reason":"<full internal rationale; not customer-facing>","values":["a","b"],"token_kind":"color","default":"<value>"}
81
+
82
+ {"tool":"exclude_prop","prop":"<propName>","reason":"<why excluded>"}
83
+
84
+ {"tool":"classify_slot","slot":"<slotName>","required":<bool>,"allowed_components":["ComponentName"],"description":"<short customer-facing description>","rationale":"<why this slot was kept / its role>"}
85
+ ```
86
+
87
+ **Rules:**
88
+ - Emit exactly one JSON object per line. No multi-line JSON.
89
+ - Every prop in the input must produce exactly one call: `classify_prop` OR `exclude_prop`.
90
+ - Every slot must produce exactly one `classify_slot` call.
91
+ - 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.
93
+ - `token_kind` is required for `cdf_type: "token"` — must be a DTCG `$type` string, e.g. `"color"`.
94
+ - `required` must be a JSON boolean (`true`/`false`), not a string.
95
+ - `description` on `classify_prop` is customer-facing — keep it short and subject to the description content rules below.
96
+ - `reason` on `classify_prop` is **required** and is your internal rationale — shown to the developer reviewing the import, never to end-users. Use it to explain your reasoning in detail. The customer-facing description content rules below apply to `description` only, not to `reason`.
97
+ - `rationale` on `classify_component` is **REQUIRED**. It is an object with three REQUIRED string sub-fields:
98
+ - `rationale.description` — why this component is classified the way it is (its purpose, where it fits in the design system, atom/molecule/organism reasoning). **Subject to the same "Description content rules" as the `description` field — no internal initiative names, no `INTEG-*`, no `EDSI`/`DSI`/`M1`/`M2`/wave/phase references.**
99
+ - `rationale.props` — operator-facing explanation of which props you accepted vs excluded and why. Audience is the developer reviewing the import (not the customer), so you may discuss types, framework internals, and category corrections in technical terms. Do not include internal initiative names.
100
+ - `rationale.slots` — operator-facing explanation of which slots you kept vs collapsed and why. Same audience and rules as `rationale.props`.
101
+ - All three sub-fields are required strings, minimum one sentence each. Never emit an empty string. If the component has zero slots, `rationale.slots` should state that explicitly (e.g. `"No slots — the component renders no injectable regions."`).
102
+ - `rationale` on `classify_slot` is **REQUIRED**. It is a single string explaining why this slot exists and what role it plays in the component (operator-facing; minimum one sentence). Same audience and rules as `rationale.props` / `rationale.slots` above.
103
+
104
+ **Description content rules (CRITICAL — applies to every `description` field on `classify_component`, `classify_prop`, and `classify_slot`, AND to `rationale.description` on `classify_component`):**
105
+
106
+ - Write **customer-facing technical descriptions**. The audience is a third-party developer or content editor configuring this component in Contentful — not a Contentful engineer.
107
+ - **Never** reference internal Contentful initiatives, project code names, sprint or roadmap labels, product development phases, or implementation milestones. This includes (non-exhaustive) terms like `P1`, `P2`, `P3`, `M1`, `M2`, `EXT-*`, `INTEG-*`, `DSI`, `EDSI`, `CDF compliance`, `wave 1`, `wave 2`, `phase 1`, `phase 2`, "for compliance with…", "to support the … initiative", or any internal-sounding rationale.
108
+ - Do not invent rationale. If you do not know **why** a prop exists, describe **what** it does (its observable effect on the component) — never guess at organizational context.
109
+ - Descriptions should explain WHAT the prop/component does in terms a developer reading the public component catalog would understand. Stick to behavior, appearance, and configuration semantics.
110
+
111
+ ---
112
+
113
+ ## Valid cdf_type values
114
+
115
+ Exactly **6** valid types:
116
+
117
+ | cdf_type | Use case |
118
+ |---|---|
119
+ | `string` | Plain text, URLs, href props, numbers (as string), any string-shaped value |
120
+ | `richtext` | Formatted text, HTML, ReactNode used as markup |
121
+ | `media` | Images, videos, media assets |
122
+ | `enum` | Fixed set of string choices — requires `values` |
123
+ | `token` | Design-token-linked prop — requires `token_kind` |
124
+ | `boolean` | Boolean toggle props (visible, disabled, enabled, etc.) |
125
+
126
+ > **IMPORTANT: No `number` type.** The design-systems API only supports the `String` design property variant for numeric values. All numeric props must use `cdf_type: "string"` with the number as a string default (e.g. `"0"`, `"100"`). Boolean props can now use `cdf_type: "boolean"` directly.
127
+
128
+ > **Avoid `link` type for simple URL props.** Props named `href`, `url`, or holding plain URL strings → `cdf_type: "string"`, `cdf_category: "content"`. Reserve `link` for props that hold a reference to another Contentful entry.
129
+
130
+ ---
131
+
132
+ ## Valid cdf_category values
133
+
134
+ | cdf_category | Use case |
135
+ |---|---|
136
+ | `content` | Data the component *displays* — what a copywriter or editor fills in: text, labels, headings, body copy, rich text, images, media, URLs, link targets, counts, locale |
137
+ | `design` | Values that control *how the component looks* — what a designer sets: color, size (sm/md/lg), variant (primary/secondary/ghost), layout orientation, alignment, background, visual toggles (imageOnLeft, enableEffect), design tokens |
138
+ | `state` | Runtime behavioral or interactive flags — not visible in the editor's design or content panel: disabled, loading, expanded, isOpen, isSearchVisible, preview, identifiers used for analytics/tracking (componentId, sectionKey, componentName) |
139
+
140
+ The pre-classified `category` in the raw input is a starting point — correct it when it is wrong. Contentful uses this category to decide where the property appears in the editor UI, so accuracy matters.
141
+
142
+ ---
143
+
144
+ ## Mapping guidance — Classification decision tree
145
+
146
+ For each `RawPropDefinition`, apply in order:
147
+
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:
149
+ - 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`
151
+ - 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
+ - Data attributes: any `data-*` prop
153
+ - **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
+ - **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.
156
+ 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
+ - `icon` / `leftIcon` / `rightIcon` / `prefixIcon` / `suffixIcon` — slot or `string` (icon name); see slot guidance below
158
+ - `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.
159
+ - `value` (the bare prop, not `modelValue`) — content prop, usually `string` (or `enum` if from a fixed set). Note: Vue's `modelValue` / `modelModifiers` are excluded by pre-classify because they're v-model framework wiring.
160
+ - `name` — content prop, usually `string`. Treat it as semantic component data, not as a DOM pass-through.
161
+ - `form` (when not the literal `<form>` HTML attribute) — typically content; classify as `string` unless it's a complex form-config object
162
+ - `inputId` / `componentId` — these CAN be content (anchor IDs, marketer-set tracking refs). Classify as `string`, `cdf_category: "content"` when the type is a plain string. Only exclude if the prop is clearly internal (e.g. typed as a generated React ID).
163
+ - `accessibleNameRef` / `accessibleDescriptionRef` (web components) — these are ID references for a11y wiring; classify as `string`, `cdf_category: "state"` (behavioral wiring, not design or content).
164
+ - `eventDetails` / similar telemetry props — `cdf_category: "state"`.
165
+ 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`.
168
+ 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"`.
170
+ 8. **Media/image type** (`ImageProps`, `MediaSource`, asset types)? → `cdf_type: "media"`.
171
+ 9. **Rich text / markup** (`ReactNode` used as content, HTML string)? → `cdf_type: "richtext"`.
172
+ 10. **Complex type — resolve before excluding** (see below).
173
+
174
+ ---
175
+
176
+ ## Resolving complex types — do not exclude without reasoning
177
+
178
+ A prop with a complex TypeScript type is **not automatically excluded**. Many props that appear complex carry real marketer-configurable information. Before excluding, ask: *"Could a marketer set this value in Contentful?"* If yes, classify it.
179
+
180
+ **Common resolvable patterns:**
181
+
182
+ | Raw type pattern | How to resolve |
183
+ |---|---|
184
+ | `'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. |
187
+ | `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
+ | `ExperienceConfiguration<Variant>` / deep generic | Personalization config — → `exclude_prop`, reason: `"personalization configuration — framework internal"` |
189
+ | `React.Dispatch<...>` / setter | State setter — → `exclude_prop`, reason: `"React state setter — framework internal"` |
190
+ | `React.RefObject<...>` / `ref` | → `exclude_prop`, reason: `"ref — framework internal"` |
191
+ | `() => void` / callback | → `exclude_prop`, reason: `"callback function — framework internal"` |
192
+ | `ReactNode` used as a slot-like prop (children, `icon`, `footer`) | → classify as a `slot` if it represents an injectable area, or `richtext` if it is inline markup content |
193
+ | `boolean` with a name like `hideChevron`, `imageOnLeft`, `enableBackgroundColorEffect` | → `boolean`, `cdf_category: "design"`, `default: true` or `false` — these control visual appearance |
194
+ | `boolean` with a name like `preview`, `hideContentForPersonalization` | → `boolean`, `cdf_category: "state"`, `default: false` — these control behavior |
195
+ | `string` used as a `componentId`, `sectionKey`, `componentName` | → `string`, `cdf_category: "state"` — these are identifiers for tracking/lookup |
196
+ | `string` locale (e.g. `locale: string`) | → `string`, `cdf_category: "state"` — locale is a behavioral/routing value |
197
+
198
+ **When to finally exclude:**
199
+ - The type is a callback signature or event handler
200
+ - The type is a React ref
201
+ - The type is a React state setter (`Dispatch`)
202
+ - The type is a deep generic used for personalization/A-B testing platform config (e.g. `ExperienceConfiguration<T>`)
203
+ - The type is an array of rich objects where no flat representation makes sense for a marketer
204
+
205
+ If you exclude a prop that could have been classified, the marketer loses the ability to configure it in Contentful. Prefer classifying with a reasonable inference over excluding.
206
+
207
+ ---
208
+
209
+ ## Handling nested object props
210
+
211
+ When a prop has an inline object type (e.g., `item: { url?: string; alt?: string; target?: string }`),
212
+ classify EACH field as a separate prop using underscore notation (parentName_fieldName):
213
+
214
+ Input:
215
+ {"name":"item","type":"{ url?: string; alt?: string; linkUrl?: string; target?: string }","required":true}
216
+
217
+ Output:
218
+ {"tool":"classify_prop","prop":"item_url","cdf_type":"string","cdf_category":"content","required":false,"description":"Image source URL"}
219
+ {"tool":"classify_prop","prop":"item_alt","cdf_type":"string","cdf_category":"content","required":false,"description":"Image alt text"}
220
+ {"tool":"classify_prop","prop":"item_linkUrl","cdf_type":"string","cdf_category":"content","required":false,"description":"Navigation URL"}
221
+ {"tool":"classify_prop","prop":"item_target","cdf_type":"enum","cdf_category":"design","required":false,"values":["_blank","_self","_parent","_top"],"description":"Link open behavior"}
222
+
223
+ Note: Underscore notation is used (not dot-notation) because the backend's `toDisplayName()`
224
+ function splits on `.` and takes only the last segment, producing poor display names. Underscore
225
+ produces display names like "Item Url", "Item Alt" which are more readable in the ExO editor.
226
+
227
+ Rules for nested objects:
228
+ - Flatten to max depth 2 (e.g., `item_nested_deep` is acceptable, deeper is not)
229
+ - Each leaf field gets its own classify_prop call with underscore-joined name
230
+ - Apply the same classification rules as top-level props
231
+ - If the object has > 10 fields, classify the most important 10 and exclude the rest
232
+ - If the object type cannot be resolved (opaque generic, imported interface without visible fields), exclude the parent prop with reason "opaque nested type"
233
+
234
+ ---
235
+
236
+ ## Token-aware mapping
237
+
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"`).
239
+
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`
243
+
244
+ Example:
245
+ ```
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":"..."}
250
+ ```
251
+
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
+ ---
257
+
258
+ ## Category correction rules
259
+
260
+ The pre-classified `category` is wrong in predictable ways. Correct silently (document in `description`):
261
+
262
+ - Visual style props (`color`, `size`, `padding`, `spacing`, `variant`, `theme`, `bgColor`, `imageOnLeft`, `enableXxx`) classified as `content` or `state` → `design`
263
+ - 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`
265
+ - Locale classified as `content` → `state` (it is a behavioral routing value, not editor-filled text)
266
+
267
+ > **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.
268
+
269
+ ---
270
+
271
+ ## Slot classification
272
+
273
+ The `classify_slot` tool call maps to CDF's `$slots` object. Each slot you classify becomes a `$slots` entry; the `allowed_components` field maps to `$allowedComponents` in the output CDF.
274
+
275
+ For each `RawSlotDefinition`:
276
+ - `name` → `slot` field
277
+ - `description` → `description` field
278
+ - `allowedComponents` → `allowed_components` (pass through)
279
+ - `required`:
280
+ - `true` if the component does not render correctly without content in this slot (primary content area of a Card, Dialog body, etc.)
281
+ - `false` if clearly optional (icon slot, footer slot with a default, decorative slot)
282
+ - Default to `true` when the source gives no signal
283
+
284
+ **Pre-named slots:** If the input contains a slot whose `name` was already inferred by the pipeline (e.g. `"children"`, `"slot_0"`), treat it as you would any named slot — classify it normally. The pipeline renames empty-named slots to heuristic names before passing them to you; your job is to confirm or enrich the classification (set `required`, `description`, `allowed_components`), not to rename again.
285
+
286
+ ---
287
+
288
+ ## Examples
289
+
290
+ ### Simple component
291
+
292
+ Input:
293
+ ```json
294
+ {
295
+ "name": "Button",
296
+ "props": [
297
+ {"name":"label","type":"string","category":"content","required":true},
298
+ {"name":"variant","type":"'primary'|'secondary'|'ghost'","category":"design","defaultValue":"'primary'"},
299
+ {"name":"disabled","type":"boolean","category":"design"},
300
+ {"name":"onClick","type":"()=>void","category":"state"},
301
+ {"name":"className","type":"string"}
302
+ ],
303
+ "slots": [{"name":"icon","description":"Optional leading icon"}]
304
+ }
305
+ ```
306
+
307
+ Output:
308
+ ```
309
+ Starting Button classification — 5 props, 1 slot
310
+ {"tool":"classify_component","description":"Primary action button with variant and state support","rationale":{"description":"Button is an atom — a single interactive control that triggers an action. It carries a label, a small set of visual variants, and a disabled flag, which is the minimal surface a marketer needs to configure a call-to-action.","props":"Kept label (content), variant (enum, design), disabled (boolean, state), and className (string, design escape hatch). Excluded onClick because it is an event handler — framework-internal and not configurable in Contentful.","slots":"Kept the icon slot as optional because the button renders correctly without it and the icon is purely decorative."}}
311
+ label is a required string content prop
312
+ {"tool":"classify_prop","prop":"label","cdf_type":"string","cdf_category":"content","required":true,"description":"Button label text"}
313
+ variant is a string union — enum type, category design
314
+ {"tool":"classify_prop","prop":"variant","cdf_type":"enum","cdf_category":"design","required":false,"values":["primary","secondary","ghost"],"default":"primary","description":"Visual variant"}
315
+ disabled is a boolean state prop — raw category says design, correcting to state
316
+ {"tool":"classify_prop","prop":"disabled","cdf_type":"boolean","cdf_category":"state","required":false,"default":false,"description":"Disables the button"}
317
+ onClick is an event handler — framework internal
318
+ {"tool":"exclude_prop","prop":"onClick","reason":"event handler — framework internal"}
319
+ className is a DOM pass-through — developers wire CSS, marketers never set this
320
+ {"tool":"exclude_prop","prop":"className","reason":"DOM pass-through — not a marketer-configurable surface"}
321
+ icon slot is clearly optional (decorative leading icon)
322
+ {"tool":"classify_slot","slot":"icon","required":false,"description":"Optional leading icon","rationale":"Icon is a decorative leading glyph — optional because the button reads cleanly without it, but kept as a slot so marketers can inject a brand-specific icon component when desired."}
323
+ ```
324
+
325
+ ### Component with multiple slots (Card)
326
+
327
+ Input:
328
+ ```json
329
+ {
330
+ "name": "Card",
331
+ "props": [
332
+ {"name":"title","type":"string","category":"content","required":true},
333
+ {"name":"elevation","type":"'flat'|'raised'|'floating'","category":"design","defaultValue":"'raised'"}
334
+ ],
335
+ "slots": [
336
+ {"name":"body","description":"Main card body"},
337
+ {"name":"footer","description":"Optional footer area"}
338
+ ]
339
+ }
340
+ ```
341
+
342
+ Output:
343
+ ```
344
+ Starting Card classification — 2 props, 2 slots
345
+ {"tool":"classify_component","description":"Container that groups related content with a title, body, and optional footer","rationale":{"description":"Card is a molecule — it composes a title with body and footer slots into a single visual container. Useful as a building block for lists and grids of related content.","props":"Kept title (string, content) as the customer-facing label and elevation (enum, design) for the three visual depth variants. Nothing was excluded — both raw props map cleanly to CDF.","slots":"Kept both body (required, primary content area) and footer (optional, supplementary area). Body is required because a card with no body renders empty; footer is optional because many cards do not need one."}}
346
+ title is a required content string
347
+ {"tool":"classify_prop","prop":"title","cdf_type":"string","cdf_category":"content","required":true,"description":"Card title text"}
348
+ elevation is a finite visual variant — enum, design
349
+ {"tool":"classify_prop","prop":"elevation","cdf_type":"enum","cdf_category":"design","required":false,"values":["flat","raised","floating"],"default":"raised","description":"Visual depth variant"}
350
+ body is the primary content region — required
351
+ {"tool":"classify_slot","slot":"body","required":true,"description":"Main card body content","rationale":"Body is the primary content region of the card. Required because a card with no body renders an empty container, which is never a useful editor state."}
352
+ footer is supplementary and optional
353
+ {"tool":"classify_slot","slot":"footer","required":false,"description":"Optional footer area for actions or metadata","rationale":"Footer is a supplementary region typically used for actions or metadata. Optional because most cards do not need one and the card renders correctly without it."}
354
+ ```
355
+
356
+ ### Named type (HeadingSize, ButtonVariant, etc.)
357
+
358
+ When a prop has a named TypeScript type that is not inlined as a union literal, reason from the prop name and type name to infer the finite value set.
359
+
360
+ ```
361
+ titleSize has type HeadingSize — this is a named enum controlling heading size
362
+ inferring likely values: ["h1", "h2", "h3", "h4", "h5", "h6"] — documenting inference
363
+ {"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
+ ```
365
+
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
+ ### href prop
374
+
375
+ ```
376
+ href is a URL string — cdf_type string (not link), category content
377
+ {"tool":"classify_prop","prop":"href","cdf_type":"string","cdf_category":"content","required":false,"description":"Navigation URL"}
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Edge cases
383
+
384
+ - **Prop with unresolvable type** (generics, intersection, callback) → `exclude_prop` with reason `"complex type — not representable in CDF"`.
385
+ - **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
+ - **Slot not in DB** → skipped with a warning; does not abort the run.
388
+ - **Prop not in DB** → skipped with a warning; does not abort the run.
389
+
390
+ ## Validation step — Pre-emit checklist
391
+
392
+ Before emitting any tool calls, verify:
393
+
394
+ 1. Every prop in the input has exactly one `classify_prop` or `exclude_prop` call
395
+ 2. Every slot has exactly one `classify_slot` call
396
+ 3. `classify_component` is emitted exactly once
397
+ 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)
399
+ 6. No `cdf_type: "link"` — all href/url props use `string`
400
+ 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.
402
+ 9. No `cdf_type: "link"` used — `link` is reserved and rejected by the CLI parser
403
+ 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
+ 11. `classify_component` includes a `rationale` object with all three sub-fields (`rationale.description`, `rationale.props`, `rationale.slots`) populated as non-empty strings.
405
+ 12. Every `classify_slot` includes a non-empty `rationale` string.
406
+ 13. `rationale.description` follows the same "Description content rules" as `description` — no internal initiative names (`INTEG-*`, `EDSI`, `DSI`, `M1`, `M2`, wave/phase references, etc.).
407
+
408
+ After the run completes, the developer can validate the pipeline output with:
409
+
410
+ ```
411
+ experience-design-system-cli print validate --components <out-path>
412
+ ```
413
+
414
+ Re-run or re-iterate on any components flagged by warnings until the output passes validation.
415
+
416
+ ---
417
+
418
+ ## CRITICAL: Zero-output is a failure
419
+
420
+ You MUST produce at least one classify_prop call for this component. A response with zero
421
+ classify_prop/exclude_prop calls means the component will be pushed with no configurable
422
+ properties — this is never acceptable.
423
+
424
+ If you are genuinely uncertain about every prop, classify each as:
425
+ {"tool":"classify_prop","prop":"<name>","cdf_type":"string","cdf_category":"content","required":false,"description":"Uncertain classification — review recommended"}
426
+
427
+ An imperfect classification is infinitely better than no classification.
@@ -0,0 +1,194 @@
1
+ # Generate Tokens — Classification Skill
2
+
3
+ ## Purpose
4
+
5
+ Classify every raw token from the input into a DTCG token tree. Output one JSON tool call per line to stdout. The CLI reads your stdout and writes each token and group directly to the pipeline database — you do not produce a JSON file.
6
+
7
+ ---
8
+
9
+ ## Prerequisites — Input
10
+
11
+ All input is embedded inline in the prompt before this file:
12
+
13
+ - **Raw token source** — the original source file as-is. This may be any format: SCSS/CSS variable declarations, a JavaScript/TypeScript token module, a JSON object, a Style Dictionary config, Tailwind config, or any other token definition format the project uses. Read it as you would any source file.
14
+ - **Token-name sidecar** — if provided, maps raw names to DTCG paths (read-only context). Use these as authoritative path assignments when present.
15
+
16
+ ---
17
+
18
+ ## Target schema
19
+
20
+ The CLI assembles your output into a DTCG token tree. Each `set_group` call produces an intermediate group node; each `set_token` call produces a leaf with `$type` and `$value`. You do not produce JSON directly — emit tool calls and the CLI writes the rows.
21
+
22
+ ---
23
+
24
+ ## Output protocol
25
+
26
+ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not starting with `{` are treated as prose and ignored — use them freely for reasoning.
27
+
28
+ **Two tool calls:**
29
+
30
+ ```
31
+ {"tool":"set_group","path":"<dot.notation.path>","description":"<optional>"}
32
+
33
+ {"tool":"set_token","path":"<dot.notation.path>","type":"<DTCG type>","value":<value>,"description":"<reason>"}
34
+ ```
35
+
36
+ Every leaf token must have an explicit `$type` — do not rely on group-level inheritance. Intermediate nodes (groups) must NOT have a `type` field.
37
+
38
+ **Rules:**
39
+ - Emit exactly one JSON object per line. No multi-line JSON.
40
+ - Every intermediate group must have a `set_group` call.
41
+ - Every leaf token must have a `set_token` call.
42
+ - `path` is dot-notation from root, e.g. `colors.brand.primary` — no leading dots or slashes.
43
+ - `type` must be one of the 13 valid DTCG types (see table below).
44
+ - `value` must be valid JSON — string, number, array, or object depending on the type. Do NOT quote complex values.
45
+ - Emit `set_group` calls before the `set_token` calls under them.
46
+ - `description` on `set_token` documents your reasoning — always include it.
47
+
48
+ ---
49
+
50
+ ## Valid types
51
+
52
+ Exactly **13** valid DTCG `$type` values:
53
+
54
+ | type | Typical values |
55
+ |---|---|
56
+ | `color` | `"#0066ff"`, `"rgb(0,102,255)"` |
57
+ | `dimension` | `"8px"`, `"1rem"`, `"0.5em"` |
58
+ | `fontFamily` | `"Inter, sans-serif"` |
59
+ | `fontWeight` | `400`, `"bold"` |
60
+ | `duration` | `"200ms"`, `"0.3s"` |
61
+ | `cubicBezier` | `[0.42, 0, 0.58, 1]` |
62
+ | `number` | `1.5`, `100` |
63
+ | `strokeStyle` | `"solid"`, `"dashed"` |
64
+ | `border` | `{"width":"1px","style":"solid","color":"#000"}` |
65
+ | `transition` | `{"duration":"200ms","timingFunction":[0.42,0,0.58,1],"delay":"0ms"}` |
66
+ | `shadow` | `{"offsetX":"0px","offsetY":"4px","blur":"8px","spread":"0px","color":"#00000026"}` |
67
+ | `gradient` | `[{"color":"#000","position":0},{"color":"#fff","position":1}]` |
68
+ | `typography` | `{"fontFamily":"Inter","fontSize":"16px","fontWeight":400,"lineHeight":1.5,"letterSpacing":"0px"}` |
69
+
70
+ ---
71
+
72
+ ## Mapping guidance — Type resolution
73
+
74
+ For each token value, determine the DTCG `$type` by reading the value and its context:
75
+
76
+ | Value pattern | Type |
77
+ |---|---|
78
+ | Starts with `#`, `rgb(`, `hsl(`, `rgba(` | `color` |
79
+ | Ends with `px`, `rem`, `em`, `%`, `vw`, `vh` | `dimension` |
80
+ | `cubic-bezier(...)` or 4-element array | `cubicBezier` |
81
+ | Ends with `ms` or `s` (animation/transition) | `duration` |
82
+ | Font family name string | `fontFamily` |
83
+ | Numeric `100`–`900`, or `bold`/`normal`/`light` | `fontWeight` |
84
+ | Multi-property shadow value | `shadow` |
85
+ | Multi-property border value | `border` |
86
+ | `solid`, `dashed`, `dotted` alone | `strokeStyle` |
87
+ | Composite font shorthand | `typography` |
88
+ | Unitless number | `number` |
89
+
90
+ When a token's type is ambiguous from its value alone, use its name as context (e.g. `$spacing-*` → `dimension`, `$duration-*` → `duration`). Document your reasoning in `description`. If no type fits, use `number` with a `"WARNING: type unknown"` description.
91
+
92
+ Resolve variable references: if a value references another variable (e.g. SCSS `$black` in `rgba($black, 0.6)`, or a JS alias), substitute the referenced value before classifying.
93
+
94
+ ---
95
+
96
+ ## Grouping
97
+
98
+ Organize tokens into a nested DTCG hierarchy that reflects their semantic purpose:
99
+
100
+ 1. Examine naming patterns and existing structure in the source (variable prefixes, object nesting, file-level categories).
101
+ 2. **For already-nested sources (JS/TS objects, JSON, Style Dictionary)**: derive paths directly from the source key hierarchy — lowercase each key segment and join with dots. Do NOT reorganize, rename, or flatten unless the original key is purely implementation-specific (e.g. `DEFAULT` as a single-child wrapper can be elided). The source structure IS the path. Example: `tokens[SPACING].DEFAULT.8` → `spacing.8`; `tokens[COLOR].text.standard` → `color.text.standard`; `tokens[SHADOW].level.1` → `shadow.level.1`.
102
+ 3. For flat names (SCSS variables, CSS custom properties): strip leading `--` or `$`; replace `-` and `_` separators with `.` to form nested paths. Use the first meaningful segment as the top-level group.
103
+ 4. If neither rule applies, propose a grouping strategy: by kind (`colors`, `spacing`, `typography`), by semantic role (`brand`, `semantic`, `neutral`), or hybrid.
104
+ 5. Emit a `set_group` call for every intermediate node before emitting any `set_token` calls under it.
105
+
106
+ **Determinism rule**: Identical source input MUST produce identical paths on every run. Never invent synonyms, abbreviations, or alternative hierarchies for keys that already exist in the source. If `primary` is the key, the path segment is `primary` — not `brand-primary`, `main`, or `accent`.
107
+
108
+ If the token-name sidecar is provided, use it as authoritative path assignments — it maps raw names to DTCG dot-notation paths already.
109
+
110
+ ---
111
+
112
+ ## Value handling
113
+
114
+ Most values pass through as strings. For composite types, parse the raw string into the required JSON structure:
115
+
116
+ **`border`** — parse `1px solid #ccc` into:
117
+ ```json
118
+ {"width":"1px","style":"solid","color":"#cccccc"}
119
+ ```
120
+
121
+ **`shadow`:**
122
+ ```json
123
+ {"offsetX":"0px","offsetY":"4px","blur":"8px","spread":"0px","color":"#00000026"}
124
+ ```
125
+
126
+ **`transition`:**
127
+ ```json
128
+ {"duration":"200ms","delay":"0ms","timingFunction":[0.42,0,0.58,1]}
129
+ ```
130
+
131
+ **`gradient`** — array of color stops:
132
+ ```json
133
+ [{"color":"#000","position":0},{"color":"#fff","position":1}]
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Examples
139
+
140
+ Input (SCSS):
141
+ ```scss
142
+ $brand-primary: #0066ff;
143
+ $spacing-sm: 8px;
144
+ $anim-speed: 200ms;
145
+ $black: #1a1a1a;
146
+ $overlay: rgba($black, 0.6);
147
+ ```
148
+
149
+ Output:
150
+ ```
151
+ Analyzing SCSS variables: 5 tokens across color, spacing, duration, and composite color categories.
152
+ $overlay references $black — resolving to rgba(26, 26, 26, 0.6).
153
+ {"tool":"set_group","path":"colors","description":"Brand and UI color tokens"}
154
+ {"tool":"set_group","path":"colors.brand","description":"Brand palette"}
155
+ {"tool":"set_token","path":"colors.brand.primary","type":"color","value":"#0066ff","description":"Brand primary color"}
156
+ {"tool":"set_token","path":"colors.neutral.black","type":"color","value":"#1a1a1a","description":"Base black neutral"}
157
+ {"tool":"set_token","path":"colors.neutral.overlay","type":"color","value":"rgba(26, 26, 26, 0.6)","description":"Semi-transparent overlay; $black variable resolved to #1a1a1a"}
158
+ {"tool":"set_group","path":"spacing","description":"Spacing scale"}
159
+ {"tool":"set_token","path":"spacing.sm","type":"dimension","value":"8px","description":"Small spacing step"}
160
+ {"tool":"set_group","path":"motion","description":"Animation timing"}
161
+ {"tool":"set_group","path":"motion.duration","description":"Duration values"}
162
+ {"tool":"set_token","path":"motion.duration.speed","type":"duration","value":"200ms","description":"Standard animation speed"}
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Edge cases
168
+
169
+ - **Empty input** — emit nothing; the CLI will report 0 tokens stored.
170
+ - **Path collision** — two raw tokens resolve to the same dot-notation path: append `_2`, `_3` to disambiguate, and add a `description` warning.
171
+ - **Unrecognizable value** — no heuristic matches and `inferredKind` is invalid → use `number` with a warning in `description`.
172
+ - **Complex composite value** — parse raw string into the required JSON structure; if parsing is ambiguous, use a best-effort parse and document in `description`.
173
+
174
+ ---
175
+
176
+ ## Validation step — Pre-emit checklist
177
+
178
+ Before emitting any tool calls, verify:
179
+
180
+ 1. Every raw token in the input has exactly one `set_token` call.
181
+ 2. Every intermediate group has a `set_group` call.
182
+ 3. All `type` values are from the 13 valid DTCG types.
183
+ 4. `value` is valid JSON for the given type (string for color/dimension/etc., object for border/shadow/etc., array for gradient/cubicBezier).
184
+ 5. All `path` values use dot-notation with no leading dots or slashes.
185
+ 6. No duplicate paths.
186
+ 7. `set_group` calls precede the `set_token` calls under them.
187
+
188
+ After the run, the developer can validate with:
189
+
190
+ ```
191
+ experience-design-system-cli print validate --tokens <out-path>
192
+ ```
193
+
194
+ Re-run or iterate on any tokens flagged by warnings until validation passes.