@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.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.
Files changed (142) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +259 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +814 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  62. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  63. package/dist/src/import/tui/push-progress.d.ts +23 -0
  64. package/dist/src/import/tui/push-progress.js +14 -0
  65. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  66. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  67. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  68. package/dist/src/import/tui/run-teaser.js +9 -0
  69. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  70. package/dist/src/import/tui/runLivePreview.js +73 -0
  71. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  72. package/dist/src/import/tui/runScopeGate.js +28 -0
  73. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  74. package/dist/src/import/tui/scope-gate-host.js +20 -0
  75. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  76. package/dist/src/import/tui/spawn-generate.js +53 -0
  77. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  78. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  79. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  80. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  81. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  82. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  83. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  84. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  85. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  86. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  87. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  88. package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
  89. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  90. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  91. package/dist/src/import/tui/useLivePreview.js +120 -0
  92. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  93. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  94. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  95. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  96. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  97. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  98. package/dist/src/lib/contentful-urls.d.ts +34 -0
  99. package/dist/src/lib/contentful-urls.js +41 -0
  100. package/dist/src/program.js +2 -0
  101. package/dist/src/runs/export-helpers.d.ts +30 -0
  102. package/dist/src/runs/export-helpers.js +59 -0
  103. package/dist/src/runs/fingerprint.d.ts +47 -0
  104. package/dist/src/runs/fingerprint.js +75 -0
  105. package/dist/src/runs/ls-command.d.ts +12 -0
  106. package/dist/src/runs/ls-command.js +137 -0
  107. package/dist/src/runs/modify-launcher.d.ts +24 -0
  108. package/dist/src/runs/modify-launcher.js +36 -0
  109. package/dist/src/runs/path-prompt.d.ts +8 -0
  110. package/dist/src/runs/path-prompt.js +72 -0
  111. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  112. package/dist/src/runs/push-creds-prompt.js +39 -0
  113. package/dist/src/runs/push-helpers.d.ts +29 -0
  114. package/dist/src/runs/push-helpers.js +69 -0
  115. package/dist/src/runs/replay-helpers.d.ts +67 -0
  116. package/dist/src/runs/replay-helpers.js +148 -0
  117. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  118. package/dist/src/runs/resolve-run-target.js +45 -0
  119. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  120. package/dist/src/runs/run-picker-mount.js +72 -0
  121. package/dist/src/runs/run-picker.d.ts +17 -0
  122. package/dist/src/runs/run-picker.js +145 -0
  123. package/dist/src/runs/save-conflict.d.ts +8 -0
  124. package/dist/src/runs/save-conflict.js +56 -0
  125. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  126. package/dist/src/runs/save-path-resolver.js +71 -0
  127. package/dist/src/runs/staleness.d.ts +38 -0
  128. package/dist/src/runs/staleness.js +140 -0
  129. package/dist/src/runs/store.d.ts +78 -0
  130. package/dist/src/runs/store.js +136 -0
  131. package/dist/src/session/cache-keys.d.ts +15 -0
  132. package/dist/src/session/cache-keys.js +38 -0
  133. package/dist/src/session/db.d.ts +89 -3
  134. package/dist/src/session/db.js +447 -104
  135. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  136. package/dist/src/setup/auto-filter-prompt.js +24 -0
  137. package/dist/src/setup/command.d.ts +7 -0
  138. package/dist/src/setup/command.js +58 -2
  139. package/dist/src/types.d.ts +6 -0
  140. package/package.json +2 -2
  141. package/skills/generate-components.md +54 -6
  142. package/skills/select-components.md +13 -4
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ask the operator whether to enable the AI auto-filter by default.
3
+ *
4
+ * The helper is injectable so it can be unit-tested without a TTY. The caller
5
+ * provides an `ask` function that yields one line of input per call.
6
+ *
7
+ * Behavior:
8
+ * - Empty input returns `current` if defined, else `true` (default ON).
9
+ * - Input starting with 'y' or 'Y' returns `true`.
10
+ * - Input starting with 'n' or 'N' returns `false`.
11
+ * - Any other input falls back to the same rule as empty input.
12
+ */
13
+ export declare function promptAutoFilterPreference(ask: (q: string) => Promise<string>, current?: boolean): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Ask the operator whether to enable the AI auto-filter by default.
3
+ *
4
+ * The helper is injectable so it can be unit-tested without a TTY. The caller
5
+ * provides an `ask` function that yields one line of input per call.
6
+ *
7
+ * Behavior:
8
+ * - Empty input returns `current` if defined, else `true` (default ON).
9
+ * - Input starting with 'y' or 'Y' returns `true`.
10
+ * - Input starting with 'n' or 'N' returns `false`.
11
+ * - Any other input falls back to the same rule as empty input.
12
+ */
13
+ export async function promptAutoFilterPreference(ask, current) {
14
+ const defaultValue = current ?? true;
15
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
16
+ const answer = (await ask(` Enable AI auto-filter by default? ${hint} `)).trim().toLowerCase();
17
+ if (answer === '')
18
+ return defaultValue;
19
+ if (answer.startsWith('y'))
20
+ return true;
21
+ if (answer.startsWith('n'))
22
+ return false;
23
+ return defaultValue;
24
+ }
@@ -1,2 +1,9 @@
1
1
  import type { Command } from 'commander';
2
+ export type SkillPromptKind = 'select' | 'generate';
3
+ /**
4
+ * Prompt the operator for a custom skill prompt path. Returns the resolved
5
+ * trimmed value, or `undefined` to leave the current value unchanged, or `null`
6
+ * to clear it. `ask` is injectable so tests can stub stdin.
7
+ */
8
+ export declare function promptCustomSkillPath(kind: SkillPromptKind, current: string | undefined, ask?: (q: string) => Promise<string>): Promise<string | undefined | null>;
2
9
  export declare function registerSetupCommand(program: Command): void;
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { promisify } from 'node:util';
8
8
  import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
9
+ import { promptAutoFilterPreference } from './auto-filter-prompt.js';
9
10
  import { DEFAULT_CONFIGURED_HOST, toConfiguredHost } from '../host-utils.js';
10
11
  const execFileAsync = promisify(execFile);
11
12
  const REQUIRED_NODE_MAJOR = 24;
@@ -521,12 +522,43 @@ async function setupContentfulCredentials() {
521
522
  info('Run experiences import — credentials will be pre-filled automatically.');
522
523
  return true;
523
524
  }
525
+ /**
526
+ * Prompt the operator for a custom skill prompt path. Returns the resolved
527
+ * trimmed value, or `undefined` to leave the current value unchanged, or `null`
528
+ * to clear it. `ask` is injectable so tests can stub stdin.
529
+ */
530
+ export async function promptCustomSkillPath(kind, current, ask = prompt) {
531
+ const flagName = kind === 'select' ? '--select-prompt-path' : '--generate-prompt-path';
532
+ void flagName;
533
+ const label = kind === 'select' ? 'select (analyze select-agent)' : 'generate (generate components)';
534
+ const currentLabel = current ? ` [${current}]` : ' [none]';
535
+ const answer = await ask(` Custom ${label} prompt path${currentLabel} (empty=keep, "-"=clear): `);
536
+ const trimmed = answer.trim();
537
+ if (trimmed === '')
538
+ return undefined;
539
+ if (trimmed === '-')
540
+ return null;
541
+ return trimmed;
542
+ }
524
543
  // ── Step 6: Optional quality-of-life ─────────────────────────────────────────
525
544
  async function setupQoL(profilePath) {
526
545
  section('Step 6: Optional extras', '[optional]');
527
546
  info('These are not required for experiences import but improve the experience.');
528
547
  info('');
529
- // 6a: EDS_EXTRACT_CONCURRENCY
548
+ // 6a: AI auto-filter default
549
+ const existingCreds = await readExperiencesCredentials();
550
+ info('AI auto-filter — runs an agent pass before the manual scope-gate to prefilter components.');
551
+ info('Operators who prefer to review every component can default this OFF and override per run with --auto-filter.');
552
+ const autoFilter = await promptAutoFilterPreference((q) => prompt(q), existingCreds.autoFilter);
553
+ if (autoFilter !== (existingCreds.autoFilter ?? true)) {
554
+ await writeExperiencesCredentials({ ...existingCreds, autoFilter });
555
+ ok(`AI auto-filter default set to ${autoFilter ? 'ON' : 'OFF'}`);
556
+ }
557
+ else {
558
+ dim(' unchanged');
559
+ }
560
+ info('');
561
+ // 6b: EDS_EXTRACT_CONCURRENCY
530
562
  const hasConcurrency = await profileContains(profilePath, 'EDS_EXTRACT_CONCURRENCY');
531
563
  if (!hasConcurrency) {
532
564
  info('EDS_EXTRACT_CONCURRENCY — controls how many components are analyzed in parallel.');
@@ -543,7 +575,31 @@ async function setupQoL(profilePath) {
543
575
  else {
544
576
  ok('EDS_EXTRACT_CONCURRENCY — already set');
545
577
  }
546
- // 6b: NO_COLOR
578
+ // 6c (Feature 8): custom skill prompt paths
579
+ info('');
580
+ info('Custom skill prompt paths — point select-agent and/or generate components at your own .md prompts.');
581
+ info('When set, the bundled invariants (utility-wrapper rejection, description rules) do NOT apply.');
582
+ const offerCustomPrompts = await confirm('Configure custom skill prompt paths?', false);
583
+ if (offerCustomPrompts) {
584
+ const stored = await readExperiencesCredentials();
585
+ const selectAnswer = await promptCustomSkillPath('select', stored.selectPromptPath);
586
+ const generateAnswer = await promptCustomSkillPath('generate', stored.generatePromptPath);
587
+ const updated = { ...stored };
588
+ if (selectAnswer === null)
589
+ delete updated.selectPromptPath;
590
+ else if (selectAnswer !== undefined)
591
+ updated.selectPromptPath = selectAnswer;
592
+ if (generateAnswer === null)
593
+ delete updated.generatePromptPath;
594
+ else if (generateAnswer !== undefined)
595
+ updated.generatePromptPath = generateAnswer;
596
+ await writeExperiencesCredentials(updated);
597
+ ok(`Custom prompt paths saved to ${experiencesCredentialsPath()}`);
598
+ }
599
+ else {
600
+ dim(' skipped');
601
+ }
602
+ // 6d: NO_COLOR
547
603
  info('');
548
604
  info('NO_COLOR — set to 1 to disable ANSI color output (useful in CI or plain terminals).');
549
605
  const setNoColor = await confirm('Add NO_COLOR=1 (disable colors) to your profile?', false);
@@ -22,6 +22,10 @@ export interface RawPropDefinition {
22
22
  allowedValues?: string[];
23
23
  description?: string;
24
24
  tokenReference?: string;
25
+ /** 1-indexed source line where this prop's declaration begins; relative to RawComponentDefinition.source. */
26
+ sourceStartLine?: number;
27
+ /** 1-indexed inclusive source line where this prop's declaration ends; relative to RawComponentDefinition.source. */
28
+ sourceEndLine?: number;
25
29
  }
26
30
  export interface RawSlotDefinition {
27
31
  name: string;
@@ -45,6 +49,8 @@ export interface RawComponentDefinition {
45
49
  reviewReasons?: string[];
46
50
  needsReview?: boolean;
47
51
  validationIssues?: ExtractionValidationIssue[];
52
+ /** Absolute path to the source file this component was extracted from. Null for synthesized / multi-file. */
53
+ sourcePath?: string;
48
54
  }
49
55
  export interface ComponentExtractionResult {
50
56
  components: RawComponentDefinition[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.11.4-dev-build-d47ab65.0",
3
+ "version": "2.12.1-dev-build-589b615.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "svelte": "^5.56.4",
38
38
  "ts-morph": "^27.0.2",
39
39
  "typescript": "^5.9.3",
40
- "@contentful/experience-design-system-types": "2.11.4-dev-build-d47ab65.0"
40
+ "@contentful/experience-design-system-types": "2.12.1-dev-build-589b615.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node24": "^24.0.3",
@@ -75,13 +75,13 @@ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not
75
75
  **Four tool calls:**
76
76
 
77
77
  ```
78
- {"tool":"classify_component","description":"<required: one-sentence description of the component>"}
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
79
 
80
- {"tool":"classify_prop","prop":"<propName>","cdf_type":"<type>","cdf_category":"<category>","required":<bool>,"description":"<reason>","values":["a","b"],"token_kind":"color","default":"<value>"}
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
81
 
82
82
  {"tool":"exclude_prop","prop":"<propName>","reason":"<why excluded>"}
83
83
 
84
- {"tool":"classify_slot","slot":"<slotName>","required":<bool>,"allowed_components":["ComponentName"],"description":"<reason>"}
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
85
  ```
86
86
 
87
87
  **Rules:**
@@ -92,7 +92,21 @@ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not
92
92
  - `values` is required for `cdf_type: "enum"` — must be a non-empty string array.
93
93
  - `token_kind` is required for `cdf_type: "token"` — must be a DTCG `$type` string, e.g. `"color"`.
94
94
  - `required` must be a JSON boolean (`true`/`false`), not a string.
95
- - `description` on `classify_prop` documents your reasoning always include it.
95
+ - `description` on `classify_prop` is customer-facingkeep 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.
96
110
 
97
111
  ---
98
112
 
@@ -292,7 +306,7 @@ Input:
292
306
  Output:
293
307
  ```
294
308
  Starting Button classification — 5 props, 1 slot
295
- {"tool":"classify_component","description":"Primary action button with variant and state support"}
309
+ {"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."}}
296
310
  label is a required string content prop
297
311
  {"tool":"classify_prop","prop":"label","cdf_type":"string","cdf_category":"content","required":true,"description":"Button label text"}
298
312
  variant is a string union — enum type, category design
@@ -304,7 +318,38 @@ onClick is an event handler — framework internal
304
318
  className is a DOM pass-through — developers wire CSS, marketers never set this
305
319
  {"tool":"exclude_prop","prop":"className","reason":"DOM pass-through — not a marketer-configurable surface"}
306
320
  icon slot is clearly optional (decorative leading icon)
307
- {"tool":"classify_slot","slot":"icon","required":false,"description":"Optional leading icon"}
321
+ {"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."}
322
+ ```
323
+
324
+ ### Component with multiple slots (Card)
325
+
326
+ Input:
327
+ ```json
328
+ {
329
+ "name": "Card",
330
+ "props": [
331
+ {"name":"title","type":"string","category":"content","required":true},
332
+ {"name":"elevation","type":"'flat'|'raised'|'floating'","category":"design","defaultValue":"'raised'"}
333
+ ],
334
+ "slots": [
335
+ {"name":"body","description":"Main card body"},
336
+ {"name":"footer","description":"Optional footer area"}
337
+ ]
338
+ }
339
+ ```
340
+
341
+ Output:
342
+ ```
343
+ Starting Card classification — 2 props, 2 slots
344
+ {"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."}}
345
+ title is a required content string
346
+ {"tool":"classify_prop","prop":"title","cdf_type":"string","cdf_category":"content","required":true,"description":"Card title text"}
347
+ elevation is a finite visual variant — enum, design
348
+ {"tool":"classify_prop","prop":"elevation","cdf_type":"enum","cdf_category":"design","required":false,"values":["flat","raised","floating"],"default":"raised","description":"Visual depth variant"}
349
+ body is the primary content region — required
350
+ {"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."}
351
+ footer is supplementary and optional
352
+ {"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."}
308
353
  ```
309
354
 
310
355
  ### Named type (HeadingSize, ButtonVariant, etc.)
@@ -355,6 +400,9 @@ Before emitting any tool calls, verify:
355
400
  8. Framework, DOM, accessibility, and data-* pass-through props are excluded — `className`/`classes`/`classNames`/`rootClassName`/`prefixCls`, `style`, `id`, `role`, `tabIndex`, `name` (bare HTML form attribute), `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`, `form`, `inputId`, `componentId`) are NOT excluded — classify them per their content/design/state nature.
356
401
  9. No `cdf_type: "link"` used — `link` is reserved and rejected by the CLI parser
357
402
  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.
403
+ 11. `classify_component` includes a `rationale` object with all three sub-fields (`rationale.description`, `rationale.props`, `rationale.slots`) populated as non-empty strings.
404
+ 12. Every `classify_slot` includes a non-empty `rationale` string.
405
+ 13. `rationale.description` follows the same "Description content rules" as `description` — no internal initiative names (`INTEG-*`, `EDSI`, `DSI`, `M1`, `M2`, wave/phase references, etc.).
358
406
 
359
407
  After the run completes, the developer can validate the pipeline output with:
360
408
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Review the single extracted React/Next.js component provided below and decide whether it belongs in **Contentful Experience Orchestration** as a Component Type. Output one JSON tool call to stdout.
5
+ Review the extracted React/Next.js component(s) provided below and decide whether each belongs in **Contentful Experience Orchestration** as a Component Type. The input is a JSON array — you may receive 1–N components in a single message. Output one JSON tool call per input component to stdout, named after the component. Tool calls may appear in any order.
6
6
 
7
7
  ---
8
8
 
@@ -66,6 +66,14 @@ These are **not** valid reasons to reject a component:
66
66
 
67
67
  > **Data-fetch wrapper rule**: Reject a component if it imports or calls a generated query hook, loads data, and then forwards that data into a sibling renderer. The sibling renderer is the Component Type; the data-loader wrapper is not.
68
68
 
69
+ > **Utility-wrapper rule**: Reject a component if **all three** of the following are true:
70
+ >
71
+ > 1. It has no props that meaningfully shape user-facing content (no text strings, headings, image URLs, links, body content, rich text, or media references).
72
+ > 2. The props it does have are purely structural or behavioral — e.g., `container`, `target`, `as`, `asChild`, render-prop callbacks, internal `ref` forwarding, focus/portal targets, debug toggles, or `children` only.
73
+ > 3. It is a utility wrapper rather than a composable content surface. Concrete examples to reject under this rule: `Portal`, `SrOnly` (screen-reader-only wrappers), `FocusTrap`, `ErrorBoundary`, `Suspense` fallbacks, debug-only wrappers, and provider-shaped components whose only job is to forward children.
74
+ >
75
+ > Use `reject_component` with a reason like `"Utility wrapper — no authorable content surface"` or `"Structural-only component — no user-shaping props"`. This rule is additive to the categories above; do **not** use it to reject a component that has even one author-shaping prop (e.g., a `label`, `title`, `text`, `href`, `src`, or `richText` prop) — those still belong as Component Types per the "one rule" above.
76
+
69
77
  ## Using `selectionContext`
70
78
 
71
79
  If the input includes `selectionContext`, treat it as the only repo-level context you may use. It is already bounded to the customer-provided project files and may include:
@@ -89,7 +97,7 @@ Use that bounded context to distinguish the author-facing renderer from infrastr
89
97
 
90
98
  Emit one JSON object on a single line. Lines not starting with `{` are ignored by the parser — use them freely for reasoning.
91
99
 
92
- **Two tool calls — emit exactly one:**
100
+ **Two tool calls — emit exactly one per input component:**
93
101
 
94
102
  ```
95
103
  {"tool":"select_component","name":"<ComponentName>","reason":"<brief reason>","confidence":<1-5>}
@@ -99,8 +107,9 @@ Emit one JSON object on a single line. Lines not starting with `{` are ignored b
99
107
 
100
108
  **Rules:**
101
109
 
102
- - Emit exactly one JSON object, on one line. No multi-line JSON. No markdown fences.
103
- - The `name` must match the component name in the input.
110
+ - Emit exactly one JSON object per line. No multi-line JSON. No markdown fences.
111
+ - Emit exactly one tool call per input component. Tool calls may appear in any order.
112
+ - The `name` must match a component name from the input array exactly.
104
113
  - `reason` is a brief phrase documenting your decision.
105
114
  - `confidence` is your certainty (1–5) that the decision is correct:
106
115
  - **5** — obvious case, no doubt (clear UI atom, or clear infrastructure with no visual output)