@contentful/experience-design-system-cli 2.7.4-dev-build-595febb.0 → 2.7.5-dev-build-752dc9c.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 +1 -1
- package/dist/src/analyze/command.js +3 -1
- package/dist/src/analyze/extract/validate.d.ts +4 -0
- package/dist/src/analyze/extract/validate.js +67 -0
- package/dist/src/analyze/select/command.d.ts +10 -0
- package/dist/src/analyze/select/command.js +41 -8
- package/dist/src/analyze/select/tui/App.js +17 -11
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +4 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +19 -6
- package/dist/src/analyze/select/types.d.ts +6 -0
- package/dist/src/analyze/select/types.js +24 -7
- package/dist/src/analyze/select-agent/command.js +31 -3
- package/dist/src/import/command.js +2 -0
- package/dist/src/import/orchestrator.d.ts +1 -0
- package/dist/src/import/orchestrator.js +2 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.js +2 -0
- package/dist/src/types.d.ts +8 -0
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { preClassifyComponent } from './pre-classify.js';
|
|
|
11
11
|
import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
|
|
12
12
|
import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
|
|
13
13
|
import { describeReviewReasons, inspectComponentSource } from './extract/source-inspection.js';
|
|
14
|
+
import { validateExtractedComponents } from './extract/validate.js';
|
|
14
15
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
15
16
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
16
17
|
'.git',
|
|
@@ -168,7 +169,8 @@ export function registerAnalyzeCommand(program) {
|
|
|
168
169
|
needsReview: deriveNeedsReview(confidence) || inspection.wrapperConfidence >= 4 || inspection.keepDespiteZeroSurface,
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
|
-
|
|
172
|
+
const validatedComponents = validateExtractedComponents(filteredComponents);
|
|
173
|
+
storeRawComponents(db, sessionId, validatedComponents);
|
|
172
174
|
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
173
175
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
174
176
|
db.close();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
+
export type { ExtractionValidationIssue } from '../../types.js';
|
|
3
|
+
export declare function validateExtractedComponents(components: RawComponentDefinition[]): RawComponentDefinition[];
|
|
4
|
+
export declare function shouldExcludeDueToValidation(component: RawComponentDefinition): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function validateExtractedComponents(components) {
|
|
2
|
+
const nameCounts = new Map();
|
|
3
|
+
for (const component of components) {
|
|
4
|
+
const key = component.name.trim();
|
|
5
|
+
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
|
|
6
|
+
}
|
|
7
|
+
return components.map((component) => {
|
|
8
|
+
const issues = [];
|
|
9
|
+
if (!component.name.trim()) {
|
|
10
|
+
issues.push({
|
|
11
|
+
severity: 'error',
|
|
12
|
+
code: 'EMPTY_COMPONENT_NAME',
|
|
13
|
+
message: 'Component name must not be empty',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
for (let i = 0; i < component.props.length; i++) {
|
|
17
|
+
if (!component.props[i].name.trim()) {
|
|
18
|
+
issues.push({
|
|
19
|
+
severity: 'error',
|
|
20
|
+
code: 'EMPTY_PROP_NAME',
|
|
21
|
+
message: `Prop at index ${i} has an empty name`,
|
|
22
|
+
field: `props[${i}].name`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < component.slots.length; i++) {
|
|
27
|
+
if (!component.slots[i].name.trim()) {
|
|
28
|
+
issues.push({
|
|
29
|
+
severity: 'error',
|
|
30
|
+
code: 'EMPTY_SLOT_NAME',
|
|
31
|
+
message: `Slot at index ${i} has an empty name`,
|
|
32
|
+
field: `slots[${i}].name`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const propNames = new Set(component.props.map((p) => p.name));
|
|
37
|
+
for (let i = 0; i < component.slots.length; i++) {
|
|
38
|
+
if (component.slots[i].name && propNames.has(component.slots[i].name)) {
|
|
39
|
+
issues.push({
|
|
40
|
+
severity: 'error',
|
|
41
|
+
code: 'PROP_SLOT_NAME_COLLISION',
|
|
42
|
+
message: `"${component.slots[i].name}" is used as both a prop name and a slot name`,
|
|
43
|
+
field: `slots[${i}].name`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const nameKey = component.name.trim();
|
|
48
|
+
if (nameKey && (nameCounts.get(nameKey) ?? 0) > 1) {
|
|
49
|
+
issues.push({
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
code: 'DUPLICATE_COMPONENT_NAME',
|
|
52
|
+
message: `Component name "${component.name}" appears more than once in the extracted set`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (component.props.length === 0 && component.slots.length === 0) {
|
|
56
|
+
issues.push({
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
code: 'EMPTY_COMPONENT',
|
|
59
|
+
message: 'Component has no props or slots and will be filtered out during generation',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { ...component, validationIssues: issues };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function shouldExcludeDueToValidation(component) {
|
|
66
|
+
return (component.validationIssues ?? []).some((i) => i.severity === 'error');
|
|
67
|
+
}
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { ReviewSessionSnapshot } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Load components from the pipeline DB and re-run extraction validation.
|
|
5
|
+
*
|
|
6
|
+
* `validationIssues` is intentionally not persisted (the validator is pure
|
|
7
|
+
* and cheap to re-run), so any cold-start of `analyze select` from a prior
|
|
8
|
+
* `analyze extract` session needs to recompute it before building the
|
|
9
|
+
* review snapshot — otherwise the TUI sees no validation errors at all.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadAndValidateForReview(sessionId: string, projectRoot: string | undefined): Promise<ReviewSessionSnapshot>;
|
|
2
12
|
export declare function registerAnalyzeEditCommand(program: Command): void;
|
|
@@ -6,6 +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 } from '../extract/validate.js';
|
|
9
10
|
const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
|
|
10
11
|
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
11
12
|
function applyDotPath(obj, path, value) {
|
|
@@ -84,6 +85,9 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
84
85
|
if (rejectPatterns.some((p) => nameLower.includes(p))) {
|
|
85
86
|
return { ...c, status: 'rejected' };
|
|
86
87
|
}
|
|
88
|
+
if (selectAll && opts.excludeInvalid && shouldExcludeDueToValidation(c.originalProposal)) {
|
|
89
|
+
return { ...c, status: 'rejected' };
|
|
90
|
+
}
|
|
87
91
|
if (selectAll || selectPatterns.some((p) => nameLower.includes(p))) {
|
|
88
92
|
return { ...c, status: 'accepted' };
|
|
89
93
|
}
|
|
@@ -134,6 +138,26 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
134
138
|
}
|
|
135
139
|
process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
|
|
136
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Load components from the pipeline DB and re-run extraction validation.
|
|
143
|
+
*
|
|
144
|
+
* `validationIssues` is intentionally not persisted (the validator is pure
|
|
145
|
+
* and cheap to re-run), so any cold-start of `analyze select` from a prior
|
|
146
|
+
* `analyze extract` session needs to recompute it before building the
|
|
147
|
+
* review snapshot — otherwise the TUI sees no validation errors at all.
|
|
148
|
+
*/
|
|
149
|
+
export async function loadAndValidateForReview(sessionId, projectRoot) {
|
|
150
|
+
const db = openPipelineDb();
|
|
151
|
+
let rawComponents;
|
|
152
|
+
try {
|
|
153
|
+
rawComponents = loadRawComponents(db, sessionId);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
db.close();
|
|
157
|
+
}
|
|
158
|
+
const validatedComponents = validateExtractedComponents(rawComponents);
|
|
159
|
+
return loadReviewInput(validatedComponents, { reviewRoot: projectRoot });
|
|
160
|
+
}
|
|
137
161
|
function resolveSessionId(sessionFlag) {
|
|
138
162
|
if (sessionFlag)
|
|
139
163
|
return sessionFlag;
|
|
@@ -170,17 +194,18 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
170
194
|
.option('--accept-all', 'Alias for --select-all', false)
|
|
171
195
|
.option('--reject <pattern>', 'Alias for --deselect <pattern> (repeatable)', collect, [])
|
|
172
196
|
.option('--patch <path>', 'Path to a JSON patch file for structured component overrides')
|
|
173
|
-
.
|
|
197
|
+
.option('--exclude-invalid', 'Auto-reject components with validation errors when bulk-selecting (no-op without --select-all)')
|
|
198
|
+
.action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, excludeInvalid, }) => {
|
|
174
199
|
const sessionId = resolveSessionId(sessionFlag);
|
|
175
200
|
const db = openPipelineDb();
|
|
176
|
-
let
|
|
201
|
+
let rawComponentCount = 0;
|
|
177
202
|
try {
|
|
178
|
-
|
|
203
|
+
rawComponentCount = loadRawComponents(db, sessionId).length;
|
|
179
204
|
}
|
|
180
205
|
finally {
|
|
181
206
|
db.close();
|
|
182
207
|
}
|
|
183
|
-
if (
|
|
208
|
+
if (rawComponentCount === 0) {
|
|
184
209
|
process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
|
|
185
210
|
process.exit(1);
|
|
186
211
|
return;
|
|
@@ -195,9 +220,7 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
195
220
|
let paths;
|
|
196
221
|
let snapshot;
|
|
197
222
|
try {
|
|
198
|
-
snapshot = await
|
|
199
|
-
reviewRoot: projectRoot,
|
|
200
|
-
});
|
|
223
|
+
snapshot = await loadAndValidateForReview(sessionId, projectRoot);
|
|
201
224
|
paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
202
225
|
if (!nonInteractive) {
|
|
203
226
|
snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
|
|
@@ -216,7 +239,17 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
216
239
|
const stepDb = openPipelineDb();
|
|
217
240
|
const stepId = createStep(stepDb, sessionId, 'analyze select', { sessionId });
|
|
218
241
|
try {
|
|
219
|
-
await runNonInteractive(snapshot, {
|
|
242
|
+
await runNonInteractive(snapshot, {
|
|
243
|
+
session: sessionFlag,
|
|
244
|
+
projectRoot,
|
|
245
|
+
acceptAll,
|
|
246
|
+
selectAll,
|
|
247
|
+
reject,
|
|
248
|
+
deselect,
|
|
249
|
+
select,
|
|
250
|
+
patch,
|
|
251
|
+
excludeInvalid,
|
|
252
|
+
}, paths, sessionId);
|
|
220
253
|
updateStep(stepDb, stepId, 'complete', { sessionId });
|
|
221
254
|
}
|
|
222
255
|
catch (err) {
|
|
@@ -4,7 +4,8 @@ import { Box, Text, useStdout } from 'ink';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { createReviewSessionDetail } from '../types.js';
|
|
6
6
|
import { TopBar } from './components/TopBar.js';
|
|
7
|
-
import { Sidebar } from './components/Sidebar.js';
|
|
7
|
+
import { Sidebar, sortComponentsForSidebar } from './components/Sidebar.js';
|
|
8
|
+
import { countValidationIssues } from '../types.js';
|
|
8
9
|
import { ComponentDetail } from './components/ComponentDetail.js';
|
|
9
10
|
import { StatusBar } from './components/StatusBar.js';
|
|
10
11
|
import { HelpOverlay } from './components/HelpOverlay.js';
|
|
@@ -316,22 +317,27 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
316
317
|
onToggleHelp: () => setShowHelp((prev) => !prev),
|
|
317
318
|
});
|
|
318
319
|
// Must be before early returns — Rules of Hooks
|
|
319
|
-
const sessionSummary = useMemo(() => (session?.components ?? [])
|
|
320
|
-
.map((c) =>
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
const sessionSummary = useMemo(() => sortComponentsForSidebar((session?.components ?? [])
|
|
321
|
+
.map((c) => {
|
|
322
|
+
const counts = countValidationIssues(c.originalProposal);
|
|
323
|
+
return {
|
|
324
|
+
id: c.id,
|
|
325
|
+
name: c.name,
|
|
326
|
+
status: c.status,
|
|
327
|
+
previewAnnotation: previewAnnotations[c.name],
|
|
328
|
+
extractionConfidence: c.originalProposal.extractionConfidence ?? null,
|
|
329
|
+
needsReview: c.originalProposal.needsReview ?? false,
|
|
330
|
+
validationErrorCount: counts.errors,
|
|
331
|
+
validationWarningCount: counts.warnings,
|
|
332
|
+
};
|
|
333
|
+
})
|
|
328
334
|
.sort((a, b) => {
|
|
329
335
|
const aF = a.needsReview && a.status === 'needs-review' ? 0 : 1;
|
|
330
336
|
const bF = b.needsReview && b.status === 'needs-review' ? 0 : 1;
|
|
331
337
|
if (aF !== bF)
|
|
332
338
|
return aF - bF;
|
|
333
339
|
return (a.extractionConfidence ?? 6) - (b.extractionConfidence ?? 6);
|
|
334
|
-
}), [session?.components, previewAnnotations]);
|
|
340
|
+
})), [session?.components, previewAnnotations]);
|
|
335
341
|
if (loading) {
|
|
336
342
|
return _jsx(Text, { children: "Loading session..." });
|
|
337
343
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { ReviewComponentSummary } from '../../types.js';
|
|
2
|
+
import type { ReviewComponentSummary, ReviewComponentStatus } from '../../types.js';
|
|
3
3
|
type SidebarProps = {
|
|
4
4
|
components: ReviewComponentSummary[];
|
|
5
5
|
selectedId: string | null;
|
|
@@ -11,5 +11,8 @@ type SidebarProps = {
|
|
|
11
11
|
collapsed?: boolean;
|
|
12
12
|
width?: number;
|
|
13
13
|
};
|
|
14
|
+
export declare function statusIcon(status: ReviewComponentStatus, validationErrorCount: number, validationWarningCount: number): string;
|
|
15
|
+
export declare function statusColor(status: ReviewComponentStatus, validationErrorCount: number, validationWarningCount: number): string;
|
|
16
|
+
export declare function sortComponentsForSidebar(components: ReviewComponentSummary[]): ReviewComponentSummary[];
|
|
14
17
|
export declare function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed, width: widthProp, }: SidebarProps): React.ReactElement;
|
|
15
18
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
function statusIcon(status) {
|
|
3
|
+
export function statusIcon(status, validationErrorCount, validationWarningCount) {
|
|
4
|
+
if (validationErrorCount > 0 || validationWarningCount > 0)
|
|
5
|
+
return '⚠';
|
|
4
6
|
switch (status) {
|
|
5
7
|
case 'accepted':
|
|
6
8
|
return '✓';
|
|
@@ -12,7 +14,11 @@ function statusIcon(status) {
|
|
|
12
14
|
return '·';
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
|
-
function statusColor(status) {
|
|
17
|
+
export function statusColor(status, validationErrorCount, validationWarningCount) {
|
|
18
|
+
if (validationErrorCount > 0)
|
|
19
|
+
return 'red';
|
|
20
|
+
if (validationWarningCount > 0)
|
|
21
|
+
return 'yellow';
|
|
16
22
|
switch (status) {
|
|
17
23
|
case 'accepted':
|
|
18
24
|
return 'green';
|
|
@@ -29,15 +35,22 @@ function truncateName(name, maxLen) {
|
|
|
29
35
|
return name;
|
|
30
36
|
return name.slice(0, maxLen) + '…';
|
|
31
37
|
}
|
|
38
|
+
export function sortComponentsForSidebar(components) {
|
|
39
|
+
const withErrors = components.filter((c) => c.validationErrorCount > 0);
|
|
40
|
+
const withWarnings = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount > 0);
|
|
41
|
+
const clean = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount === 0);
|
|
42
|
+
return [...withErrors, ...withWarnings, ...clean];
|
|
43
|
+
}
|
|
32
44
|
export function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed = false, width: widthProp, }) {
|
|
33
|
-
const
|
|
45
|
+
const sorted = sortComponentsForSidebar(components);
|
|
46
|
+
const visible = sorted.slice(scrollOffset, scrollOffset + visibleCount);
|
|
34
47
|
const showScrollUp = scrollOffset > 0;
|
|
35
|
-
const showScrollDown = scrollOffset + visibleCount <
|
|
48
|
+
const showScrollDown = scrollOffset + visibleCount < sorted.length;
|
|
36
49
|
const width = collapsed ? 3 : (widthProp ?? 18);
|
|
37
50
|
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: focused ? 'white' : undefined, children: [showScrollUp && !collapsed && _jsx(Text, { dimColor: true, children: "\u25B2" }), visible.map((component) => {
|
|
38
51
|
const isSelected = component.id === selectedId;
|
|
39
|
-
const icon = statusIcon(component.status);
|
|
40
|
-
const color = statusColor(component.status);
|
|
52
|
+
const icon = statusIcon(component.status, component.validationErrorCount, component.validationWarningCount);
|
|
53
|
+
const color = statusColor(component.status, component.validationErrorCount, component.validationWarningCount);
|
|
41
54
|
const maxNameLen = Math.max(1, width - 4);
|
|
42
55
|
const name = truncateName(component.name, maxNameLen);
|
|
43
56
|
if (collapsed) {
|
|
@@ -24,6 +24,8 @@ export type ReviewComponentSummary = {
|
|
|
24
24
|
previewAnnotation?: PreviewAnnotation;
|
|
25
25
|
extractionConfidence: number | null;
|
|
26
26
|
needsReview: boolean;
|
|
27
|
+
validationErrorCount: number;
|
|
28
|
+
validationWarningCount: number;
|
|
27
29
|
};
|
|
28
30
|
export type ReviewSessionSnapshot = {
|
|
29
31
|
components: ReviewComponentRecord[];
|
|
@@ -44,5 +46,9 @@ export type ReviewSessionPaths = {
|
|
|
44
46
|
eventsPath: string;
|
|
45
47
|
statePath: string;
|
|
46
48
|
};
|
|
49
|
+
export declare function countValidationIssues(component: RawComponentDefinition): {
|
|
50
|
+
errors: number;
|
|
51
|
+
warnings: number;
|
|
52
|
+
};
|
|
47
53
|
export declare function createReviewSessionSummary(session: ReviewSessionSnapshot): ReviewSessionSummary;
|
|
48
54
|
export declare function createReviewSessionDetail(session: ReviewSessionSnapshot): ReviewSessionDetail;
|
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
export function countValidationIssues(component) {
|
|
2
|
+
const issues = component.validationIssues ?? [];
|
|
3
|
+
let errors = 0;
|
|
4
|
+
let warnings = 0;
|
|
5
|
+
for (const issue of issues) {
|
|
6
|
+
if (issue.severity === 'error')
|
|
7
|
+
errors++;
|
|
8
|
+
else if (issue.severity === 'warning')
|
|
9
|
+
warnings++;
|
|
10
|
+
}
|
|
11
|
+
return { errors, warnings };
|
|
12
|
+
}
|
|
1
13
|
export function createReviewSessionSummary(session) {
|
|
2
14
|
return {
|
|
3
|
-
components: session.components.map((component) =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
components: session.components.map((component) => {
|
|
16
|
+
const counts = countValidationIssues(component.originalProposal);
|
|
17
|
+
return {
|
|
18
|
+
id: component.id,
|
|
19
|
+
name: component.name,
|
|
20
|
+
status: component.status,
|
|
21
|
+
extractionConfidence: component.originalProposal.extractionConfidence ?? null,
|
|
22
|
+
needsReview: component.originalProposal.needsReview ?? false,
|
|
23
|
+
validationErrorCount: counts.errors,
|
|
24
|
+
validationWarningCount: counts.warnings,
|
|
25
|
+
};
|
|
26
|
+
}),
|
|
10
27
|
};
|
|
11
28
|
}
|
|
12
29
|
export function createReviewSessionDetail(session) {
|
|
@@ -6,6 +6,7 @@ import { parseSelectToolCallLines, runAgent } from '../../generate/agent-runner.
|
|
|
6
6
|
import { OutputFormatter, c } from '../../output/format.js';
|
|
7
7
|
import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
|
|
8
8
|
import { isAbsolute, resolve } from 'node:path';
|
|
9
|
+
import { validateExtractedComponents, shouldExcludeDueToValidation } from '../extract/validate.js';
|
|
9
10
|
const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
10
11
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
|
|
11
12
|
const DEFAULT_CONCURRENCY = 5;
|
|
@@ -177,6 +178,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
177
178
|
.option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
|
|
178
179
|
.option('--verbose', 'Show full agent output including reasoning text')
|
|
179
180
|
.option('--dry-run', 'Print the prompt for the first component without invoking the agent')
|
|
181
|
+
.option('--exclude-invalid', 'Automatically reject components with validation errors (empty names, collisions)')
|
|
180
182
|
.action(async (opts) => {
|
|
181
183
|
if (!VALID_AGENTS.has(opts.agent)) {
|
|
182
184
|
process.stderr.write(`Error: unknown agent '${opts.agent}'. Accepted values: claude, codex, opencode, cursor\n`);
|
|
@@ -201,6 +203,23 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
201
203
|
process.exit(1);
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
206
|
+
// Re-run validation (not persisted to DB, so always recompute).
|
|
207
|
+
const validatedComponents = validateExtractedComponents(rawComponents);
|
|
208
|
+
// If --exclude-invalid, split out errored components before sending to LLM.
|
|
209
|
+
const invalidComponents = opts.excludeInvalid ? validatedComponents.filter(shouldExcludeDueToValidation) : [];
|
|
210
|
+
const componentsForAgent = opts.excludeInvalid
|
|
211
|
+
? validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp))
|
|
212
|
+
: validatedComponents;
|
|
213
|
+
if (invalidComponents.length > 0) {
|
|
214
|
+
process.stderr.write(c.yellow(`Auto-rejecting ${invalidComponents.length} component(s) with validation errors (--exclude-invalid):\n`));
|
|
215
|
+
for (const comp of invalidComponents) {
|
|
216
|
+
const codes = (comp.validationIssues ?? [])
|
|
217
|
+
.filter((i) => i.severity === 'error')
|
|
218
|
+
.map((i) => i.code)
|
|
219
|
+
.join(', ');
|
|
220
|
+
process.stderr.write(` ✗ ${comp.name} ${codes}\n`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
204
223
|
if (selectionRoot && scannedFiles.length > 0) {
|
|
205
224
|
scannedFiles = scannedFiles.map((f) => (isAbsolute(f) ? f : resolve(selectionRoot, f)));
|
|
206
225
|
}
|
|
@@ -208,17 +227,22 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
208
227
|
process.stderr.write('warn: session has no scanned-files index (likely extracted on an older CLI version). ' +
|
|
209
228
|
'Re-run `analyze extract` to enable data-fetch wrapper detection during selection.\n');
|
|
210
229
|
}
|
|
211
|
-
let selectionCandidates =
|
|
230
|
+
let selectionCandidates = componentsForAgent.map((component) => ({ component }));
|
|
212
231
|
if (selectionRoot && scannedFiles.length > 0) {
|
|
213
232
|
const repoIndex = await buildRepoContextIndex(selectionRoot, scannedFiles).catch(() => null);
|
|
214
233
|
if (repoIndex) {
|
|
215
|
-
selectionCandidates =
|
|
234
|
+
selectionCandidates = componentsForAgent.map((component) => ({
|
|
216
235
|
component,
|
|
217
236
|
selectionContext: buildSelectionContext(repoIndex, component),
|
|
218
237
|
}));
|
|
219
238
|
}
|
|
220
239
|
}
|
|
221
240
|
if (opts.dryRun) {
|
|
241
|
+
if (selectionCandidates.length === 0) {
|
|
242
|
+
process.stderr.write('No valid components to preview — all components were excluded due to validation errors.\n');
|
|
243
|
+
process.exit(0);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
222
246
|
const first = selectionCandidates[0];
|
|
223
247
|
const prompt = await buildPrompt({
|
|
224
248
|
skill: 'select',
|
|
@@ -231,14 +255,18 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
231
255
|
return;
|
|
232
256
|
}
|
|
233
257
|
const selectResults = await selectAllComponents(agent, opts.model, selectionCandidates, opts.verbose ?? false);
|
|
258
|
+
// Build decision map from results. Seed auto-rejections from validation exclusions first.
|
|
234
259
|
const decisions = new Map();
|
|
260
|
+
for (const comp of invalidComponents) {
|
|
261
|
+
decisions.set(componentKey(comp), 'rejected');
|
|
262
|
+
}
|
|
235
263
|
for (const r of selectResults) {
|
|
236
264
|
decisions.set(r.componentKey, r.decision);
|
|
237
265
|
}
|
|
238
266
|
const artifactsRoot = getRefineArtifactsRoot();
|
|
239
267
|
let snapshot;
|
|
240
268
|
try {
|
|
241
|
-
snapshot = await loadReviewInput(
|
|
269
|
+
snapshot = await loadReviewInput(validatedComponents, {
|
|
242
270
|
reviewRoot: selectionRoot ?? undefined,
|
|
243
271
|
});
|
|
244
272
|
snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
|
|
@@ -24,6 +24,7 @@ export function registerImportCommand(program) {
|
|
|
24
24
|
.option('--no-cache', 'Re-run all steps even if output already exists')
|
|
25
25
|
.option('--yes', 'Skip interactive confirmation in apply push')
|
|
26
26
|
.option('--verbose', 'Show full agent output and all entity progress')
|
|
27
|
+
.option('--exclude-invalid', 'Automatically reject components with validation errors (empty names, collisions)')
|
|
27
28
|
.option('--viewports <path>', 'JSON file with viewport array (passed to apply push)')
|
|
28
29
|
.option('--host <url>', 'Override API base URL (passed to apply push)')
|
|
29
30
|
.option('--dry-run', 'Print generate components prompt without invoking the agent')
|
|
@@ -84,6 +85,7 @@ export function registerImportCommand(program) {
|
|
|
84
85
|
noCache: opts.cache === false,
|
|
85
86
|
yes: opts.yes ?? false,
|
|
86
87
|
verbose: opts.verbose ?? false,
|
|
88
|
+
excludeInvalid: opts.excludeInvalid ?? false,
|
|
87
89
|
viewports: opts.viewports,
|
|
88
90
|
host: opts.host,
|
|
89
91
|
dryRun: opts.dryRun,
|
|
@@ -128,6 +128,8 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
128
128
|
editArgs = ['analyze', 'select-agent', '--session', extractSessionId, '--agent', opts.agent];
|
|
129
129
|
if (opts.model)
|
|
130
130
|
editArgs.push('--model', opts.model);
|
|
131
|
+
if (opts.excludeInvalid)
|
|
132
|
+
editArgs.push('--exclude-invalid');
|
|
131
133
|
}
|
|
132
134
|
else {
|
|
133
135
|
editArgs = ['analyze', 'select', '--session', extractSessionId];
|
|
@@ -187,6 +187,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
187
187
|
status: c.status,
|
|
188
188
|
extractionConfidence: null,
|
|
189
189
|
needsReview: false,
|
|
190
|
+
validationErrorCount: 0,
|
|
191
|
+
validationWarningCount: 0,
|
|
190
192
|
}));
|
|
191
193
|
const longestName = components.reduce((m, c) => Math.max(m, c.key.length), 0);
|
|
192
194
|
const sidebarWidth = Math.min(Math.max(longestName + 4, 14), 22);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { DesignTokenType } from '@contentful/experience-design-system-types';
|
|
2
|
+
export type ExtractionValidationIssueCode = 'EMPTY_COMPONENT_NAME' | 'EMPTY_PROP_NAME' | 'EMPTY_SLOT_NAME' | 'PROP_SLOT_NAME_COLLISION' | 'DUPLICATE_COMPONENT_NAME' | 'EMPTY_COMPONENT';
|
|
3
|
+
export type ExtractionValidationIssue = {
|
|
4
|
+
severity: 'error' | 'warning';
|
|
5
|
+
code: ExtractionValidationIssueCode;
|
|
6
|
+
message: string;
|
|
7
|
+
field?: string;
|
|
8
|
+
};
|
|
2
9
|
export interface RawTokenDefinition {
|
|
3
10
|
name: string;
|
|
4
11
|
value: string;
|
|
@@ -37,6 +44,7 @@ export interface RawComponentDefinition {
|
|
|
37
44
|
extractionConfidence?: number | null;
|
|
38
45
|
reviewReasons?: string[];
|
|
39
46
|
needsReview?: boolean;
|
|
47
|
+
validationIssues?: ExtractionValidationIssue[];
|
|
40
48
|
}
|
|
41
49
|
export interface ComponentExtractionResult {
|
|
42
50
|
components: RawComponentDefinition[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5-dev-build-752dc9c.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"react-dom": "^18.3.1",
|
|
37
37
|
"ts-morph": "^27.0.2",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
|
-
"@contentful/experience-design-system-types": "2.7.
|
|
39
|
+
"@contentful/experience-design-system-types": "2.7.5-dev-build-752dc9c.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@tsconfig/node24": "^24.0.3",
|