@contentful/experience-design-system-cli 2.28.1-dev-build-60a02cc.0 → 2.28.1-dev-build-090b2d1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.28.1-dev-build-60a02cc.0",
3
+ "version": "2.28.1-dev-build-090b2d1.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,6 @@
38
38
  "lint:fix": "nx lint:fix experience-design-system-cli"
39
39
  },
40
40
  "dependencies": {
41
- "@contentful/experience-design-system-cli-v2": "workspace:*",
42
41
  "@contentful/experience-design-system-types": "workspace:*",
43
42
  "@segment/analytics-node": "^3.0.0",
44
43
  "@vue/compiler-sfc": "^3.5.31",
package/dist/src/index.js CHANGED
@@ -46,6 +46,7 @@ function findJsonObjectEnd(line) {
46
46
  function readToolCallObjects(stdout) {
47
47
  const objects = [];
48
48
  const warnings = [];
49
+ let hadDroppedLine = false;
49
50
  for (const raw of stdout.split("\n")) {
50
51
  const line = raw.trim();
51
52
  if (!line.startsWith("{"))
@@ -53,6 +54,7 @@ function readToolCallObjects(stdout) {
53
54
  const end = findJsonObjectEnd(line);
54
55
  if (end === -1) {
55
56
  warnings.push(`unparseable line: ${line.slice(0, 120)}`);
57
+ hadDroppedLine = true;
56
58
  continue;
57
59
  }
58
60
  let parsed;
@@ -60,6 +62,7 @@ function readToolCallObjects(stdout) {
60
62
  parsed = JSON.parse(line.slice(0, end + 1));
61
63
  } catch {
62
64
  warnings.push(`unparseable line: ${line.slice(0, 120)}`);
65
+ hadDroppedLine = true;
63
66
  continue;
64
67
  }
65
68
  const trailing = line.slice(end + 1);
@@ -68,9 +71,11 @@ function readToolCallObjects(stdout) {
68
71
  }
69
72
  if (typeof parsed === "object" && parsed !== null && "tool" in parsed) {
70
73
  objects.push(parsed);
74
+ } else {
75
+ hadDroppedLine = true;
71
76
  }
72
77
  }
73
- return { objects, warnings };
78
+ return { objects, warnings, hadDroppedLine };
74
79
  }
75
80
  function parseSelectToolCallLines(stdout) {
76
81
  const calls = [];
@@ -101,7 +106,7 @@ function parseSelectToolCallLines(stdout) {
101
106
  }
102
107
  function parseToolCallLines(stdout) {
103
108
  const calls = [];
104
- const { objects, warnings } = readToolCallObjects(stdout);
109
+ const { objects, warnings, hadDroppedLine } = readToolCallObjects(stdout);
105
110
  for (const rec of objects) {
106
111
  if (!VALID_TOOL_NAMES.has(rec.tool)) {
107
112
  warnings.push(`unknown tool: ${String(rec.tool)}`);
@@ -186,7 +191,7 @@ function parseToolCallLines(stdout) {
186
191
  calls.push(call);
187
192
  }
188
193
  }
189
- return { calls, warnings };
194
+ return { calls, warnings, hadDroppedLine };
190
195
  }
191
196
  function parseTokenToolCallLines(stdout) {
192
197
  const calls = [];
@@ -9574,6 +9579,26 @@ function loadRawComponents(db, sessionId2, allowedNames) {
9574
9579
  })
9575
9580
  );
9576
9581
  }
9582
+ function rewrittenSourcePaths(basePath) {
9583
+ for (const [emitted, sources] of TS_ESM_EXTENSION_REWRITES) {
9584
+ if (!basePath.endsWith(emitted)) continue;
9585
+ const stem = basePath.slice(0, -emitted.length);
9586
+ return sources.map((ext) => `${stem}${ext}`);
9587
+ }
9588
+ return [];
9589
+ }
9590
+ function identifiersIn(propTypes) {
9591
+ const names = /* @__PURE__ */ new Set();
9592
+ for (const propType of propTypes) {
9593
+ for (const match2 of propType.matchAll(IDENTIFIER_PATTERN)) names.add(match2[0]);
9594
+ }
9595
+ return [...names];
9596
+ }
9597
+ function declaresAnyType(text, typeNames) {
9598
+ return typeNames.some(
9599
+ (name) => new RegExp(`\\b(?:type|interface|enum|const|class)\\s+${escapeForRegExp(name)}\\b`).test(text)
9600
+ );
9601
+ }
9577
9602
  function extractRelativeImportPaths(sourceText) {
9578
9603
  const specifiers = /* @__PURE__ */ new Set();
9579
9604
  for (const match2 of sourceText.matchAll(RELATIVE_IMPORT_PATTERN)) {
@@ -9585,6 +9610,7 @@ async function resolveRelativeImport(specifier, fromDir) {
9585
9610
  const basePath = resolve7(fromDir, specifier);
9586
9611
  const candidates = [
9587
9612
  basePath,
9613
+ ...rewrittenSourcePaths(basePath),
9588
9614
  ...SIBLING_FILE_EXTENSIONS.map((ext) => `${basePath}${ext}`),
9589
9615
  ...SIBLING_FILE_EXTENSIONS.map((ext) => resolve7(basePath, `index${ext}`))
9590
9616
  ];
@@ -9605,7 +9631,7 @@ async function resolveImportSpecifiers(sourceText, fromDir) {
9605
9631
  }
9606
9632
  return resolvedPaths;
9607
9633
  }
9608
- async function loadSiblingFiles(sourceText, sourcePath, propNames) {
9634
+ async function loadSiblingFiles(sourceText, sourcePath, propNames, candidateTypeNames = []) {
9609
9635
  const visited = /* @__PURE__ */ new Set([resolve7(sourcePath)]);
9610
9636
  const discovered = [];
9611
9637
  let frontier = [{ text: sourceText, dir: dirname7(sourcePath) }];
@@ -9630,9 +9656,16 @@ async function loadSiblingFiles(sourceText, sourcePath, propNames) {
9630
9656
  }
9631
9657
  frontier = nextFrontier;
9632
9658
  }
9659
+ const inHand = [sourceText, ...discovered.map((d) => d.content)];
9660
+ const declaredTypeNames = candidateTypeNames.filter((name) => inHand.some((text) => declaresAnyType(text, [name])));
9633
9661
  const usesNotShown = /* @__PURE__ */ new Set();
9662
+ const searchNames = [.../* @__PURE__ */ new Set([...propNames, ...declaredTypeNames])];
9663
+ let enlarged = 0;
9634
9664
  const siblings = discovered.slice(0, MAX_SIBLING_FILES).map((d) => {
9635
- const excerpt = excerptAroundNames(d.content, propNames, MAX_SIBLING_SNIPPET_CHARS);
9665
+ const enlarge = enlarged < MAX_ENLARGED_SIBLINGS && declaresAnyType(d.content, declaredTypeNames);
9666
+ if (enlarge) enlarged++;
9667
+ const budget = enlarge ? MAX_TYPE_DECLARING_SIBLING_CHARS : MAX_SIBLING_SNIPPET_CHARS;
9668
+ const excerpt = excerptAroundNames(d.content, searchNames, budget);
9636
9669
  for (const name of excerpt.usesNotShown) usesNotShown.add(name);
9637
9670
  return { path: d.path, content: excerpt.content };
9638
9671
  });
@@ -9640,25 +9673,32 @@ async function loadSiblingFiles(sourceText, sourcePath, propNames) {
9640
9673
  return {
9641
9674
  siblings,
9642
9675
  truncatedCount,
9643
- usesNotShown: propNames.filter((name) => usesNotShown.has(name))
9676
+ usesNotShown: propNames.filter((name) => usesNotShown.has(name)),
9677
+ declaredTypeNames
9644
9678
  };
9645
9679
  }
9646
- async function loadComponentSourceRef(name, sourcePath, propNames = []) {
9680
+ async function loadComponentSourceRef(name, sourcePath, propNames = [], propTypes = []) {
9647
9681
  let content = null;
9648
9682
  let siblingFiles = [];
9649
9683
  let truncatedSiblingCount = 0;
9650
9684
  const usesNotShown = /* @__PURE__ */ new Set();
9651
9685
  try {
9652
9686
  const rawText = await readFile7(sourcePath, "utf8");
9653
- const mainExcerpt = excerptAroundNames(rawText, propNames, MAX_COMPONENT_SOURCE_CHARS);
9654
- content = mainExcerpt.content;
9655
- for (const name2 of mainExcerpt.usesNotShown) usesNotShown.add(name2);
9656
9687
  let siblingUsesNotShown;
9688
+ let declaredTypeNames;
9657
9689
  ({
9658
9690
  siblings: siblingFiles,
9659
9691
  truncatedCount: truncatedSiblingCount,
9660
- usesNotShown: siblingUsesNotShown
9661
- } = await loadSiblingFiles(rawText, sourcePath, propNames));
9692
+ usesNotShown: siblingUsesNotShown,
9693
+ declaredTypeNames
9694
+ } = await loadSiblingFiles(rawText, sourcePath, propNames, identifiersIn(propTypes)));
9695
+ const mainExcerpt = excerptAroundNames(
9696
+ rawText,
9697
+ [.../* @__PURE__ */ new Set([...propNames, ...declaredTypeNames])],
9698
+ MAX_COMPONENT_SOURCE_CHARS
9699
+ );
9700
+ content = mainExcerpt.content;
9701
+ for (const name2 of mainExcerpt.usesNotShown) usesNotShown.add(name2);
9662
9702
  for (const name2 of siblingUsesNotShown) usesNotShown.add(name2);
9663
9703
  } catch {
9664
9704
  }
@@ -9673,13 +9713,18 @@ async function loadComponentSourceRefs(db, sessionId2) {
9673
9713
  const rows = db.prepare(
9674
9714
  `SELECT component_id, name, source, source_path FROM raw_components WHERE session_id = ? AND status = 'generated' ORDER BY rowid`
9675
9715
  ).all(sessionId2);
9676
- const propNamesFor = db.prepare(
9677
- `SELECT name FROM raw_props WHERE session_id = ? AND component_id = ? ORDER BY position`
9716
+ const propsFor = db.prepare(
9717
+ `SELECT name, type FROM raw_props WHERE session_id = ? AND component_id = ? ORDER BY position`
9678
9718
  );
9679
9719
  return Promise.all(
9680
9720
  rows.map((r) => {
9681
- const propNames = propNamesFor.all(sessionId2, r.component_id).map((p) => p.name);
9682
- return loadComponentSourceRef(r.name, r.source_path ?? r.source, propNames);
9721
+ const props = propsFor.all(sessionId2, r.component_id);
9722
+ return loadComponentSourceRef(
9723
+ r.name,
9724
+ r.source_path ?? r.source,
9725
+ props.map((p) => p.name),
9726
+ props.map((p) => p.type)
9727
+ );
9683
9728
  })
9684
9729
  );
9685
9730
  }
@@ -10562,7 +10607,7 @@ function lookupSelectCache(db, componentHash, promptHash, cliVersion2) {
10562
10607
  updatedAt: row.updated_at
10563
10608
  };
10564
10609
  }
10565
- var SCHEMA, MAX_COMPONENT_SOURCE_CHARS, MAX_SIBLING_FILES, MAX_SIBLING_SNIPPET_CHARS, MAX_SIBLING_DEPTH, MAX_SIBLING_CANDIDATES_EXPLORED, RELATIVE_IMPORT_PATTERN, SIBLING_FILE_EXTENSIONS, _cliCacheVersionCache;
10610
+ var SCHEMA, MAX_COMPONENT_SOURCE_CHARS, MAX_SIBLING_FILES, MAX_SIBLING_SNIPPET_CHARS, MAX_TYPE_DECLARING_SIBLING_CHARS, MAX_ENLARGED_SIBLINGS, MAX_SIBLING_DEPTH, MAX_SIBLING_CANDIDATES_EXPLORED, RELATIVE_IMPORT_PATTERN, SIBLING_FILE_EXTENSIONS, TS_ESM_EXTENSION_REWRITES, IDENTIFIER_PATTERN, _cliCacheVersionCache;
10566
10611
  var init_db = __esm({
10567
10612
  "packages/experience-design-system-cli/src/session/db.ts"() {
10568
10613
  "use strict";
@@ -10748,10 +10793,19 @@ CREATE INDEX IF NOT EXISTS idx_scanned_files_session ON scanned_files(session
10748
10793
  MAX_COMPONENT_SOURCE_CHARS = 8e3;
10749
10794
  MAX_SIBLING_FILES = 5;
10750
10795
  MAX_SIBLING_SNIPPET_CHARS = 1200;
10796
+ MAX_TYPE_DECLARING_SIBLING_CHARS = 4e3;
10797
+ MAX_ENLARGED_SIBLINGS = 2;
10751
10798
  MAX_SIBLING_DEPTH = 2;
10752
10799
  MAX_SIBLING_CANDIDATES_EXPLORED = 25;
10753
10800
  RELATIVE_IMPORT_PATTERN = /from\s+['"](\.[^'"]+)['"]/g;
10754
10801
  SIBLING_FILE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
10802
+ TS_ESM_EXTENSION_REWRITES = [
10803
+ [".js", [".ts", ".tsx", ".js", ".jsx"]],
10804
+ [".jsx", [".tsx", ".jsx"]],
10805
+ [".mjs", [".mts", ".mjs"]],
10806
+ [".cjs", [".cts", ".cjs"]]
10807
+ ];
10808
+ IDENTIFIER_PATTERN = /[A-Za-z_$][A-Za-z0-9_$]*/g;
10755
10809
  _cliCacheVersionCache = null;
10756
10810
  }
10757
10811
  });
@@ -32468,7 +32522,8 @@ async function runOneComponent(agent, model, db, sessionId2, component, tokensIn
32468
32522
  const sourceRef = await loadComponentSourceRef(
32469
32523
  component.name,
32470
32524
  component.sourcePath ?? component.source,
32471
- component.props.map((p) => p.name)
32525
+ component.props.map((p) => p.name),
32526
+ component.props.map((p) => p.type)
32472
32527
  );
32473
32528
  const prompt2 = await buildPrompt({
32474
32529
  skill: "components",
@@ -32516,11 +32571,16 @@ ${outputBuf}`);
32516
32571
  lastError = describeAgentFailure(result);
32517
32572
  continue;
32518
32573
  }
32519
- const { calls, warnings } = parseToolCallLines(result.stdout);
32574
+ const { calls, warnings, hadDroppedLine } = parseToolCallLines(result.stdout);
32520
32575
  if (calls.length === 0) {
32521
32576
  lastError = describeAgentFailure(result);
32522
32577
  continue;
32523
32578
  }
32579
+ if (hadDroppedLine) {
32580
+ lastError = `agent output dropped at least one tool-call line: ${warnings.join("; ").slice(0, 200)}`;
32581
+ if (attempt < maxAttempts) continue;
32582
+ break;
32583
+ }
32524
32584
  const applied = applyToolCalls(db, sessionId2, component.component_id, component.name, calls, warnings);
32525
32585
  if (!noCache) {
32526
32586
  const inputHash = computeComponentInputHash(component);
@@ -32709,7 +32769,8 @@ async function runGenerateSkill(skill, opts, verbose = false) {
32709
32769
  const sampleSourceRef = sampleComponent ? await loadComponentSourceRef(
32710
32770
  sampleComponent.name,
32711
32771
  sampleComponent.sourcePath ?? sampleComponent.source,
32712
- sampleComponent.props.map((p) => p.name)
32772
+ sampleComponent.props.map((p) => p.name),
32773
+ sampleComponent.props.map((p) => p.type)
32713
32774
  ) : void 0;
32714
32775
  const prompt2 = await buildPrompt({
32715
32776
  skill,
@@ -32785,6 +32846,11 @@ async function runGenerateSkill(skill, opts, verbose = false) {
32785
32846
  `Error: all ${componentResults.length} component(s) failed to generate \u2014 see the per-component errors above.`
32786
32847
  );
32787
32848
  }
32849
+ if (failed.length > 0) {
32850
+ die2(
32851
+ `Error: ${failed.length}/${componentResults.length} component(s) failed to generate \u2014 see the per-component errors above.`
32852
+ );
32853
+ }
32788
32854
  } else if (skill === "tokens") {
32789
32855
  const noCache = opts.cache === false || process.env.EDS_NO_CACHE === "1";
32790
32856
  const tokenInputContent = rawTokensInline ?? "";
@@ -33412,7 +33478,7 @@ import { basename as basename5, resolve as resolve19 } from "node:path";
33412
33478
 
33413
33479
  // packages/experience-design-system-cli/src/print/validate/validators/cdf-validator.ts
33414
33480
  import { readFile as readFile19 } from "node:fs/promises";
33415
- import { validateCDF as validateCDF2 } from "@contentful/experience-design-system-types";
33481
+ import { validateCDF as validateCDF2, checkCDFComponentInvariants } from "@contentful/experience-design-system-types";
33416
33482
  function extractValue(input, path) {
33417
33483
  if (path === "/") return void 0;
33418
33484
  const parts = path.split("/").filter(Boolean);
@@ -33496,6 +33562,14 @@ async function validateCDFFile(filePath) {
33496
33562
  }).map((e) => rewriteDiagnostic(e, parsed));
33497
33563
  return { valid: false, summary: "", diagnostics };
33498
33564
  }
33565
+ const invariantErrors = checkCDFComponentInvariants(cdfResult.components);
33566
+ if (invariantErrors.length > 0) {
33567
+ return {
33568
+ valid: false,
33569
+ summary: "",
33570
+ diagnostics: invariantErrors.map((e) => rewriteDiagnostic(e, parsed))
33571
+ };
33572
+ }
33499
33573
  const count = cdfResult.components.length;
33500
33574
  return {
33501
33575
  valid: true,
@@ -36824,15 +36898,6 @@ async function runBuild(opts) {
36824
36898
  child.on("exit", (code) => settle(code ?? 1));
36825
36899
  });
36826
36900
  }
36827
- function registerImportV2Command(program) {
36828
- program.command("importv2").description("Launch the v2 import TUI (experience-design-system-cli-v2)").action(async () => {
36829
- const { render: render7 } = await import("ink");
36830
- const { createElement: createElement7 } = await import("react");
36831
- const { App: App2 } = await import("@contentful/experience-design-system-cli-v2/app");
36832
- const { waitUntilExit } = render7(createElement7(App2));
36833
- await waitUntilExit();
36834
- });
36835
- }
36836
36901
  function registerBuildCommand(program) {
36837
36902
  program.command("build").description("Rebuild from source and re-link exo/experiences binaries to this build").action(async () => {
36838
36903
  const pkgRoot = resolve26(dirname14(fileURLToPath5(import.meta.url)), "..", "..");
@@ -36853,7 +36918,6 @@ function createProgram() {
36853
36918
  registerApplyCommand(program);
36854
36919
  registerSessionCommand(program);
36855
36920
  registerImportCommand(program);
36856
- registerImportV2Command(program);
36857
36921
  registerSetupCommand(program);
36858
36922
  registerRunsCommand(program);
36859
36923
  registerBuildCommand(program);
@@ -107,7 +107,7 @@ export type RawComponentWithId = RawComponentDefinition & {
107
107
  component_id: string;
108
108
  };
109
109
  export declare function loadRawComponents(db: DatabaseSync, sessionId: string, allowedNames?: Set<string>): RawComponentWithId[];
110
- export declare function loadComponentSourceRef(name: string, sourcePath: string, propNames?: string[]): Promise<ComponentSourceRef>;
110
+ export declare function loadComponentSourceRef(name: string, sourcePath: string, propNames?: string[], propTypes?: string[]): Promise<ComponentSourceRef>;
111
111
  export declare function loadComponentSourceRefs(db: DatabaseSync, sessionId: string): Promise<ComponentSourceRef[]>;
112
112
  export declare function renameEmptySlots(db: DatabaseSync, sessionId: string, componentId: string, componentName: string, slotCount: number): {
113
113
  renames: Array<{
@@ -16,6 +16,7 @@ export interface SourceExcerpt {
16
16
  */
17
17
  usesNotShown: string[];
18
18
  }
19
+ export declare function escapeForRegExp(value: string): string;
19
20
  /**
20
21
  * Returns `text` whole when it fits `maxChars`. Otherwise returns windows of
21
22
  * `contextLines` lines around every line that mentions one of `names`, merged
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.28.1-dev-build-60a02cc.0",
3
+ "version": "2.28.1-dev-build-090b2d1.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,8 +39,7 @@
39
39
  "svelte": "^5.56.4",
40
40
  "ts-morph": "^27.0.2",
41
41
  "typescript": "^5.9.3",
42
- "@contentful/experience-design-system-cli-v2": "2.28.1-dev-build-60a02cc.0",
43
- "@contentful/experience-design-system-types": "2.28.1-dev-build-60a02cc.0"
42
+ "@contentful/experience-design-system-types": "2.28.1-dev-build-090b2d1.0"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@tsconfig/node24": "^24.0.4",
@@ -52,9 +51,9 @@
52
51
  "ink-testing-library": "^4.0.0",
53
52
  "typescript-eslint": "^8.67.0",
54
53
  "vitest": "^4.0.16",
55
- "@contentful/experience-design-system-extraction": "2.28.1-dev-build-60a02cc.0",
56
- "@contentful/experience-design-system-client": "2.28.1-dev-build-60a02cc.0",
57
- "@contentful/experience-design-system-generation": "2.28.1-dev-build-60a02cc.0"
54
+ "@contentful/experience-design-system-extraction": "2.28.1-dev-build-090b2d1.0",
55
+ "@contentful/experience-design-system-client": "2.28.1-dev-build-090b2d1.0",
56
+ "@contentful/experience-design-system-generation": "2.28.1-dev-build-090b2d1.0"
58
57
  },
59
58
  "repository": {
60
59
  "type": "git",
@@ -91,7 +91,7 @@ Emit one JSON object per line. The CLI parses lines starting with `{`. Lines not
91
91
  - Every prop in the input must produce exactly one call: `classify_prop` OR `exclude_prop`.
92
92
  - Every slot must produce exactly one `classify_slot` call.
93
93
  - Emit `classify_component` once at the start (required). The `description` field is **required** — always provide a brief description of the component's purpose.
94
- - `values` is required for `cdf_type: "enum"` — must be a non-empty string array of the variant names the prop accepts.
94
+ - `values` is required for `cdf_type: "enum"` — must be a non-empty string array of the variant names the prop accepts. **There is no legal `enum` with a guessed list.** If the value set is shown to you anywhere in the material below — the type's own declaration, an `allowedValues` list on the raw prop, a variant/lookup map, a default, a sibling excerpt — copy it and emit `enum`. If it is shown nowhere, emit `cdf_type: "string"` and leave `values` off. `string` is an unrestricted enum: the author can still pass the real value. A guessed list silently rejects it.
95
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.
96
96
  - `token_kind` is required for `cdf_type: "token"` — must be a DTCG `$type` string, e.g. `"color"`.
97
97
  - `required` must be a JSON boolean (`true`/`false`), not a string.
@@ -208,12 +208,14 @@ the default "could be overridden" (every default can), or because the tie-break
208
208
  below exists. Nothing downstream re-derives, corrects, or second-guesses this —
209
209
  what you emit is what ships.
210
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
211
+ **Ambiguity resolves to `enum` — but only when Q1–Q3 cannot be answered, and only
212
+ when you can still cite its values.** The tie-break applies solely when the source
213
+ shown does not let you answer the three questions: the property is interpolated
214
+ in one place and looked up in another, the use sits in a file you cannot see, or
215
+ the source is truncated at the point of use (the "uses not shown" line). In those
216
+ cases emit `enum` if a value set is citable from the source shown, else `cdf_type:
217
+ "string"` per "no legal `enum` with a guessed list" — either way, record the
218
+ ambiguity in `reason`. When the three
217
219
  questions *can* be answered from the source shown, they are answered and the
218
220
  tie-break does not apply: a clear Q1-yes is `enum`, a clear Q2-yes + Q3-yes is
219
221
  `token`, and a type annotation does not create ambiguity. An `enum` is delivered
@@ -297,7 +299,7 @@ A prop with a complex TypeScript type is **not automatically excluded**. Many pr
297
299
  | Raw type pattern | How to resolve |
298
300
  |---|---|
299
301
  | `'primary' \| 'secondary' \| 'ghost'` (union of literals) | → `enum`, extract `values` |
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. |
302
+ | `HeadingSize` / `ButtonVariant` / any named type that is clearly a finite set of visual options | → `enum` if you can cite its values from the source shown (the type's own declaration, a lookup table, a default) — copy them, never invent them. If the source shown does not enumerate the values, this is not the Q1–Q3 ambiguity tie-break — it is the "no legal `enum` with a guessed list" rule: emit `cdf_type: "string"` instead, with the missing evidence noted in `reason`. 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
303
  | `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. |
302
304
  | `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. |
303
305
  | `ExperienceConfiguration<Variant>` / deep generic | Personalization config — → `exclude_prop`, reason: `"personalization configuration — framework internal"` |
@@ -364,7 +366,7 @@ as enum so the author picks "primary" and the component receives "primary".
364
366
 
365
367
  ---
366
368
 
367
- ## Category correction rules
369
+ ## Category and required corrections
368
370
 
369
371
  The pre-classified `category` is wrong in predictable ways. Correct silently (document in `description`):
370
372
 
@@ -375,6 +377,11 @@ The pre-classified `category` is wrong in predictable ways. Correct silently (do
375
377
 
376
378
  > **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.
377
379
 
380
+ **`required` is also a hint, and it is also wrong.** Verify it against the declaration:
381
+ - A prop with a default value — a `$default`, a destructure default, or a `defaultVariants` entry — is **never** `required: true`. The two together are incoherent.
382
+ - A prop declared `foo?:` is `required: false`, whatever the hint says.
383
+ - Props reached through `VariantProps<typeof x>` or a Radix/`ComponentPropsWithoutRef` base are optional unless the component's own interface redeclares them as required.
384
+
378
385
  ---
379
386
 
380
387
  ## Slot classification
@@ -462,14 +469,30 @@ footer is supplementary and optional
462
469
  {"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."}
463
470
  ```
464
471
 
465
- ### Named type (HeadingSize, ButtonVariant, etc.)
472
+ ### Named type (HeadingSize, ButtonVariant, etc.) — do NOT infer the values
473
+
474
+ When a prop has a named TypeScript type that is not inlined as a union literal,
475
+ the value set is whatever that type's declaration says. A type *name* is not
476
+ evidence of its members: `HeadingSize` could be `h1`–`h6`, `sm|md|lg`, or
477
+ `s|m|l|xl`. Guessing picks one design system's vocabulary and is wrong more
478
+ often than it is right, and a wrong name matches no style rule — the author's
479
+ setting silently does nothing.
480
+
481
+ So: look for the declaration in the source shown. If it is there, copy it. If it
482
+ is not, **emit `string`** — `string` is an unrestricted enum, so the author stays
483
+ unrestricted but correct.
466
484
 
467
- 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.
485
+ Members visible in the source shown — copy them, do not paraphrase:
486
+ ```
487
+ titleSize has type HeadingSize; HeadingSize is declared in the excerpt as 'sm' | 'md' | 'lg'
488
+ {"tool":"classify_prop","prop":"titleSize","cdf_type":"enum","cdf_category":"design","required":false,"values":["sm","md","lg"],"description":"Heading size","reason":"Copied from the HeadingSize declaration shown in the excerpt."}
489
+ ```
468
490
 
491
+ Members NOT visible in the source shown — degrade to `string`:
469
492
  ```
470
- titleSize has type HeadingSize — this is a named enum controlling heading size
471
- inferring likely values: ["h1", "h2", "h3", "h4", "h5", "h6"] — documenting inference
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"}
493
+ titleSize has type HeadingSize; HeadingSize is not declared anywhere in the source shown
494
+ no evidence for its members, so no values list — string, not a guess
495
+ {"tool":"classify_prop","prop":"titleSize","cdf_type":"string","cdf_category":"design","required":false,"description":"Heading size","reason":"HeadingSize is not declared in the source shown, so its members cannot be cited. Emitted as string rather than inventing a value list."}
473
496
  ```
474
497
 
475
498
  ### href prop
@@ -495,7 +518,7 @@ Before emitting any tool calls, verify:
495
518
  1. Every prop in the input has exactly one `classify_prop` or `exclude_prop` call
496
519
  2. Every slot has exactly one `classify_slot` call
497
520
  3. `classify_component` is emitted exactly once
498
- 4. Every `cdf_type: "enum"` has a non-empty `values` array
521
+ 4. Every `cdf_type: "enum"` has a non-empty `values` array taken from the material shown. A prop whose value set was shown nowhere is `cdf_type: "string"` with no `values` — never an `enum` with a plausible-looking list, and never an `exclude_prop`.
499
522
  5. Every `cdf_type: "token"` has `token_kind` (or a warning in `description` if lookup failed) and does **not** have `values` set
500
523
  6. No `cdf_type: "link"` — all href/url props use `string`
501
524
  7. `required` values are JSON booleans, not strings
@@ -507,7 +530,8 @@ Before emitting any tool calls, verify:
507
530
  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
531
  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
532
  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`.
533
+ 16. Every `enum`'s `values` array is copied from evidence in the material shown — a type declaration, an `allowedValues` list on the raw prop, a variant or lookup map, a default, in the component's own file or in any sibling excerpt — and none of it was invented. Where the value set appears nowhere, the prop is `cdf_type: "string"`, with the reason recorded in `reason`. Do not drop a prop merely because you could not enumerate its values.
534
+ 17. No prop has `required: true` alongside a default value (`$default`, a destructure default, or a `defaultVariants` entry) — the two are incoherent. A prop declared `foo?:`, or reached through `VariantProps<typeof x>` or a Radix/`ComponentPropsWithoutRef` base without its own required redeclaration, is `required: false`.
511
535
 
512
536
  After the run completes, the developer can validate the pipeline output with:
513
537
 
@@ -528,4 +552,6 @@ properties — this is never acceptable.
528
552
  If you are genuinely uncertain about every prop, classify each as:
529
553
  {"tool":"classify_prop","prop":"<name>","cdf_type":"string","cdf_category":"content","required":false,"description":"Uncertain classification — review recommended"}
530
554
 
531
- An imperfect classification is infinitely better than no classification.
555
+ An imperfect classification is infinitely better than no classification. That is
556
+ about **classifying a prop rather than dropping it** — it is not a licence to
557
+ invent a `values` list. The uncertain answer is `cdf_type: "string"`, every time.