@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
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.4-dev-build-ff57340.0",
3
+ "version": "2.13.1-dev-build-fa478b3.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,16 +37,13 @@
37
37
  "lint:fix": "nx lint:fix experience-design-system-cli"
38
38
  },
39
39
  "dependencies": {
40
+ "@contentful/experience-design-system-extraction": "workspace:*",
40
41
  "@contentful/experience-design-system-types": "workspace:*",
41
- "@vue/compiler-sfc": "^3.5.31",
42
42
  "commander": "^13.1.0",
43
43
  "ink": "^4.4.1",
44
44
  "react": "^18.3.1",
45
45
  "react-devtools-core": "^4.19.1",
46
- "react-dom": "^18.3.1",
47
- "svelte": "^5.56.4",
48
- "ts-morph": "^27.0.2",
49
- "typescript": "^5.9.3"
46
+ "react-dom": "^18.3.1"
50
47
  },
51
48
  "devDependencies": {
52
49
  "@tsconfig/node24": "^24.0.3",
@@ -2,17 +2,11 @@ import { createElement } from 'react';
2
2
  import { render } from 'ink';
3
3
  import { mkdir, readdir, stat } from 'node:fs/promises';
4
4
  import { isAbsolute, join, relative, resolve } from 'node:path';
5
- import { extractComponents } from './extract/pipeline.js';
5
+ import { extractComponents, preClassifyComponent, isNonAuthorableComponent, computeExtractionScore, deriveNeedsReview, describeReviewReasons, inspectComponentSource, validateExtractedComponents, } from '@contentful/experience-design-system-extraction';
6
6
  import { AnalyzeView } from './tui/AnalyzeView.js';
7
7
  import { registerAnalyzeEditCommand } from './select/command.js';
8
8
  import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
9
- import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, storeSlotCycles, } from '../session/db.js';
10
- import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
11
- import { preClassifyComponent } from './pre-classify.js';
12
- import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
13
- import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
14
- import { describeReviewReasons, inspectComponentSource } from './extract/source-inspection.js';
15
- import { validateExtractedComponents } from './extract/validate.js';
9
+ import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, } from '../session/db.js';
16
10
  import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
17
11
  const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
18
12
  const IGNORED_DIRECTORY_NAMES = new Set([
@@ -192,20 +186,6 @@ export function registerAnalyzeCommand(program) {
192
186
  }
193
187
  const validatedComponents = validateExtractedComponents(filteredComponents);
194
188
  storeRawComponents(db, sessionId, validatedComponents);
195
- // INTEG-4401: post-extract slot-dependency cycle detection. The apply
196
- // worker rejects cyclic slot graphs at push time, so we surface the
197
- // same violation locally as a soft warning here (TUI reads
198
- // slot_cycles) and as a hard block at manifest finalization.
199
- const cycleInput = validatedComponents.map((c) => ({
200
- name: c.name,
201
- slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
202
- }));
203
- const cycles = findSlotCycles(cycleInput);
204
- const withBreaks = cycles.map((cycle) => ({
205
- ...cycle,
206
- suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
207
- }));
208
- storeSlotCycles(db, sessionId, withBreaks);
209
189
  storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
210
190
  updateStep(db, stepId, 'complete', { sessionId });
211
191
  db.close();
@@ -6,7 +6,7 @@ import { getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, sav
6
6
  import { loadReviewInput } from './parser.js';
7
7
  import { App } from './tui/App.js';
8
8
  import { openPipelineDb, loadRawComponents, storeRawComponents, createStep, updateStep } from '../../session/db.js';
9
- import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
9
+ import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
10
10
  const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
11
11
  const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
12
12
  function applyDotPath(obj, path, value) {
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import { findSlotCycles, type ComponentSlotInfo } from '../../../cycle-detection.js';
3
2
  /** Per-prop / per-component metadata captured by the extractor + generate phase.
4
3
  * Optional; when omitted, the FieldEditor renders without rationale/source
5
4
  * affordances (backwards-compatible with mounts that pre-date Feature 1). */
@@ -64,28 +63,6 @@ type FieldEditorProps = {
64
63
  * description editors, string default editors, and value-list text entry.
65
64
  */
66
65
  onTextEntryActiveChange?: (active: boolean) => void;
67
- /**
68
- * INTEG-4401: project-wide slot graph. When supplied, the
69
- * $allowedComponents add-mode shows a cycle-filtered picker. Optional.
70
- */
71
- projectSlotGraph?: ComponentSlotInfo[];
72
- /**
73
- * INTEG-4401: name of the component being edited. Self-loop guard +
74
- * self-entry replacement in the cycle simulation.
75
- */
76
- currentComponentName?: string;
77
66
  };
78
- /** Simulate adding a candidate to a slot; replaces self-entry with live edits. */
79
- export declare function simulateGraphWithCandidate(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
80
- name: string;
81
- allowedComponents: string[];
82
- }[], targetSlotName: string, candidate: string): ComponentSlotInfo[];
83
- /** True iff `next` contains a cycle whose canonical edge set is absent in `before`. */
84
- export declare function introducesNewCycle(before: ReturnType<typeof findSlotCycles>, next: ReturnType<typeof findSlotCycles>): boolean;
85
- /** Candidate names that can be added to a slot without creating a new cycle. */
86
- export declare function computeAllowedComponentCandidates(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
87
- name: string;
88
- allowedComponents: string[];
89
- }[], targetSlotName: string): string[];
90
- export declare function FieldEditor({ value, width, height, active, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, projectSlotGraph, currentComponentName, }: FieldEditorProps): React.ReactElement;
67
+ export declare function FieldEditor({ value, width, height, active, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, }: FieldEditorProps): React.ReactElement;
91
68
  export {};
@@ -4,7 +4,6 @@ import { Box, Text } from 'ink';
4
4
  import { CDF_PROPERTY_TYPES, CDF_PROPERTY_CATEGORIES, DESIGN_TOKEN_TYPES, } from '@contentful/experience-design-system-types';
5
5
  import { useImmediateInput } from '../hooks/useImmediateInput.js';
6
6
  import { computeNextScrollOffset } from '../hooks/scroll-offset.js';
7
- import { findSlotCycles } from '../../../cycle-detection.js';
8
7
  // Section indices for top-level navigation
9
8
  // 0 = component $description row
10
9
  // 1 = $properties header (then props[0..n-1])
@@ -202,34 +201,18 @@ function PropRow({ prop, selected, activeField, textCursor, valueCursor, cursorV
202
201
  return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
203
202
  }), editingValue?.mode === 'add' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))] }))] }));
204
203
  }
205
- function SlotRow({ slot, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, pickerCandidates, pickerCursor, }) {
204
+ function SlotRow({ slot, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, }) {
206
205
  const cursor = cursorVisible ? '█' : ' ';
207
206
  const bg = selected ? 'blue' : undefined;
208
207
  const nameDisplay = slot.name.length > 14 ? slot.name.slice(0, 13) + '…' : slot.name.padEnd(14);
209
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), !selected && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "allowed:" }), slot.allowedComponents.length === 0 ? (_jsx(Text, { dimColor: true, children: "(any)" })) : (_jsx(Text, { color: "cyan", children: slot.allowedComponents.join(', ') }))] })), selected && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "allowed:" }), activeField === 'allowedComponents' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), slot.allowedComponents.length === 0 && !editingValue && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: activeField === 'allowedComponents' ? '(any — press [a] to add)' : '(any)' }) })), slot.allowedComponents.map((v, i) => {
208
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), selected && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "allowed:" }), activeField === 'allowedComponents' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), slot.allowedComponents.length === 0 && !editingValue && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: activeField === 'allowedComponents' ? '(any — press [a] to add)' : '(any)' }) })), slot.allowedComponents.map((v, i) => {
210
209
  const isActiveCursor = activeField === 'allowedComponents' && valueCursor === i;
211
210
  const isBeingEdited = editingValue?.mode === 'edit' && editingValue.index === i;
212
211
  if (isBeingEdited) {
213
212
  return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '✎ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }, i));
214
213
  }
215
214
  return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
216
- }), editingValue?.mode === 'add' && activeField === 'allowedComponents' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] })), editingValue?.mode === 'add' && activeField === 'allowedComponents' && pickerCandidates !== null && (_jsx(Box, { paddingLeft: 2, flexDirection: "column", children: pickerCandidates.length === 0 ? (_jsx(Text, { dimColor: true, children: '(no valid components to add all remaining candidates would create cycles)' })) : ((() => {
217
- const filtered = valueText.length === 0
218
- ? pickerCandidates
219
- : pickerCandidates.filter((n) => n.toLowerCase().includes(valueText.toLowerCase()));
220
- if (filtered.length === 0) {
221
- return _jsx(Text, { dimColor: true, children: '(no candidates match — Enter to add as free text)' });
222
- }
223
- const cursor = pickerCursor % filtered.length;
224
- const MAX_VISIBLE = 5;
225
- const start = Math.max(0, Math.min(filtered.length - MAX_VISIBLE, cursor - 2));
226
- const slice = filtered.slice(start, start + MAX_VISIBLE);
227
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: ' candidates (↑↓ cycle, Enter to add):' }), slice.map((name, i) => {
228
- const absIdx = start + i;
229
- const isCursor = absIdx === cursor;
230
- return (_jsx(Text, { color: isCursor ? 'cyan' : undefined, dimColor: !isCursor, children: isCursor ? ` ▶ ${name}` : ` ${name}` }, name));
231
- })] }));
232
- })()) }))] })), selected && activeField === 'description' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: slot.description.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: slot.description[textCursor] ?? cursor }), _jsx(Text, { children: slot.description.slice(textCursor + 1) })] })] })), selected && activeField !== 'description' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsx(Text, { color: "green", children: slot.description || '—' })] }))] }));
215
+ }), editingValue?.mode === 'add' && activeField === 'allowedComponents' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))] })), selected && activeField === 'description' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: slot.description.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: slot.description[textCursor] ?? cursor }), _jsx(Text, { children: slot.description.slice(textCursor + 1) })] })] })), selected && activeField !== 'description' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsx(Text, { color: "green", children: slot.description || '—' })] }))] }));
233
216
  }
234
217
  // Field order is intentional: description is LAST so it's the "edge" of the
235
218
  // field cycle. The user reaches description by walking through type → category
@@ -255,59 +238,8 @@ function propFields(prop) {
255
238
  // components is a list-typed field that swallows j/k similarly to enum $values
256
239
  // — placing it before description preserves description-as-last.
257
240
  const SLOT_FIELDS = ['required', 'allowedComponents', 'description'];
258
- // ── INTEG-4401: cycle-aware $allowedComponents picker helpers ─────────────
259
- /** Simulate adding a candidate to a slot; replaces self-entry with live edits. */
260
- export function simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, targetSlotName, candidate) {
261
- const selfEntry = {
262
- name: selfName,
263
- slots: currentSlots.map((s) => ({
264
- name: s.name,
265
- allowedComponents: s.name === targetSlotName ? [...s.allowedComponents, candidate] : [...s.allowedComponents],
266
- })),
267
- };
268
- const withoutSelf = projectSlotGraph.filter((c) => c.name !== selfName);
269
- return [...withoutSelf, selfEntry];
270
- }
271
- /** True iff `next` contains a cycle whose canonical edge set is absent in `before`. */
272
- export function introducesNewCycle(before, next) {
273
- const key = (edges) => {
274
- if (edges.length === 0)
275
- return '';
276
- const encoded = edges.map((e) => `${e.fromComponent}|${e.slotName}|${e.toComponent}`);
277
- let minIdx = 0;
278
- for (let i = 1; i < encoded.length; i += 1)
279
- if (encoded[i] < encoded[minIdx])
280
- minIdx = i;
281
- return [...encoded.slice(minIdx), ...encoded.slice(0, minIdx)].join(';');
282
- };
283
- const beforeSet = new Set(before.map((c) => key(c.edges)));
284
- for (const cycle of next)
285
- if (!beforeSet.has(key(cycle.edges)))
286
- return true;
287
- return false;
288
- }
289
- /** Candidate names that can be added to a slot without creating a new cycle. */
290
- export function computeAllowedComponentCandidates(projectSlotGraph, selfName, currentSlots, targetSlotName) {
291
- const targetSlot = currentSlots.find((s) => s.name === targetSlotName);
292
- const alreadyAdded = new Set(targetSlot?.allowedComponents ?? []);
293
- const universe = new Set();
294
- for (const c of projectSlotGraph)
295
- universe.add(c.name);
296
- universe.delete(selfName);
297
- for (const added of alreadyAdded)
298
- universe.delete(added);
299
- const baseline = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, '', ''));
300
- const safe = [];
301
- for (const candidate of universe) {
302
- const nextCycles = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, selfName, currentSlots, targetSlotName, candidate));
303
- if (!introducesNewCycle(baseline, nextCycles))
304
- safe.push(candidate);
305
- }
306
- safe.sort((a, b) => a.localeCompare(b));
307
- return safe;
308
- }
309
241
  // ── Main component ────────────────────────────────────────────────────────────
310
- export function FieldEditor({ value, width, height, active = true, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, projectSlotGraph, currentComponentName, }) {
242
+ export function FieldEditor({ value, width, height, active = true, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, }) {
311
243
  const { state: initialState, error: parseError } = parseToState(value);
312
244
  const [editorState, setEditorState] = useState(initialState);
313
245
  const [parseErr] = useState(parseError);
@@ -359,8 +291,6 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
359
291
  // mode='add' — append on Enter; mode='edit' — replace at index on Enter.
360
292
  const [editingValue, setEditingValue] = useState(null);
361
293
  const [valueText, setValueText] = useState('');
362
- // INTEG-4401: picker cursor for the cycle-filtered $allowedComponents list.
363
- const [pickerCursor, setPickerCursor] = useState(0);
364
294
  const [validationError, setValidationError] = useState(null);
365
295
  const [cursorVisible] = useState(true);
366
296
  // Feature 1: source-view panel toggle. Opens with `s`, closes with `s` or Esc.
@@ -429,57 +359,11 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
429
359
  // same add/edit/remove/reorder list shape.
430
360
  const isPropValuesEntry = editingValue && currentProp && activeField === 'values' && !inSlots;
431
361
  const isSlotAllowedEntry = editingValue && currentSlot && activeField === 'allowedComponents' && inSlots;
432
- // INTEG-4401: cycle-filtered candidates for slot-add.
433
- const slotAddCandidates = isSlotAllowedEntry && editingValue?.mode === 'add' && projectSlotGraph && currentSlot && currentComponentName
434
- ? computeAllowedComponentCandidates(projectSlotGraph, currentComponentName, slots, currentSlot.name)
435
- : null;
436
- const filteredCandidates = slotAddCandidates
437
- ? valueText.length === 0
438
- ? slotAddCandidates
439
- : slotAddCandidates.filter((n) => n.toLowerCase().includes(valueText.toLowerCase()))
440
- : null;
441
362
  if (isPropValuesEntry || isSlotAllowedEntry) {
442
363
  const vals = isPropValuesEntry ? currentProp.values : currentSlot.allowedComponents;
443
- const pickerActive = isSlotAllowedEntry && editingValue?.mode === 'add' && slotAddCandidates !== null;
444
- if (pickerActive && (key.upArrow || key.downArrow)) {
445
- const len = filteredCandidates.length;
446
- if (len > 0) {
447
- setPickerCursor((c) => (key.upArrow ? (c - 1 + len) % len : (c + 1) % len));
448
- }
449
- return;
450
- }
451
364
  if (key.return) {
452
- let chosen = null;
453
- if (pickerActive && filteredCandidates && filteredCandidates.length > 0) {
454
- if (valueText.trim() === '') {
455
- chosen = filteredCandidates[pickerCursor % filteredCandidates.length] ?? null;
456
- }
457
- else {
458
- const exact = filteredCandidates.find((n) => n === valueText.trim());
459
- if (exact)
460
- chosen = exact;
461
- }
462
- }
463
- const trimmed = chosen ?? valueText.trim();
365
+ const trimmed = valueText.trim();
464
366
  if (trimmed) {
465
- // Cycle-aware free-text validation. Picker path is safe by construction.
466
- if (isSlotAllowedEntry &&
467
- editingValue?.mode === 'add' &&
468
- projectSlotGraph &&
469
- currentSlot &&
470
- currentComponentName &&
471
- chosen === null) {
472
- if (trimmed === currentComponentName) {
473
- setValidationError(`"${trimmed}" cannot be added to its own slot (self-loop).`);
474
- return;
475
- }
476
- const baseline = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, currentComponentName, slots, '', ''));
477
- const next = findSlotCycles(simulateGraphWithCandidate(projectSlotGraph, currentComponentName, slots, currentSlot.name, trimmed));
478
- if (introducesNewCycle(baseline, next)) {
479
- setValidationError(`Adding "${trimmed}" to slot "${currentSlot.name}" would create a slot-dependency cycle. Choose a different component.`);
480
- return;
481
- }
482
- }
483
367
  let nextVals;
484
368
  let cursorAfter;
485
369
  if (editingValue.mode === 'add') {
@@ -500,28 +384,22 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
500
384
  commit({ ...editorState, slots: nextSlots });
501
385
  }
502
386
  setValueCursor(cursorAfter);
503
- setValidationError(null);
504
387
  }
505
388
  setEditingValue(null);
506
389
  setValueText('');
507
- setPickerCursor(0);
508
390
  return;
509
391
  }
510
392
  if (key.escape) {
511
393
  setEditingValue(null);
512
394
  setValueText('');
513
- setPickerCursor(0);
514
- setValidationError(null);
515
395
  return;
516
396
  }
517
397
  if (key.backspace) {
518
398
  setValueText((t) => t.slice(0, -1));
519
- setPickerCursor(0);
520
399
  return;
521
400
  }
522
401
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
523
402
  setValueText((t) => t + input);
524
- setPickerCursor(0);
525
403
  return;
526
404
  }
527
405
  return;
@@ -1046,7 +924,10 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
1046
924
  if (props.length === 0 && slots.length === 0) {
1047
925
  return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "yellow", children: [_jsx(Text, { bold: true, color: "yellow", children: "FIELD EDITOR \u2014 no fields" }), _jsx(Text, { color: "yellow", children: "⚠ No properties classified for this component. The LLM didn't find anything to classify." }), _jsx(Text, { dimColor: true, children: "You can add fields manually below or reject this component." }), _jsx(Text, { dimColor: true, children: "Ctrl+S to save \u00B7 Esc to discard" })] }));
1048
926
  }
1049
- const hasEmptyProperties = props.length === 0 && slots.length === 0;
927
+ // When $properties is empty but $slots exist, surface the same warning
928
+ // prominently in the panel so the user understands why the component looks
929
+ // sparse — and can act on it (manually add a prop or reject).
930
+ const hasEmptyProperties = props.length === 0;
1050
931
  const modeLabel = (() => {
1051
932
  if (editingValue) {
1052
933
  return editingValue.mode === 'add' ? 'Enter to add · Esc to cancel' : 'Enter to save edit · Esc to cancel';
@@ -1107,15 +988,7 @@ export function FieldEditor({ value, width, height, active = true, onChange, onS
1107
988
  // slot row
1108
989
  const s = slots[row.idx];
1109
990
  const isSelected = inSlots && row.idx === slotIdx;
1110
- // INTEG-4401: compute picker candidates for selected slot in add-mode.
1111
- const slotPickerCandidates = isSelected &&
1112
- editingValue?.mode === 'add' &&
1113
- activeField === 'allowedComponents' &&
1114
- projectSlotGraph &&
1115
- currentComponentName
1116
- ? computeAllowedComponentCandidates(projectSlotGraph, currentComponentName, slots, s.name)
1117
- : null;
1118
- return (_jsx(SlotRow, { slot: s, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, editingValue: isSelected ? editingValue : null, valueText: isSelected ? valueText : '', width: innerWidth, pickerCandidates: slotPickerCandidates, pickerCursor: pickerCursor }, `slot-${row.idx}`));
991
+ return (_jsx(SlotRow, { slot: s, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, editingValue: isSelected ? editingValue : null, valueText: isSelected ? valueText : '', width: innerWidth }, `slot-${row.idx}`));
1119
992
  }) }), sourceOpen &&
1120
993
  !onToggleSourceExternal &&
1121
994
  (() => {
@@ -11,7 +11,7 @@ import { OutputFormatter, c } from '../../output/format.js';
11
11
  import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
12
12
  import { runShowRationale } from './show-rationale.js';
13
13
  import { isAbsolute, resolve } from 'node:path';
14
- import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
14
+ import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
15
15
  const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
16
16
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
17
17
  export const DEFAULT_CONCURRENCY = 10;
@@ -1,39 +1,6 @@
1
1
  import type { Command } from 'commander';
2
- import type { CDFComponentEntry, DTCGTokenEntry } from '@contentful/experience-design-system-types';
3
- import { findSlotCycles } from '../analyze/cycle-detection.js';
2
+ import type { DTCGTokenEntry } from '@contentful/experience-design-system-types';
4
3
  import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
5
4
  export declare function readTokensFromPath(flag: string, p: string): Promise<DTCGTokenEntry[]>;
6
- /**
7
- * Pure cycle detection — returns [] when the graph is acyclic. Callers own
8
- * how to react (CLI standalone uses `assertNoSlotCycles` which exits 1; the
9
- * wizard uses `detectSlotCycles` directly to route to an in-TUI error step
10
- * without ever POSTing to EDSI).
11
- */
12
- export declare function detectSlotCycles(components: Array<{
13
- key: string;
14
- entry: CDFComponentEntry;
15
- }>): ReturnType<typeof findSlotCycles>;
16
- /**
17
- * Build a stderr-style message block for a set of cycles. Extracted so the
18
- * wizard can render the same text in an in-TUI error panel and any future
19
- * headless surface can log identical output.
20
- */
21
- export declare function formatSlotCycleReport(cycles: ReturnType<typeof findSlotCycles>): string[];
22
- export declare function assertNoSlotCycles(components: Array<{
23
- key: string;
24
- entry: CDFComponentEntry;
25
- }>): void;
26
- /**
27
- * Reconstruct the `{key, entry}` shape from a built `ManifestPayload`. The
28
- * wizard's push path holds a serialized manifest rather than the underlying
29
- * CDF records, so we unwrap it here to feed `detectSlotCycles`. Skips the
30
- * `$schema` sentinel key. Safe on undefined/empty manifests (returns []).
31
- */
32
- export declare function extractComponentsFromManifest(manifest: {
33
- componentsManifest?: Record<string, unknown>;
34
- } | null | undefined): Array<{
35
- key: string;
36
- entry: CDFComponentEntry;
37
- }>;
38
5
  export declare function hasBreakingChangesWithImpact(preview: ServerPreviewResponse): boolean;
39
6
  export declare function registerApplyCommand(program: Command): void;
@@ -5,7 +5,6 @@ import { join } from 'node:path';
5
5
  import { validateCDF, flattenDTCG, validateDTCG, buildManifest, buildFilteredManifest, } from '@contentful/experience-design-system-types';
6
6
  import { ApiError, ImportApiClient } from './api-client.js';
7
7
  import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
- import { findSlotCycles, suggestCycleBreakEdge, formatCyclePath } from '../analyze/cycle-detection.js';
9
8
  import { isEmptyPreview } from './preview-utils.js';
10
9
  import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
11
10
  import { SelectView, makeSelectKey } from './tui/SelectView.js';
@@ -171,75 +170,6 @@ async function resolveSharedInputs(opts) {
171
170
  });
172
171
  return { components, tokens, client };
173
172
  }
174
- // --- INTEG-4401: pre-push slot-cycle hard block ---
175
- //
176
- // The backend apply worker rejects cyclic slot graphs at topo-sort time, but
177
- // by then the request has already been serialized, transported, and partially
178
- // applied — leading to confusing partial-failure states. Detecting the same
179
- // violation locally lets us fail fast with a clear message and zero
180
- // side-effects. Called from the `apply push` and `apply select` flows AND
181
- // from the wizard's `experiences import` push path (via `detectSlotCycles`).
182
- // `apply preview` is read-only and still runs (its diff is used to warn but
183
- // not to block).
184
- /**
185
- * Pure cycle detection — returns [] when the graph is acyclic. Callers own
186
- * how to react (CLI standalone uses `assertNoSlotCycles` which exits 1; the
187
- * wizard uses `detectSlotCycles` directly to route to an in-TUI error step
188
- * without ever POSTing to EDSI).
189
- */
190
- export function detectSlotCycles(components) {
191
- const cycleInput = components.map(({ key, entry }) => ({
192
- name: key,
193
- slots: Object.entries(entry.$slots ?? {}).map(([slotName, slotDef]) => ({
194
- name: slotName,
195
- allowedComponents: slotDef.$allowedComponents ?? [],
196
- })),
197
- }));
198
- return findSlotCycles(cycleInput);
199
- }
200
- /**
201
- * Build a stderr-style message block for a set of cycles. Extracted so the
202
- * wizard can render the same text in an in-TUI error panel and any future
203
- * headless surface can log identical output.
204
- */
205
- export function formatSlotCycleReport(cycles) {
206
- const lines = [];
207
- lines.push(`Error: manifest:components/slot-cycles — ${cycles.length} slot dependency cycle(s) detected. Push refused.`);
208
- for (let i = 0; i < cycles.length; i += 1) {
209
- const cycle = cycles[i];
210
- lines.push(` Cycle ${i + 1}: ${formatCyclePath(cycle)}`);
211
- const suggested = suggestCycleBreakEdge(cycle, cycles);
212
- lines.push(` Fix: remove '${suggested.toComponent}' from ${suggested.fromComponent}.$slots.${suggested.slotName}.$allowedComponents`);
213
- }
214
- return lines;
215
- }
216
- export function assertNoSlotCycles(components) {
217
- const cycles = detectSlotCycles(components);
218
- if (cycles.length === 0)
219
- return;
220
- process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
221
- process.exit(1);
222
- }
223
- /**
224
- * Reconstruct the `{key, entry}` shape from a built `ManifestPayload`. The
225
- * wizard's push path holds a serialized manifest rather than the underlying
226
- * CDF records, so we unwrap it here to feed `detectSlotCycles`. Skips the
227
- * `$schema` sentinel key. Safe on undefined/empty manifests (returns []).
228
- */
229
- export function extractComponentsFromManifest(manifest) {
230
- const componentsManifest = manifest?.componentsManifest;
231
- if (!componentsManifest)
232
- return [];
233
- const out = [];
234
- for (const [key, value] of Object.entries(componentsManifest)) {
235
- if (key === '$schema')
236
- continue;
237
- if (!value || typeof value !== 'object')
238
- continue;
239
- out.push({ key, entry: value });
240
- }
241
- return out;
242
- }
243
173
  // --- Output helpers ---
244
174
  export function hasBreakingChangesWithImpact(preview) {
245
175
  const allChanged = [...preview.components.changed, ...preview.tokens.changed];
@@ -515,12 +445,6 @@ export function registerApplyCommand(program) {
515
445
  throw e;
516
446
  }
517
447
  const { components, tokens, client } = inputs;
518
- // INTEG-4401: refuse before spending any credentials or bytes on a
519
- // manifest the backend will reject. Cycle detection is deterministic
520
- // and cheap so we run it here (not just at extract time) — this also
521
- // guards `apply push --components` where extract-time cycles are not
522
- // in the session DB.
523
- assertNoSlotCycles(components);
524
448
  try {
525
449
  await client.validateToken();
526
450
  }
@@ -699,11 +623,6 @@ export function registerApplyCommand(program) {
699
623
  throw e;
700
624
  }
701
625
  const { components, tokens, client } = inputs;
702
- // INTEG-4401: same pre-push cycle block as `apply push`. Running the
703
- // check on the FULL component set (not the filtered selection) is
704
- // deliberate: partial selection cannot resolve a cycle whose
705
- // participants are all still present in the target space.
706
- assertNoSlotCycles(components);
707
626
  try {
708
627
  await client.validateToken();
709
628
  }
@@ -33,8 +33,6 @@ import { nextStateAfterPrint } from './run-print-files-helpers.js';
33
33
  import { PushDecisionGateStep } from './steps/PushDecisionGateStep.js';
34
34
  import { chooseGateAction } from './push-decision-gate-helpers.js';
35
35
  import { ImportApiClient, ApiError } from '../../apply/api-client.js';
36
- import { detectSlotCycles, extractComponentsFromManifest, formatSlotCycleReport } from '../../apply/command.js';
37
- import { parseEdsiError, formatParsedEdsiError } from '../../apply/error-parser.js';
38
36
  import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
39
37
  import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
40
38
  import { spawnGenerateChild } from './spawn-generate.js';
@@ -1036,23 +1034,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1036
1034
  return;
1037
1035
  }
1038
1036
  }
1039
- // INTEG-4401 Fix A — pre-push slot-cycle hard block for the wizard's
1040
- // direct-API push path. The standalone `apply push` / `apply select`
1041
- // commands run `assertNoSlotCycles` before ever constructing an API
1042
- // client, but `experiences import` calls `client.applyImport` from here
1043
- // without shelling out — so the guard has to run again on this path.
1044
- // Otherwise a cyclic graph reaches EDSI and the operator sees a raw
1045
- // Lambda error dump (see Fix C) instead of the clear local report.
1046
- const cycles = detectSlotCycles(extractComponentsFromManifest(manifest));
1047
- if (cycles.length > 0) {
1048
- update({
1049
- step: 'error',
1050
- errorStep: 'apply push',
1051
- errorMessage: formatSlotCycleReport(cycles).join('\n'),
1052
- errorAllowCredentialRetry: false,
1053
- });
1054
- return;
1055
- }
1056
1037
  update({ step: 'pushing', pushProgress: null });
1057
1038
  try {
1058
1039
  const resolvedHost = resolveWizardHost(host);
@@ -1143,57 +1124,27 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1143
1124
  };
1144
1125
  }
1145
1126
  else {
1146
- // API didn't return items. INTEG-4401 Fix B do NOT report preview
1147
- // counts as "created": if the server said something failed, echo it
1148
- // truthfully so the summary can't lie. Historically this branch used
1149
- // preview.new/changed/removed as success counts and only surfaced
1150
- // failed:0, which meant a cycle rejection (summary: 0 succeeded /
1151
- // 11 failed, empty items) rendered as "Done ✓ 2 Component Types
1152
- // created" — the exact regression this fix guards against.
1153
- const summary = operation.summary ?? { total: 0, pending: 0, succeeded: 0, failed: 0 };
1154
- const anyFailure = summary.failed > 0 || operation.sys.status === 'failed' || operation.sys.status === 'partial';
1127
+ // API didn't return items fall back to summary + preview counts
1155
1128
  pushResult = {
1156
1129
  componentTypes: {
1157
- created: anyFailure ? 0 : (preview?.components.new.length ?? 0),
1158
- updated: anyFailure ? 0 : (preview?.components.changed.length ?? 0),
1159
- removed: anyFailure ? 0 : (preview?.components.removed.length ?? 0),
1160
- // Attribute failures to component types by default — without a
1161
- // per-item breakdown we can't split components vs tokens, and
1162
- // components are the dominant entity in an import.
1163
- failed: summary.failed,
1130
+ created: preview?.components.new.length ?? 0,
1131
+ updated: preview?.components.changed.length ?? 0,
1132
+ removed: preview?.components.removed.length ?? 0,
1133
+ failed: 0,
1164
1134
  },
1165
1135
  designTokens: {
1166
- created: anyFailure ? 0 : (preview?.tokens.new.length ?? 0),
1167
- updated: anyFailure ? 0 : (preview?.tokens.changed.length ?? 0),
1168
- removed: anyFailure ? 0 : (preview?.tokens.removed.length ?? 0),
1136
+ created: preview?.tokens.new.length ?? 0,
1137
+ updated: preview?.tokens.changed.length ?? 0,
1138
+ removed: preview?.tokens.removed.length ?? 0,
1169
1139
  failed: 0,
1170
1140
  },
1171
- summary,
1141
+ summary: operation.summary,
1172
1142
  };
1173
1143
  }
1174
1144
  update({ step: 'done', pushResult });
1175
1145
  }
1176
1146
  catch (e) {
1177
- // INTEG-4401 Fix C parse EDSI error bodies into a `[CODE] message`
1178
- // block before handing off to ErrorStep, so cycle rejections and other
1179
- // structured failures don't render as raw Lambda log lines
1180
- // (timestamp / request-id / dd.trace_id / etc.).
1181
- let msg;
1182
- if (e instanceof ApiError) {
1183
- const parsed = parseEdsiError(e.body || e.message);
1184
- msg = formatParsedEdsiError(parsed, {
1185
- verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
1186
- raw: e.body,
1187
- });
1188
- if (!msg)
1189
- msg = e.message;
1190
- }
1191
- else if (e instanceof Error) {
1192
- msg = e.message;
1193
- }
1194
- else {
1195
- msg = 'Push failed';
1196
- }
1147
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : 'Push failed';
1197
1148
  update({
1198
1149
  step: 'error',
1199
1150
  errorStep: 'apply push',
@@ -37,6 +37,6 @@ type GenerateReviewStepProps = {
37
37
  export declare function sortComponentsForSidebar<T extends {
38
38
  key: string;
39
39
  entry: CDFComponentEntry;
40
- }>(components: T[], cycleParticipants?: Set<string>): T[];
40
+ }>(components: T[]): T[];
41
41
  export declare function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, livePreview, spaceId, environmentId, cmaToken, host, tokensPath, initialFinalizeError, }: GenerateReviewStepProps): React.ReactElement;
42
42
  export {};