@contentful/experience-design-system-cli 2.17.1-dev-build-c40b4d2.0 → 2.17.1-dev-build-36625f3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.17.1-dev-build-c40b4d2.0",
3
+ "version": "2.17.1-dev-build-36625f3.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -238,9 +238,9 @@ export function registerAnalyzeCommand(program) {
238
238
  for (const { component, inspection } of inspectedComponents) {
239
239
  const verdict = isNonAuthorableComponent(component);
240
240
  const keepDespiteZeroSurface = verdict.skip && verdict.reason === 'component has no props and no slots' && inspection.keepDespiteZeroSurface;
241
- if (verdict.skip && !keepDespiteZeroSurface) {
242
- filterWarnings.push(`Skipped non-authorable component: ${component.name} (${verdict.reason})`);
243
- continue;
241
+ const retainedForReview = verdict.skip && !keepDespiteZeroSurface;
242
+ if (retainedForReview) {
243
+ filterWarnings.push(`${component.name}: requires operator review (${verdict.reason})`);
244
244
  }
245
245
  if (keepDespiteZeroSurface) {
246
246
  filterWarnings.push(`${component.name}: retained despite 0 props/slots because the source renders visible or compositional UI`);
@@ -257,9 +257,12 @@ export function registerAnalyzeCommand(program) {
257
257
  // from the Svelte parser) by merging them into the post-processing recompute.
258
258
  // Without this, recomputing here clobbers the per-extractor signal.
259
259
  const extractorReasons = component.reviewReasons ?? [];
260
+ const nonAuthorableReason = retainedForReview ? [`non-authorable:${verdict.reason}`] : [];
260
261
  const { confidence, reasons } = computeExtractionScore(component, {
261
- additionalIssueCount: wrapperConfidenceToIssueCount(inspection.wrapperConfidence) + extractorReasons.length,
262
- additionalReasons: [...extractorReasons, ...inspection.reviewReasons],
262
+ additionalIssueCount: wrapperConfidenceToIssueCount(inspection.wrapperConfidence) +
263
+ extractorReasons.length +
264
+ nonAuthorableReason.length,
265
+ additionalReasons: [...extractorReasons, ...inspection.reviewReasons, ...nonAuthorableReason],
263
266
  });
264
267
  filteredComponents.push({
265
268
  ...component,
@@ -268,6 +271,7 @@ export function registerAnalyzeCommand(program) {
268
271
  needsReview: deriveNeedsReview(confidence) ||
269
272
  inspection.wrapperConfidence >= 4 ||
270
273
  inspection.keepDespiteZeroSurface ||
274
+ retainedForReview ||
271
275
  // An extractor-level type-resolution failure is a strong signal regardless
272
276
  // of the otherwise-derived confidence threshold; force review.
273
277
  extractorReasons.includes('props-type-unresolved') ||
@@ -1,5 +1,6 @@
1
1
  export type AiFlaggable = {
2
2
  aiDecision?: 'accepted' | 'rejected' | 'failed' | null;
3
+ needsReview?: boolean;
3
4
  };
4
5
  export declare function isAiFlagged(row: AiFlaggable): boolean;
5
6
  export declare function isDefaultIncluded(row: AiFlaggable): boolean;
@@ -1,5 +1,5 @@
1
1
  export function isAiFlagged(row) {
2
- return row.aiDecision === 'rejected' || row.aiDecision === 'failed';
2
+ return row.needsReview === true || row.aiDecision === 'rejected' || row.aiDecision === 'failed';
3
3
  }
4
4
  export function isDefaultIncluded(row) {
5
5
  return !isAiFlagged(row);
@@ -40,8 +40,7 @@ export function AtomicScopeGateStep({ components, onConfirm, onQuit, aiFilterSta
40
40
  return false;
41
41
  if (userUnExcluded.has(row.name))
42
42
  return true;
43
- // INTEG-4318: exclude on 'rejected' AND 'failed'.
44
- return row.aiDecision !== 'rejected' && row.aiDecision !== 'failed';
43
+ return !isAiFlagged(row);
45
44
  };
46
45
  const partition = () => {
47
46
  const accepted = [];
@@ -189,7 +188,7 @@ export function AtomicScopeGateStep({ components, onConfirm, onQuit, aiFilterSta
189
188
  groups: 0,
190
189
  total,
191
190
  };
192
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", total, " component", total === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), _jsx(AutoFilterBanner, { status: aiFilterStatus, progress: aiFilterProgress, error: aiFilterError }), _jsx(CounterStrip, { counters: counters, totalWidth: totalWidth }), reasonPanelOpen && flatList[cursor] !== undefined && isAiFlagged(flatList[cursor]) && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.border, paddingX: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `AI rejection reason: ${flatList[cursor].name}` }), _jsx(Text, { children: flatList[cursor].aiReason ?? '<no reason given>' }), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] })), allRejected ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: PALETTE.warning, children: "AI excluded all components \u2014 press [a] to override or [q] to quit" }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [above > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", above, " above"] }), visible.map((c, vi) => {
191
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", total, " component", total === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), _jsx(AutoFilterBanner, { status: aiFilterStatus, progress: aiFilterProgress, error: aiFilterError }), _jsx(CounterStrip, { counters: counters, totalWidth: totalWidth }), reasonPanelOpen && flatList[cursor] !== undefined && isAiFlagged(flatList[cursor]) && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.border, paddingX: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `Review reason: ${flatList[cursor].name}` }), _jsx(Text, { children: flatList[cursor].aiReason ?? '<no reason given>' }), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] })), allRejected ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: PALETTE.warning, children: "AI excluded all components \u2014 press [a] to override or [q] to quit" }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [above > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", above, " above"] }), visible.map((c, vi) => {
193
192
  const i = vi + scrollOffset;
194
193
  const isCursor = i === cursor;
195
194
  const included = isIncluded(c);
@@ -201,11 +200,11 @@ export function AtomicScopeGateStep({ components, onConfirm, onQuit, aiFilterSta
201
200
  const inlineReason = !isCursor && aiFlagged ? ` ${truncateReason(c.aiReason)}` : '';
202
201
  const showAiHeader = aiList.length > 0 && i === 0;
203
202
  const showComponentsHeader = componentsList.length > 0 && i === aiList.length;
204
- const header = showAiHeader ? (_jsx(Text, { bold: true, children: `AI recommended exclusions (${aiList.length})` }, `hdr-ai-${i}`)) : showComponentsHeader ? (_jsx(Text, { bold: true, children: `Components (${componentsList.length})` }, `hdr-comp-${i}`)) : null;
203
+ const header = showAiHeader ? (_jsx(Text, { bold: true, children: `Review flags (${aiList.length})` }, `hdr-ai-${i}`)) : showComponentsHeader ? (_jsx(Text, { bold: true, children: `Components (${componentsList.length})` }, `hdr-comp-${i}`)) : null;
205
204
  if (isCursor) {
206
205
  const wrapReason = aiFlagged && c.aiReason !== null && c.aiReason !== undefined && c.aiReason.length > 0;
207
206
  return (_jsxs(React.Fragment, { children: [header, _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: `${prefix} ` }), aiMarkerNode, _jsx(Text, { color: stateColor, children: stateGlyph }), _jsx(Text, { color: PALETTE.info, children: ` ${c.name}` })] }), wrapReason && _jsx(Text, { dimColor: true, children: `${REASON_WRAP_INDENT}${c.aiReason}` })] }, c.componentId));
208
207
  }
209
208
  return (_jsxs(React.Fragment, { children: [header, _jsxs(Text, { children: [_jsx(Text, { children: `${prefix} ` }), aiMarkerNode, _jsx(Text, { color: stateColor, children: stateGlyph }), _jsx(Text, { color: stateColor, children: ` ${c.name}` }), inlineReason !== '' && _jsx(Text, { dimColor: true, children: inlineReason })] })] }, c.componentId));
210
- }), below > 0 && _jsxs(Text, { dimColor: true, children: ["\u2193 ", below, " below"] })] })), _jsxs(Box, { gap: 3, marginTop: 1, children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.success, children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", total, " included"] })] })) : (_jsx(Text, { color: PALETTE.warning, children: "none included" })), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[j/k]" }), " ", _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[a/space]" }), " ", _jsx(Text, { dimColor: true, children: "toggle" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[A]" }), " ", _jsx(Text, { dimColor: true, children: "toggle all" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[f]" }), " ", _jsx(Text, { dimColor: true, children: "continue" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[q]" }), " ", _jsx(Text, { dimColor: true, children: "quit" })] }), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[s]" }), " ", _jsx(Text, { dimColor: true, children: "AI reason" })] })), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "*" }), " ", _jsx(Text, { dimColor: true, children: "originally excluded by AI" })] }))] })] }));
209
+ }), below > 0 && _jsxs(Text, { dimColor: true, children: ["\u2193 ", below, " below"] })] })), _jsxs(Box, { gap: 3, marginTop: 1, children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.success, children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", total, " included"] })] })) : (_jsx(Text, { color: PALETTE.warning, children: "none included" })), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[j/k]" }), " ", _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[a/space]" }), " ", _jsx(Text, { dimColor: true, children: "toggle" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[A]" }), " ", _jsx(Text, { dimColor: true, children: "toggle all" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[f]" }), " ", _jsx(Text, { dimColor: true, children: "continue" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[q]" }), " ", _jsx(Text, { dimColor: true, children: "quit" })] }), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "[s]" }), " ", _jsx(Text, { dimColor: true, children: "review reason" })] })), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: "*" }), " ", _jsx(Text, { dimColor: true, children: "requires review" })] }))] })] }));
211
210
  }
@@ -4,6 +4,7 @@ export type ScopeComponent = {
4
4
  componentId: string;
5
5
  aiDecision?: 'accepted' | 'rejected' | 'failed' | null;
6
6
  aiReason?: string | null;
7
+ needsReview?: boolean;
7
8
  slots?: Array<{
8
9
  name: string;
9
10
  allowedComponents: string[];
@@ -58,7 +58,7 @@ const HELP_SECTIONS = [
58
58
  title: 'Panels',
59
59
  entries: [
60
60
  { keys: 'c', label: 'Cycle list' },
61
- { keys: 'x', label: 'AI exclusions' },
61
+ { keys: 'x', label: 'Review flags' },
62
62
  ],
63
63
  },
64
64
  {
@@ -797,7 +797,7 @@ export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus =
797
797
  if (showHelp) {
798
798
  return _jsx(HelpOverlay, { sections: HELP_SECTIONS, onClose: () => setShowHelp(false) });
799
799
  }
800
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", totalComponents, " component", totalComponents === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), _jsx(AutoFilterBanner, { status: aiFilterStatus, progress: aiFilterProgress, error: aiFilterError }), hasAnyAi && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [`AI recommended exclusions (${aiExcludedCount})`, aiRows.length > 0 && _jsx(Text, { color: PALETTE.info, children: ' — [x] review & jump' })] }) })), _jsx(CounterStrip, { counters: counters, totalWidth: totalWidth }), hasCycles && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If you must have components with cycles, select them together into the generate step and then use the editor to fix them." }) })), nothingIncluded && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: PALETTE.warning, children: ["nothing selected \u2014 press ", _jsx(Text, { color: PALETTE.info, children: "[Y]" }), " to accept all non-flagged,", ' ', _jsx(Text, { color: PALETTE.info, children: "[A]" }), " to toggle all, or ", _jsx(Text, { color: PALETTE.info, children: "[a]" }), " to accept the highlighted row"] }) })), _jsxs(Box, { flexDirection: "row", children: [aiRationalePanel.isOpen ? (_jsx(GotoBanner, { title: `AI recommended exclusions (${aiExcludedCount})`, rows: aiRows, cursor: aiCursor, maxRows: panelMaxRows, width: sidebarWidth, footerHint: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [x/Esc] close" })) : lineagePanel.isOpen && focusedComponent ? (_jsx(LineagePanel, { focusedComponentKey: focusedComponent.name, entries: lineageEntries, cursor: lineageCursor, jumpables: lineageJumpables, maxRows: panelMaxRows, width: sidebarWidth })) : (_jsx(GroupedSidebar, { items: groupedItems, cycleParticipants: cycleParticipants, selectedIdx: selectedItemIdx, selectedRowIdx: safeCursor, onSelect: () => { }, expandedGroups: expandedGroups, onToggleExpanded: toggleExpanded, width: sidebarWidth, focused: focusedColumn === 'main', scrollOffset: scrollOffset, visibleCount: visibleCount, showFlatTier: false, selectionStateByKey: selectionStateByKey, aiFlaggedByKey: aiFlaggedByKey, dimPredicate: dimPredicate, visibleRows: visibleRows, viewMode: columnOneView, graph: graph })), columnPlan.layout === 'three-column' && (_jsxs(_Fragment, { children: [_jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedComponentsColumn, { width: columnPlan.added, entries: addedComponents, cursor: safeAddedComponentsCursor, focused: focusedColumn === 'added-components', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount }), _jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedGroupsColumn, { width: columnPlan.groups, entries: addedGroups, cursor: safeAddedGroupsCursor, focused: focusedColumn === 'added-groups', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount })] }))] }), focusedComponent && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: focusedComponent.name }), _jsx(Text, { dimColor: true, children: ' — ' }), isIncluded(focusedComponent.name) ? (_jsx(Text, { color: PALETTE.success, children: "included" })) : (_jsx(Text, { color: PALETTE.error, children: "excluded" })), isAiFlagged(focusedComponent) && (_jsx(Text, { color: PALETTE.warning, bold: true, children: ' [×]' }))] }), isAiFlagged(focusedComponent) &&
800
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713 Extraction complete" }), _jsxs(Text, { dimColor: true, children: ["Found ", totalComponents, " component", totalComponents === 1 ? '' : 's', ". Pick which ones to import. Generation runs only on the included set."] }), _jsx(AutoFilterBanner, { status: aiFilterStatus, progress: aiFilterProgress, error: aiFilterError }), hasAnyAi && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [`Review flags (${aiExcludedCount})`, aiRows.length > 0 && _jsx(Text, { color: PALETTE.info, children: ' — [x] review & jump' })] }) })), _jsx(CounterStrip, { counters: counters, totalWidth: totalWidth }), hasCycles && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If you must have components with cycles, select them together into the generate step and then use the editor to fix them." }) })), nothingIncluded && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: PALETTE.warning, children: ["nothing selected \u2014 press ", _jsx(Text, { color: PALETTE.info, children: "[Y]" }), " to accept all non-flagged,", ' ', _jsx(Text, { color: PALETTE.info, children: "[A]" }), " to toggle all, or ", _jsx(Text, { color: PALETTE.info, children: "[a]" }), " to accept the highlighted row"] }) })), _jsxs(Box, { flexDirection: "row", children: [aiRationalePanel.isOpen ? (_jsx(GotoBanner, { title: `Review flags (${aiExcludedCount})`, rows: aiRows, cursor: aiCursor, maxRows: panelMaxRows, width: sidebarWidth, footerHint: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [x/Esc] close" })) : lineagePanel.isOpen && focusedComponent ? (_jsx(LineagePanel, { focusedComponentKey: focusedComponent.name, entries: lineageEntries, cursor: lineageCursor, jumpables: lineageJumpables, maxRows: panelMaxRows, width: sidebarWidth })) : (_jsx(GroupedSidebar, { items: groupedItems, cycleParticipants: cycleParticipants, selectedIdx: selectedItemIdx, selectedRowIdx: safeCursor, onSelect: () => { }, expandedGroups: expandedGroups, onToggleExpanded: toggleExpanded, width: sidebarWidth, focused: focusedColumn === 'main', scrollOffset: scrollOffset, visibleCount: visibleCount, showFlatTier: false, selectionStateByKey: selectionStateByKey, aiFlaggedByKey: aiFlaggedByKey, dimPredicate: dimPredicate, visibleRows: visibleRows, viewMode: columnOneView, graph: graph })), columnPlan.layout === 'three-column' && (_jsxs(_Fragment, { children: [_jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedComponentsColumn, { width: columnPlan.added, entries: addedComponents, cursor: safeAddedComponentsCursor, focused: focusedColumn === 'added-components', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount }), _jsx(Box, { width: 2, flexShrink: 0 }), _jsx(AddedGroupsColumn, { width: columnPlan.groups, entries: addedGroups, cursor: safeAddedGroupsCursor, focused: focusedColumn === 'added-groups', aiFlaggedByKey: aiFlaggedByKey, visibleCount: visibleCount })] }))] }), focusedComponent && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.info, children: focusedComponent.name }), _jsx(Text, { dimColor: true, children: ' — ' }), isIncluded(focusedComponent.name) ? (_jsx(Text, { color: PALETTE.success, children: "included" })) : (_jsx(Text, { color: PALETTE.error, children: "excluded" })), isAiFlagged(focusedComponent) && (_jsx(Text, { color: PALETTE.warning, bold: true, children: ' [×]' }))] }), isAiFlagged(focusedComponent) &&
801
801
  focusedComponent.aiReason !== null &&
802
802
  focusedComponent.aiReason !== undefined &&
803
803
  focusedComponent.aiReason !== '' && (_jsx(Box, { width: totalWidth, height: FOCUSED_REASON_MAX_LINES, flexShrink: 0, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: capReasonForFocusedRow(focusedComponent.aiReason, totalWidth) }) }))] })), cyclesPanelOpen && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.warning, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Cycles detected (${slotCycles.length}):` }), _jsx(Text, { children: " " }), slotCycles.map((cycle, i) => {
@@ -810,7 +810,7 @@ export function ScopeGateStep({ components, onConfirm, onQuit, aiFilterStatus =
810
810
  parts.push(cycle.path[cycle.path.length - 1]);
811
811
  const label = `Cycle ${i + 1}: ${parts.join(' → ')}`;
812
812
  return (_jsxs(Text, { children: [isCursor ? (_jsx(Text, { color: PALETTE.info, bold: true, children: '▶' })) : (_jsx(Text, { children: " " })), _jsx(Text, { color: PALETTE.warning, inverse: isCursor, children: ' ' + label })] }, i));
813
- }), _jsx(Text, { dimColor: true, children: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [c/Esc] close" })] })), pendingRejectCascade && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.warning, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Rejecting ${pendingRejectCascade.target} will:` }), pendingRejectCascade.ancestors.length > 0 && (_jsx(Text, { children: `- Reject ancestors: ${pendingRejectCascade.ancestors.join(', ')}` })), pendingRejectCascade.descendants.length > 0 && (_jsx(Text, { children: `- Deselect descendants: ${pendingRejectCascade.descendants.join(', ')}` })), _jsx(Text, { dimColor: true, children: "[y] confirm \u00B7 [n]/[Esc] cancel" })] })), searchOpen && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [`/${searchQuery}`, _jsx(Text, { color: PALETTE.info, children: '▎' }), searchQuery && _jsx(Text, { dimColor: true, children: ` (${totalMatches}/${totalComponents} matches)` })] }), autocompleteCandidates.length > 1 && (_jsx(Text, { dimColor: true, children: ` possibilities: ${autocompleteCandidates.join(' · ').slice(0, 120)}` }))] })), !searchOpen && searchQuery && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `/${searchQuery} (${totalMatches}/${totalComponents} matches) · [Esc] clear` }) })), _jsxs(Box, { columnGap: 2, marginTop: 1, flexWrap: "wrap", children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.success, children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", totalComponents, " included"] })] })) : (_jsx(Text, { color: PALETTE.warning, children: "none included" })), legendEntry('[j/k]', 'move'), legendEntry('[a]', 'accept'), legendEntry('[r]', 'reject'), hasGroupRoots && legendEntry('[space]', 'expand/collapse group'), hasGroupRoots && legendEntry('[E/C]', 'expand/collapse all'), legendEntry('[A]', 'toggle all'), legendEntry('[Y]', 'accept non-flagged'), legendEntry('[L]', 'flat', columnOneView === 'flat'), legendEntry('[l]', 'lineage', lineagePanel.isOpen), legendEntry('[i]', 'focus lineage', jumpFilterTarget !== null), hasCycles && legendEntry('[o]', 'only cycles', activeFilters.has('cycles')), hasCycles && legendEntry('[c]', 'cycle list', cyclesPanelOpen), legendEntry('[/]', 'search', searchOpen || searchQuery.length > 0), legendEntry('[f]', 'continue'), legendEntry('[?]', 'help'), legendEntry('[q]', 'quit'), columnPlan.layout === 'three-column' && legendEntry('[Tab/Shift-Tab]', 'switch column'), columnPlan.layout === 'three-column' && legendEntry('[Enter]', 'jump to main'), hasAnyAi && legendEntry('[x]', 'AI exclusions', aiRationalePanel.isOpen), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.warning, bold: true, children: "[\u00D7]" }), ' ', _jsx(Text, { dimColor: true, children: "AI recommends excluding" })] }))] })] }));
813
+ }), _jsx(Text, { dimColor: true, children: "[\u2191/\u2193] move \u00B7 [Enter] jump \u00B7 [c/Esc] close" })] })), pendingRejectCascade && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: PALETTE.warning, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warning, children: `Rejecting ${pendingRejectCascade.target} will:` }), pendingRejectCascade.ancestors.length > 0 && (_jsx(Text, { children: `- Reject ancestors: ${pendingRejectCascade.ancestors.join(', ')}` })), pendingRejectCascade.descendants.length > 0 && (_jsx(Text, { children: `- Deselect descendants: ${pendingRejectCascade.descendants.join(', ')}` })), _jsx(Text, { dimColor: true, children: "[y] confirm \u00B7 [n]/[Esc] cancel" })] })), searchOpen && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [`/${searchQuery}`, _jsx(Text, { color: PALETTE.info, children: '▎' }), searchQuery && _jsx(Text, { dimColor: true, children: ` (${totalMatches}/${totalComponents} matches)` })] }), autocompleteCandidates.length > 1 && (_jsx(Text, { dimColor: true, children: ` possibilities: ${autocompleteCandidates.join(' · ').slice(0, 120)}` }))] })), !searchOpen && searchQuery && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `/${searchQuery} (${totalMatches}/${totalComponents} matches) · [Esc] clear` }) })), _jsxs(Box, { columnGap: 2, marginTop: 1, flexWrap: "wrap", children: [includedCount > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.success, children: includedCount }), _jsxs(Text, { dimColor: true, children: ["/", totalComponents, " included"] })] })) : (_jsx(Text, { color: PALETTE.warning, children: "none included" })), legendEntry('[j/k]', 'move'), legendEntry('[a]', 'accept'), legendEntry('[r]', 'reject'), hasGroupRoots && legendEntry('[space]', 'expand/collapse group'), hasGroupRoots && legendEntry('[E/C]', 'expand/collapse all'), legendEntry('[A]', 'toggle all'), legendEntry('[Y]', 'accept non-flagged'), legendEntry('[L]', 'flat', columnOneView === 'flat'), legendEntry('[l]', 'lineage', lineagePanel.isOpen), legendEntry('[i]', 'focus lineage', jumpFilterTarget !== null), hasCycles && legendEntry('[o]', 'only cycles', activeFilters.has('cycles')), hasCycles && legendEntry('[c]', 'cycle list', cyclesPanelOpen), legendEntry('[/]', 'search', searchOpen || searchQuery.length > 0), legendEntry('[f]', 'continue'), legendEntry('[?]', 'help'), legendEntry('[q]', 'quit'), columnPlan.layout === 'three-column' && legendEntry('[Tab/Shift-Tab]', 'switch column'), columnPlan.layout === 'three-column' && legendEntry('[Enter]', 'jump to main'), hasAnyAi && legendEntry('[x]', 'review flags', aiRationalePanel.isOpen), hasAnyAi && (_jsxs(Text, { children: [_jsx(Text, { color: PALETTE.warning, bold: true, children: "[\u00D7]" }), ' ', _jsx(Text, { dimColor: true, children: "requires review" })] }))] })] }));
814
814
  }
815
815
  function ColumnHeader(props) {
816
816
  const { title, width, focused } = props;
@@ -103,6 +103,7 @@ export type ScopeComponentRow = {
103
103
  componentId: string;
104
104
  aiDecision: 'accepted' | 'rejected' | null;
105
105
  aiReason: string | null;
106
+ needsReview: boolean;
106
107
  slots: Array<{
107
108
  name: string;
108
109
  allowedComponents: string[];
@@ -683,14 +683,7 @@ export function loadRawComponents(db, sessionId, allowedNames) {
683
683
  source: c.source,
684
684
  framework: c.framework,
685
685
  extractionConfidence: c.extraction_confidence ?? null,
686
- reviewReasons: (() => {
687
- try {
688
- return JSON.parse(c.review_reasons ?? '[]');
689
- }
690
- catch {
691
- return [];
692
- }
693
- })(),
686
+ reviewReasons: parseReviewReasons(c.review_reasons),
694
687
  needsReview: Boolean(c.needs_review),
695
688
  sourcePath: c.source_path ?? undefined,
696
689
  props: (propsByComponent.get(c.component_id) ?? []).map((p) => {
@@ -757,6 +750,16 @@ export function renameEmptySlots(db, sessionId, componentId, componentName, slot
757
750
  }
758
751
  return { renames, warnings };
759
752
  }
753
+ function parseReviewReasons(raw) {
754
+ try {
755
+ const parsed = JSON.parse(raw ?? '[]');
756
+ return Array.isArray(parsed) ? parsed.filter((reason) => typeof reason === 'string') : [];
757
+ }
758
+ catch {
759
+ // A malformed historical value should not prevent callers from loading.
760
+ return [];
761
+ }
762
+ }
760
763
  function groupBy(items, key) {
761
764
  const map = new Map();
762
765
  for (const item of items) {
@@ -930,7 +933,7 @@ export function loadCDFComponents(db, sessionId) {
930
933
  }
931
934
  export function loadScopeComponents(db, sessionId) {
932
935
  const rows = db
933
- .prepare(`SELECT name, component_id, status, reject_reason FROM raw_components
936
+ .prepare(`SELECT name, component_id, status, reject_reason, review_reasons, needs_review FROM raw_components
934
937
  WHERE session_id = ? AND status IN ('extracted', 'accepted', 'rejected')
935
938
  ORDER BY name`)
936
939
  .all(sessionId);
@@ -946,16 +949,21 @@ export function loadScopeComponents(db, sessionId) {
946
949
  .all(sessionId);
947
950
  const slotsByComponent = groupBy(slotRows, (s) => s.component_id);
948
951
  const allowedBySlot = groupBy(allowedRows, (a) => `${a.component_id}::${a.slot_name}`);
949
- return rows.map((r) => ({
950
- name: r.name,
951
- componentId: r.component_id,
952
- aiDecision: r.status === 'accepted' ? 'accepted' : r.status === 'rejected' ? 'rejected' : null,
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
- })),
958
- }));
952
+ return rows.map((r) => {
953
+ const reviewReasons = parseReviewReasons(r.review_reasons);
954
+ const nonAuthorableReason = reviewReasons.find((reason) => reason.startsWith('non-authorable:'));
955
+ return {
956
+ name: r.name,
957
+ componentId: r.component_id,
958
+ aiDecision: r.status === 'accepted' ? 'accepted' : r.status === 'rejected' ? 'rejected' : null,
959
+ aiReason: r.reject_reason ?? nonAuthorableReason?.slice('non-authorable:'.length) ?? null,
960
+ needsReview: Boolean(r.needs_review),
961
+ slots: (slotsByComponent.get(r.component_id) ?? []).map((s) => ({
962
+ name: s.name,
963
+ allowedComponents: (allowedBySlot.get(`${r.component_id}::${s.name}`) ?? []).map((a) => a.allowed_component),
964
+ })),
965
+ };
966
+ });
959
967
  }
960
968
  export function applyScopeDecisions(db, sessionId, decisions) {
961
969
  const now = new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.17.1-dev-build-c40b4d2.0",
3
+ "version": "2.17.1-dev-build-36625f3.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,8 +34,8 @@
34
34
  "react": "^18.3.1",
35
35
  "react-devtools-core": "^4.19.1",
36
36
  "react-dom": "^18.3.1",
37
- "@contentful/experience-design-system-extraction": "2.17.1-dev-build-c40b4d2.0",
38
- "@contentful/experience-design-system-types": "2.17.1-dev-build-c40b4d2.0"
37
+ "@contentful/experience-design-system-extraction": "2.17.1-dev-build-36625f3.0",
38
+ "@contentful/experience-design-system-types": "2.17.1-dev-build-36625f3.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@tsconfig/node24": "^24.0.3",
@@ -147,7 +147,7 @@ For each `RawPropDefinition`, apply in order:
147
147
 
148
148
  1. **Framework / DOM / accessibility pass-through?** → `exclude_prop`. These are escape hatches for developers, not configurable surfaces for marketers. Exposing them in the ExO editor adds noise that obscures the props that actually carry intent. Always exclude:
149
149
  - Framework internals: `ref`, `innerRef`, event handlers (any `onSomething`), `testId`, `data-testid`, `key`
150
- - DOM pass-through: `className`, `class`, `classes`, `classNames`, `rootClassName`, `prefixCls`, `style`, `styles`, `id`, `role`, `tabIndex`, `name` (the bare HTML form `name` attribute), `htmlFor`, `for`, `slot`, `is`, `lang`, `dir`, `hidden`, `draggable`, `spellCheck`, `contentEditable`, `inputMode`, `autoComplete`, `autoFocus`, `translate`, `part`, `exportparts`
150
+ - DOM pass-through: `className`, `class`, `classes`, `classNames`, `rootClassName`, `prefixCls`, `style`, `styles`, `id`, `role`, `tabIndex`, `htmlFor`, `for`, `slot`, `is`, `lang`, `dir`, `hidden`, `draggable`, `spellCheck`, `contentEditable`, `inputMode`, `autoComplete`, `autoFocus`, `translate`, `part`, `exportparts`
151
151
  - Accessibility pass-through: any `aria-*` or `ariaSomething` prop (including bare `aria` as an aria-attributes object), `aria-label`, `aria-hidden`, `aria-describedby`, `aria-controls`
152
152
  - Data attributes: any `data-*` prop
153
153
  - **Polymorphic component props**: `as`, `element`, `component` (when typed as an HTML tag string or component reference) — these change rendered HTML, not marketer-visible behavior
@@ -157,6 +157,7 @@ For each `RawPropDefinition`, apply in order:
157
157
  - `icon` / `leftIcon` / `rightIcon` / `prefixIcon` / `suffixIcon` — slot or `string` (icon name); see slot guidance below
158
158
  - `items` / `options` / `actions` / `links` — usually array content; if the element shape is simple, classify as `string` (comma-separated names/IDs) and note in `description`. Only exclude when elements are deep nested objects with no flat representation.
159
159
  - `value` (the bare prop, not `modelValue`) — content prop, usually `string` (or `enum` if from a fixed set). Note: Vue's `modelValue` / `modelModifiers` are excluded by pre-classify because they're v-model framework wiring.
160
+ - `name` — content prop, usually `string`. Treat it as semantic component data, not as a DOM pass-through.
160
161
  - `form` (when not the literal `<form>` HTML attribute) — typically content; classify as `string` unless it's a complex form-config object
161
162
  - `inputId` / `componentId` — these CAN be content (anchor IDs, marketer-set tracking refs). Classify as `string`, `cdf_category: "content"` when the type is a plain string. Only exclude if the prop is clearly internal (e.g. typed as a generated React ID).
162
163
  - `accessibleNameRef` / `accessibleDescriptionRef` (web components) — these are ID references for a11y wiring; classify as `string`, `cdf_category: "state"` (behavioral wiring, not design or content).
@@ -397,7 +398,7 @@ Before emitting any tool calls, verify:
397
398
  5. Every `cdf_type: "token"` has `token_kind` (or a warning in `description` if lookup failed)
398
399
  6. No `cdf_type: "link"` — all href/url props use `string`
399
400
  7. `required` values are JSON booleans, not strings
400
- 8. Framework, DOM, accessibility, and data-* pass-through props are excluded — `className`/`classes`/`classNames`/`rootClassName`/`prefixCls`, `style`, `id`, `role`, `tabIndex`, `name` (bare HTML form attribute), `aria-*` (and bare `aria`), `data-*`, polymorphic `as`/`element`/`component`, framework theming `dt`/`pt`/`ptOptions`/`unstyled`/`sx`. Discrete positional/geometric props (`top`, `bottom`, `left`, `right`, `rotation`, etc.) ARE classified as `string` design props. Common semantic props (`icon`, `items`, `actions`, `options`, `value`, `form`, `inputId`, `componentId`) are NOT excluded — classify them per their content/design/state nature.
401
+ 8. Framework, DOM, accessibility, and data-* pass-through props are excluded — `className`/`classes`/`classNames`/`rootClassName`/`prefixCls`, `style`, `id`, `role`, `tabIndex`, `aria-*` (and bare `aria`), `data-*`, polymorphic `as`/`element`/`component`, framework theming `dt`/`pt`/`ptOptions`/`unstyled`/`sx`. Discrete positional/geometric props (`top`, `bottom`, `left`, `right`, `rotation`, etc.) ARE classified as `string` design props. Common semantic props (`icon`, `items`, `actions`, `options`, `value`, `name`, `form`, `inputId`, `componentId`) are NOT excluded — classify them per their content/design/state nature.
401
402
  9. No `cdf_type: "link"` used — `link` is reserved and rejected by the CLI parser
402
403
  10. No `cdf_type: "number"` used — this is not a supported type; use `"string"` with numeric defaults. `cdf_type: "boolean"` IS valid — use it for boolean toggle props.
403
404
  11. `classify_component` includes a `rationale` object with all three sub-fields (`rationale.description`, `rationale.props`, `rationale.slots`) populated as non-empty strings.