@contentful/experience-design-system-cli 2.7.5-dev-build-752dc9c.0 → 2.10.1-dev-build-90df010.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/build-analyze-view-rows.d.ts +23 -0
- package/dist/src/analyze/build-analyze-view-rows.js +39 -0
- package/dist/src/analyze/command.js +13 -10
- package/dist/src/analyze/extract/validate.d.ts +13 -1
- package/dist/src/analyze/extract/validate.js +32 -5
- package/dist/src/analyze/select/command.d.ts +37 -0
- package/dist/src/analyze/select/command.js +200 -9
- package/dist/src/analyze/select/tui/App.js +1 -9
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +1 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +26 -3
- package/dist/src/analyze/select-agent/command.js +22 -10
- package/dist/src/analyze/tui/AnalyzeView.d.ts +8 -0
- package/dist/src/analyze/tui/AnalyzeView.js +5 -2
- package/dist/src/apply/api-client.d.ts +20 -0
- package/dist/src/apply/api-client.js +71 -5
- package/dist/src/generate/command.js +21 -2
- package/dist/src/import/orchestrator.d.ts +21 -0
- package/dist/src/import/orchestrator.js +95 -14
- package/dist/src/import/tui/WizardApp.d.ts +13 -0
- package/dist/src/import/tui/WizardApp.js +224 -52
- package/dist/src/import/tui/steps/GateStep.d.ts +2 -1
- package/dist/src/import/tui/steps/GateStep.js +4 -2
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.d.ts +11 -0
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +14 -0
- package/dist/src/import/tui/wizard-422-helpers.d.ts +48 -0
- package/dist/src/import/tui/wizard-422-helpers.js +60 -0
- package/dist/src/program.d.ts +17 -0
- package/dist/src/program.js +27 -4
- package/dist/src/session/db.d.ts +13 -0
- package/dist/src/session/db.js +50 -0
- package/dist/src/types.d.ts +1 -1
- package/package.json +2 -2
- package/skills/generate-components.md +2 -0
package/dist/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RawComponentDefinition } from '../types.js';
|
|
2
|
+
import type { AnalyzeViewResult } from './tui/AnalyzeView.js';
|
|
3
|
+
type AnalyzeViewRow = AnalyzeViewResult['components'][number];
|
|
4
|
+
/**
|
|
5
|
+
* Build the per-component rows shown in the analyze TUI / non-TTY summary,
|
|
6
|
+
* pairing each filtered component with its corresponding validation result.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT — positional indexing is intentional and load-bearing.
|
|
9
|
+
* `validateExtractedComponents` is implemented as `.map()` over its input,
|
|
10
|
+
* so `validatedComponents[i]` always corresponds to `filteredComponents[i]`.
|
|
11
|
+
*
|
|
12
|
+
* We previously keyed validated components by `name` to "avoid a positional
|
|
13
|
+
* dependency" — that got the tradeoff exactly backwards. `Map.set` overwrites
|
|
14
|
+
* by key, so for two components named "Button" only the second's
|
|
15
|
+
* `validationIssues` survived in the Map. The first row's errors then
|
|
16
|
+
* disappeared from `analyzeResult` entirely: no Errors-section entry, no
|
|
17
|
+
* row badge, no count. Pairing by index restores correctness.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildAnalyzeViewRows(filteredComponents: RawComponentDefinition[], validatedComponents: RawComponentDefinition[], allWarnings: string[]): {
|
|
20
|
+
rows: AnalyzeViewRow[];
|
|
21
|
+
totalErrors: number;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the per-component rows shown in the analyze TUI / non-TTY summary,
|
|
3
|
+
* pairing each filtered component with its corresponding validation result.
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT — positional indexing is intentional and load-bearing.
|
|
6
|
+
* `validateExtractedComponents` is implemented as `.map()` over its input,
|
|
7
|
+
* so `validatedComponents[i]` always corresponds to `filteredComponents[i]`.
|
|
8
|
+
*
|
|
9
|
+
* We previously keyed validated components by `name` to "avoid a positional
|
|
10
|
+
* dependency" — that got the tradeoff exactly backwards. `Map.set` overwrites
|
|
11
|
+
* by key, so for two components named "Button" only the second's
|
|
12
|
+
* `validationIssues` survived in the Map. The first row's errors then
|
|
13
|
+
* disappeared from `analyzeResult` entirely: no Errors-section entry, no
|
|
14
|
+
* row badge, no count. Pairing by index restores correctness.
|
|
15
|
+
*/
|
|
16
|
+
export function buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings) {
|
|
17
|
+
if (validatedComponents.length !== filteredComponents.length) {
|
|
18
|
+
// Defensive: caller must pass aligned arrays. If this ever fires we want
|
|
19
|
+
// to know loudly rather than silently mis-pair rows with their issues.
|
|
20
|
+
throw new Error(`buildAnalyzeViewRows: validatedComponents length (${validatedComponents.length}) does not match filteredComponents length (${filteredComponents.length})`);
|
|
21
|
+
}
|
|
22
|
+
let totalErrors = 0;
|
|
23
|
+
const rows = filteredComponents.map((c, i) => {
|
|
24
|
+
const validated = validatedComponents[i];
|
|
25
|
+
const errorIssues = (validated.validationIssues ?? []).filter((issue) => issue.severity === 'error');
|
|
26
|
+
totalErrors += errorIssues.length;
|
|
27
|
+
return {
|
|
28
|
+
name: c.name,
|
|
29
|
+
framework: c.framework,
|
|
30
|
+
propCount: c.props.length,
|
|
31
|
+
slotCount: c.slots.length,
|
|
32
|
+
warnings: allWarnings.filter((w) => w.startsWith(c.name + ':')),
|
|
33
|
+
errors: errorIssues.map((issue) => issue.message),
|
|
34
|
+
extractionConfidence: c.extractionConfidence ?? null,
|
|
35
|
+
needsReview: c.needsReview ?? false,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return { rows, totalErrors };
|
|
39
|
+
}
|
|
@@ -12,6 +12,7 @@ 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
14
|
import { validateExtractedComponents } from './extract/validate.js';
|
|
15
|
+
import { buildAnalyzeViewRows } from './build-analyze-view-rows.js';
|
|
15
16
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
16
17
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
17
18
|
'.git',
|
|
@@ -175,20 +176,14 @@ export function registerAnalyzeCommand(program) {
|
|
|
175
176
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
176
177
|
db.close();
|
|
177
178
|
const allWarnings = [...extraction.warnings, ...filterWarnings];
|
|
179
|
+
const { rows: componentRows, totalErrors } = buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings);
|
|
178
180
|
const analyzeResult = {
|
|
179
181
|
sourceDirectory,
|
|
180
182
|
sessionId,
|
|
181
183
|
fileCount: sourceFiles.length,
|
|
182
|
-
components:
|
|
183
|
-
name: c.name,
|
|
184
|
-
framework: c.framework,
|
|
185
|
-
propCount: c.props.length,
|
|
186
|
-
slotCount: c.slots.length,
|
|
187
|
-
warnings: allWarnings.filter((w) => w.startsWith(c.name + ':')),
|
|
188
|
-
extractionConfidence: c.extractionConfidence ?? null,
|
|
189
|
-
needsReview: c.needsReview ?? false,
|
|
190
|
-
})),
|
|
184
|
+
components: componentRows,
|
|
191
185
|
totalWarnings: allWarnings.length,
|
|
186
|
+
totalErrors,
|
|
192
187
|
};
|
|
193
188
|
if (process.stdout.isTTY) {
|
|
194
189
|
const { waitUntilExit } = render(createElement(AnalyzeView, {
|
|
@@ -204,11 +199,19 @@ export function registerAnalyzeCommand(program) {
|
|
|
204
199
|
`Scanned ${pluralize(sourceFiles.length, 'source file')} in ${sourceDirectory}`,
|
|
205
200
|
`Extracted ${pluralize(extraction.components.length, 'component')}`,
|
|
206
201
|
];
|
|
202
|
+
if (totalErrors > 0) {
|
|
203
|
+
summaryLines.push(`Errors (${totalErrors}):`);
|
|
204
|
+
for (const c of componentRows) {
|
|
205
|
+
for (const e of c.errors) {
|
|
206
|
+
summaryLines.push(`- ${c.name}: ${e}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
207
210
|
if (allWarnings.length > 0) {
|
|
208
211
|
summaryLines.push(`Warnings (${allWarnings.length}):`);
|
|
209
212
|
summaryLines.push(...allWarnings.map((w) => `- ${w}`));
|
|
210
213
|
}
|
|
211
|
-
else {
|
|
214
|
+
else if (totalErrors === 0) {
|
|
212
215
|
summaryLines.push('Warnings: none');
|
|
213
216
|
}
|
|
214
217
|
process.stderr.write(summaryLines.join('\n') + '\n');
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import type { RawComponentDefinition } from '../../types.js';
|
|
1
|
+
import type { RawComponentDefinition, ExtractionValidationIssue } from '../../types.js';
|
|
2
2
|
export type { ExtractionValidationIssue } from '../../types.js';
|
|
3
3
|
export declare function validateExtractedComponents(components: RawComponentDefinition[]): RawComponentDefinition[];
|
|
4
4
|
export declare function shouldExcludeDueToValidation(component: RawComponentDefinition): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Format a stderr-ready warning describing components auto-rejected by the
|
|
7
|
+
* extraction gate. Used by `analyze select --select-all --exclude-invalid`
|
|
8
|
+
* and `analyze select-agent --exclude-invalid` so both opt-in paths emit a
|
|
9
|
+
* consistent message — non-interactive callers (CI, orchestrator, scripted
|
|
10
|
+
* pipeline) need to see WHICH components were excluded and WHY, not just
|
|
11
|
+
* the bare counts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatExclusionWarning(rejected: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
validationIssues?: ExtractionValidationIssue[];
|
|
16
|
+
}>): string;
|
|
@@ -25,21 +25,27 @@ export function validateExtractedComponents(components) {
|
|
|
25
25
|
}
|
|
26
26
|
for (let i = 0; i < component.slots.length; i++) {
|
|
27
27
|
if (!component.slots[i].name.trim()) {
|
|
28
|
+
// Warning, not error: SP-2's renameEmptySlots auto-recovers empty slot
|
|
29
|
+
// names to children/slot_<n> before the LLM prompt, so the component
|
|
30
|
+
// can still be generated. Asymmetric with EMPTY_PROP_NAME (still error)
|
|
31
|
+
// because that path silently drops empty-named props in
|
|
32
|
+
// loadCDFComponents — real data loss — instead of recovering them.
|
|
28
33
|
issues.push({
|
|
29
|
-
severity: '
|
|
34
|
+
severity: 'warning',
|
|
30
35
|
code: 'EMPTY_SLOT_NAME',
|
|
31
36
|
message: `Slot at index ${i} has an empty name`,
|
|
32
37
|
field: `slots[${i}].name`,
|
|
33
38
|
});
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
|
-
const propNames = new Set(component.props.map((p) => p.name));
|
|
41
|
+
const propNames = new Set(component.props.map((p) => p.name.trim()).filter(Boolean));
|
|
37
42
|
for (let i = 0; i < component.slots.length; i++) {
|
|
38
|
-
|
|
43
|
+
const slotName = component.slots[i].name.trim();
|
|
44
|
+
if (slotName && propNames.has(slotName)) {
|
|
39
45
|
issues.push({
|
|
40
46
|
severity: 'error',
|
|
41
47
|
code: 'PROP_SLOT_NAME_COLLISION',
|
|
42
|
-
message: `"${
|
|
48
|
+
message: `"${slotName}" is used as both a prop name and a slot name`,
|
|
43
49
|
field: `slots[${i}].name`,
|
|
44
50
|
});
|
|
45
51
|
}
|
|
@@ -47,7 +53,7 @@ export function validateExtractedComponents(components) {
|
|
|
47
53
|
const nameKey = component.name.trim();
|
|
48
54
|
if (nameKey && (nameCounts.get(nameKey) ?? 0) > 1) {
|
|
49
55
|
issues.push({
|
|
50
|
-
severity: '
|
|
56
|
+
severity: 'error',
|
|
51
57
|
code: 'DUPLICATE_COMPONENT_NAME',
|
|
52
58
|
message: `Component name "${component.name}" appears more than once in the extracted set`,
|
|
53
59
|
});
|
|
@@ -65,3 +71,24 @@ export function validateExtractedComponents(components) {
|
|
|
65
71
|
export function shouldExcludeDueToValidation(component) {
|
|
66
72
|
return (component.validationIssues ?? []).some((i) => i.severity === 'error');
|
|
67
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Format a stderr-ready warning describing components auto-rejected by the
|
|
76
|
+
* extraction gate. Used by `analyze select --select-all --exclude-invalid`
|
|
77
|
+
* and `analyze select-agent --exclude-invalid` so both opt-in paths emit a
|
|
78
|
+
* consistent message — non-interactive callers (CI, orchestrator, scripted
|
|
79
|
+
* pipeline) need to see WHICH components were excluded and WHY, not just
|
|
80
|
+
* the bare counts.
|
|
81
|
+
*/
|
|
82
|
+
export function formatExclusionWarning(rejected) {
|
|
83
|
+
if (rejected.length === 0)
|
|
84
|
+
return '';
|
|
85
|
+
const lines = [`Warning: ${rejected.length} component(s) excluded due to validation errors:`];
|
|
86
|
+
for (const comp of rejected) {
|
|
87
|
+
const codes = (comp.validationIssues ?? [])
|
|
88
|
+
.filter((i) => i.severity === 'error')
|
|
89
|
+
.map((i) => i.code)
|
|
90
|
+
.join(', ');
|
|
91
|
+
lines.push(` ✗ ${comp.name} ${codes}`);
|
|
92
|
+
}
|
|
93
|
+
return lines.join('\n') + '\n';
|
|
94
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { ReviewSessionSnapshot } from './types.js';
|
|
3
|
+
import type { PreviewValidationError } from '../../apply/api-client.js';
|
|
3
4
|
/**
|
|
4
5
|
* Load components from the pipeline DB and re-run extraction validation.
|
|
5
6
|
*
|
|
@@ -9,4 +10,40 @@ import type { ReviewSessionSnapshot } from './types.js';
|
|
|
9
10
|
* review snapshot — otherwise the TUI sees no validation errors at all.
|
|
10
11
|
*/
|
|
11
12
|
export declare function loadAndValidateForReview(sessionId: string, projectRoot: string | undefined): Promise<ReviewSessionSnapshot>;
|
|
13
|
+
/**
|
|
14
|
+
* Synthesize SERVER_VALIDATION_FAILED issues onto the review state file's
|
|
15
|
+
* matching components. Used by the wizard's 422 recovery path: after a
|
|
16
|
+
* preview-API validation error, this runs before re-launching the
|
|
17
|
+
* analyze-select TUI so the Sidebar's existing red-warning rendering
|
|
18
|
+
* pins the offenders to the top.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent on identical input is NOT a contract: each call appends.
|
|
21
|
+
* The wizard's 422 path is a one-shot per preview attempt, so duplicate
|
|
22
|
+
* issues only matter if a caller invokes this twice without the user
|
|
23
|
+
* round-tripping through the TUI in between.
|
|
24
|
+
*/
|
|
25
|
+
export declare function patchReviewStateWithValidationErrors(sessionId: string, errors: PreviewValidationError[], opts?: {
|
|
26
|
+
artifactsRoot?: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
patchedNames: string[];
|
|
29
|
+
missingNames: string[];
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Exclude components whose names appear in `names` from the next preview/push.
|
|
33
|
+
* Used by the wizard's "skip and retry" path after a 422.
|
|
34
|
+
*
|
|
35
|
+
* Two stores are updated:
|
|
36
|
+
* - Pipeline DB (`raw_components.status` → `'generate-rejected'`) so that
|
|
37
|
+
* `loadCDFComponents()` excludes them from the next manifest. This is the
|
|
38
|
+
* load-bearing change — preview reads from the DB, not the JSON file.
|
|
39
|
+
* - Review state JSON (`status` → `'rejected'`) so that if the user re-enters
|
|
40
|
+
* the analyze-select TUI later, the rejected status is reflected.
|
|
41
|
+
*
|
|
42
|
+
* `'generate-rejected'` matches the existing convention for "component was
|
|
43
|
+
* generated successfully but later excluded" (see GenerateReviewStep) — the
|
|
44
|
+
* component generated fine locally but failed server-side validation.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rejectComponentsByName(sessionId: string, names: string[], opts?: {
|
|
47
|
+
artifactsRoot?: string;
|
|
48
|
+
}): Promise<void>;
|
|
12
49
|
export declare function registerAnalyzeEditCommand(program: Command): void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { createElement } from 'react';
|
|
4
4
|
import { render } from 'ink';
|
|
5
5
|
import { getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState } from './persistence.js';
|
|
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
|
+
import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '../extract/validate.js';
|
|
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) {
|
|
@@ -73,10 +73,66 @@ function applyPatch(snapshot, ops) {
|
|
|
73
73
|
return { ...snapshot, components };
|
|
74
74
|
}
|
|
75
75
|
async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
76
|
+
// --exclude-components on its own (no --select-all / --select / --deselect /
|
|
77
|
+
// --reject / --patch) is the orchestrator's 422 retry-loop invocation: only
|
|
78
|
+
// the named components should change. The rebuild path below calls
|
|
79
|
+
// storeRawComponents which DELETEs all rows and re-inserts with default
|
|
80
|
+
// status='extracted', wiping the post-generate state required by
|
|
81
|
+
// loadCDFComponents (status='generated' + raw_props.cdf_type populated).
|
|
82
|
+
// Short-circuit through rejectComponentsByName, which is a pure UPDATE.
|
|
83
|
+
const onlyExcludeComponents = !!opts.excludeComponents &&
|
|
84
|
+
!opts.acceptAll &&
|
|
85
|
+
!opts.selectAll &&
|
|
86
|
+
!(opts.select ?? []).length &&
|
|
87
|
+
!(opts.deselect ?? []).length &&
|
|
88
|
+
!(opts.reject ?? []).length &&
|
|
89
|
+
!opts.patch;
|
|
90
|
+
if (onlyExcludeComponents) {
|
|
91
|
+
const names = opts
|
|
92
|
+
.excludeComponents.split(',')
|
|
93
|
+
.map((n) => n.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
if (names.length > 0) {
|
|
96
|
+
// paths.sessionDir = resolve(artifactsRoot, sessionId), so dirname gives
|
|
97
|
+
// back the artifactsRoot — keeps rejectComponentsByName from re-resolving
|
|
98
|
+
// it via env var when the caller already knows it.
|
|
99
|
+
const artifactsRoot = dirname(paths.sessionDir);
|
|
100
|
+
await rejectComponentsByName(sessionId, names, { artifactsRoot });
|
|
101
|
+
}
|
|
102
|
+
const accepted = snapshot.components.filter((c) => c.status === 'accepted').length;
|
|
103
|
+
const rejected = snapshot.components.filter((c) => c.status === 'rejected').length + (names.length || 0);
|
|
104
|
+
process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected}\n`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
76
107
|
let result = { ...snapshot };
|
|
77
108
|
const rejectPatterns = [...(opts.reject ?? []), ...(opts.deselect ?? [])].map((p) => p.toLowerCase());
|
|
78
109
|
const selectPatterns = (opts.select ?? []).map((p) => p.toLowerCase());
|
|
79
110
|
const selectAll = opts.acceptAll || opts.selectAll;
|
|
111
|
+
// Fail-loud gate: --select-all stops with a non-zero exit when ANY component
|
|
112
|
+
// has error-severity validation issues. The user must opt in to auto-rejection
|
|
113
|
+
// with --exclude-invalid. Silent exclusion in CI / orchestrator contexts is
|
|
114
|
+
// dangerous — the caller should see and acknowledge that components are
|
|
115
|
+
// being dropped from the import.
|
|
116
|
+
if (selectAll && !opts.excludeInvalid) {
|
|
117
|
+
const invalid = result.components.filter((c) => shouldExcludeDueToValidation(c.originalProposal));
|
|
118
|
+
if (invalid.length > 0) {
|
|
119
|
+
const lines = [
|
|
120
|
+
`Error: ${invalid.length} component(s) failed validation; refusing --select-all without --exclude-invalid:`,
|
|
121
|
+
];
|
|
122
|
+
for (const c of invalid) {
|
|
123
|
+
const codes = (c.originalProposal.validationIssues ?? [])
|
|
124
|
+
.filter((i) => i.severity === 'error')
|
|
125
|
+
.map((i) => i.code)
|
|
126
|
+
.join(', ');
|
|
127
|
+
lines.push(` ✗ ${c.name} ${codes}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Re-run with --exclude-invalid to auto-reject these components, or run analyze select interactively to fix them.');
|
|
131
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
80
136
|
if (selectAll || rejectPatterns.length > 0 || selectPatterns.length > 0) {
|
|
81
137
|
result = {
|
|
82
138
|
...result,
|
|
@@ -95,6 +151,21 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
95
151
|
}),
|
|
96
152
|
};
|
|
97
153
|
}
|
|
154
|
+
// Apply --exclude-components: force-reject the named components regardless of
|
|
155
|
+
// how --select-all / --select / --deselect classified them. Used by the
|
|
156
|
+
// orchestrator's 422 retry loop to exclude server-validation offenders.
|
|
157
|
+
if (opts.excludeComponents) {
|
|
158
|
+
const excludeNames = new Set(opts.excludeComponents
|
|
159
|
+
.split(',')
|
|
160
|
+
.map((n) => n.trim())
|
|
161
|
+
.filter(Boolean));
|
|
162
|
+
if (excludeNames.size > 0) {
|
|
163
|
+
result = {
|
|
164
|
+
...result,
|
|
165
|
+
components: result.components.map((c) => excludeNames.has(c.name) ? { ...c, status: 'rejected' } : c),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
98
169
|
// Apply --patch
|
|
99
170
|
if (opts.patch) {
|
|
100
171
|
let patchOps;
|
|
@@ -124,10 +195,31 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
124
195
|
}
|
|
125
196
|
const accepted = result.components.filter((c) => c.status === 'accepted');
|
|
126
197
|
const rejected = result.components.filter((c) => c.status === 'rejected');
|
|
198
|
+
// Surface what was excluded by --select-all --exclude-invalid so the
|
|
199
|
+
// non-interactive caller (CI, orchestrator, scripted pipeline) doesn't
|
|
200
|
+
// have to guess what failed validation. The warning is printed BEFORE
|
|
201
|
+
// saveReviewState so the message lands ahead of the bare counts.
|
|
202
|
+
if (selectAll && opts.excludeInvalid) {
|
|
203
|
+
const autoRejected = rejected
|
|
204
|
+
.filter((c) => shouldExcludeDueToValidation(c.originalProposal))
|
|
205
|
+
.map((c) => ({ name: c.name, validationIssues: c.originalProposal.validationIssues }));
|
|
206
|
+
process.stderr.write(formatExclusionWarning(autoRejected));
|
|
207
|
+
}
|
|
127
208
|
// Persist decisions to session state so pipeline orchestrator can read them
|
|
128
209
|
await saveReviewState(paths.statePath, result);
|
|
129
|
-
// Sync edited proposals back to the DB so generation uses the user's edits
|
|
130
|
-
|
|
210
|
+
// Sync edited proposals back to the DB so generation uses the user's edits.
|
|
211
|
+
//
|
|
212
|
+
// Only --patch can mutate editedProposal — the other non-interactive flags
|
|
213
|
+
// (--select-all / --select / --deselect / --reject / --exclude-invalid)
|
|
214
|
+
// change status only. Calling storeRawComponents in the no-edit case is
|
|
215
|
+
// not just wasteful: it's destructive. storeRawComponents starts with
|
|
216
|
+
// `DELETE FROM raw_components WHERE session_id = ?` and re-inserts only
|
|
217
|
+
// the components passed in — so on a --select-all run, every rejected
|
|
218
|
+
// component (including its raw_slots / raw_props children) is physically
|
|
219
|
+
// removed. A second invocation of the same command would then see a
|
|
220
|
+
// smaller snapshot, lose the rejected component(s), and report different
|
|
221
|
+
// counts.
|
|
222
|
+
if (opts.patch && accepted.length > 0) {
|
|
131
223
|
const db = openPipelineDb();
|
|
132
224
|
try {
|
|
133
225
|
storeRawComponents(db, sessionId, accepted.map((c) => c.editedProposal));
|
|
@@ -158,6 +250,100 @@ export async function loadAndValidateForReview(sessionId, projectRoot) {
|
|
|
158
250
|
const validatedComponents = validateExtractedComponents(rawComponents);
|
|
159
251
|
return loadReviewInput(validatedComponents, { reviewRoot: projectRoot });
|
|
160
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Synthesize SERVER_VALIDATION_FAILED issues onto the review state file's
|
|
255
|
+
* matching components. Used by the wizard's 422 recovery path: after a
|
|
256
|
+
* preview-API validation error, this runs before re-launching the
|
|
257
|
+
* analyze-select TUI so the Sidebar's existing red-warning rendering
|
|
258
|
+
* pins the offenders to the top.
|
|
259
|
+
*
|
|
260
|
+
* Idempotent on identical input is NOT a contract: each call appends.
|
|
261
|
+
* The wizard's 422 path is a one-shot per preview attempt, so duplicate
|
|
262
|
+
* issues only matter if a caller invokes this twice without the user
|
|
263
|
+
* round-tripping through the TUI in between.
|
|
264
|
+
*/
|
|
265
|
+
export async function patchReviewStateWithValidationErrors(sessionId, errors, opts = {}) {
|
|
266
|
+
const artifactsRoot = opts.artifactsRoot ?? getRefineArtifactsRoot();
|
|
267
|
+
const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
268
|
+
let snapshot;
|
|
269
|
+
try {
|
|
270
|
+
await access(paths.statePath);
|
|
271
|
+
snapshot = JSON.parse(await readFile(paths.statePath, 'utf8'));
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
snapshot = await loadAndValidateForReview(sessionId, undefined);
|
|
275
|
+
snapshot = await ensureRefineSession(sessionId, artifactsRoot, snapshot);
|
|
276
|
+
}
|
|
277
|
+
const errorsByName = new Map();
|
|
278
|
+
for (const err of errors) {
|
|
279
|
+
const list = errorsByName.get(err.componentName) ?? [];
|
|
280
|
+
list.push(err);
|
|
281
|
+
errorsByName.set(err.componentName, list);
|
|
282
|
+
}
|
|
283
|
+
const patchedNames = [];
|
|
284
|
+
const knownNames = new Set(snapshot.components.map((c) => c.name));
|
|
285
|
+
const components = snapshot.components.map((c) => {
|
|
286
|
+
const matched = errorsByName.get(c.name);
|
|
287
|
+
if (!matched || matched.length === 0)
|
|
288
|
+
return c;
|
|
289
|
+
patchedNames.push(c.name);
|
|
290
|
+
const newIssues = matched.map((err) => ({
|
|
291
|
+
severity: 'error',
|
|
292
|
+
code: 'SERVER_VALIDATION_FAILED',
|
|
293
|
+
message: err.message,
|
|
294
|
+
}));
|
|
295
|
+
const existing = c.originalProposal.validationIssues ?? [];
|
|
296
|
+
return {
|
|
297
|
+
...c,
|
|
298
|
+
originalProposal: {
|
|
299
|
+
...c.originalProposal,
|
|
300
|
+
validationIssues: [...existing, ...newIssues],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
const missingNames = [...errorsByName.keys()].filter((n) => !knownNames.has(n));
|
|
305
|
+
await saveReviewState(paths.statePath, { ...snapshot, components });
|
|
306
|
+
return { patchedNames, missingNames };
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Exclude components whose names appear in `names` from the next preview/push.
|
|
310
|
+
* Used by the wizard's "skip and retry" path after a 422.
|
|
311
|
+
*
|
|
312
|
+
* Two stores are updated:
|
|
313
|
+
* - Pipeline DB (`raw_components.status` → `'generate-rejected'`) so that
|
|
314
|
+
* `loadCDFComponents()` excludes them from the next manifest. This is the
|
|
315
|
+
* load-bearing change — preview reads from the DB, not the JSON file.
|
|
316
|
+
* - Review state JSON (`status` → `'rejected'`) so that if the user re-enters
|
|
317
|
+
* the analyze-select TUI later, the rejected status is reflected.
|
|
318
|
+
*
|
|
319
|
+
* `'generate-rejected'` matches the existing convention for "component was
|
|
320
|
+
* generated successfully but later excluded" (see GenerateReviewStep) — the
|
|
321
|
+
* component generated fine locally but failed server-side validation.
|
|
322
|
+
*/
|
|
323
|
+
export async function rejectComponentsByName(sessionId, names, opts = {}) {
|
|
324
|
+
if (names.length === 0)
|
|
325
|
+
return;
|
|
326
|
+
const db = openPipelineDb();
|
|
327
|
+
try {
|
|
328
|
+
const placeholders = names.map(() => '?').join(',');
|
|
329
|
+
db.prepare(`UPDATE raw_components SET status = 'generate-rejected' WHERE session_id = ? AND name IN (${placeholders})`).run(sessionId, ...names);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
db.close();
|
|
333
|
+
}
|
|
334
|
+
const artifactsRoot = opts.artifactsRoot ?? getRefineArtifactsRoot();
|
|
335
|
+
const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
|
|
336
|
+
const nameSet = new Set(names);
|
|
337
|
+
let snapshot;
|
|
338
|
+
try {
|
|
339
|
+
snapshot = JSON.parse(await readFile(paths.statePath, 'utf8'));
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const components = snapshot.components.map((c) => (nameSet.has(c.name) ? { ...c, status: 'rejected' } : c));
|
|
345
|
+
await saveReviewState(paths.statePath, { ...snapshot, components });
|
|
346
|
+
}
|
|
161
347
|
function resolveSessionId(sessionFlag) {
|
|
162
348
|
if (sessionFlag)
|
|
163
349
|
return sessionFlag;
|
|
@@ -194,8 +380,9 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
194
380
|
.option('--accept-all', 'Alias for --select-all', false)
|
|
195
381
|
.option('--reject <pattern>', 'Alias for --deselect <pattern> (repeatable)', collect, [])
|
|
196
382
|
.option('--patch <path>', 'Path to a JSON patch file for structured component overrides')
|
|
197
|
-
.option('--exclude-invalid', '
|
|
198
|
-
.
|
|
383
|
+
.option('--exclude-invalid', 'With --select-all: auto-reject components with validation errors instead of failing loud (opt-in bypass for the gate)')
|
|
384
|
+
.option('--exclude-components <names>', 'Comma-separated component names to force-reject, regardless of other selection flags')
|
|
385
|
+
.action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, excludeInvalid, excludeComponents, }) => {
|
|
199
386
|
const sessionId = resolveSessionId(sessionFlag);
|
|
200
387
|
const db = openPipelineDb();
|
|
201
388
|
let rawComponentCount = 0;
|
|
@@ -216,7 +403,8 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
216
403
|
(reject ?? []).length > 0 ||
|
|
217
404
|
(deselect ?? []).length > 0 ||
|
|
218
405
|
(select ?? []).length > 0 ||
|
|
219
|
-
!!patch
|
|
406
|
+
!!patch ||
|
|
407
|
+
!!excludeComponents;
|
|
220
408
|
let paths;
|
|
221
409
|
let snapshot;
|
|
222
410
|
try {
|
|
@@ -237,7 +425,9 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
237
425
|
// Non-interactive path
|
|
238
426
|
if (nonInteractive) {
|
|
239
427
|
const stepDb = openPipelineDb();
|
|
240
|
-
const stepId = createStep(stepDb, sessionId, 'analyze select', {
|
|
428
|
+
const stepId = createStep(stepDb, sessionId, 'analyze select', {
|
|
429
|
+
sessionId,
|
|
430
|
+
});
|
|
241
431
|
try {
|
|
242
432
|
await runNonInteractive(snapshot, {
|
|
243
433
|
session: sessionFlag,
|
|
@@ -249,6 +439,7 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
249
439
|
select,
|
|
250
440
|
patch,
|
|
251
441
|
excludeInvalid,
|
|
442
|
+
excludeComponents,
|
|
252
443
|
}, paths, sessionId);
|
|
253
444
|
updateStep(stepDb, stepId, 'complete', { sessionId });
|
|
254
445
|
}
|
|
@@ -317,8 +317,7 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
317
317
|
onToggleHelp: () => setShowHelp((prev) => !prev),
|
|
318
318
|
});
|
|
319
319
|
// Must be before early returns — Rules of Hooks
|
|
320
|
-
const sessionSummary = useMemo(() => sortComponentsForSidebar((session?.components ?? [])
|
|
321
|
-
.map((c) => {
|
|
320
|
+
const sessionSummary = useMemo(() => sortComponentsForSidebar((session?.components ?? []).map((c) => {
|
|
322
321
|
const counts = countValidationIssues(c.originalProposal);
|
|
323
322
|
return {
|
|
324
323
|
id: c.id,
|
|
@@ -330,13 +329,6 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
330
329
|
validationErrorCount: counts.errors,
|
|
331
330
|
validationWarningCount: counts.warnings,
|
|
332
331
|
};
|
|
333
|
-
})
|
|
334
|
-
.sort((a, b) => {
|
|
335
|
-
const aF = a.needsReview && a.status === 'needs-review' ? 0 : 1;
|
|
336
|
-
const bF = b.needsReview && b.status === 'needs-review' ? 0 : 1;
|
|
337
|
-
if (aF !== bF)
|
|
338
|
-
return aF - bF;
|
|
339
|
-
return (a.extractionConfidence ?? 6) - (b.extractionConfidence ?? 6);
|
|
340
332
|
})), [session?.components, previewAnnotations]);
|
|
341
333
|
if (loading) {
|
|
342
334
|
return _jsx(Text, { children: "Loading session..." });
|
|
@@ -11,7 +11,7 @@ type SidebarProps = {
|
|
|
11
11
|
collapsed?: boolean;
|
|
12
12
|
width?: number;
|
|
13
13
|
};
|
|
14
|
-
export declare function statusIcon(status: ReviewComponentStatus, validationErrorCount: number,
|
|
14
|
+
export declare function statusIcon(status: ReviewComponentStatus, validationErrorCount: number, _validationWarningCount: number): string;
|
|
15
15
|
export declare function statusColor(status: ReviewComponentStatus, validationErrorCount: number, validationWarningCount: number): string;
|
|
16
16
|
export declare function sortComponentsForSidebar(components: ReviewComponentSummary[]): ReviewComponentSummary[];
|
|
17
17
|
export declare function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed, width: widthProp, }: SidebarProps): React.ReactElement;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
export function statusIcon(status, validationErrorCount,
|
|
4
|
-
|
|
3
|
+
export function statusIcon(status, validationErrorCount,
|
|
4
|
+
// Warnings deliberately do NOT override the icon — the user's accept/reject
|
|
5
|
+
// decision must remain visible. Color is already yellow via statusColor for
|
|
6
|
+
// warning-only components, so the warning cue is preserved.
|
|
7
|
+
_validationWarningCount) {
|
|
8
|
+
// Errors override — a structurally broken component should never render as ✓/✗.
|
|
9
|
+
if (validationErrorCount > 0)
|
|
5
10
|
return '⚠';
|
|
6
11
|
switch (status) {
|
|
7
12
|
case 'accepted':
|
|
@@ -35,11 +40,29 @@ function truncateName(name, maxLen) {
|
|
|
35
40
|
return name;
|
|
36
41
|
return name.slice(0, maxLen) + '…';
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Sort key for stable secondary ordering within a validation tier:
|
|
45
|
+
* needs-review components first, then by extractionConfidence ascending
|
|
46
|
+
* (lowest confidence first, null treated as highest = last). Used to be
|
|
47
|
+
* applied inline in App.tsx, then this helper sorted again — pulled in
|
|
48
|
+
* here so the sort happens once.
|
|
49
|
+
*/
|
|
50
|
+
function withinTierComparator(a, b) {
|
|
51
|
+
const aPending = a.needsReview && a.status === 'needs-review' ? 0 : 1;
|
|
52
|
+
const bPending = b.needsReview && b.status === 'needs-review' ? 0 : 1;
|
|
53
|
+
if (aPending !== bPending)
|
|
54
|
+
return aPending - bPending;
|
|
55
|
+
return (a.extractionConfidence ?? 6) - (b.extractionConfidence ?? 6);
|
|
56
|
+
}
|
|
38
57
|
export function sortComponentsForSidebar(components) {
|
|
39
58
|
const withErrors = components.filter((c) => c.validationErrorCount > 0);
|
|
40
59
|
const withWarnings = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount > 0);
|
|
41
60
|
const clean = components.filter((c) => c.validationErrorCount === 0 && c.validationWarningCount === 0);
|
|
42
|
-
return [
|
|
61
|
+
return [
|
|
62
|
+
...[...withErrors].sort(withinTierComparator),
|
|
63
|
+
...[...withWarnings].sort(withinTierComparator),
|
|
64
|
+
...[...clean].sort(withinTierComparator),
|
|
65
|
+
];
|
|
43
66
|
}
|
|
44
67
|
export function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed = false, width: widthProp, }) {
|
|
45
68
|
const sorted = sortComponentsForSidebar(components);
|