@contentful/experience-design-system-cli 2.7.3-dev-build-2a7c81e.0 → 2.7.4-dev-build-595febb.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 -2
- package/dist/src/analyze/extract/source-inspection.js +9 -6
- package/dist/src/analyze/select/tui/components/TopBar.js +6 -1
- package/dist/src/analyze/select/types.d.ts +0 -3
- package/dist/src/analyze/select/types.js +0 -1
- package/dist/src/analyze/select-agent/command.js +72 -90
- package/dist/src/analyze/select-agent/context-builder.d.ts +7 -2
- package/dist/src/analyze/select-agent/context-builder.js +5 -61
- package/dist/src/session/db.d.ts +2 -0
- package/dist/src/session/db.js +26 -0
- package/package.json +2 -2
- package/dist/src/analyze/select-agent/consensus.d.ts +0 -32
- package/dist/src/analyze/select-agent/consensus.js +0 -60
package/dist/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { render } from 'ink';
|
|
3
3
|
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
4
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { extractComponents } from './extract/pipeline.js';
|
|
6
6
|
import { AnalyzeView } from './tui/AnalyzeView.js';
|
|
7
7
|
import { registerAnalyzeEditCommand } from './select/command.js';
|
|
8
8
|
import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
|
|
9
|
-
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents } from '../session/db.js';
|
|
9
|
+
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, } from '../session/db.js';
|
|
10
10
|
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';
|
|
@@ -169,6 +169,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
storeRawComponents(db, sessionId, filteredComponents);
|
|
172
|
+
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
172
173
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
173
174
|
db.close();
|
|
174
175
|
const allWarnings = [...extraction.warnings, ...filterWarnings];
|
|
@@ -103,18 +103,22 @@ export async function inspectComponentSource(component) {
|
|
|
103
103
|
}
|
|
104
104
|
const reviewReasons = [];
|
|
105
105
|
let wrapperScore = 0;
|
|
106
|
+
const hasGeneratedQueryHook = GENERATED_IMPORT_PATTERN.test(sourceText) && GENERATED_QUERY_HOOK_PATTERN.test(sourceText);
|
|
107
|
+
const infraProps = infraPropNames(component);
|
|
108
|
+
const siblingImports = collectSiblingRendererImports(sourceText);
|
|
106
109
|
if (GQL_FILENAME_PATTERN.test(component.source)) {
|
|
107
110
|
reviewReasons.push('data-wrapper:gql-filename');
|
|
108
111
|
wrapperScore += 1;
|
|
109
112
|
}
|
|
110
|
-
if (
|
|
113
|
+
if (hasGeneratedQueryHook) {
|
|
111
114
|
reviewReasons.push('data-wrapper:generated-query-hook');
|
|
112
115
|
wrapperScore += 3;
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
// Require corroboration from a stronger signal before counting sibling imports —
|
|
118
|
+
// otherwise any composed component that imports two sub-components scores +1 here.
|
|
119
|
+
if (siblingImports.length > 0 && (hasGeneratedQueryHook || infraProps.length > 0)) {
|
|
116
120
|
reviewReasons.push('data-wrapper:sibling-renderer-import');
|
|
117
|
-
wrapperScore +=
|
|
121
|
+
wrapperScore += 1;
|
|
118
122
|
}
|
|
119
123
|
if (hasSiblingForwardRender(sourceText, siblingImports)) {
|
|
120
124
|
reviewReasons.push('data-wrapper:fetch-forward-render');
|
|
@@ -124,12 +128,11 @@ export async function inspectComponentSource(component) {
|
|
|
124
128
|
reviewReasons.push('data-wrapper:contentful-runtime');
|
|
125
129
|
wrapperScore += 1;
|
|
126
130
|
}
|
|
127
|
-
const infraProps = infraPropNames(component);
|
|
128
131
|
if (infraProps.length > 0) {
|
|
129
132
|
reviewReasons.push('data-wrapper:infra-props');
|
|
130
133
|
wrapperScore += 2;
|
|
131
134
|
}
|
|
132
|
-
if (LOADING_NULL_GUARD_PATTERN.test(sourceText)
|
|
135
|
+
if (LOADING_NULL_GUARD_PATTERN.test(sourceText)) {
|
|
133
136
|
reviewReasons.push('data-wrapper:loading-null-guard');
|
|
134
137
|
wrapperScore += 1;
|
|
135
138
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
4
|
+
// createRequire is needed because this is an ESM package — require() doesn't
|
|
5
|
+
// exist natively, but it's the simplest way to read a JSON file at runtime.
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
const VERSION = _require('../../../../../package.json').version;
|
|
3
8
|
export function TopBar({ subcommand, hints }) {
|
|
4
|
-
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: 'experience-design-system-cli ' + subcommand }),
|
|
9
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: 'experience-design-system-cli ' + subcommand }), _jsxs(Text, { dimColor: true, children: [hints.map((h) => `[${h.key}] ${h.label}`).join(' '), ' v' + VERSION] })] }));
|
|
5
10
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
-
import type { SelectionAudit } from '../select-agent/consensus.js';
|
|
3
2
|
export type PreviewAnnotation = 'new' | 'changed' | 'removed' | 'breaking';
|
|
4
3
|
export type ReviewComponentStatus = 'needs-review' | 'reviewed' | 'accepted' | 'rejected';
|
|
5
4
|
export type ReviewComponentRecord = {
|
|
@@ -10,7 +9,6 @@ export type ReviewComponentRecord = {
|
|
|
10
9
|
originalProposal: RawComponentDefinition;
|
|
11
10
|
editedProposal: RawComponentDefinition;
|
|
12
11
|
status: ReviewComponentStatus;
|
|
13
|
-
selectionAudit?: SelectionAudit;
|
|
14
12
|
};
|
|
15
13
|
export type ReviewComponentDetail = {
|
|
16
14
|
id: string;
|
|
@@ -18,7 +16,6 @@ export type ReviewComponentDetail = {
|
|
|
18
16
|
originalProposal: RawComponentDefinition;
|
|
19
17
|
editedProposal: RawComponentDefinition;
|
|
20
18
|
status: ReviewComponentStatus;
|
|
21
|
-
selectionAudit?: SelectionAudit;
|
|
22
19
|
};
|
|
23
20
|
export type ReviewComponentSummary = {
|
|
24
21
|
id: string;
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { openPipelineDb, loadRawComponents, createStep, updateStep } from '../../session/db.js';
|
|
1
|
+
import { openPipelineDb, loadRawComponents, loadScannedFiles, createStep, updateStep } from '../../session/db.js';
|
|
2
2
|
import { appendReviewEvent, getRefineArtifactsRoot, ensureRefineSession, getRefineSessionPaths, saveReviewState, } from '../select/persistence.js';
|
|
3
3
|
import { loadReviewInput } from '../select/parser.js';
|
|
4
4
|
import { buildPrompt } from '../../generate/prompt-builder.js';
|
|
5
5
|
import { parseSelectToolCallLines, runAgent } from '../../generate/agent-runner.js';
|
|
6
6
|
import { OutputFormatter, c } from '../../output/format.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { resolve } from 'node:path';
|
|
7
|
+
import { buildRepoContextIndex, buildSelectionContext } from './context-builder.js';
|
|
8
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
10
9
|
const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
11
10
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
|
|
12
11
|
const DEFAULT_CONCURRENCY = 5;
|
|
13
|
-
const REVIEW_VOTE_COUNT = Math.max(1, Number(process.env.EDS_SELECT_VOTE_COUNT ?? DEFAULT_REVIEW_VOTE_COUNT));
|
|
14
|
-
const SINGLE_PASS_VOTE_COUNT = 1;
|
|
15
12
|
function resolveSessionId(sessionFlag) {
|
|
16
13
|
if (sessionFlag)
|
|
17
14
|
return sessionFlag;
|
|
@@ -60,15 +57,6 @@ function buildComponentData(candidate) {
|
|
|
60
57
|
}
|
|
61
58
|
return payload;
|
|
62
59
|
}
|
|
63
|
-
function getSelectionVoteCount(component) {
|
|
64
|
-
if (component.needsReview === true) {
|
|
65
|
-
return REVIEW_VOTE_COUNT;
|
|
66
|
-
}
|
|
67
|
-
if (typeof component.extractionConfidence === 'number' && component.extractionConfidence <= 3) {
|
|
68
|
-
return REVIEW_VOTE_COUNT;
|
|
69
|
-
}
|
|
70
|
-
return SINGLE_PASS_VOTE_COUNT;
|
|
71
|
-
}
|
|
72
60
|
function resolveProjectRoot(sessionId, projectRootFlag) {
|
|
73
61
|
if (projectRootFlag)
|
|
74
62
|
return resolve(projectRootFlag);
|
|
@@ -94,8 +82,7 @@ function resolveProjectRoot(sessionId, projectRootFlag) {
|
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
async function selectOneComponent(agent, model, candidate, index, total, verbose) {
|
|
97
|
-
const { component
|
|
98
|
-
const voteCount = getSelectionVoteCount(component);
|
|
85
|
+
const { component } = candidate;
|
|
99
86
|
const prompt = await buildPrompt({
|
|
100
87
|
skill: 'select',
|
|
101
88
|
mode: 'autonomous',
|
|
@@ -103,71 +90,65 @@ async function selectOneComponent(agent, model, candidate, index, total, verbose
|
|
|
103
90
|
outDir: process.cwd(),
|
|
104
91
|
});
|
|
105
92
|
const pos = c.dim(`[${index + 1}/${total}]`);
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!call) {
|
|
145
|
-
votes.push({
|
|
146
|
-
attempt,
|
|
147
|
-
decision: null,
|
|
148
|
-
error: 'agent produced no tool call for this component',
|
|
149
|
-
});
|
|
150
|
-
continue;
|
|
93
|
+
let outputBuf = '';
|
|
94
|
+
const formatter = new OutputFormatter(verbose, (s) => {
|
|
95
|
+
outputBuf += s;
|
|
96
|
+
});
|
|
97
|
+
const result = await runAgent({
|
|
98
|
+
agent,
|
|
99
|
+
model,
|
|
100
|
+
prompt,
|
|
101
|
+
interactive: false,
|
|
102
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
103
|
+
onOutput: (chunk) => formatter.push(chunk),
|
|
104
|
+
});
|
|
105
|
+
formatter.flush();
|
|
106
|
+
if (verbose) {
|
|
107
|
+
process.stderr.write(` ${pos} ${c.bold(component.name)}\n${outputBuf}`);
|
|
108
|
+
}
|
|
109
|
+
if (result.timedOut) {
|
|
110
|
+
process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('timed out')}\n`);
|
|
111
|
+
return {
|
|
112
|
+
componentKey: componentKey(component),
|
|
113
|
+
componentName: component.name,
|
|
114
|
+
decision: null,
|
|
115
|
+
failed: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (result.exitCode !== 0) {
|
|
119
|
+
process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.red(`agent exited with code ${result.exitCode}`)}\n`);
|
|
120
|
+
return {
|
|
121
|
+
componentKey: componentKey(component),
|
|
122
|
+
componentName: component.name,
|
|
123
|
+
decision: null,
|
|
124
|
+
failed: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const { calls, warnings } = parseSelectToolCallLines(result.stdout);
|
|
128
|
+
if (warnings.length > 0) {
|
|
129
|
+
for (const warning of warnings) {
|
|
130
|
+
process.stderr.write(` ${c.yellow('⚠')} ${component.name}: ${warning}\n`);
|
|
151
131
|
}
|
|
152
|
-
votes.push({
|
|
153
|
-
attempt,
|
|
154
|
-
decision: call.tool === 'select_component' ? 'accepted' : 'rejected',
|
|
155
|
-
reason: call.reason,
|
|
156
|
-
confidence: call.confidence,
|
|
157
|
-
});
|
|
158
132
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
133
|
+
const call = calls.find((toolCall) => toolCall.name === component.name) ?? calls[0];
|
|
134
|
+
if (!call) {
|
|
135
|
+
process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.yellow('no tool call')}\n`);
|
|
136
|
+
return {
|
|
137
|
+
componentKey: componentKey(component),
|
|
138
|
+
componentName: component.name,
|
|
139
|
+
decision: null,
|
|
140
|
+
failed: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const decision = call.tool === 'select_component' ? 'accepted' : 'rejected';
|
|
144
|
+
const finalColor = decision === 'accepted' ? c.green : c.red;
|
|
145
|
+
process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(decision)}${call.reason ? ` ${c.dim(call.reason)}` : ''}\n`);
|
|
164
146
|
return {
|
|
165
147
|
componentKey: componentKey(component),
|
|
166
148
|
componentName: component.name,
|
|
167
|
-
decision
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
failed: consensus.failed,
|
|
149
|
+
decision,
|
|
150
|
+
reason: call.reason,
|
|
151
|
+
failed: false,
|
|
171
152
|
};
|
|
172
153
|
}
|
|
173
154
|
async function selectAllComponents(agent, model, components, verbose) {
|
|
@@ -207,8 +188,10 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
207
188
|
const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
|
|
208
189
|
const db = openPipelineDb();
|
|
209
190
|
let rawComponents;
|
|
191
|
+
let scannedFiles = [];
|
|
210
192
|
try {
|
|
211
193
|
rawComponents = loadRawComponents(db, sessionId);
|
|
194
|
+
scannedFiles = loadScannedFiles(db, sessionId);
|
|
212
195
|
}
|
|
213
196
|
finally {
|
|
214
197
|
db.close();
|
|
@@ -218,9 +201,16 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
218
201
|
process.exit(1);
|
|
219
202
|
return;
|
|
220
203
|
}
|
|
204
|
+
if (selectionRoot && scannedFiles.length > 0) {
|
|
205
|
+
scannedFiles = scannedFiles.map((f) => (isAbsolute(f) ? f : resolve(selectionRoot, f)));
|
|
206
|
+
}
|
|
207
|
+
if (selectionRoot && scannedFiles.length === 0 && rawComponents.length > 0) {
|
|
208
|
+
process.stderr.write('warn: session has no scanned-files index (likely extracted on an older CLI version). ' +
|
|
209
|
+
'Re-run `analyze extract` to enable data-fetch wrapper detection during selection.\n');
|
|
210
|
+
}
|
|
221
211
|
let selectionCandidates = rawComponents.map((component) => ({ component }));
|
|
222
|
-
if (selectionRoot) {
|
|
223
|
-
const repoIndex = await buildRepoContextIndex(selectionRoot).catch(() => null);
|
|
212
|
+
if (selectionRoot && scannedFiles.length > 0) {
|
|
213
|
+
const repoIndex = await buildRepoContextIndex(selectionRoot, scannedFiles).catch(() => null);
|
|
224
214
|
if (repoIndex) {
|
|
225
215
|
selectionCandidates = rawComponents.map((component) => ({
|
|
226
216
|
component,
|
|
@@ -241,14 +231,10 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
241
231
|
return;
|
|
242
232
|
}
|
|
243
233
|
const selectResults = await selectAllComponents(agent, opts.model, selectionCandidates, opts.verbose ?? false);
|
|
244
|
-
// Build decision map from results
|
|
245
234
|
const decisions = new Map();
|
|
246
|
-
const audits = new Map();
|
|
247
235
|
for (const r of selectResults) {
|
|
248
236
|
decisions.set(r.componentKey, r.decision);
|
|
249
|
-
audits.set(r.componentKey, r.audit);
|
|
250
237
|
}
|
|
251
|
-
// Load existing snapshot and apply decisions
|
|
252
238
|
const artifactsRoot = getRefineArtifactsRoot();
|
|
253
239
|
let snapshot;
|
|
254
240
|
try {
|
|
@@ -268,23 +254,20 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
268
254
|
components: snapshot.components.map((comp) => {
|
|
269
255
|
const key = componentKey(comp.originalProposal);
|
|
270
256
|
const decision = decisions.get(key);
|
|
271
|
-
const selectionAudit = audits.get(key);
|
|
272
257
|
if (!decision)
|
|
273
258
|
return comp;
|
|
274
|
-
|
|
275
|
-
return { ...comp, status, selectionAudit };
|
|
259
|
+
return { ...comp, status: decision };
|
|
276
260
|
}),
|
|
277
261
|
};
|
|
278
262
|
await saveReviewState(paths.statePath, updated);
|
|
279
263
|
await Promise.all(updated.components
|
|
280
|
-
.filter((component) => component.
|
|
264
|
+
.filter((component) => component.status === 'accepted' || component.status === 'rejected')
|
|
281
265
|
.map((component) => appendReviewEvent(paths.eventsPath, {
|
|
282
266
|
type: 'select_agent_decision',
|
|
283
267
|
payload: {
|
|
284
268
|
component: component.name,
|
|
285
269
|
source: component.originalProposal.source,
|
|
286
270
|
status: component.status,
|
|
287
|
-
selectionAudit: component.selectionAudit,
|
|
288
271
|
},
|
|
289
272
|
})));
|
|
290
273
|
const accepted = updated.components.filter((comp) => comp.status === 'accepted');
|
|
@@ -294,10 +277,9 @@ export function registerAnalyzeSelectAgentCommand(program) {
|
|
|
294
277
|
if (failed.length > 0) {
|
|
295
278
|
process.stderr.write(c.red(`Failed (${failed.length}/${selectResults.length}):`) + '\n');
|
|
296
279
|
for (const f of failed) {
|
|
297
|
-
process.stderr.write(` ${c.red('✗')} ${f.componentName}
|
|
280
|
+
process.stderr.write(` ${c.red('✗')} ${f.componentName}\n`);
|
|
298
281
|
}
|
|
299
282
|
}
|
|
300
|
-
// Write step to DB
|
|
301
283
|
const stepDb = openPipelineDb();
|
|
302
284
|
const stepId = createStep(stepDb, sessionId, 'analyze select', {
|
|
303
285
|
sessionId,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
-
|
|
2
|
+
export type SelectionContextSummary = {
|
|
3
|
+
boundaryRoot: string;
|
|
4
|
+
siblingFileCount: number;
|
|
5
|
+
resolverReferenceCount: number;
|
|
6
|
+
hasParentUsageSite: boolean;
|
|
7
|
+
};
|
|
3
8
|
export type SelectionImportSummary = {
|
|
4
9
|
source: string;
|
|
5
10
|
names: string[];
|
|
@@ -39,7 +44,7 @@ export type RepoContextIndex = {
|
|
|
39
44
|
byDirectory: Map<string, IndexedFile[]>;
|
|
40
45
|
filePaths: Set<string>;
|
|
41
46
|
};
|
|
42
|
-
export declare function buildRepoContextIndex(root: string): Promise<RepoContextIndex | null>;
|
|
47
|
+
export declare function buildRepoContextIndex(root: string, filePaths: string[]): Promise<RepoContextIndex | null>;
|
|
43
48
|
export declare function buildSelectionContext(index: RepoContextIndex, component: RawComponentDefinition): SelectionContext | undefined;
|
|
44
49
|
export declare function summarizeSelectionContext(context: SelectionContext | undefined): SelectionContextSummary | undefined;
|
|
45
50
|
export {};
|
|
@@ -1,35 +1,6 @@
|
|
|
1
|
-
import { readFile
|
|
2
|
-
import { dirname,
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
4
|
-
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
5
|
-
'.git',
|
|
6
|
-
'.next',
|
|
7
|
-
'.nuxt',
|
|
8
|
-
'build',
|
|
9
|
-
'coverage',
|
|
10
|
-
'demo',
|
|
11
|
-
'demos',
|
|
12
|
-
'dist',
|
|
13
|
-
'example',
|
|
14
|
-
'examples',
|
|
15
|
-
'node_modules',
|
|
16
|
-
'out',
|
|
17
|
-
'storybook-static',
|
|
18
|
-
]);
|
|
19
|
-
const IGNORED_FILE_SUFFIXES = new Set([
|
|
20
|
-
'.stories.ts',
|
|
21
|
-
'.stories.tsx',
|
|
22
|
-
'.stories.js',
|
|
23
|
-
'.stories.jsx',
|
|
24
|
-
'.story.ts',
|
|
25
|
-
'.story.tsx',
|
|
26
|
-
'.story.js',
|
|
27
|
-
'.story.jsx',
|
|
28
|
-
'.spec.ts',
|
|
29
|
-
'.spec.tsx',
|
|
30
|
-
'.test.ts',
|
|
31
|
-
'.test.tsx',
|
|
32
|
-
]);
|
|
33
4
|
const IMPORT_PATTERN = /import\s+(?:type\s+)?(.+?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
34
5
|
const EXPORT_NAMED_PATTERN = /export\s+(?:const|function|class|type|interface|enum)\s+([A-Za-z0-9_]+)/g;
|
|
35
6
|
const EXPORT_DEFAULT_PATTERN = /export\s+default\s+([A-Za-z0-9_]+)/g;
|
|
@@ -49,31 +20,6 @@ function isWithinRoot(path, root) {
|
|
|
49
20
|
const relativePath = relative(root, path);
|
|
50
21
|
return relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
51
22
|
}
|
|
52
|
-
async function collectSourceFiles(directory) {
|
|
53
|
-
const files = [];
|
|
54
|
-
async function visit(currentDirectory) {
|
|
55
|
-
const entries = await readdir(currentDirectory, { withFileTypes: true });
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
const fullPath = join(currentDirectory, entry.name);
|
|
58
|
-
if (entry.isDirectory()) {
|
|
59
|
-
if (!IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
60
|
-
await visit(fullPath);
|
|
61
|
-
}
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (!entry.isFile())
|
|
65
|
-
continue;
|
|
66
|
-
const extension = extname(entry.name);
|
|
67
|
-
if (!SCANNED_FILE_EXTENSIONS.has(extension) || entry.name.endsWith('.d.ts'))
|
|
68
|
-
continue;
|
|
69
|
-
if ([...IGNORED_FILE_SUFFIXES].some((suffix) => entry.name.endsWith(suffix)))
|
|
70
|
-
continue;
|
|
71
|
-
files.push(fullPath);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
await visit(directory);
|
|
75
|
-
return files.sort();
|
|
76
|
-
}
|
|
77
23
|
function parseImportedNames(importClause) {
|
|
78
24
|
const names = [];
|
|
79
25
|
const parts = importClause.split(',').map((part) => part.trim());
|
|
@@ -195,12 +141,10 @@ function findParentUsageSite(root, files, componentName, currentFile) {
|
|
|
195
141
|
snippet: extractSnippet(match.text, componentName, MAX_REFERENCE_CHARS),
|
|
196
142
|
};
|
|
197
143
|
}
|
|
198
|
-
export async function buildRepoContextIndex(root) {
|
|
199
|
-
|
|
200
|
-
const rootStats = await stat(resolvedRoot).catch(() => null);
|
|
201
|
-
if (!rootStats?.isDirectory())
|
|
144
|
+
export async function buildRepoContextIndex(root, filePaths) {
|
|
145
|
+
if (filePaths.length === 0)
|
|
202
146
|
return null;
|
|
203
|
-
const
|
|
147
|
+
const resolvedRoot = resolve(root);
|
|
204
148
|
const files = await Promise.all(filePaths.map(async (absolutePath) => ({
|
|
205
149
|
absolutePath,
|
|
206
150
|
relativePath: relative(resolvedRoot, absolutePath),
|
package/dist/src/session/db.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export declare function computeTokenInputHash(rawTokenContent: string): string;
|
|
|
106
106
|
export declare function lookupCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string): CacheEntry | null;
|
|
107
107
|
export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): CacheEntry | null;
|
|
108
108
|
export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean): void;
|
|
109
|
+
export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
|
|
110
|
+
export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
|
|
109
111
|
export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
|
|
110
112
|
export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
|
|
111
113
|
export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;
|
package/dist/src/session/db.js
CHANGED
|
@@ -127,6 +127,12 @@ CREATE TABLE IF NOT EXISTS generation_cache (
|
|
|
127
127
|
PRIMARY KEY (input_hash, entity_type, entity_id)
|
|
128
128
|
);
|
|
129
129
|
|
|
130
|
+
CREATE TABLE IF NOT EXISTS scanned_files (
|
|
131
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
132
|
+
path TEXT NOT NULL,
|
|
133
|
+
PRIMARY KEY (session_id, path)
|
|
134
|
+
);
|
|
135
|
+
|
|
130
136
|
CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
|
|
131
137
|
CREATE INDEX IF NOT EXISTS idx_steps_command ON steps(session_id, command);
|
|
132
138
|
CREATE INDEX IF NOT EXISTS idx_raw_components_session ON raw_components(session_id);
|
|
@@ -136,6 +142,7 @@ CREATE INDEX IF NOT EXISTS idx_raw_tokens_session ON raw_tokens(session_id
|
|
|
136
142
|
CREATE INDEX IF NOT EXISTS idx_raw_token_groups_session ON raw_token_groups(session_id);
|
|
137
143
|
CREATE INDEX IF NOT EXISTS idx_generation_cache_entity ON generation_cache(entity_type, entity_id);
|
|
138
144
|
CREATE INDEX IF NOT EXISTS idx_generation_cache_session ON generation_cache(source_session_id);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_scanned_files_session ON scanned_files(session_id);
|
|
139
146
|
`;
|
|
140
147
|
export function getPipelineDbPath() {
|
|
141
148
|
if (process.env.EDS_PIPELINE_DB_PATH) {
|
|
@@ -1067,6 +1074,25 @@ export function storeCache(db, inputHash, entityType, entityId, sourceSessionId,
|
|
|
1067
1074
|
human_edited = CASE WHEN generation_cache.human_edited = 1 THEN 1 ELSE excluded.human_edited END,
|
|
1068
1075
|
updated_at = excluded.updated_at`).run(inputHash, entityType, entityId, sourceSessionId, humanEdited ? 1 : 0, now, now);
|
|
1069
1076
|
}
|
|
1077
|
+
export function storeScannedFiles(db, sessionId, filePaths) {
|
|
1078
|
+
db.exec('BEGIN');
|
|
1079
|
+
try {
|
|
1080
|
+
db.prepare('DELETE FROM scanned_files WHERE session_id = ?').run(sessionId);
|
|
1081
|
+
const insert = db.prepare('INSERT INTO scanned_files (session_id, path) VALUES (?, ?)');
|
|
1082
|
+
for (const path of filePaths) {
|
|
1083
|
+
insert.run(sessionId, path);
|
|
1084
|
+
}
|
|
1085
|
+
db.exec('COMMIT');
|
|
1086
|
+
}
|
|
1087
|
+
catch (e) {
|
|
1088
|
+
db.exec('ROLLBACK');
|
|
1089
|
+
throw e;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
export function loadScannedFiles(db, sessionId) {
|
|
1093
|
+
const rows = db.prepare('SELECT path FROM scanned_files WHERE session_id = ? ORDER BY path').all(sessionId);
|
|
1094
|
+
return rows.map((r) => r.path);
|
|
1095
|
+
}
|
|
1070
1096
|
export function markCacheHumanEdited(db, entityType, entityId) {
|
|
1071
1097
|
const now = new Date().toISOString();
|
|
1072
1098
|
db.prepare(`UPDATE generation_cache SET human_edited = 1, updated_at = ? WHERE entity_type = ? AND entity_id = ?`).run(now, entityType, entityId);
|
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.4-dev-build-595febb.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.4-dev-build-595febb.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@tsconfig/node24": "^24.0.3",
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export type SelectionDecision = 'accepted' | 'rejected' | 'needs-review';
|
|
2
|
-
export type SelectionVote = {
|
|
3
|
-
attempt: number;
|
|
4
|
-
decision: 'accepted' | 'rejected' | null;
|
|
5
|
-
reason?: string;
|
|
6
|
-
confidence?: number;
|
|
7
|
-
error?: string;
|
|
8
|
-
};
|
|
9
|
-
export type SelectionContextSummary = {
|
|
10
|
-
boundaryRoot: string;
|
|
11
|
-
siblingFileCount: number;
|
|
12
|
-
resolverReferenceCount: number;
|
|
13
|
-
hasParentUsageSite: boolean;
|
|
14
|
-
};
|
|
15
|
-
export type SelectionAudit = {
|
|
16
|
-
strategy: 'single-pass' | 'multi-vote-consensus';
|
|
17
|
-
voteCount: number;
|
|
18
|
-
acceptedVotes: number;
|
|
19
|
-
rejectedVotes: number;
|
|
20
|
-
failedVotes: number;
|
|
21
|
-
finalDecision: SelectionDecision;
|
|
22
|
-
winningReason?: string;
|
|
23
|
-
votes: SelectionVote[];
|
|
24
|
-
contextSummary?: SelectionContextSummary;
|
|
25
|
-
};
|
|
26
|
-
export type ConsensusResult = {
|
|
27
|
-
decision: SelectionDecision;
|
|
28
|
-
audit: SelectionAudit;
|
|
29
|
-
failed: boolean;
|
|
30
|
-
};
|
|
31
|
-
export declare const DEFAULT_REVIEW_VOTE_COUNT = 5;
|
|
32
|
-
export declare function summarizeSelectionVotes(votes: SelectionVote[], contextSummary?: SelectionContextSummary): ConsensusResult;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_REVIEW_VOTE_COUNT = 5;
|
|
2
|
-
function preferredReason(votes, decision) {
|
|
3
|
-
const candidates = votes.filter((vote) => vote.decision === decision && vote.reason);
|
|
4
|
-
if (candidates.length === 0)
|
|
5
|
-
return undefined;
|
|
6
|
-
const ranked = new Map();
|
|
7
|
-
for (const vote of candidates) {
|
|
8
|
-
const reason = vote.reason;
|
|
9
|
-
const entry = ranked.get(reason) ?? { count: 0, bestConfidence: 0 };
|
|
10
|
-
entry.count += 1;
|
|
11
|
-
entry.bestConfidence = Math.max(entry.bestConfidence, vote.confidence ?? 0);
|
|
12
|
-
ranked.set(reason, entry);
|
|
13
|
-
}
|
|
14
|
-
return [...ranked.entries()].sort((a, b) => {
|
|
15
|
-
if (b[1].count !== a[1].count)
|
|
16
|
-
return b[1].count - a[1].count;
|
|
17
|
-
return b[1].bestConfidence - a[1].bestConfidence;
|
|
18
|
-
})[0]?.[0];
|
|
19
|
-
}
|
|
20
|
-
function decideFromVotes(votes, acceptedVotes, rejectedVotes) {
|
|
21
|
-
if (votes.length === 1) {
|
|
22
|
-
if (acceptedVotes === 1)
|
|
23
|
-
return 'accepted';
|
|
24
|
-
if (rejectedVotes === 1)
|
|
25
|
-
return 'rejected';
|
|
26
|
-
return 'needs-review';
|
|
27
|
-
}
|
|
28
|
-
// A 3-2 split is still too unstable for auto-selection.
|
|
29
|
-
if (votes.length >= DEFAULT_REVIEW_VOTE_COUNT &&
|
|
30
|
-
((acceptedVotes === 3 && rejectedVotes === 2) || (acceptedVotes === 2 && rejectedVotes === 3))) {
|
|
31
|
-
return 'needs-review';
|
|
32
|
-
}
|
|
33
|
-
if (acceptedVotes > rejectedVotes)
|
|
34
|
-
return 'accepted';
|
|
35
|
-
if (rejectedVotes > acceptedVotes)
|
|
36
|
-
return 'rejected';
|
|
37
|
-
return 'needs-review';
|
|
38
|
-
}
|
|
39
|
-
export function summarizeSelectionVotes(votes, contextSummary) {
|
|
40
|
-
const acceptedVotes = votes.filter((vote) => vote.decision === 'accepted').length;
|
|
41
|
-
const rejectedVotes = votes.filter((vote) => vote.decision === 'rejected').length;
|
|
42
|
-
const failedVotes = votes.filter((vote) => vote.decision === null).length;
|
|
43
|
-
const decision = decideFromVotes(votes, acceptedVotes, rejectedVotes);
|
|
44
|
-
const winningReason = decision === 'accepted' || decision === 'rejected' ? preferredReason(votes, decision) : undefined;
|
|
45
|
-
return {
|
|
46
|
-
decision,
|
|
47
|
-
failed: failedVotes === votes.length,
|
|
48
|
-
audit: {
|
|
49
|
-
strategy: votes.length === 1 ? 'single-pass' : 'multi-vote-consensus',
|
|
50
|
-
voteCount: votes.length,
|
|
51
|
-
acceptedVotes,
|
|
52
|
-
rejectedVotes,
|
|
53
|
-
failedVotes,
|
|
54
|
-
finalDecision: decision,
|
|
55
|
-
winningReason,
|
|
56
|
-
votes,
|
|
57
|
-
contextSummary,
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|