@contentful/experience-design-system-cli 2.12.4-dev-build-ff57340.0 → 2.13.1-dev-build-fa478b3.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 (55) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/analyze/command.js +2 -22
  3. package/dist/src/analyze/select/command.js +1 -1
  4. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +1 -24
  5. package/dist/src/analyze/select/tui/components/FieldEditor.js +10 -137
  6. package/dist/src/analyze/select-agent/command.js +1 -1
  7. package/dist/src/apply/command.d.ts +1 -34
  8. package/dist/src/apply/command.js +0 -81
  9. package/dist/src/import/tui/WizardApp.js +10 -59
  10. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
  11. package/dist/src/import/tui/steps/GenerateReviewStep.js +23 -213
  12. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
  13. package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
  14. package/dist/src/import/tui/steps/preview-diff.js +0 -48
  15. package/dist/src/session/db.d.ts +0 -9
  16. package/dist/src/session/db.js +0 -46
  17. package/dist/src/types.d.ts +2 -76
  18. package/dist/src/types.js +1 -4
  19. package/package.json +3 -6
  20. package/dist/src/analyze/cycle-detection.d.ts +0 -93
  21. package/dist/src/analyze/cycle-detection.js +0 -326
  22. package/dist/src/analyze/extract/astro.d.ts +0 -5
  23. package/dist/src/analyze/extract/astro.js +0 -289
  24. package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
  25. package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
  26. package/dist/src/analyze/extract/pipeline.d.ts +0 -6
  27. package/dist/src/analyze/extract/pipeline.js +0 -314
  28. package/dist/src/analyze/extract/react.d.ts +0 -2
  29. package/dist/src/analyze/extract/react.js +0 -2041
  30. package/dist/src/analyze/extract/scoring.d.ts +0 -12
  31. package/dist/src/analyze/extract/scoring.js +0 -108
  32. package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
  33. package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
  34. package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
  35. package/dist/src/analyze/extract/slot-detection.js +0 -101
  36. package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
  37. package/dist/src/analyze/extract/source-inspection.js +0 -189
  38. package/dist/src/analyze/extract/stencil.d.ts +0 -2
  39. package/dist/src/analyze/extract/stencil.js +0 -296
  40. package/dist/src/analyze/extract/svelte.d.ts +0 -5
  41. package/dist/src/analyze/extract/svelte.js +0 -1626
  42. package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
  43. package/dist/src/analyze/extract/tsx-shared.js +0 -263
  44. package/dist/src/analyze/extract/validate.d.ts +0 -16
  45. package/dist/src/analyze/extract/validate.js +0 -94
  46. package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
  47. package/dist/src/analyze/extract/vue-tsx.js +0 -498
  48. package/dist/src/analyze/extract/vue.d.ts +0 -5
  49. package/dist/src/analyze/extract/vue.js +0 -653
  50. package/dist/src/analyze/extract/web-components.d.ts +0 -2
  51. package/dist/src/analyze/extract/web-components.js +0 -876
  52. package/dist/src/analyze/pre-classify.d.ts +0 -17
  53. package/dist/src/analyze/pre-classify.js +0 -211
  54. package/dist/src/apply/error-parser.d.ts +0 -43
  55. package/dist/src/apply/error-parser.js +0 -163
@@ -8,8 +8,7 @@ import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
8
8
  import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
9
9
  import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
10
10
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
11
- import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, loadSlotCycles, storeSlotCycles, } from '../../../session/db.js';
12
- import { formatCyclePathSegments, findSlotCycles, suggestCycleBreakEdge } from '../../../analyze/cycle-detection.js';
11
+ import { openPipelineDb, loadCDFComponents, storeCDFComponents, loadComponentReviewMetadata, loadComponentRationale, } from '../../../session/db.js';
13
12
  import { RationalePanel } from '../../../analyze/select/tui/components/RationalePanel.js';
14
13
  import { ComponentRationalePanel } from '../../../analyze/select/tui/components/ComponentRationalePanel.js';
15
14
  import { applyPreviewAnnotations } from '../../../analyze/select/preview-annotations.js';
@@ -25,22 +24,12 @@ import { computeNextScrollOffset } from '../../../analyze/select/tui/hooks/scrol
25
24
  *
26
25
  * Within each tier (empty / non-empty) we tie-break alphabetically by `key`.
27
26
  */
28
- export function sortComponentsForSidebar(components, cycleParticipants) {
29
- const isEmpty = (entry) => Object.keys(entry.$properties ?? {}).length === 0 && Object.keys(entry.$slots ?? {}).length === 0;
30
- // Tier order: cycle members first (they block push — surface loudest),
31
- // then empty (soft warning), then everything else. Ties broken alpha.
32
- const tier = (c) => {
33
- if (cycleParticipants?.has(c.key))
34
- return 0;
35
- if (isEmpty(c.entry))
36
- return 1;
37
- return 2;
38
- };
27
+ export function sortComponentsForSidebar(components) {
39
28
  return [...components].sort((a, b) => {
40
- const at = tier(a);
41
- const bt = tier(b);
42
- if (at !== bt)
43
- return at - bt;
29
+ const aEmpty = Object.keys(a.entry.$properties ?? {}).length === 0;
30
+ const bEmpty = Object.keys(b.entry.$properties ?? {}).length === 0;
31
+ if (aEmpty !== bEmpty)
32
+ return aEmpty ? -1 : 1;
44
33
  return a.key.localeCompare(b.key);
45
34
  });
46
35
  }
@@ -86,12 +75,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
86
75
  // Tracks the first `g` of a potential `gg` double-tap (jumps to top in
87
76
  // JSON-view + panel-focused state). Reset on any non-`g` key.
88
77
  const pendingGRef = useRef(false);
89
- // INTEG-4401: slot-dependency cycles loaded from the session DB. Non-empty
90
- // triggers sidebar (cycle) badges, banner + [c] detail-panel affordance,
91
- // and (at push time) a hard block.
92
- const [slotCycles, setSlotCycles] = useState([]);
93
- const [showCyclePanel, setShowCyclePanel] = useState(false);
94
- const [cyclePanelScroll, setCyclePanelScroll] = useState(0);
95
78
  const handleLivePreviewResult = (response) => {
96
79
  if (!response)
97
80
  return;
@@ -123,10 +106,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
123
106
  async function load() {
124
107
  const db = openPipelineDb();
125
108
  let cdfComponents;
126
- let cycles = [];
127
109
  try {
128
110
  cdfComponents = loadCDFComponents(db, extractSessionId);
129
- cycles = loadSlotCycles(db, extractSessionId);
130
111
  }
131
112
  finally {
132
113
  db.close();
@@ -141,12 +122,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
141
122
  entry,
142
123
  status: 'needs-review',
143
124
  }));
144
- const cycleParticipants = new Set();
145
- for (const c of cycles)
146
- for (const p of c.path)
147
- cycleParticipants.add(p);
148
- setSlotCycles(cycles);
149
- setComponents(sortComponentsForSidebar(reviewEntries, cycleParticipants));
125
+ setComponents(sortComponentsForSidebar(reviewEntries));
150
126
  setLoading(false);
151
127
  }
152
128
  load().catch((e) => {
@@ -253,66 +229,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
253
229
  }
254
230
  onFinalize(acceptedCount, explicitlyRejected.length, unresolved.length);
255
231
  };
256
- /**
257
- * INTEG-4401 (Fix 3/4): re-run cycle detection against the current in-memory
258
- * component state and, if the result differs from the persisted `slotCycles`
259
- * state, update both React state and the session DB. Called from:
260
- * - `handleEditSave` after a FieldEditor save mutates a slot's
261
- * `$allowedComponents`, so the banner / sidebar badges / [c] panel reflect
262
- * reality instead of the stale extract-time snapshot.
263
- * - The `r` reject keystroke, since dropping a component from the manifest
264
- * can collapse cycles that routed through it.
265
- *
266
- * Rejected components are excluded from the graph — they will never ship, so
267
- * they cannot contribute to a real push-time cycle even if their slot config
268
- * still references other components locally.
269
- *
270
- * Cheap for typical N: findSlotCycles is O((V+E)(C+1)) and most manifests
271
- * have zero or a handful of cycles. Wrapped in try/catch so a malformed
272
- * $slots shape can't crash the render pipeline.
273
- */
274
- const recomputeCycles = (currentComponents) => {
275
- try {
276
- const graph = currentComponents
277
- .filter((c) => c.status !== 'rejected')
278
- .map((c) => ({
279
- name: c.key,
280
- slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
281
- name: slotName,
282
- allowedComponents: Array.isArray(slotDef?.$allowedComponents)
283
- ? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
284
- : [],
285
- })),
286
- }));
287
- const rawCycles = findSlotCycles(graph);
288
- const next = rawCycles.map((cycle) => ({
289
- path: cycle.path,
290
- edges: cycle.edges,
291
- suggestedBreak: cycle.edges.length > 0 ? suggestCycleBreakEdge(cycle, rawCycles) : null,
292
- }));
293
- // Cheap structural equality via JSON.stringify — cycle count is tiny and
294
- // this only fires on user actions, not on every render.
295
- const prevSerialized = JSON.stringify(slotCycles);
296
- const nextSerialized = JSON.stringify(next);
297
- if (prevSerialized === nextSerialized)
298
- return;
299
- setSlotCycles(next);
300
- const db = openPipelineDb();
301
- try {
302
- storeSlotCycles(db, extractSessionId, next);
303
- }
304
- finally {
305
- db.close();
306
- }
307
- // If the new cycle set is empty, clear any lingering finalize error the
308
- // [F] guard may have surfaced. Otherwise leave it alone.
309
- if (next.length === 0)
310
- setFinalizeError(null);
311
- }
312
- catch {
313
- // Defensive: swallow — never let cycle detection crash the review UI.
314
- }
315
- };
316
232
  const handleEditSave = () => {
317
233
  const current = components[selectedIdx];
318
234
  if (!current)
@@ -328,11 +244,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
328
244
  setSaveError('Invalid CDF entry: must have $type: "component" and $properties object');
329
245
  return;
330
246
  }
331
- let updatedComponents = [];
332
- setComponents((prev) => {
333
- updatedComponents = prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c);
334
- return updatedComponents;
335
- });
247
+ setComponents((prev) => prev.map((c, i) => i === selectedIdx ? { ...c, entry, status: c.status === 'needs-review' ? 'accepted' : c.status } : c));
336
248
  setDraftValue('');
337
249
  setSaveError(null);
338
250
  const db = openPipelineDb();
@@ -342,12 +254,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
342
254
  finally {
343
255
  db.close();
344
256
  }
345
- // INTEG-4401 (Fix 3/4): recompute cycles against the freshly-edited
346
- // component set so banner/badges/[c]-panel stay in sync. The FieldEditor
347
- // picker filters cycle-forming candidates, but free-text edits + JSON
348
- // pastes still slip through, and edits that BREAK an existing cycle
349
- // won't propagate without this call.
350
- recomputeCycles(updatedComponents);
351
257
  // Feature 2: re-fire the live preview now that pipeline.db reflects
352
258
  // the new state. The hook owns debounce + cred-missing short-circuit.
353
259
  livePreviewHook.trigger();
@@ -376,31 +282,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
376
282
  }
377
283
  return;
378
284
  }
379
- // INTEG-4401: slot-cycle detail panel. Same modal-swallow rules as
380
- // showRemovedPanel; q/Esc close, ↑↓ scroll.
381
- if (showCyclePanel) {
382
- if (input === 'c' || input === 'q' || key.escape) {
383
- setShowCyclePanel(false);
384
- setCyclePanelScroll(0);
385
- return;
386
- }
387
- if (key.upArrow || input === 'k') {
388
- setCyclePanelScroll((v) => Math.max(0, v - 1));
389
- return;
390
- }
391
- if (key.downArrow || input === 'j') {
392
- setCyclePanelScroll((v) => v + 1);
393
- return;
394
- }
395
- return;
396
- }
397
- // Open the cycle panel from sidebar-focused state when there is at
398
- // least one cycle to display.
399
- if (input === 'c' && sidebarFocused && slotCycles.length > 0) {
400
- setShowCyclePanel(true);
401
- setCyclePanelScroll(0);
402
- return;
403
- }
404
285
  // `d` opens the panel only when live-preview is enabled and there is at
405
286
  // least one removed component to display. Sidebar-focused only so it
406
287
  // doesn't collide with FieldEditor input.
@@ -530,14 +411,6 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
530
411
  return;
531
412
  }
532
413
  if (input === 'F') {
533
- // INTEG-4401: refuse to open the finalize dialog while any slot cycle
534
- // is unresolved. The push-time hard block (`assertNoSlotCycles`) would
535
- // catch this downstream, but showing an inline banner here saves the
536
- // operator from confirming the dialog only to hit a stderr crash.
537
- if (slotCycles.length > 0) {
538
- setFinalizeError('Cannot finalize — resolve slot dependency cycle(s) first (press [c] for detail)');
539
- return;
540
- }
541
414
  setShowFinalize(true);
542
415
  return;
543
416
  }
@@ -547,14 +420,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
547
420
  return;
548
421
  }
549
422
  if (input === 'r') {
550
- // INTEG-4401 (Fix 4): rejecting removes the component from the effective
551
- // manifest, so recompute cycles across the reduced graph — any cycle
552
- // that routed through this component collapses. We build the "next"
553
- // components array inline (mirroring updateStatus) so recomputeCycles
554
- // sees the post-update graph without waiting for a render tick.
555
- const next = components.map((c, i) => i === selectedIdx ? { ...c, status: 'rejected' } : c);
556
- setComponents(next);
557
- recomputeCycles(next);
423
+ updateStatus(selectedIdx, 'rejected');
558
424
  return;
559
425
  }
560
426
  if (input === 'A') {
@@ -607,89 +473,37 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
607
473
  }
608
474
  const selected = components[selectedIdx] ?? null;
609
475
  const selectedJson = selected ? JSON.stringify({ [selected.key]: selected.entry }, null, 2) : '';
610
- const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0 && Object.keys(c.entry.$slots ?? {}).length === 0;
476
+ // A component with zero classified $properties is a real defensibility issue
477
+ // it can't be pushed to Contentful (no fields). Surface it in the sidebar via
478
+ // the existing warning-color path (yellow) and a "(empty)" suffix so the user
479
+ // can see what went wrong. They can manually add props in FieldEditor or
480
+ // explicitly reject the component.
481
+ const isEmpty = (c) => Object.keys(c.entry.$properties).length === 0;
611
482
  const emptyCount = components.filter(isEmpty).length;
612
- // INTEG-4401: cycle-participant set drives sidebar `(cycle)` badges plus
613
- // the banner counts. Recomputed on every render — cheap for typical N.
614
- const cycleParticipantSet = new Set();
615
- for (const cycle of slotCycles)
616
- for (const p of cycle.path)
617
- cycleParticipantSet.add(p);
618
- const sidebarSuffix = (c) => {
619
- if (cycleParticipantSet.has(c.key))
620
- return ' (cycle)';
621
- if (isEmpty(c))
622
- return ' (empty)';
623
- return '';
624
- };
625
483
  const sidebarItems = components.map((c) => ({
626
484
  id: c.key,
627
- name: `${c.key}${sidebarSuffix(c)}`,
485
+ name: isEmpty(c) ? `${c.key} (empty)` : c.key,
628
486
  status: c.status,
629
487
  previewAnnotation: previewAnnotations.get(c.key),
630
488
  extractionConfidence: null,
631
489
  needsReview: false,
632
490
  validationErrorCount: 0,
633
- validationWarningCount: sidebarSuffix(c) !== '' ? 1 : 0,
491
+ validationWarningCount: isEmpty(c) ? 1 : 0,
634
492
  }));
635
- // Account for the "(cycle)" / "(empty)" suffix added to badged component
636
- // names so the sidebar doesn't truncate them.
637
- const longestName = components.reduce((m, c) => Math.max(m, c.key.length + sidebarSuffix(c).length), 0);
493
+ // Account for the "(empty)" suffix added to zero-prop component names so the
494
+ // sidebar doesn't truncate it.
495
+ const longestName = components.reduce((m, c) => Math.max(m, c.key.length + (isEmpty(c) ? ' (empty)'.length : 0)), 0);
638
496
  // +5 = border (1) + status icon (1) + badge column (1) + space (1) + border (1).
639
497
  // The badge column is reserved even when no annotation is present so the
640
498
  // sidebar width doesn't jitter as live-preview annotations flip in/out.
641
499
  const sidebarWidth = Math.min(Math.max(longestName + 5, 14), 30);
642
500
  const panelWidth = Math.max(10, terminalWidth - sidebarWidth - 4);
643
- // INTEG-4401: project-wide slot graph passed to FieldEditor so its
644
- // $allowedComponents picker can filter out cycle-forming candidates. Built
645
- // from every review entry's $slots — includes accepted, rejected, and
646
- // needs-review components so the graph reflects what will actually be
647
- // pushed. The FieldEditor replaces its own entry in-simulation with its
648
- // live editor state, so pending edits are always reflected.
649
- const projectSlotGraph = components.map((c) => ({
650
- name: c.key,
651
- slots: Object.entries(c.entry.$slots ?? {}).map(([slotName, slotDef]) => ({
652
- name: slotName,
653
- allowedComponents: Array.isArray(slotDef?.$allowedComponents)
654
- ? slotDef.$allowedComponents.filter((v) => typeof v === 'string')
655
- : [],
656
- })),
657
- }));
658
501
  const accepted = components.filter((c) => c.status === 'accepted').length;
659
502
  const rejected = components.filter((c) => c.status === 'rejected').length;
660
503
  const needsReview = components.filter((c) => c.status === 'needs-review').length;
661
504
  const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
662
505
  const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
663
- return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `Removed components (${removedComponents.length})` }), _jsx(Text, { dimColor: true, children: "these will be DELETED from the target space" }), _jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: `- ${rc.name}${rc.id ? ` (${rc.id})` : ''}` }, rc.id))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press d or Esc to close" })] })), showCyclePanel &&
664
- !dialogOpen &&
665
- (() => {
666
- // Materialize the full panel body as a flat list of Text lines,
667
- // then slice by cyclePanelScroll so ↑↓ can walk arbitrarily long
668
- // content. Each cycle contributes 3-4 lines: heading, path,
669
- // suggested fix (if any), and a blank separator.
670
- const PANEL_H = 20;
671
- const lines = [];
672
- lines.push(_jsx(Text, { bold: true, color: "yellow", children: `SLOT DEPENDENCY CYCLES (${slotCycles.length})` }, "cyc-title"));
673
- lines.push(_jsx(Text, { dimColor: true, children: 'push will fail until these are resolved' }, "cyc-sub"));
674
- lines.push(_jsx(Text, { children: " " }, "cyc-space"));
675
- slotCycles.forEach((cycle, idx) => {
676
- const nodeCount = new Set(cycle.path).size;
677
- lines.push(_jsx(Text, { bold: true, children: `▸ Cycle ${idx + 1} (${nodeCount} component${nodeCount === 1 ? '' : 's'}):` }, `cyc-h-${idx}`));
678
- // Colorize slots (cyan, bracketed) distinctly from components so
679
- // the operator can visually parse the alternating structure.
680
- // Brackets on slot names ensure the distinction survives when
681
- // ANSI is stripped (logs, redirected output).
682
- const segs = formatCyclePathSegments(cycle, 16);
683
- lines.push(_jsxs(Text, { children: [' ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: "cyan", children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { children: seg.text }, si)))] }, `cyc-p-${idx}`));
684
- if (cycle.suggestedBreak) {
685
- const b = cycle.suggestedBreak;
686
- lines.push(_jsx(Text, { dimColor: true, children: ` Suggested fix: remove '${b.toComponent}' from ${b.fromComponent}.$slots.${b.slotName}.$allowedComponents` }, `cyc-f-${idx}`));
687
- }
688
- lines.push(_jsx(Text, { children: " " }, `cyc-s-${idx}`));
689
- });
690
- const visible = lines.slice(cyclePanelScroll, cyclePanelScroll + PANEL_H);
691
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [visible, _jsx(Text, { dimColor: true, children: '[↑↓/j/k] scroll [c/q/Esc] close' })] }));
692
- })(), !dialogOpen &&
506
+ return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), showRemovedPanel && !dialogOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `Removed components (${removedComponents.length})` }), _jsx(Text, { dimColor: true, children: "these will be DELETED from the target space" }), _jsx(Text, { children: " " }), removedComponents.map((rc) => (_jsx(Text, { children: `- ${rc.name}${rc.id ? ` (${rc.id})` : ''}` }, rc.id))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press d or Esc to close" })] })), !dialogOpen &&
693
507
  livePreview &&
694
508
  (() => {
695
509
  // Pilot-2026-06-23 R2: at-a-glance diff summary at the top of the
@@ -712,10 +526,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
712
526
  if (!hasCounts)
713
527
  return null;
714
528
  return (_jsxs(Box, { children: [_jsx(Text, { children: 'Preview: ' }), _jsx(Text, { color: "green", children: `${counts.new} new` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "yellow", children: `${counts.changed} changed` }), _jsx(Text, { children: ' · ' }), _jsx(Text, { dimColor: true, children: `${counts.removed} removed` }), removedComponents.length > 0 && _jsx(Text, { dimColor: true, children: ' ([d] removed list)' }), _jsx(Text, { children: ' · ' }), _jsx(Text, { color: "red", bold: true, children: `${counts.breaking} breaking` })] }));
715
- })(), !dialogOpen && slotCycles.length > 0 && !showCyclePanel && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: `⚠ ${slotCycles.length} slot dependency cycle${slotCycles.length === 1 ? '' : 's'} detectedpush will fail` }), slotCycles.slice(0, 3).map((cycle, idx) => {
716
- const segs = formatCyclePathSegments(cycle);
717
- return (_jsxs(Text, { color: "yellow", children: [' Cycle: ', segs.map((seg, si) => seg.kind === 'slot' ? (_jsx(Text, { color: "cyan", children: seg.text }, si)) : seg.kind === 'arrow' ? (_jsx(Text, { dimColor: true, children: seg.text }, si)) : (_jsx(Text, { color: "yellow", children: seg.text }, si)))] }, `cyc-banner-${idx}`));
718
- }), slotCycles.length > 3 && _jsx(Text, { color: "yellow", children: ` …${slotCycles.length - 3} more` }), _jsx(Text, { dimColor: true, children: ' press [c] for detail' })] })), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props — review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: "red", children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
529
+ })(), !dialogOpen && emptyCount > 0 && (_jsx(Text, { color: "yellow", children: `⚠ ${emptyCount} component${emptyCount === 1 ? '' : 's'} had no classifiable props review with care` })), !dialogOpen && finalizeError && _jsx(Text, { color: "red", children: `⚠ ${finalizeError}` }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
719
530
  const idx = components.findIndex((c) => c.key === id);
720
531
  if (idx >= 0) {
721
532
  setSelectedIdx(idx);
@@ -764,12 +575,11 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, liveP
764
575
  }, onToggleSourceExternal: () => {
765
576
  setPanelOpen('source');
766
577
  setPanelScrollOffset(() => 0);
767
- }, onTextEntryActiveChange: setTextEntryActive, projectSlotGraph: projectSlotGraph, currentComponentName: selected.key }, selected.key)), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
578
+ }, onTextEntryActiveChange: setTextEntryActive }, selected.key)), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsxs(Text, { dimColor: true, children: [sidebarFocused
768
579
  ? ' [a] accept [r] reject [A] accept all [J] ' +
769
580
  (showJson ? 'hide JSON' : 'show JSON') +
770
581
  ' [F] finalize [e/Tab] focus panel' +
771
582
  (livePreview && removedComponents.length > 0 ? ' [d] removed list' : '') +
772
- (slotCycles.length > 0 ? ' [c] cycles' : '') +
773
583
  ' [q] quit'
774
584
  : showJson
775
585
  ? ' [j/k] scroll [Ctrl+u/d] half-page [gg/G] top/bottom [Tab] focus list'
@@ -1,15 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
3
- export interface PreviewDiffLine {
4
- key: string;
5
- color: 'green' | 'red' | 'yellow' | 'gray';
6
- text: string;
7
- }
8
- /**
9
- * Pure builder for the wizard preview diff lines. Extracted from the render
10
- * layer so it can be unit-tested independently of Ink.
11
- */
12
- export declare function buildPreviewDiffLines(preview: ServerPreviewResponse): PreviewDiffLine[];
13
3
  type WizardPreviewStepProps = {
14
4
  preview: ServerPreviewResponse;
15
5
  spaceId: string;
@@ -1,99 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
5
  import { hasBreakingChangesWithImpact } from '../../../apply/manifest.js';
6
6
  import { computeComponentDiffLines } from './preview-diff.js';
7
- /**
8
- * Pure builder for the wizard preview diff lines. Extracted from the render
9
- * layer so it can be unit-tested independently of Ink.
10
- */
11
- export function buildPreviewDiffLines(preview) {
12
- const lines = [];
13
- const { components, tokens } = preview;
14
- for (const item of components.new) {
15
- const raw = item;
16
- const name = raw.key ?? raw.$name ?? 'unknown';
17
- lines.push({ key: `comp-new-${name}`, color: 'green', text: ` + ${name}` });
18
- const slots = (raw.$slots ?? {});
19
- for (const slotName of Object.keys(slots).sort()) {
20
- lines.push({
21
- key: `comp-new-${name}-slot-${slotName}`,
22
- color: 'green',
23
- text: ` slot: ${slotName}`,
24
- });
25
- const allowed = slots[slotName]?.['$allowedComponents'];
26
- if (Array.isArray(allowed) && allowed.length > 0) {
27
- const names = allowed.filter((n) => typeof n === 'string');
28
- lines.push({
29
- key: `comp-new-${name}-slot-${slotName}-allow`,
30
- color: 'green',
31
- text: ` allowedComponents: [${names.join(', ')}]`,
32
- });
33
- }
34
- }
35
- }
36
- for (const item of components.removed) {
37
- lines.push({ key: `comp-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
38
- }
39
- for (const item of components.changed) {
40
- lines.push({
41
- key: `comp-h-${item.current.name}`,
42
- color: 'yellow',
43
- text: ` ~ ${item.current.name}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
44
- });
45
- if (item.changeClassification?.classification === 'breaking') {
46
- const reasons = item.changeClassification.breakingChanges
47
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
48
- .join(', ');
49
- lines.push({ key: `comp-b-${item.current.name}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
50
- }
51
- const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
52
- for (const d of diffLines) {
53
- lines.push({ key: `comp-d-${item.current.name}-${d.key}`, color: d.color, text: ` ${d.text}` });
54
- }
55
- // Also enumerate slots + allowedComponents from the proposed side so the
56
- // reviewer sees the shape they're pushing, not just the delta.
57
- const proposedSlots = item.proposed['$slots'] ?? {};
58
- for (const slotName of Object.keys(proposedSlots).sort()) {
59
- const allowed = proposedSlots[slotName]?.['$allowedComponents'];
60
- if (Array.isArray(allowed) && allowed.length > 0) {
61
- const names = allowed.filter((n) => typeof n === 'string');
62
- const key = `comp-d-${item.current.name}-slot-${slotName}-allow-list`;
63
- if (!lines.some((l) => l.key === key)) {
64
- lines.push({
65
- key,
66
- color: 'gray',
67
- text: ` slot ${slotName} allowedComponents: [${names.join(', ')}]`,
68
- });
69
- }
70
- }
71
- }
72
- }
73
- for (const item of tokens.new) {
74
- const raw = item;
75
- const name = raw.name ?? raw.path ?? 'unknown';
76
- lines.push({ key: `tok-new-${name}`, color: 'green', text: ` + ${name}` });
77
- }
78
- for (const item of tokens.removed) {
79
- lines.push({ key: `tok-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
80
- }
81
- for (const item of tokens.changed) {
82
- const tokenName = item.current.name;
83
- lines.push({
84
- key: `tok-h-${tokenName}`,
85
- color: 'yellow',
86
- text: ` ~ ${tokenName}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
87
- });
88
- if (item.changeClassification?.classification === 'breaking') {
89
- const reasons = item.changeClassification.breakingChanges
90
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
91
- .join(', ');
92
- lines.push({ key: `tok-b-${tokenName}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
93
- }
94
- }
95
- return lines;
96
- }
97
7
  export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }) {
98
8
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
99
9
  const [diffExpanded, setDiffExpanded] = useState(false);
@@ -104,10 +14,56 @@ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber,
104
14
  const allDiffLines = useMemo(() => {
105
15
  if (!diffExpanded)
106
16
  return [];
107
- return buildPreviewDiffLines(preview).map((line) => ({
108
- key: line.key,
109
- element: (_jsx(Text, { color: line.color === 'gray' ? undefined : line.color, dimColor: line.color === 'gray', children: line.text })),
110
- }));
17
+ const lines = [];
18
+ const { components, tokens } = preview;
19
+ for (const item of components.new) {
20
+ const name = item.key ??
21
+ item.$name ??
22
+ 'unknown';
23
+ lines.push({ key: `comp-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
24
+ }
25
+ for (const item of components.removed) {
26
+ lines.push({ key: `comp-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
27
+ }
28
+ for (const item of components.changed) {
29
+ lines.push({
30
+ key: `comp-h-${item.current.name}`,
31
+ element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", item.current.name, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
32
+ });
33
+ if (item.changeClassification?.classification === 'breaking') {
34
+ const reasons = item.changeClassification.breakingChanges
35
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
36
+ .join(', ');
37
+ lines.push({ key: `comp-b-${item.current.name}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
38
+ }
39
+ const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
40
+ for (const d of diffLines) {
41
+ lines.push({ key: `comp-d-${item.current.name}-${d.key}`, element: _jsxs(Text, { color: d.color, children: [" ", d.text] }) });
42
+ }
43
+ }
44
+ for (const item of tokens.new) {
45
+ const name = item.name ??
46
+ item.path ??
47
+ 'unknown';
48
+ lines.push({ key: `tok-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
49
+ }
50
+ for (const item of tokens.removed) {
51
+ lines.push({ key: `tok-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
52
+ }
53
+ for (const item of tokens.changed) {
54
+ const tokenName = item.current.name;
55
+ lines.push({
56
+ key: `tok-h-${tokenName}`,
57
+ element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", tokenName, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
58
+ });
59
+ if (item.changeClassification?.classification === 'breaking') {
60
+ const reasons = item.changeClassification.breakingChanges
61
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
62
+ .join(', ');
63
+ lines.push({ key: `tok-b-${tokenName}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
64
+ }
65
+ }
66
+ return lines;
111
67
  }, [diffExpanded, preview]);
112
68
  const maxScroll = Math.max(0, allDiffLines.length - viewportHeight);
113
69
  useImmediateInput((input, key) => {
@@ -59,7 +59,6 @@ export function computeComponentDiffLines(current, proposed, changeClassificatio
59
59
  const currentSlots = new Set(current.slots);
60
60
  const proposedSlots = (proposed['$slots'] ?? {});
61
61
  const proposedSlotNames = new Set(Object.keys(proposedSlots));
62
- const currentSlotAllowed = current.currentSlotAllowed ?? {};
63
62
  for (const name of [...proposedSlotNames].sort()) {
64
63
  if (!currentSlots.has(name)) {
65
64
  lines.push({ key: `slot-${name}-add`, color: 'green', text: `+ slot: ${name}` });
@@ -70,55 +69,8 @@ export function computeComponentDiffLines(current, proposed, changeClassificatio
70
69
  lines.push({ key: `slot-${name}-rm`, color: 'red', text: `- slot: ${name}` });
71
70
  }
72
71
  }
73
- // $allowedComponents diffs — for each slot present in both sides, or newly-added
74
- // with a non-empty allowedComponents list.
75
- for (const name of [...proposedSlotNames].sort()) {
76
- const nextAllowed = normalizeAllowedComponents(proposedSlots[name]?.['$allowedComponents']);
77
- const prevAllowed = normalizeAllowedComponents(currentSlotAllowed[name]);
78
- const prevExists = currentSlots.has(name);
79
- if (!prevExists) {
80
- if (nextAllowed.length > 0) {
81
- lines.push({
82
- key: `slot-${name}-allow-new`,
83
- color: 'green',
84
- text: `+ slot ${name} allowedComponents: [${nextAllowed.join(', ')}]`,
85
- });
86
- }
87
- continue;
88
- }
89
- if (arraysEqual(prevAllowed, nextAllowed))
90
- continue;
91
- lines.push({
92
- key: `slot-${name}-allow-old`,
93
- color: 'red',
94
- text: `- slot ${name} allowedComponents: [${prevAllowed.join(', ')}]`,
95
- });
96
- lines.push({
97
- key: `slot-${name}-allow-new`,
98
- color: 'green',
99
- text: `+ slot ${name} allowedComponents: [${nextAllowed.join(', ')}]`,
100
- });
101
- }
102
72
  return lines;
103
73
  }
104
- function normalizeAllowedComponents(value) {
105
- if (!Array.isArray(value))
106
- return [];
107
- const out = [];
108
- for (const item of value) {
109
- if (typeof item === 'string' && item.length > 0)
110
- out.push(item);
111
- }
112
- return out.sort();
113
- }
114
- function arraysEqual(a, b) {
115
- if (a.length !== b.length)
116
- return false;
117
- for (let i = 0; i < a.length; i++)
118
- if (a[i] !== b[i])
119
- return false;
120
- return true;
121
- }
122
74
  function normalizeType(type) {
123
75
  switch (type.toLowerCase()) {
124
76
  case 'boolean':
@@ -3,7 +3,6 @@ import type { RawComponentDefinition } from '../types.js';
3
3
  import type { CDFComponentEntry, DTCGTokenEntry, DTCGTokenGroup } from '@contentful/experience-design-system-types';
4
4
  import type { ToolCall, TokenToolCall } from '../generate/agent-runner.js';
5
5
  import type { ComponentTypeSummary } from '@contentful/experience-design-system-types';
6
- import type { SlotCycle, SlotEdge } from '../analyze/cycle-detection.js';
7
6
  export type StepStatus = 'pending' | 'complete' | 'failed' | 'interrupted';
8
7
  export type CommandName = 'analyze extract' | 'analyze select' | 'generate components' | 'generate tokens' | 'generate edit' | 'apply preview' | 'apply select' | 'apply push' | 'print components' | 'print tokens' | 'import';
9
8
  export interface SessionRow {
@@ -177,14 +176,6 @@ export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'compo
177
176
  export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
178
177
  export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
179
178
  export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
180
- export declare function storeSlotCycles(db: DatabaseSync, sessionId: string, cycles: Array<SlotCycle & {
181
- suggestedBreak?: SlotEdge | null;
182
- }>): void;
183
- export interface StoredSlotCycle extends SlotCycle {
184
- suggestedBreak: SlotEdge | null;
185
- }
186
- export declare function loadSlotCycles(db: DatabaseSync, sessionId: string): StoredSlotCycle[];
187
- export declare function clearSlotCycles(db: DatabaseSync, sessionId: string): void;
188
179
  export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
189
180
  export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
190
181
  export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;