@contentful/experience-design-system-cli 2.14.2 → 2.15.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 (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
@@ -138,6 +138,15 @@ CREATE TABLE IF NOT EXISTS scanned_files (
138
138
  PRIMARY KEY (session_id, path)
139
139
  );
140
140
 
141
+ CREATE TABLE IF NOT EXISTS slot_cycles (
142
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
143
+ cycle_index INTEGER NOT NULL,
144
+ path_json TEXT NOT NULL,
145
+ edges_json TEXT NOT NULL,
146
+ suggested_break_json TEXT,
147
+ PRIMARY KEY (session_id, cycle_index)
148
+ );
149
+
141
150
  CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
142
151
  CREATE INDEX IF NOT EXISTS idx_steps_command ON steps(session_id, command);
143
152
  CREATE INDEX IF NOT EXISTS idx_raw_components_session ON raw_components(session_id);
@@ -160,18 +169,6 @@ export function openPipelineDb(dbPath) {
160
169
  mkdirSync(dirname(path), { recursive: true });
161
170
  try {
162
171
  const db = new DatabaseSync(path);
163
- // Configure connection-level pragmas BEFORE running schema/migrations.
164
- // - journal_mode=WAL: persistent on the DB file; readers don't block writers
165
- // and vice versa. (Also set in SCHEMA, but harmless to assert per-open.)
166
- // - busy_timeout: per-connection; SQLite waits for a lock instead of
167
- // immediately throwing 'database is locked'. Critical when the wizard
168
- // main process and select-agent subprocesses contend on writes.
169
- // - synchronous=NORMAL: safe under WAL, faster than FULL.
170
- // busy_timeout MUST come first. PRAGMA journal_mode = WAL needs a
171
- // RESERVED lock briefly even when the DB is already WAL; if the
172
- // select-agent child is mid-write at that moment, the wizard's open
173
- // fails instantly without retry. Setting busy_timeout first makes
174
- // SQLite wait up to 5s for the lock.
175
172
  db.exec('PRAGMA busy_timeout = 5000');
176
173
  db.exec('PRAGMA journal_mode = WAL');
177
174
  db.exec('PRAGMA synchronous = NORMAL');
@@ -182,23 +179,16 @@ export function openPipelineDb(dbPath) {
182
179
  catch (e) {
183
180
  const msg = e instanceof Error ? e.message : String(e);
184
181
  if (msg.includes('database is locked')) {
185
- // Throw a typed-ish error rather than process.exit(1). Calling exit
186
- // from inside Ink's render reconciler crashes the TUI mid-frame and
187
- // emits a noisy 'setState during render' warning. The CLI entry
188
- // point (src/index.ts) catches and exits cleanly for non-TUI
189
- // contexts; the wizard catches and transitions to its error step.
190
182
  throw new Error('database is locked: another CLI process may be running. Retry once it exits.');
191
183
  }
192
184
  throw e;
193
185
  }
194
186
  }
195
187
  function applyDbMigrations(db) {
196
- // Add raw_slots.required column if it doesn't exist yet (added in v0.5.14).
197
188
  const cols = db.prepare('PRAGMA table_info(raw_slots)').all();
198
189
  if (!cols.some((c) => c.name === 'required')) {
199
190
  db.exec('ALTER TABLE raw_slots ADD COLUMN required INTEGER NOT NULL DEFAULT 1 CHECK (required IN (0, 1))');
200
191
  }
201
- // Add extraction confidence scoring columns if they don't exist yet (added in v0.6.0).
202
192
  const rawCompCols = db.prepare('PRAGMA table_info(raw_components)').all();
203
193
  const rawCompColNames = new Set(rawCompCols.map((c) => c.name));
204
194
  if (!rawCompColNames.has('extraction_confidence')) {
@@ -210,8 +200,6 @@ function applyDbMigrations(db) {
210
200
  if (!rawCompColNames.has('needs_review')) {
211
201
  db.exec('ALTER TABLE raw_components ADD COLUMN needs_review INTEGER NOT NULL DEFAULT 0');
212
202
  }
213
- // Feature 1: per-component source path on raw_components, rationale + per-prop
214
- // source location on raw_props. All nullable, no DEFAULT — existing rows survive.
215
203
  if (!rawCompColNames.has('source_path')) {
216
204
  db.exec('ALTER TABLE raw_components ADD COLUMN source_path TEXT');
217
205
  }
@@ -226,15 +214,9 @@ function applyDbMigrations(db) {
226
214
  if (!rawPropColNames.has('source_end_line')) {
227
215
  db.exec('ALTER TABLE raw_props ADD COLUMN source_end_line INTEGER');
228
216
  }
229
- // Feature 3: persist select-agent's reject reason on raw_components. Nullable,
230
- // no DEFAULT — accepted components keep NULL, rejected ones store the LLM's
231
- // reason (or `validation error: <codes>` for validation auto-rejections).
232
217
  if (!rawCompColNames.has('reject_reason')) {
233
218
  db.exec('ALTER TABLE raw_components ADD COLUMN reject_reason TEXT');
234
219
  }
235
- // Component rationale: WHY a component was classified, why these props,
236
- // why these slots — surfaced via the `I` panel. All nullable, no DEFAULT
237
- // so existing rows survive. Plus per-slot rationale on raw_slots.
238
220
  if (!rawCompColNames.has('component_description_rationale')) {
239
221
  db.exec('ALTER TABLE raw_components ADD COLUMN component_description_rationale TEXT');
240
222
  }
@@ -248,9 +230,6 @@ function applyDbMigrations(db) {
248
230
  if (!rawSlotColsForRationale.some((c) => c.name === 'rationale')) {
249
231
  db.exec('ALTER TABLE raw_slots ADD COLUMN rationale TEXT');
250
232
  }
251
- // Fine-grained cache: extract_cache holds per-file extracted components keyed
252
- // on (file_hash, cli_version). cli_version is a content hash of bundled skill
253
- // files so cache busts on a prompt change (not on every npm patch bump).
254
233
  const hasExtractCache = db
255
234
  .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='extract_cache'`)
256
235
  .all();
@@ -268,9 +247,6 @@ function applyDbMigrations(db) {
268
247
  CREATE INDEX idx_extract_cache_file ON extract_cache(file_path);
269
248
  `);
270
249
  }
271
- // Fine-grained cache: select_cache stores accept/reject decisions per
272
- // (component_hash, prompt_hash, cli_version). On hit, the wizard replays the
273
- // decision without an LLM call.
274
250
  const hasSelectCache = db
275
251
  .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='select_cache'`)
276
252
  .all();
@@ -288,7 +264,6 @@ function applyDbMigrations(db) {
288
264
  );
289
265
  `);
290
266
  }
291
- // Add generation_cache table if it doesn't exist yet.
292
267
  const tables = db
293
268
  .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='generation_cache'`)
294
269
  .all();
@@ -308,16 +283,6 @@ function applyDbMigrations(db) {
308
283
  CREATE INDEX idx_generation_cache_session ON generation_cache(source_session_id);
309
284
  `);
310
285
  }
311
- // Fine-grained cache: add prompt_hash to generation_cache so cache busts when
312
- // the operator switches prompt files. Existing rows get '' (matches the
313
- // historical "default bundled prompt" — note that lookups using a non-empty
314
- // hash for the bundled prompt won't match, so first run after upgrade busts
315
- // the cache. Documented as acceptable.
316
- //
317
- // The primary key also needs to expand to include prompt_hash so multiple
318
- // prompt variants can coexist per (input_hash, entity_type, entity_id).
319
- // SQLite doesn't allow ALTERing a PK, so when promoting the schema we rebuild
320
- // the table in-place.
321
286
  const genCacheCols = db.prepare(`PRAGMA table_info(generation_cache)`).all();
322
287
  const hasPromptHash = genCacheCols.some((c) => c.name === 'prompt_hash');
323
288
  const oldPkCols = genCacheCols.filter((c) => c.pk > 0).map((c) => c.name);
@@ -356,6 +321,21 @@ function applyDbMigrations(db) {
356
321
  throw e;
357
322
  }
358
323
  }
324
+ const hasCompositionCache = db
325
+ .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='composition_cache'`)
326
+ .all();
327
+ if (hasCompositionCache.length === 0) {
328
+ db.exec(`
329
+ CREATE TABLE composition_cache (
330
+ input_hash TEXT NOT NULL,
331
+ cli_version TEXT NOT NULL,
332
+ agent_output TEXT NOT NULL,
333
+ created_at TEXT NOT NULL,
334
+ updated_at TEXT NOT NULL,
335
+ PRIMARY KEY (input_hash, cli_version)
336
+ );
337
+ `);
338
+ }
359
339
  }
360
340
  export function loadComponentReviewMetadata(db, sessionId, componentName) {
361
341
  const compRow = db
@@ -393,13 +373,6 @@ export function loadComponentReviewMetadata(db, sessionId, componentName) {
393
373
  props,
394
374
  };
395
375
  }
396
- /**
397
- * Component-rationale loader: data contract for the `I` ComponentRationalePanel.
398
- * Returns the component's description + the three component-level rationale
399
- * strings (why-description / why-props / why-slots), plus per-prop and per-slot
400
- * rationale rows for the operator-facing panel. Returns null when no component
401
- * matches.
402
- */
403
376
  export function loadComponentRationale(db, sessionId, componentName) {
404
377
  const compRow = db
405
378
  .prepare(`SELECT component_id, name, description, component_description_rationale, props_rationale, slots_rationale
@@ -458,8 +431,6 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
458
431
  if (call.description !== undefined) {
459
432
  db.prepare('UPDATE raw_components SET description = ? WHERE session_id = ? AND component_id = ?').run(call.description, sessionId, componentId);
460
433
  }
461
- // Component-level rationale: sparse-update each field only when present
462
- // so missing keys don't overwrite previously-stored values.
463
434
  if (call.rationale) {
464
435
  if (call.rationale.description !== undefined) {
465
436
  db.prepare('UPDATE raw_components SET component_description_rationale = ? WHERE session_id = ? AND component_id = ?').run(call.rationale.description, sessionId, componentId);
@@ -499,8 +470,6 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
499
470
  warnings.push(`${componentName}: classify_slot '${call.slot}' — slot not found, skipped`);
500
471
  continue;
501
472
  }
502
- // Per-slot rationale: sparse-update only when present so omitting it
503
- // doesn't blank existing values.
504
473
  if (call.rationale !== undefined) {
505
474
  db.prepare('UPDATE raw_slots SET rationale = ? WHERE session_id = ? AND component_id = ? AND name = ?').run(call.rationale, sessionId, componentId, call.slot);
506
475
  }
@@ -521,11 +490,7 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
521
490
  }
522
491
  return { classified, excluded, slots, warnings };
523
492
  }
524
- export function getOrCreateSession(db, sessionFlag, sessionName,
525
- // `_hints` is retained for call-site compatibility but no longer drives an
526
- // implicit auto-match by inputPath/outDir — operators who want to resume must
527
- // pass an explicit --session <id>.
528
- _hints) {
493
+ export function getOrCreateSession(db, sessionFlag, sessionName, _hints) {
529
494
  const now = new Date().toISOString();
530
495
  if (sessionFlag === 'new' || sessionFlag === undefined) {
531
496
  const id = generateSessionId();
@@ -626,7 +591,6 @@ export function storeRawComponents(db, sessionId, components, options) {
626
591
  }
627
592
  }
628
593
  }
629
- // Restore CDF classification data for props that still exist
630
594
  if (options?.preserveCDF && cdfSnapshot.length > 0) {
631
595
  const updateCDFByName = db.prepare(`UPDATE raw_props SET cdf_type = ?, cdf_category = ?, cdf_token_kind = ?
632
596
  WHERE session_id = ? AND component_id = ? AND name = ?`);
@@ -634,7 +598,6 @@ export function storeRawComponents(db, sessionId, components, options) {
634
598
  WHERE session_id = ? AND component_id = ? AND position = ? AND cdf_type IS NULL`);
635
599
  const restoredPropKeys = new Set();
636
600
  const unmatchedSnaps = [];
637
- // First pass: match by name
638
601
  for (const snap of cdfSnapshot) {
639
602
  const result = updateCDFByName.run(snap.cdf_type, snap.cdf_category, snap.cdf_token_kind, sessionId, snap.component_id, snap.name);
640
603
  if (Number(result.changes) > 0) {
@@ -644,7 +607,6 @@ export function storeRawComponents(db, sessionId, components, options) {
644
607
  unmatchedSnaps.push(snap);
645
608
  }
646
609
  }
647
- // Second pass: for renamed props, fall back to position-based matching
648
610
  for (const snap of unmatchedSnaps) {
649
611
  const result = updateCDFByPosition.run(snap.cdf_type, snap.cdf_category, snap.cdf_token_kind, sessionId, snap.component_id, snap.position);
650
612
  if (Number(result.changes) > 0) {
@@ -660,7 +622,6 @@ export function storeRawComponents(db, sessionId, components, options) {
660
622
  for (const snap of descSnapshot) {
661
623
  updateDesc.run(snap.description, sessionId, snap.component_id);
662
624
  }
663
- // Restore allowed values only for props that still exist (to avoid FK violations)
664
625
  const relevantAV = avSnapshot.filter((av) => restoredPropKeys.has(`${av.component_id}::${av.prop_name}`));
665
626
  if (relevantAV.length > 0) {
666
627
  const deleteAV = db.prepare(`DELETE FROM raw_prop_allowed_values WHERE session_id = ? AND component_id = ? AND prop_name = ?`);
@@ -712,7 +673,6 @@ export function loadRawComponents(db, sessionId, allowedNames) {
712
673
  .prepare(`SELECT component_id, slot_name, position, allowed_component
713
674
  FROM raw_slot_allowed_components WHERE session_id = ? ORDER BY component_id, slot_name, position`)
714
675
  .all(sessionId);
715
- // Group children by component_id
716
676
  const propsByComponent = groupBy(props, (p) => p.component_id);
717
677
  const allowedValuesByProp = groupBy(allowedValues, (av) => `${av.component_id}::${av.prop_name}`);
718
678
  const slotsByComponent = groupBy(slots, (s) => s.component_id);
@@ -770,12 +730,6 @@ export function loadRawComponents(db, sessionId, allowedNames) {
770
730
  }),
771
731
  }));
772
732
  }
773
- /**
774
- * Rename empty-named slots in the DB to heuristic names before generation.
775
- * The LLM classify_slot tool call is matched back to the DB row by name, so a
776
- * row with name="" can never be updated by the LLM — renaming it here makes it
777
- * classifiable. Returns one warning string per renamed slot.
778
- */
779
733
  export function renameEmptySlots(db, sessionId, componentId, componentName, slotCount) {
780
734
  const emptySlots = db
781
735
  .prepare(`SELECT name, position FROM raw_slots
@@ -787,15 +741,9 @@ export function renameEmptySlots(db, sessionId, componentId, componentName, slot
787
741
  const renames = [];
788
742
  const warnings = [];
789
743
  const rename = db.prepare(`UPDATE raw_slots SET name = ? WHERE session_id = ? AND component_id = ? AND name = ? AND position = ?`);
790
- // Multi-statement write — wrap in a transaction so a SIGINT or driver error
791
- // mid-loop leaves raw_slots either fully renamed or fully untouched, never
792
- // half-renamed (which would corrupt the prompt-vs-DB invariant the LLM
793
- // relies on for classify_slot). Matches the existing BEGIN/COMMIT pattern
794
- // throughout this file (see storeRawComponents, createStep, etc.).
795
744
  db.exec('BEGIN');
796
745
  try {
797
746
  for (const slot of emptySlots) {
798
- // Single unnamed slot → "children". Multiple → positional names.
799
747
  const newName = slotCount === 1 ? 'children' : `slot_${slot.position}`;
800
748
  rename.run(newName, sessionId, componentId, slot.name, slot.position);
801
749
  renames.push({ oldName: slot.name, newName });
@@ -834,6 +782,11 @@ export function storeCDFComponents(db, sessionId, components) {
834
782
  VALUES (?, ?, ?, ?, ?)`);
835
783
  const insertAllowedComponent = db.prepare(`INSERT INTO raw_slot_allowed_components (session_id, component_id, slot_name, allowed_component, position)
836
784
  VALUES (?, ?, ?, ?, ?)`);
785
+ const deleteSlots = db.prepare(`DELETE FROM raw_slots WHERE session_id = ? AND component_id = ?`);
786
+ const deleteSlotAllowedComponents = db.prepare(`DELETE FROM raw_slot_allowed_components WHERE session_id = ? AND component_id = ?`);
787
+ const readExistingSlotDefaults = db.prepare(`SELECT name, is_default FROM raw_slots WHERE session_id = ? AND component_id = ?`);
788
+ const insertSlotOnUpdate = db.prepare(`INSERT INTO raw_slots (session_id, component_id, name, is_default, required, description, position)
789
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
837
790
  db.exec('BEGIN');
838
791
  try {
839
792
  for (const { key, entry } of components) {
@@ -850,9 +803,19 @@ export function storeCDFComponents(db, sessionId, components) {
850
803
  prop.$values.forEach((v, i) => insertAllowedValue.run(sessionId, componentId, propName, v, i));
851
804
  }
852
805
  }
806
+ const existingDefaults = new Map(readExistingSlotDefaults.all(sessionId, componentId).map((r) => [r.name, r.is_default]));
807
+ deleteSlotAllowedComponents.run(sessionId, componentId);
808
+ deleteSlots.run(sessionId, componentId);
809
+ let slotPos = 0;
810
+ for (const [slotName, slot] of Object.entries(entry.$slots ?? {})) {
811
+ const isDefault = existingDefaults.get(slotName) ?? 0;
812
+ insertSlotOnUpdate.run(sessionId, componentId, slotName, isDefault, slot.$required ? 1 : 0, slot.$description ?? null, slotPos++);
813
+ if (slot.$allowedComponents && slot.$allowedComponents.length > 0) {
814
+ slot.$allowedComponents.forEach((ac, i) => insertAllowedComponent.run(sessionId, componentId, slotName, ac, i));
815
+ }
816
+ }
853
817
  }
854
818
  else {
855
- // Component not in raw_components (e.g. agent added a new one) — insert a minimal record
856
819
  const componentId = createHash('sha256').update(`${key}:generated`).digest('hex').slice(0, 12);
857
820
  db.prepare(`INSERT INTO raw_components (session_id, component_id, name, source, framework, extracted_at, status, description)
858
821
  VALUES (?, ?, ?, '', 'react', ?, 'generated', ?)`).run(sessionId, componentId, key, now, entry.$description ?? null);
@@ -915,20 +878,8 @@ export function loadCDFComponents(db, sessionId) {
915
878
  const allowedComponentsBySlot = groupBy(allowedComponents, (ac) => `${ac.component_id}::${ac.slot_name}`);
916
879
  return components.map(({ component_id, name, description }) => {
917
880
  const compProps = propsByComponent.get(component_id) ?? [];
918
- // NOTE: Components with zero CDF-classified props are intentionally
919
- // returned with an empty `$properties` object. Filtering them out here
920
- // silently dropped them from the wizard's final-review (INTEG-4257),
921
- // so the user couldn't see that the LLM had failed to classify anything
922
- // and couldn't reject the empty component. Surface them instead — the
923
- // wizard tags them with a ⚠ badge and an in-panel banner so the user
924
- // can manually add props in FieldEditor or reject the component.
925
- // Downstream consumers (buildManifest, push) tolerate empty $properties.
926
881
  const $properties = {};
927
882
  for (const p of compProps) {
928
- // Hallucination insurance: drop any prop whose name didn't survive trim.
929
- // The pre-generate rename guard catches empty-named slots, but if the LLM
930
- // hallucinated a classify_prop with an empty name into the DB, surface
931
- // nothing to the manifest builder rather than emitting "$properties: { '': ... }".
932
883
  if (!p.name.trim())
933
884
  continue;
934
885
  const av = allowedValuesByProp.get(`${component_id}::${p.name}`);
@@ -957,9 +908,6 @@ export function loadCDFComponents(db, sessionId) {
957
908
  const compSlots = slotsByComponent.get(component_id) ?? [];
958
909
  const $slots = {};
959
910
  for (const s of compSlots) {
960
- // Hallucination insurance: same as $properties — drop empty-named slots
961
- // before they reach buildManifest, which faithfully passes empty keys
962
- // through and triggers a 422 from the preview API.
963
911
  if (!s.name.trim())
964
912
  continue;
965
913
  const ac = allowedComponentsBySlot.get(`${component_id}::${s.name}`);
@@ -981,30 +929,39 @@ export function loadCDFComponents(db, sessionId) {
981
929
  });
982
930
  }
983
931
  export function loadScopeComponents(db, sessionId) {
984
- // Loads extract-stage components: those still pending operator review (`extracted`)
985
- // plus those the Feature 3 auto-filter has already classified (`accepted` /
986
- // `rejected`). Excludes `generated` (already past scope-gate) so re-runs of the
987
- // wizard don't show already-confirmed components.
988
932
  const rows = db
989
933
  .prepare(`SELECT name, component_id, status, reject_reason FROM raw_components
990
934
  WHERE session_id = ? AND status IN ('extracted', 'accepted', 'rejected')
991
935
  ORDER BY name`)
992
936
  .all(sessionId);
937
+ if (rows.length === 0)
938
+ return [];
939
+ const slotRows = db
940
+ .prepare(`SELECT component_id, name, position
941
+ FROM raw_slots WHERE session_id = ? ORDER BY component_id, position`)
942
+ .all(sessionId);
943
+ const allowedRows = db
944
+ .prepare(`SELECT component_id, slot_name, position, allowed_component
945
+ FROM raw_slot_allowed_components WHERE session_id = ? ORDER BY component_id, slot_name, position`)
946
+ .all(sessionId);
947
+ const slotsByComponent = groupBy(slotRows, (s) => s.component_id);
948
+ const allowedBySlot = groupBy(allowedRows, (a) => `${a.component_id}::${a.slot_name}`);
993
949
  return rows.map((r) => ({
994
950
  name: r.name,
995
951
  componentId: r.component_id,
996
952
  aiDecision: r.status === 'accepted' ? 'accepted' : r.status === 'rejected' ? 'rejected' : null,
997
953
  aiReason: r.reject_reason,
954
+ slots: (slotsByComponent.get(r.component_id) ?? []).map((s) => ({
955
+ name: s.name,
956
+ allowedComponents: (allowedBySlot.get(`${r.component_id}::${s.name}`) ?? []).map((a) => a.allowed_component),
957
+ })),
998
958
  }));
999
959
  }
1000
960
  export function applyScopeDecisions(db, sessionId, decisions) {
1001
961
  const now = new Date().toISOString();
1002
962
  const acceptedSet = new Set(decisions.accepted);
1003
963
  const accepted = [...acceptedSet];
1004
- // Conflict precedence: accepted wins over rejected.
1005
964
  const rejected = [...new Set(decisions.rejected)].filter((n) => !acceptedSet.has(n));
1006
- // Rejected first so accepted writes can flip back to 'generated' afterwards
1007
- // if a name appears in both lists.
1008
965
  if (rejected.length > 0) {
1009
966
  const placeholders = rejected.map(() => '?').join(',');
1010
967
  db.prepare(`UPDATE raw_components SET status = 'rejected' WHERE session_id = ? AND name IN (${placeholders})`).run(sessionId, ...rejected);
@@ -1107,8 +1064,6 @@ export function findLatestSessionForCommand(db, command) {
1107
1064
  return row?.id ?? null;
1108
1065
  }
1109
1066
  export function seedCDFFromPriorSession(db, targetSessionId) {
1110
- // Find the most recent session (other than target) that has CDF data for any of the
1111
- // target's component_ids. This works regardless of which command produced the CDF.
1112
1067
  const targetComponentIds = db
1113
1068
  .prepare(`SELECT component_id FROM raw_components WHERE session_id = ?`)
1114
1069
  .all(targetSessionId);
@@ -1124,7 +1079,6 @@ export function seedCDFFromPriorSession(db, targetSessionId) {
1124
1079
  if (!priorRow)
1125
1080
  return 0;
1126
1081
  const priorSessionId = priorRow.session_id;
1127
- // Copy CDF columns from prior session's props into matching props in the target session.
1128
1082
  const result = db
1129
1083
  .prepare(`UPDATE raw_props SET
1130
1084
  cdf_type = (SELECT p2.cdf_type FROM raw_props p2
@@ -1141,7 +1095,6 @@ export function seedCDFFromPriorSession(db, targetSessionId) {
1141
1095
  WHERE p2.session_id = ? AND p2.component_id = raw_props.component_id
1142
1096
  AND p2.name = raw_props.name AND p2.cdf_type IS NOT NULL)`)
1143
1097
  .run(priorSessionId, priorSessionId, priorSessionId, targetSessionId, priorSessionId);
1144
- // Also copy descriptions from prior session's components
1145
1098
  db.prepare(`UPDATE raw_components SET description = (
1146
1099
  SELECT c2.description FROM raw_components c2
1147
1100
  WHERE c2.session_id = ? AND c2.component_id = raw_components.component_id
@@ -1151,7 +1104,6 @@ export function seedCDFFromPriorSession(db, targetSessionId) {
1151
1104
  AND EXISTS (SELECT 1 FROM raw_components c2
1152
1105
  WHERE c2.session_id = ? AND c2.component_id = raw_components.component_id
1153
1106
  AND c2.description IS NOT NULL)`).run(priorSessionId, targetSessionId, priorSessionId);
1154
- // Copy allowed values for seeded props from prior session
1155
1107
  if (result.changes > 0) {
1156
1108
  db.prepare(`INSERT OR IGNORE INTO raw_prop_allowed_values (session_id, component_id, prop_name, position, value)
1157
1109
  SELECT ?, av.component_id, av.prop_name, av.position, av.value
@@ -1199,12 +1151,6 @@ export function seedCDFFromPreviewResponse(db, sessionId, removedItems) {
1199
1151
  }
1200
1152
  return totalSeeded;
1201
1153
  }
1202
- /**
1203
- * Seeds server-side metadata for changed components:
1204
- * 1. Fills in defaults the server has but we don't (prevents accidental removal)
1205
- * 2. Seeds cdf_type/cdf_category for props that exist on server but lack CDF locally
1206
- * (e.g. props with non-standard source types the generation step couldn't classify)
1207
- */
1208
1154
  export function seedDefaultsFromChangedItems(db, sessionId, changedItems) {
1209
1155
  if (changedItems.length === 0)
1210
1156
  return 0;
@@ -1224,13 +1170,11 @@ export function seedDefaultsFromChangedItems(db, sessionId, changedItems) {
1224
1170
  const contentProps = new Set(current.contentProperties);
1225
1171
  const designProps = new Set(current.designProperties);
1226
1172
  for (const [propName, propSummary] of Object.entries(current.fullProperties)) {
1227
- // Seed defaults
1228
1173
  if (propSummary.default !== undefined && propSummary.default !== null) {
1229
1174
  const defaultStr = typeof propSummary.default === 'string' ? propSummary.default : JSON.stringify(propSummary.default);
1230
1175
  const result = updateDefaultStmt.run(defaultStr, sessionId, localComponent.component_id, propName);
1231
1176
  totalSeeded += Number(result.changes);
1232
1177
  }
1233
- // Seed CDF type/category for props that lack it
1234
1178
  const cdfType = mapServerTypeToCDFType(propSummary.type);
1235
1179
  if (!cdfType)
1236
1180
  continue;
@@ -1249,14 +1193,7 @@ export function seedDefaultsFromChangedItems(db, sessionId, changedItems) {
1249
1193
  }
1250
1194
  return totalSeeded;
1251
1195
  }
1252
- /**
1253
- * Ensures all props on generated components have a cdf_type.
1254
- * Uses the pre-classification `category` column when available.
1255
- * Falls back to cdf_type: 'string', cdf_category: 'content' for props
1256
- * with no pre-classification (safe default — content is the most common category).
1257
- */
1258
1196
  export function backfillUnclassifiedProps(db, sessionId) {
1259
- // First: backfill props that HAVE a pre-classification category
1260
1197
  const withCategory = db
1261
1198
  .prepare(`UPDATE raw_props SET cdf_type = 'string', cdf_category = category
1262
1199
  WHERE session_id = ? AND cdf_type IS NULL AND category IS NOT NULL
@@ -1264,7 +1201,6 @@ export function backfillUnclassifiedProps(db, sessionId) {
1264
1201
  SELECT component_id FROM raw_components WHERE session_id = ? AND status = 'generated'
1265
1202
  )`)
1266
1203
  .run(sessionId, sessionId);
1267
- // Second: backfill props with NO category (complex types the rules couldn't classify)
1268
1204
  const withoutCategory = db
1269
1205
  .prepare(`UPDATE raw_props SET cdf_type = 'string', cdf_category = 'content'
1270
1206
  WHERE session_id = ? AND cdf_type IS NULL AND category IS NULL
@@ -1297,12 +1233,6 @@ function mapServerTypeToCDFType(serverType) {
1297
1233
  }
1298
1234
  }
1299
1235
  export function computeComponentInputHash(component) {
1300
- // Narrow the hashed payload to extractor-only fields. Any field that
1301
- // `applyToolCalls` may overwrite (prop description/required/default/allowed
1302
- // values, slot description/allowed components) is excluded so that the cache
1303
- // key does not depend on its own output — otherwise re-reading raw props
1304
- // after a generation pass would drift the hash even when the underlying
1305
- // source has not changed.
1306
1236
  const payload = {
1307
1237
  framework: component.framework,
1308
1238
  name: component.name,
@@ -1314,6 +1244,7 @@ export function computeComponentInputHash(component) {
1314
1244
  slots: component.slots.map((s) => ({
1315
1245
  name: s.name,
1316
1246
  isDefault: s.isDefault,
1247
+ allowedComponents: [...(s.allowedComponents ?? [])].sort(),
1317
1248
  })),
1318
1249
  };
1319
1250
  return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
@@ -1388,6 +1319,36 @@ export function loadScannedFiles(db, sessionId) {
1388
1319
  const rows = db.prepare('SELECT path FROM scanned_files WHERE session_id = ? ORDER BY path').all(sessionId);
1389
1320
  return rows.map((r) => r.path);
1390
1321
  }
1322
+ export function storeSlotCycles(db, sessionId, cycles) {
1323
+ db.exec('BEGIN');
1324
+ try {
1325
+ db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
1326
+ const insert = db.prepare(`INSERT INTO slot_cycles (session_id, cycle_index, path_json, edges_json, suggested_break_json)
1327
+ VALUES (?, ?, ?, ?, ?)`);
1328
+ for (let i = 0; i < cycles.length; i += 1) {
1329
+ const cycle = cycles[i];
1330
+ insert.run(sessionId, i, JSON.stringify(cycle.path), JSON.stringify(cycle.edges), cycle.suggestedBreak ? JSON.stringify(cycle.suggestedBreak) : null);
1331
+ }
1332
+ db.exec('COMMIT');
1333
+ }
1334
+ catch (e) {
1335
+ db.exec('ROLLBACK');
1336
+ throw e;
1337
+ }
1338
+ }
1339
+ export function loadSlotCycles(db, sessionId) {
1340
+ const rows = db
1341
+ .prepare('SELECT cycle_index, path_json, edges_json, suggested_break_json FROM slot_cycles WHERE session_id = ? ORDER BY cycle_index')
1342
+ .all(sessionId);
1343
+ return rows.map((r) => ({
1344
+ path: JSON.parse(r.path_json),
1345
+ edges: JSON.parse(r.edges_json),
1346
+ suggestedBreak: r.suggested_break_json ? JSON.parse(r.suggested_break_json) : null,
1347
+ }));
1348
+ }
1349
+ export function clearSlotCycles(db, sessionId) {
1350
+ db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
1351
+ }
1391
1352
  export function markCacheHumanEdited(db, entityType, entityId) {
1392
1353
  const now = new Date().toISOString();
1393
1354
  db.prepare(`UPDATE generation_cache SET human_edited = 1, updated_at = ? WHERE entity_type = ? AND entity_id = ?`).run(now, entityType, entityId);
@@ -1458,18 +1419,7 @@ export function copyTokensFromCache(db, sourceSessionId, targetSessionId) {
1458
1419
  throw e;
1459
1420
  }
1460
1421
  }
1461
- // ---------------------------------------------------------------------------
1462
- // Fine-grained cache: extract_cache helpers
1463
- // ---------------------------------------------------------------------------
1464
1422
  let _cliCacheVersionCache = null;
1465
- /**
1466
- * Returns the CLI cache version used to namespace cache rows. To avoid busting
1467
- * the cache on every npm patch bump, this hashes the content of the bundled
1468
- * skill prompts. When the prompts change, cache invalidates; when only library
1469
- * code or test fixtures change, the cache stays warm.
1470
- *
1471
- * Memoized per-process. Falls back to package version on any I/O error.
1472
- */
1473
1423
  export async function getCliCacheVersion() {
1474
1424
  if (_cliCacheVersionCache)
1475
1425
  return _cliCacheVersionCache;
@@ -1529,6 +1479,28 @@ export function lookupExtractCache(db, fileHash, cliVersion) {
1529
1479
  components,
1530
1480
  };
1531
1481
  }
1482
+ /**
1483
+ * Content-addressed cache for the composition resolver's raw agent output
1484
+ * (authored parser source or emitted-edge JSONL). Keyed by a hash of the
1485
+ * candidate files + producer identity (`composition-cache-key.ts`), scoped by
1486
+ * `cli_version` so a prompt/skill change invalidates stale entries. Replaces
1487
+ * the former on-disk `~/.contentful/.../composition-cache` store so all CLI
1488
+ * caches live in the one pipeline DB.
1489
+ */
1490
+ export function storeCompositionCache(db, inputHash, cliVersion, agentOutput) {
1491
+ const now = new Date().toISOString();
1492
+ db.prepare(`INSERT INTO composition_cache (input_hash, cli_version, agent_output, created_at, updated_at)
1493
+ VALUES (?, ?, ?, ?, ?)
1494
+ ON CONFLICT(input_hash, cli_version) DO UPDATE SET
1495
+ agent_output = excluded.agent_output,
1496
+ updated_at = excluded.updated_at`).run(inputHash, cliVersion, agentOutput, now, now);
1497
+ }
1498
+ export function lookupCompositionCache(db, inputHash, cliVersion) {
1499
+ const row = db
1500
+ .prepare(`SELECT agent_output FROM composition_cache WHERE input_hash = ? AND cli_version = ?`)
1501
+ .get(inputHash, cliVersion);
1502
+ return row ? row.agent_output : null;
1503
+ }
1532
1504
  export function storeSelectCache(db, componentHash, promptHash, cliVersion, decision, reason) {
1533
1505
  const now = new Date().toISOString();
1534
1506
  db.prepare(`INSERT INTO select_cache (component_hash, prompt_hash, cli_version, decision, reason, created_at, updated_at)
@@ -0,0 +1,14 @@
1
+ import type { CompositionMode } from '../lib/composition-mode.js';
2
+ /**
3
+ * Setup prompts for composition (spec T10). Parallels `promptAutoFilterPreference`
4
+ * — injectable `ask` for TTY-free unit testing.
5
+ *
6
+ * (a) Composite-mode default. Default N → atomic (composition OFF); the
7
+ * low-surprise default. `--composite` opts in per invocation.
8
+ */
9
+ export declare function promptCompositeModePreference(ask: (q: string) => Promise<string>, current?: CompositionMode): Promise<CompositionMode>;
10
+ /**
11
+ * (b) Agentic mapping resolution. Default N — opt-in (it costs tokens and the
12
+ * output needs review), and only meaningful in composite mode.
13
+ */
14
+ export declare function promptAgenticResolutionPreference(ask: (q: string) => Promise<string>, current?: boolean): Promise<boolean>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Setup prompts for composition (spec T10). Parallels `promptAutoFilterPreference`
3
+ * — injectable `ask` for TTY-free unit testing.
4
+ *
5
+ * (a) Composite-mode default. Default N → atomic (composition OFF); the
6
+ * low-surprise default. `--composite` opts in per invocation.
7
+ */
8
+ export async function promptCompositeModePreference(ask, current) {
9
+ const defaultValue = current ?? 'atomic';
10
+ const hint = defaultValue === 'composite' ? '[Y/n]' : '[y/N]';
11
+ const answer = (await ask(` Enable composite (embedded-component) mode by default? ${hint} `)).trim().toLowerCase();
12
+ if (answer === '')
13
+ return defaultValue;
14
+ if (answer.startsWith('y'))
15
+ return 'composite';
16
+ if (answer.startsWith('n'))
17
+ return 'atomic';
18
+ return defaultValue;
19
+ }
20
+ /**
21
+ * (b) Agentic mapping resolution. Default N — opt-in (it costs tokens and the
22
+ * output needs review), and only meaningful in composite mode.
23
+ */
24
+ export async function promptAgenticResolutionPreference(ask, current) {
25
+ const defaultValue = current ?? false;
26
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
27
+ const answer = (await ask(` Enable agentic mapping resolution when no groups are found? ${hint} `))
28
+ .trim()
29
+ .toLowerCase();
30
+ if (answer === '')
31
+ return defaultValue;
32
+ if (answer.startsWith('y'))
33
+ return true;
34
+ if (answer.startsWith('n'))
35
+ return false;
36
+ return defaultValue;
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.14.2",
3
+ "version": "2.15.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "files": [
26
26
  "bin/",
27
27
  "dist/",
28
- "skills/"
28
+ "skills/",
29
+ "prompts/"
29
30
  ],
30
31
  "dependencies": {
31
32
  "commander": "^13.1.0",
@@ -33,8 +34,8 @@
33
34
  "react": "^18.3.1",
34
35
  "react-devtools-core": "^4.19.1",
35
36
  "react-dom": "^18.3.1",
36
- "@contentful/experience-design-system-extraction": "2.14.2",
37
- "@contentful/experience-design-system-types": "2.14.2"
37
+ "@contentful/experience-design-system-types": "2.15.0",
38
+ "@contentful/experience-design-system-extraction": "2.15.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@tsconfig/node24": "^24.0.3",
@@ -0,0 +1,7 @@
1
+ These directories were NOT selected as composition candidates by a keyword filter.
2
+ Judging by the PATH/NAME alone, which (if any) might contain component composition,
3
+ mapping, or parent→child relationship declarations worth inspecting? Be selective —
4
+ skip obvious utilities, tests, styles, and unrelated feature code.
5
+
6
+ Reply with ONLY a JSON array of the directory strings to include, e.g. ["src/registry"].
7
+ Use [] if none look relevant. Copy the strings exactly as listed.
@@ -0,0 +1,8 @@
1
+ You are extracting parent→child component composition from a design system by reading the files below.
2
+
3
+ STRICT RULES — follow exactly, they keep the output deterministic:
4
+ 1. Emit an edge ONLY when the candidate files contain explicit evidence that the parent renders/accepts the child (e.g. a mapping declaration, a slot/`allowedComponents` list, a `withParentType`/`requiredParent`/`allowedTagNames` entry). Direct textual evidence only.
5
+ 2. Do NOT infer, guess, or generalize from naming, category, or what "usually" nests. If the files do not state the relationship, do not emit it.
6
+ 3. Every edge MUST include a `reason` that quotes or cites the exact file + declaration that justifies it. If you cannot cite evidence, omit the edge.
7
+ 4. Emit each parent→child pair at most once. Do not repeat edges.
8
+ 5. Prefer completeness of EVIDENCED edges over quantity — a smaller, fully-justified set is correct; padding with plausible-but-unstated edges is wrong.
@@ -0,0 +1 @@
1
+ Your previous parser ran without error but returned 0 edges, even though the candidate files clearly contain composition relationships. Re-examine the patterns (parent attribution is the usual culprit) and return only the corrected function in a single fenced code block.
@@ -0,0 +1,2 @@
1
+ Your previous parser failed with: {{error}}
2
+ Fix it and return only the corrected function in a single fenced code block.