@contentful/experience-design-system-cli 2.6.2-dev-build-ea30bad.0 → 2.7.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 +10 -1
- package/dist/src/analyze/extract/scoring.d.ts +8 -0
- package/dist/src/analyze/extract/scoring.js +104 -0
- package/dist/src/analyze/select/tui/App.js +63 -28
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +8 -3
- package/dist/src/analyze/select/tui/components/Sidebar.js +1 -1
- package/dist/src/analyze/select/types.d.ts +2 -0
- package/dist/src/analyze/select/types.js +2 -0
- package/dist/src/analyze/tui/AnalyzeView.d.ts +2 -0
- package/dist/src/analyze/tui/AnalyzeView.js +27 -4
- package/dist/src/generate/agent-runner.d.ts +2 -0
- package/dist/src/generate/agent-runner.js +8 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.js +2 -0
- package/dist/src/session/db.js +38 -13
- package/dist/src/types.d.ts +5 -0
- package/dist/src/types.js +4 -1
- package/package.json +2 -2
- package/skills/select-components.md +8 -2
package/dist/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
|
|
|
9
9
|
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents } from '../session/db.js';
|
|
10
10
|
import { preClassifyComponent } from './pre-classify.js';
|
|
11
11
|
import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
|
|
12
|
+
import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
|
|
12
13
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
13
14
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
14
15
|
'.git',
|
|
@@ -130,7 +131,13 @@ export function registerAnalyzeCommand(program) {
|
|
|
130
131
|
filterWarnings.push(`Skipped non-authorable component: ${component.name} (${verdict.reason})`);
|
|
131
132
|
continue;
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
+
const { confidence, reasons } = computeExtractionScore(component);
|
|
135
|
+
filteredComponents.push({
|
|
136
|
+
...component,
|
|
137
|
+
extractionConfidence: confidence,
|
|
138
|
+
reviewReasons: reasons,
|
|
139
|
+
needsReview: deriveNeedsReview(confidence),
|
|
140
|
+
});
|
|
134
141
|
}
|
|
135
142
|
storeRawComponents(db, sessionId, filteredComponents);
|
|
136
143
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
@@ -146,6 +153,8 @@ export function registerAnalyzeCommand(program) {
|
|
|
146
153
|
propCount: c.props.length,
|
|
147
154
|
slotCount: c.slots.length,
|
|
148
155
|
warnings: allWarnings.filter((w) => w.startsWith(c.name + ':')),
|
|
156
|
+
extractionConfidence: c.extractionConfidence ?? null,
|
|
157
|
+
needsReview: c.needsReview ?? false,
|
|
149
158
|
})),
|
|
150
159
|
totalWarnings: allWarnings.length,
|
|
151
160
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
+
export type ExtractionConfidence = 1 | 2 | 3 | 4 | 5;
|
|
3
|
+
export type ExtractionScore = {
|
|
4
|
+
confidence: ExtractionConfidence;
|
|
5
|
+
reasons: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function computeExtractionScore(component: RawComponentDefinition): ExtractionScore;
|
|
8
|
+
export declare function deriveNeedsReview(confidence: ExtractionConfidence): boolean;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Prop type strings that indicate the extractor couldn't resolve a concrete type
|
|
2
|
+
const OPAQUE_TYPES = new Set(['any', 'unknown', 'object', 'Record<string, unknown>', 'Record<string, any>']);
|
|
3
|
+
// Prop names that are non-obvious and benefit from a description
|
|
4
|
+
const OBVIOUS_PROP_NAMES = new Set([
|
|
5
|
+
'className',
|
|
6
|
+
'style',
|
|
7
|
+
'id',
|
|
8
|
+
'children',
|
|
9
|
+
'disabled',
|
|
10
|
+
'hidden',
|
|
11
|
+
'onClick',
|
|
12
|
+
'onChange',
|
|
13
|
+
'onSubmit',
|
|
14
|
+
'onBlur',
|
|
15
|
+
'onFocus',
|
|
16
|
+
'href',
|
|
17
|
+
'src',
|
|
18
|
+
'alt',
|
|
19
|
+
'type',
|
|
20
|
+
'value',
|
|
21
|
+
'checked',
|
|
22
|
+
'placeholder',
|
|
23
|
+
'label',
|
|
24
|
+
'title',
|
|
25
|
+
'name',
|
|
26
|
+
'required',
|
|
27
|
+
'readOnly',
|
|
28
|
+
'autoFocus',
|
|
29
|
+
'tabIndex',
|
|
30
|
+
'role',
|
|
31
|
+
'aria-label',
|
|
32
|
+
'aria-describedby',
|
|
33
|
+
]);
|
|
34
|
+
// A union is "wide" if it mixes 3+ distinct base primitive types (ignoring nullability modifiers).
|
|
35
|
+
// e.g. "string | number | boolean" → wide. "string | null | undefined" → NOT wide (just nullable string).
|
|
36
|
+
function isWidePrimitiveUnion(type) {
|
|
37
|
+
const parts = type.split('|').map((p) => p.trim());
|
|
38
|
+
if (parts.length < 3)
|
|
39
|
+
return false;
|
|
40
|
+
const basePrimitives = new Set(['string', 'number', 'boolean']);
|
|
41
|
+
const nullability = new Set(['null', 'undefined']);
|
|
42
|
+
const baseCount = parts.filter((p) => basePrimitives.has(p)).length;
|
|
43
|
+
const nullabilityCount = parts.filter((p) => nullability.has(p)).length;
|
|
44
|
+
return baseCount >= 3 || (baseCount >= 2 && nullabilityCount > 0 && baseCount + nullabilityCount >= 3);
|
|
45
|
+
}
|
|
46
|
+
// Count the number of issues found for scoring
|
|
47
|
+
function countIssues(component) {
|
|
48
|
+
let count = 0;
|
|
49
|
+
const reasons = [];
|
|
50
|
+
if (component.props.length === 0 && component.slots.length === 0) {
|
|
51
|
+
count++;
|
|
52
|
+
reasons.push('no-props-or-slots');
|
|
53
|
+
}
|
|
54
|
+
if (component.props.length > 50) {
|
|
55
|
+
count++;
|
|
56
|
+
reasons.push(`high-prop-count:${component.props.length}`);
|
|
57
|
+
}
|
|
58
|
+
for (const prop of component.props) {
|
|
59
|
+
if (OPAQUE_TYPES.has(prop.type.trim())) {
|
|
60
|
+
count++;
|
|
61
|
+
reasons.push(`opaque-type:${prop.name}`);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (isWidePrimitiveUnion(prop.type)) {
|
|
65
|
+
count++;
|
|
66
|
+
reasons.push(`wide-union:${prop.name}`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (!prop.description && !OBVIOUS_PROP_NAMES.has(prop.name)) {
|
|
70
|
+
count++;
|
|
71
|
+
reasons.push('props-missing-description');
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { count, reasons: [...new Set(reasons)] };
|
|
76
|
+
}
|
|
77
|
+
// Maps issue count to a 1–5 confidence scale:
|
|
78
|
+
// 0 issues → 5 (clean)
|
|
79
|
+
// 1 issue → 4 (minor concern)
|
|
80
|
+
// 2 issues → 3 (moderate concern)
|
|
81
|
+
// 3 issues → 2 (significant concern)
|
|
82
|
+
// 4+ issues → 1 (likely wrong)
|
|
83
|
+
function issueCountToConfidence(count) {
|
|
84
|
+
if (count === 0)
|
|
85
|
+
return 5;
|
|
86
|
+
if (count === 1)
|
|
87
|
+
return 4;
|
|
88
|
+
if (count === 2)
|
|
89
|
+
return 3;
|
|
90
|
+
if (count === 3)
|
|
91
|
+
return 2;
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
export function computeExtractionScore(component) {
|
|
95
|
+
const { count, reasons } = countIssues(component);
|
|
96
|
+
return {
|
|
97
|
+
confidence: issueCountToConfidence(count),
|
|
98
|
+
reasons,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Flag components scoring 2 or below for human review
|
|
102
|
+
export function deriveNeedsReview(confidence) {
|
|
103
|
+
return confidence <= 2;
|
|
104
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { createReviewSessionDetail } from '../types.js';
|
|
6
|
+
import { stripScoringFields } from '../../../types.js';
|
|
6
7
|
import { TopBar } from './components/TopBar.js';
|
|
7
8
|
import { Sidebar } from './components/Sidebar.js';
|
|
8
9
|
import { ComponentDetail } from './components/ComponentDetail.js';
|
|
@@ -40,6 +41,11 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
40
41
|
const [finalizedResult, setFinalizedResult] = useState(null);
|
|
41
42
|
const [sidebarScrollOffset, setSidebarScrollOffset] = useState(0);
|
|
42
43
|
const [jsonScrollOffset, setJsonScrollOffset] = useState(0);
|
|
44
|
+
// Keeps the visual sort order in sync for use inside keymap handlers (which close over a stale render)
|
|
45
|
+
const sortedIdsRef = useRef([]);
|
|
46
|
+
// Source code kept separate from session state so lazy loading never mutates
|
|
47
|
+
// session.components — that would invalidate useMemo(sessionSummary) and flash the sidebar
|
|
48
|
+
const [sourceCodeById, setSourceCodeById] = useState({});
|
|
43
49
|
const [previewAnnotations, setPreviewAnnotations] = useState(() => {
|
|
44
50
|
const raw = process.env['EDS_PREVIEW_ANNOTATIONS'];
|
|
45
51
|
if (!raw)
|
|
@@ -138,13 +144,22 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
138
144
|
clearTimeout(previewDebounceRef.current);
|
|
139
145
|
};
|
|
140
146
|
}, []);
|
|
141
|
-
// Sync loaded session into local state
|
|
147
|
+
// Sync loaded session into local state; selectedId is set to null here
|
|
148
|
+
// and will be corrected to the first sorted item on first render via the effect below
|
|
142
149
|
useEffect(() => {
|
|
143
150
|
if (loadedSession && !session) {
|
|
144
151
|
setSession(loadedSession);
|
|
145
|
-
setSelectedId(
|
|
152
|
+
setSelectedId(null);
|
|
146
153
|
}
|
|
147
154
|
}, [loadedSession]);
|
|
155
|
+
// Once the sort order is known (after first render), select the first sorted item.
|
|
156
|
+
// Runs only when session transitions from null → loaded.
|
|
157
|
+
const sessionLoaded = session !== null;
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (sessionLoaded && selectedId === null && sortedIdsRef.current.length > 0) {
|
|
160
|
+
setSelectedId(sortedIdsRef.current[0]);
|
|
161
|
+
}
|
|
162
|
+
}, [sessionLoaded]);
|
|
148
163
|
useEffect(() => {
|
|
149
164
|
if (session && !previewResponse) {
|
|
150
165
|
const raw = process.env['EDS_PREVIEW_COUNTS'];
|
|
@@ -172,26 +187,22 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
189
|
}, [session]);
|
|
175
|
-
// Lazy source code loading
|
|
190
|
+
// Lazy source code loading — writes to sourceCodeById, NOT session.components,
|
|
191
|
+
// so it doesn't invalidate useMemo(sessionSummary) and cause a sidebar flash
|
|
176
192
|
useEffect(() => {
|
|
177
193
|
if (!session || !selectedId)
|
|
178
194
|
return;
|
|
195
|
+
if (sourceCodeById[selectedId] !== undefined)
|
|
196
|
+
return; // already loaded
|
|
179
197
|
const selectedComponent = session.components.find((c) => c.id === selectedId);
|
|
180
|
-
if (!selectedComponent
|
|
198
|
+
if (!selectedComponent)
|
|
181
199
|
return;
|
|
182
200
|
readFile(selectedComponent.resolvedSourcePath, 'utf8')
|
|
183
201
|
.then((code) => {
|
|
184
|
-
|
|
185
|
-
if (!prev)
|
|
186
|
-
return prev;
|
|
187
|
-
return {
|
|
188
|
-
...prev,
|
|
189
|
-
components: prev.components.map((c) => (c.id === selectedComponent.id ? { ...c, sourceCode: code } : c)),
|
|
190
|
-
};
|
|
191
|
-
});
|
|
202
|
+
setSourceCodeById((prev) => ({ ...prev, [selectedId]: code }));
|
|
192
203
|
})
|
|
193
204
|
.catch(() => {
|
|
194
|
-
|
|
205
|
+
setSourceCodeById((prev) => ({ ...prev, [selectedId]: '' }));
|
|
195
206
|
});
|
|
196
207
|
}, [selectedId]);
|
|
197
208
|
// SIGINT handler
|
|
@@ -235,9 +246,10 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
235
246
|
onSidebarUp: () => {
|
|
236
247
|
if (!session)
|
|
237
248
|
return;
|
|
238
|
-
const
|
|
249
|
+
const ids = sortedIdsRef.current;
|
|
250
|
+
const idx = ids.indexOf(selectedId ?? '');
|
|
239
251
|
if (idx > 0) {
|
|
240
|
-
setSelectedId(
|
|
252
|
+
setSelectedId(ids[idx - 1]);
|
|
241
253
|
setJsonScrollOffset(0);
|
|
242
254
|
setSidebarScrollOffset((prev) => Math.min(prev, idx - 1));
|
|
243
255
|
}
|
|
@@ -245,9 +257,10 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
245
257
|
onSidebarDown: () => {
|
|
246
258
|
if (!session)
|
|
247
259
|
return;
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
const ids = sortedIdsRef.current;
|
|
261
|
+
const idx = ids.indexOf(selectedId ?? '');
|
|
262
|
+
if (idx < ids.length - 1) {
|
|
263
|
+
setSelectedId(ids[idx + 1]);
|
|
251
264
|
setJsonScrollOffset(0);
|
|
252
265
|
setSidebarScrollOffset((prev) => {
|
|
253
266
|
const newIdx = idx + 1;
|
|
@@ -273,7 +286,7 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
273
286
|
setEditMode(true);
|
|
274
287
|
setDraftsByComponentId((prev) => ({
|
|
275
288
|
...prev,
|
|
276
|
-
[selectedId]: prev[selectedId] ?? JSON.stringify(component.editedProposal, null, 2),
|
|
289
|
+
[selectedId]: prev[selectedId] ?? JSON.stringify(stripScoringFields(component.editedProposal), null, 2),
|
|
277
290
|
}));
|
|
278
291
|
},
|
|
279
292
|
onToggleSource: () => {
|
|
@@ -315,6 +328,31 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
315
328
|
},
|
|
316
329
|
onToggleHelp: () => setShowHelp((prev) => !prev),
|
|
317
330
|
});
|
|
331
|
+
// Sorted order: flagged+unresolved first, then ascending confidence.
|
|
332
|
+
// Memoised above early returns (React rules of hooks) so the array reference
|
|
333
|
+
// is stable between renders — prevents Sidebar repainting on scroll/select.
|
|
334
|
+
const sessionSummary = useMemo(() => (session?.components ?? [])
|
|
335
|
+
.map((c) => ({
|
|
336
|
+
id: c.id,
|
|
337
|
+
name: c.name,
|
|
338
|
+
status: c.status,
|
|
339
|
+
previewAnnotation: previewAnnotations[c.name],
|
|
340
|
+
extractionConfidence: c.originalProposal.extractionConfidence ?? null,
|
|
341
|
+
needsReview: c.originalProposal.needsReview ?? false,
|
|
342
|
+
}))
|
|
343
|
+
.sort((a, b) => {
|
|
344
|
+
const aFlagged = a.needsReview && a.status === 'needs-review' ? 0 : 1;
|
|
345
|
+
const bFlagged = b.needsReview && b.status === 'needs-review' ? 0 : 1;
|
|
346
|
+
if (aFlagged !== bFlagged)
|
|
347
|
+
return aFlagged - bFlagged;
|
|
348
|
+
// null (unscored) sorts last; lower numeric score sorts first (most concerning)
|
|
349
|
+
const aConf = a.extractionConfidence ?? 6;
|
|
350
|
+
const bConf = b.extractionConfidence ?? 6;
|
|
351
|
+
return aConf - bConf;
|
|
352
|
+
}), [session?.components, previewAnnotations]);
|
|
353
|
+
// Stable ID order — kept in a ref for use inside keymap handlers
|
|
354
|
+
const sortedIds = useMemo(() => sessionSummary.map((c) => c.id), [sessionSummary]);
|
|
355
|
+
sortedIdsRef.current = sortedIds;
|
|
318
356
|
if (loading) {
|
|
319
357
|
return _jsx(Text, { children: "Loading session..." });
|
|
320
358
|
}
|
|
@@ -327,12 +365,6 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
327
365
|
if (finalizedResult) {
|
|
328
366
|
return _jsx(FinalizedScreen, { result: finalizedResult });
|
|
329
367
|
}
|
|
330
|
-
const sessionSummary = session.components.map((c) => ({
|
|
331
|
-
id: c.id,
|
|
332
|
-
name: c.name,
|
|
333
|
-
status: c.status,
|
|
334
|
-
previewAnnotation: previewAnnotations[c.name],
|
|
335
|
-
}));
|
|
336
368
|
const selectedRecord = session.components.find((c) => c.id === selectedId) ?? null;
|
|
337
369
|
const sessionDetail = selectedRecord ? createReviewSessionDetail({ ...session, components: [selectedRecord] }) : null;
|
|
338
370
|
const selectedDetail = sessionDetail?.components[0] ?? null;
|
|
@@ -352,7 +384,10 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
352
384
|
{ key: 'q', label: 'quit' },
|
|
353
385
|
];
|
|
354
386
|
const collapsed = terminalWidth < 80;
|
|
355
|
-
|
|
387
|
+
// TopBar(1) + statusbar(1) + footer(1) + border padding(2) = ~5 chrome rows
|
|
388
|
+
const CHROME_ROWS = 5;
|
|
389
|
+
const terminalRows = stdout?.rows ?? 24;
|
|
390
|
+
const visibleCount = Math.max(1, terminalRows - CHROME_ROWS);
|
|
356
391
|
const longestName = session.components.reduce((max, c) => Math.max(max, c.name.length), 0);
|
|
357
392
|
// icon + space + name + 2 border chars; min 14, max 22
|
|
358
393
|
const sidebarWidth = collapsed ? 3 : Math.min(Math.max(longestName + 4, 14), 22);
|
|
@@ -468,7 +503,7 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
|
|
|
468
503
|
}, onCancel: () => setShowQuitDialog(false) })), !showHelp && !showFinalizeDialog && !showQuitDialog && (_jsxs(Box, { flexGrow: 1, children: [_jsx(Sidebar, { components: sessionSummary, selectedId: selectedId, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: visibleCount, onSelect: (id) => {
|
|
469
504
|
setSelectedId(id);
|
|
470
505
|
setJsonScrollOffset(0);
|
|
471
|
-
}, onScrollChange: setSidebarScrollOffset, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode:
|
|
506
|
+
}, onScrollChange: setSidebarScrollOffset, collapsed: collapsed, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: selectedDetail ? (_jsx(ComponentDetail, { component: selectedDetail, sourceCode: selectedId ? (sourceCodeById[selectedId] ?? null) : null, draftValue: selectedId ? (draftsByComponentId[selectedId] ?? '') : '', editMode: editMode, sourceVisible: sourceVisible, jsonScrollOffset: jsonScrollOffset, sourceScrollX: 0, sourceScrollY: 0, terminalWidth: terminalWidth, previewAnnotation: selectedRecord ? previewAnnotations[selectedRecord.name] : undefined, onDraftChange: (value) => {
|
|
472
507
|
if (!selectedId)
|
|
473
508
|
return;
|
|
474
509
|
setDraftsByComponentId((prev) => ({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { stripScoringFields } from '../../../../types.js';
|
|
3
4
|
import { JsonPanel } from './JsonPanel.js';
|
|
4
5
|
import { FieldEditor } from './FieldEditor.js';
|
|
5
6
|
import { SourcePanel } from './SourcePanel.js';
|
|
@@ -34,10 +35,14 @@ export function ComponentDetail({ component, sourceCode, draftValue, editMode, s
|
|
|
34
35
|
editWidth = availableWidth - originalWidth - 3;
|
|
35
36
|
sourceWidth = 0;
|
|
36
37
|
}
|
|
37
|
-
const originalJson = JSON.stringify(component.originalProposal, null, 2);
|
|
38
|
-
const editedJson = JSON.stringify(component.editedProposal, null, 2);
|
|
38
|
+
const originalJson = JSON.stringify(stripScoringFields(component.originalProposal), null, 2);
|
|
39
|
+
const editedJson = JSON.stringify(stripScoringFields(component.editedProposal), null, 2);
|
|
40
|
+
const conf = component.originalProposal.extractionConfidence ?? null;
|
|
41
|
+
const nr = component.originalProposal.needsReview ?? false;
|
|
42
|
+
const confColor = conf === null ? 'gray' : nr ? 'red' : conf >= 4 ? 'white' : conf >= 3 ? 'yellow' : 'red';
|
|
43
|
+
const confLabel = conf === null ? 'confidence: —' : (nr ? '⚑ ' : '') + 'confidence: ' + String(conf) + '/5';
|
|
39
44
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: component.name }), (() => {
|
|
40
45
|
const ann = annotationLabel(previewAnnotation);
|
|
41
46
|
return ann ? _jsx(Text, { color: ann.color, children: ann.text }) : null;
|
|
42
|
-
})(), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [sourceVisible ? '[s] hide src' : '[s] src', editMode ? '' : ' [e] edit'] })] }), _jsxs(Box, { children: [_jsx(JsonPanel, { label: "ORIGINAL (read-only)", value: originalJson, scrollOffset: jsonScrollOffset, width: originalWidth, height: panelHeight, active: false }), _jsx(Text, { children: " " }), editMode ? (_jsx(FieldEditor, { value: draftValue || editedJson, width: editWidth, height: panelHeight, onChange: onDraftChange, onSave: onSaveDraft, onDiscard: onDiscardDraft })) : (_jsx(JsonPanel, { label: "EDIT (draft)", value: draftValue || editedJson, scrollOffset: jsonScrollOffset, width: editWidth, height: panelHeight, active: true })), sourceVisible && sourceWidth > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(SourcePanel, { sourceCode: sourceCode, filePath: component.originalProposal.source, width: sourceWidth, height: panelHeight, scrollX: sourceScrollX, scrollY: sourceScrollY })] }))] }), !editMode && _jsx(Text, { dimColor: true, children: ' [a] accept [r] reject [e] edit [A] accept all [↑↓] scroll' })] }));
|
|
47
|
+
})(), _jsx(Text, { color: confColor, children: ' — ' + confLabel }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [sourceVisible ? '[s] hide src' : '[s] src', editMode ? '' : ' [e] edit'] })] }), _jsxs(Box, { children: [_jsx(JsonPanel, { label: "ORIGINAL (read-only)", value: originalJson, scrollOffset: jsonScrollOffset, width: originalWidth, height: panelHeight, active: false }), _jsx(Text, { children: " " }), editMode ? (_jsx(FieldEditor, { value: draftValue || editedJson, width: editWidth, height: panelHeight, onChange: onDraftChange, onSave: onSaveDraft, onDiscard: onDiscardDraft })) : (_jsx(JsonPanel, { label: "EDIT (draft)", value: draftValue || editedJson, scrollOffset: jsonScrollOffset, width: editWidth, height: panelHeight, active: true })), sourceVisible && sourceWidth > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(SourcePanel, { sourceCode: sourceCode, filePath: component.originalProposal.source, width: sourceWidth, height: panelHeight, scrollX: sourceScrollX, scrollY: sourceScrollY })] }))] }), !editMode && _jsx(Text, { dimColor: true, children: ' [a] accept [r] reject [e] edit [A] accept all [↑↓] scroll' })] }));
|
|
43
48
|
}
|
|
@@ -38,7 +38,7 @@ export function Sidebar({ components, selectedId, focused, scrollOffset, visible
|
|
|
38
38
|
const isSelected = component.id === selectedId;
|
|
39
39
|
const icon = statusIcon(component.status);
|
|
40
40
|
const color = statusColor(component.status);
|
|
41
|
-
const maxNameLen = Math.max(1, width -
|
|
41
|
+
const maxNameLen = Math.max(1, width - 4);
|
|
42
42
|
const name = truncateName(component.name, maxNameLen);
|
|
43
43
|
if (collapsed) {
|
|
44
44
|
return (_jsx(Box, { children: _jsx(Text, { color: color, inverse: isSelected && focused, underline: isSelected && !focused, children: icon }) }, component.id));
|
|
@@ -22,6 +22,8 @@ export type ReviewComponentSummary = {
|
|
|
22
22
|
name: string;
|
|
23
23
|
status: ReviewComponentStatus;
|
|
24
24
|
previewAnnotation?: PreviewAnnotation;
|
|
25
|
+
extractionConfidence: number | null;
|
|
26
|
+
needsReview: boolean;
|
|
25
27
|
};
|
|
26
28
|
export type ReviewSessionSnapshot = {
|
|
27
29
|
components: ReviewComponentRecord[];
|
|
@@ -4,6 +4,8 @@ export function createReviewSessionSummary(session) {
|
|
|
4
4
|
id: component.id,
|
|
5
5
|
name: component.name,
|
|
6
6
|
status: component.status,
|
|
7
|
+
extractionConfidence: component.originalProposal.extractionConfidence ?? null,
|
|
8
|
+
needsReview: component.originalProposal.needsReview ?? false,
|
|
7
9
|
})),
|
|
8
10
|
};
|
|
9
11
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { TopBar } from '../select/tui/components/TopBar.js';
|
|
5
5
|
import { useImmediateInput } from '../select/tui/hooks/useImmediateInput.js';
|
|
6
6
|
function truncateName(name, maxLen = 30) {
|
|
@@ -10,6 +10,12 @@ function truncateName(name, maxLen = 30) {
|
|
|
10
10
|
}
|
|
11
11
|
export function AnalyzeView({ result, onExit }) {
|
|
12
12
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
13
|
+
const { stdout } = useStdout();
|
|
14
|
+
// Header lines: TopBar(1) + summary(3) + blank(1) + dividers+header(3) + blank(1) + footer(1) + footer-bar(1) = ~12
|
|
15
|
+
const HEADER_ROWS = 12;
|
|
16
|
+
const terminalRows = stdout?.rows ?? 24;
|
|
17
|
+
const visibleCount = Math.max(1, terminalRows - HEADER_ROWS);
|
|
18
|
+
const maxOffset = Math.max(0, result.components.length - visibleCount);
|
|
13
19
|
useImmediateInput((input, key) => {
|
|
14
20
|
if (input === 'q' || key.return) {
|
|
15
21
|
onExit();
|
|
@@ -19,19 +25,36 @@ export function AnalyzeView({ result, onExit }) {
|
|
|
19
25
|
setScrollOffset((o) => Math.max(0, o - 1));
|
|
20
26
|
}
|
|
21
27
|
else if (key.downArrow || input === 'j') {
|
|
22
|
-
setScrollOffset((o) => o + 1);
|
|
28
|
+
setScrollOffset((o) => Math.min(maxOffset, o + 1));
|
|
23
29
|
}
|
|
24
30
|
else if (input === 'g') {
|
|
25
31
|
setScrollOffset(0);
|
|
26
32
|
}
|
|
27
33
|
else if (input === 'G') {
|
|
28
|
-
setScrollOffset(
|
|
34
|
+
setScrollOffset(maxOffset);
|
|
29
35
|
}
|
|
30
36
|
});
|
|
37
|
+
const visible = result.components.slice(scrollOffset, scrollOffset + visibleCount);
|
|
38
|
+
const showScrollUp = scrollOffset > 0;
|
|
39
|
+
const showScrollDown = scrollOffset + visibleCount < result.components.length;
|
|
31
40
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "analyze", hints: [
|
|
32
41
|
{ key: '?', label: 'help' },
|
|
33
42
|
{ key: 'q', label: 'quit' },
|
|
34
|
-
] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { children: 'Scanned ' + result.fileCount + ' source files in ' + result.sourceDirectory }), _jsx(Text, { children: 'Extracted ' + result.components.length + ' components' }), _jsx(Text, { dimColor: true, children: 'Session: ' + result.sessionId }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, children:
|
|
43
|
+
] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { children: 'Scanned ' + result.fileCount + ' source files in ' + result.sourceDirectory }), _jsx(Text, { children: 'Extracted ' + result.components.length + ' components' }), _jsx(Text, { dimColor: true, children: 'Session: ' + result.sessionId }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, children: 'Components' +
|
|
44
|
+
(showScrollUp || showScrollDown
|
|
45
|
+
? ' (' +
|
|
46
|
+
(scrollOffset + 1) +
|
|
47
|
+
'–' +
|
|
48
|
+
Math.min(scrollOffset + visibleCount, result.components.length) +
|
|
49
|
+
' of ' +
|
|
50
|
+
result.components.length +
|
|
51
|
+
')'
|
|
52
|
+
: '') }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), showScrollUp && _jsx(Text, { dimColor: true, children: " \u25B2 scroll up" }), visible.map((component) => {
|
|
53
|
+
const conf = component.extractionConfidence;
|
|
54
|
+
const confColor = conf === null ? 'gray' : component.needsReview ? 'red' : conf >= 4 ? 'white' : conf >= 3 ? 'yellow' : 'red';
|
|
55
|
+
const confLabel = conf === null ? '—' : (component.needsReview ? '⚑ ' : '') + String(conf);
|
|
56
|
+
return (_jsxs(Box, { children: [component.warnings.length > 0 && _jsx(Text, { color: "yellow", children: "\u26A0 " }), component.warnings.length === 0 && _jsx(Text, { children: " " }), _jsx(Text, { children: truncateName(component.name).padEnd(20) }), _jsx(Text, { dimColor: true, children: component.framework.padEnd(10) }), _jsx(Text, { children: (component.propCount + ' props').padEnd(10) }), _jsx(Text, { children: (component.slotCount + ' ' + (component.slotCount === 1 ? 'slot' : 'slots')).padEnd(8) }), _jsx(Text, { color: confColor, children: confLabel }), component.warnings.length > 0 && (_jsx(Text, { color: "yellow", children: ' ⚠ ' + component.warnings.length + ' warning' + (component.warnings.length === 1 ? '' : 's') }))] }, component.name));
|
|
57
|
+
}), showScrollDown && _jsx(Text, { dimColor: true, children: " \u25BC scroll down" }), result.totalWarnings > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, color: "yellow", children: 'Warnings (' + result.totalWarnings + ')' }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components
|
|
35
58
|
.filter((c) => c.warnings.length > 0)
|
|
36
59
|
.flatMap((c) => c.warnings.map((w) => ({ component: c.name, warning: w })))
|
|
37
60
|
.map((w, i) => (_jsx(Text, { color: "yellow", children: ' ⚠ ' + w.component + ': ' + w.warning }, i)))] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: 'Run: analyze select --session ' + result.sessionId }), _jsx(Text, { children: " " })] }), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or q to exit" }) })] }));
|
|
@@ -37,11 +37,13 @@ export interface SelectComponentCall {
|
|
|
37
37
|
tool: 'select_component';
|
|
38
38
|
name: string;
|
|
39
39
|
reason?: string;
|
|
40
|
+
confidence?: number;
|
|
40
41
|
}
|
|
41
42
|
export interface RejectComponentCall {
|
|
42
43
|
tool: 'reject_component';
|
|
43
44
|
name: string;
|
|
44
45
|
reason?: string;
|
|
46
|
+
confidence?: number;
|
|
45
47
|
}
|
|
46
48
|
export type SelectToolCall = SelectComponentCall | RejectComponentCall;
|
|
47
49
|
export interface ParsedSelectToolCalls {
|
|
@@ -30,6 +30,14 @@ export function parseSelectToolCallLines(stdout) {
|
|
|
30
30
|
};
|
|
31
31
|
if (typeof rec.reason === 'string')
|
|
32
32
|
call.reason = rec.reason;
|
|
33
|
+
if (typeof rec.confidence === 'number' && rec.confidence >= 1 && rec.confidence <= 5) {
|
|
34
|
+
if (call.tool === 'select_component') {
|
|
35
|
+
call.confidence = rec.confidence;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
call.confidence = rec.confidence;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
calls.push(call);
|
|
34
42
|
}
|
|
35
43
|
return { calls, warnings };
|
|
@@ -185,6 +185,8 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
|
|
|
185
185
|
id: c.key,
|
|
186
186
|
name: c.key,
|
|
187
187
|
status: c.status,
|
|
188
|
+
extractionConfidence: null,
|
|
189
|
+
needsReview: false,
|
|
188
190
|
}));
|
|
189
191
|
const longestName = components.reduce((m, c) => Math.max(m, c.key.length), 0);
|
|
190
192
|
const sidebarWidth = Math.min(Math.max(longestName + 4, 14), 22);
|
package/dist/src/session/db.js
CHANGED
|
@@ -34,15 +34,18 @@ CREATE TABLE IF NOT EXISTS steps (
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
CREATE TABLE IF NOT EXISTS raw_components (
|
|
37
|
-
session_id
|
|
38
|
-
component_id
|
|
39
|
-
name
|
|
40
|
-
source
|
|
41
|
-
framework
|
|
42
|
-
extracted_at
|
|
43
|
-
status
|
|
44
|
-
cdf_schema
|
|
45
|
-
description
|
|
37
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
38
|
+
component_id TEXT NOT NULL,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
framework TEXT NOT NULL,
|
|
42
|
+
extracted_at TEXT NOT NULL,
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'extracted',
|
|
44
|
+
cdf_schema TEXT,
|
|
45
|
+
description TEXT,
|
|
46
|
+
extraction_confidence INTEGER,
|
|
47
|
+
review_reasons TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
needs_review INTEGER NOT NULL DEFAULT 0,
|
|
46
49
|
PRIMARY KEY (session_id, component_id)
|
|
47
50
|
);
|
|
48
51
|
|
|
@@ -164,6 +167,18 @@ function applyDbMigrations(db) {
|
|
|
164
167
|
if (!cols.some((c) => c.name === 'required')) {
|
|
165
168
|
db.exec('ALTER TABLE raw_slots ADD COLUMN required INTEGER NOT NULL DEFAULT 1 CHECK (required IN (0, 1))');
|
|
166
169
|
}
|
|
170
|
+
// Add extraction confidence scoring columns if they don't exist yet (added in v0.6.0).
|
|
171
|
+
const rawCompCols = db.prepare('PRAGMA table_info(raw_components)').all();
|
|
172
|
+
const rawCompColNames = new Set(rawCompCols.map((c) => c.name));
|
|
173
|
+
if (!rawCompColNames.has('extraction_confidence')) {
|
|
174
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN extraction_confidence INTEGER');
|
|
175
|
+
}
|
|
176
|
+
if (!rawCompColNames.has('review_reasons')) {
|
|
177
|
+
db.exec("ALTER TABLE raw_components ADD COLUMN review_reasons TEXT NOT NULL DEFAULT '[]'");
|
|
178
|
+
}
|
|
179
|
+
if (!rawCompColNames.has('needs_review')) {
|
|
180
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN needs_review INTEGER NOT NULL DEFAULT 0');
|
|
181
|
+
}
|
|
167
182
|
// Add generation_cache table if it doesn't exist yet.
|
|
168
183
|
const tables = db
|
|
169
184
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='generation_cache'`)
|
|
@@ -352,8 +367,8 @@ export function updateStep(db, stepId, status, outputs, error) {
|
|
|
352
367
|
}
|
|
353
368
|
export function storeRawComponents(db, sessionId, components, options) {
|
|
354
369
|
const now = new Date().toISOString();
|
|
355
|
-
const insertComp = db.prepare(`INSERT INTO raw_components (session_id, component_id, name, source, framework, extracted_at)
|
|
356
|
-
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
370
|
+
const insertComp = db.prepare(`INSERT INTO raw_components (session_id, component_id, name, source, framework, extracted_at, extraction_confidence, review_reasons, needs_review)
|
|
371
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
357
372
|
const insertProp = db.prepare(`INSERT INTO raw_props
|
|
358
373
|
(session_id, component_id, name, type, required, category, default_value, description, token_reference, position)
|
|
359
374
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
@@ -387,7 +402,7 @@ export function storeRawComponents(db, sessionId, components, options) {
|
|
|
387
402
|
db.prepare('DELETE FROM raw_components WHERE session_id = ?').run(sessionId);
|
|
388
403
|
for (const comp of components) {
|
|
389
404
|
const componentId = deriveComponentId(comp.name, comp.source);
|
|
390
|
-
insertComp.run(sessionId, componentId, comp.name, comp.source, comp.framework, now);
|
|
405
|
+
insertComp.run(sessionId, componentId, comp.name, comp.source, comp.framework, now, comp.extractionConfidence ?? null, JSON.stringify(comp.reviewReasons ?? []), comp.needsReview ? 1 : 0);
|
|
391
406
|
for (let i = 0; i < comp.props.length; i++) {
|
|
392
407
|
const prop = comp.props[i];
|
|
393
408
|
insertProp.run(sessionId, componentId, prop.name, prop.type, prop.required ? 1 : 0, prop.category ?? null, prop.defaultValue ?? null, prop.description ?? null, prop.tokenReference ?? null, i);
|
|
@@ -467,7 +482,7 @@ export function storeRawComponents(db, sessionId, components, options) {
|
|
|
467
482
|
}
|
|
468
483
|
export function loadRawComponents(db, sessionId, allowedNames) {
|
|
469
484
|
const all = db
|
|
470
|
-
.prepare('SELECT component_id, name, source, framework FROM raw_components WHERE session_id = ? ORDER BY rowid')
|
|
485
|
+
.prepare('SELECT component_id, name, source, framework, extraction_confidence, review_reasons, needs_review FROM raw_components WHERE session_id = ? ORDER BY rowid')
|
|
471
486
|
.all(sessionId);
|
|
472
487
|
const components = allowedNames ? all.filter((c) => allowedNames.has(c.name)) : all;
|
|
473
488
|
if (components.length === 0)
|
|
@@ -498,6 +513,16 @@ export function loadRawComponents(db, sessionId, allowedNames) {
|
|
|
498
513
|
name: c.name,
|
|
499
514
|
source: c.source,
|
|
500
515
|
framework: c.framework,
|
|
516
|
+
extractionConfidence: c.extraction_confidence ?? null,
|
|
517
|
+
reviewReasons: (() => {
|
|
518
|
+
try {
|
|
519
|
+
return JSON.parse(c.review_reasons ?? '[]');
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
})(),
|
|
525
|
+
needsReview: Boolean(c.needs_review),
|
|
501
526
|
props: (propsByComponent.get(c.component_id) ?? []).map((p) => {
|
|
502
527
|
const av = allowedValuesByProp.get(`${c.component_id}::${p.name}`);
|
|
503
528
|
const prop = {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface RawComponentDefinition {
|
|
|
34
34
|
* non-authorable context-provider components.
|
|
35
35
|
*/
|
|
36
36
|
usesCreateContext?: boolean;
|
|
37
|
+
extractionConfidence?: number | null;
|
|
38
|
+
reviewReasons?: string[];
|
|
39
|
+
needsReview?: boolean;
|
|
37
40
|
}
|
|
38
41
|
export interface ComponentExtractionResult {
|
|
39
42
|
components: RawComponentDefinition[];
|
|
@@ -52,3 +55,5 @@ export interface TokenExtractor {
|
|
|
52
55
|
name: string;
|
|
53
56
|
extract(projectRoot: string): Promise<RawTokenDefinition[]>;
|
|
54
57
|
}
|
|
58
|
+
/** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
|
|
59
|
+
export declare function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }: RawComponentDefinition): Omit<RawComponentDefinition, 'extractionConfidence' | 'reviewReasons' | 'needsReview'>;
|
package/dist/src/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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.
|
|
39
|
+
"@contentful/experience-design-system-types": "2.7.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@tsconfig/node24": "^24.0.3",
|
|
@@ -70,15 +70,21 @@ Emit one JSON object on a single line. Lines not starting with `{` are ignored b
|
|
|
70
70
|
**Two tool calls — emit exactly one:**
|
|
71
71
|
|
|
72
72
|
```
|
|
73
|
-
{"tool":"select_component","name":"<ComponentName>","reason":"<brief reason>"}
|
|
73
|
+
{"tool":"select_component","name":"<ComponentName>","reason":"<brief reason>","confidence":<1-5>}
|
|
74
74
|
|
|
75
|
-
{"tool":"reject_component","name":"<ComponentName>","reason":"<brief reason>"}
|
|
75
|
+
{"tool":"reject_component","name":"<ComponentName>","reason":"<brief reason>","confidence":<1-5>}
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
**Rules:**
|
|
79
79
|
- Emit exactly one JSON object, on one line. No multi-line JSON. No markdown fences.
|
|
80
80
|
- The `name` must match the component name in the input.
|
|
81
81
|
- `reason` is a brief phrase documenting your decision.
|
|
82
|
+
- `confidence` is your certainty (1–5) that the decision is correct:
|
|
83
|
+
- **5** — obvious case, no doubt (clear UI atom, or clear infrastructure with no visual output)
|
|
84
|
+
- **4** — likely correct, minor ambiguity
|
|
85
|
+
- **3** — uncertain, borderline component (few props, ambiguous purpose, could go either way)
|
|
86
|
+
- **2** — low confidence, guessing
|
|
87
|
+
- **1** — very unsure, human review strongly recommended
|
|
82
88
|
- Emit prose lines (not starting with `{`) to log your reasoning before the final tool call.
|
|
83
89
|
|
|
84
90
|
---
|