@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.3-dev-build-2a7c81e.0",
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",
@@ -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 (GENERATED_IMPORT_PATTERN.test(sourceText) && GENERATED_QUERY_HOOK_PATTERN.test(sourceText)) {
113
+ if (hasGeneratedQueryHook) {
111
114
  reviewReasons.push('data-wrapper:generated-query-hook');
112
115
  wrapperScore += 3;
113
116
  }
114
- const siblingImports = collectSiblingRendererImports(sourceText);
115
- if (siblingImports.length > 0) {
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 += 2;
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) || /return\s+null\b/.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 }), _jsx(Text, { dimColor: true, children: hints.map((h) => `[${h.key}] ${h.label}`).join(' ') })] }));
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;
@@ -17,7 +17,6 @@ export function createReviewSessionDetail(session) {
17
17
  originalProposal: component.originalProposal,
18
18
  editedProposal: component.editedProposal,
19
19
  status: component.status,
20
- selectionAudit: component.selectionAudit,
21
20
  })),
22
21
  };
23
22
  }
@@ -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 { DEFAULT_REVIEW_VOTE_COUNT, summarizeSelectionVotes, } from './consensus.js';
8
- import { buildRepoContextIndex, buildSelectionContext, summarizeSelectionContext, } from './context-builder.js';
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, selectionContext } = candidate;
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
- const votes = [];
107
- const contextSummary = summarizeSelectionContext(selectionContext);
108
- for (let attempt = 1; attempt <= voteCount; attempt++) {
109
- let outputBuf = '';
110
- const formatter = new OutputFormatter(verbose, (s) => {
111
- outputBuf += s;
112
- });
113
- const result = await runAgent({
114
- agent,
115
- model,
116
- prompt,
117
- interactive: false,
118
- timeoutMs: DEFAULT_TIMEOUT_MS,
119
- onOutput: (chunk) => formatter.push(chunk),
120
- });
121
- formatter.flush();
122
- if (verbose) {
123
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.dim(`vote ${attempt}/${voteCount}`)}\n${outputBuf}`);
124
- }
125
- if (result.timedOut) {
126
- votes.push({ attempt, decision: null, error: 'timed out' });
127
- continue;
128
- }
129
- if (result.exitCode !== 0) {
130
- votes.push({
131
- attempt,
132
- decision: null,
133
- error: `agent exited with code ${result.exitCode}`,
134
- });
135
- continue;
136
- }
137
- const { calls, warnings } = parseSelectToolCallLines(result.stdout);
138
- if (warnings.length > 0) {
139
- for (const warning of warnings) {
140
- process.stderr.write(` ${c.yellow('⚠')} ${component.name}: ${warning}\n`);
141
- }
142
- }
143
- const call = calls.find((toolCall) => toolCall.name === component.name) ?? calls[0];
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 consensus = summarizeSelectionVotes(votes, contextSummary);
160
- const finalColor = consensus.decision === 'accepted' ? c.green : consensus.decision === 'rejected' ? c.red : c.yellow;
161
- const finalLabel = consensus.decision === 'accepted' ? 'accepted' : consensus.decision === 'rejected' ? 'rejected' : 'needs review';
162
- const detail = consensus.audit.winningReason;
163
- process.stderr.write(` ${pos} ${c.bold(component.name)} ${finalColor(finalLabel)}${detail ? ` ${c.dim(detail)}` : ''}\n`);
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: consensus.decision,
168
- audit: consensus.audit,
169
- reason: consensus.audit.winningReason,
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
- const status = decision === 'needs-review' ? 'needs-review' : decision;
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.selectionAudit)
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} ${c.dim('all votes failed')}\n`);
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
- import type { SelectionContextSummary } from './consensus.js';
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, readdir, stat } from 'node:fs/promises';
2
- import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
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
- const resolvedRoot = resolve(root);
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 filePaths = await collectSourceFiles(resolvedRoot);
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),
@@ -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;
@@ -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-dev-build-2a7c81e.0",
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.3-dev-build-2a7c81e.0"
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
- }