@ryuenn3123/agentic-senior-core 3.0.14 → 3.0.16

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.
Files changed (37) hide show
  1. package/.agent-context/prompts/bootstrap-design.md +30 -16
  2. package/.agent-context/prompts/init-project.md +4 -0
  3. package/.agent-context/rules/architecture.md +13 -0
  4. package/.agent-context/rules/docker-runtime.md +12 -0
  5. package/.agent-context/rules/efficiency-vs-hype.md +17 -6
  6. package/.agent-context/rules/frontend-architecture.md +5 -0
  7. package/.agent-context/state/memory-continuity-benchmark.json +1 -1
  8. package/.agent-context/state/onboarding-report.json +0 -1
  9. package/.cursorrules +66 -29
  10. package/.gemini/instructions.md +1 -1
  11. package/.github/copilot-instructions.md +1 -1
  12. package/.instructions.md +4 -3
  13. package/.windsurfrules +66 -29
  14. package/AGENTS.md +1 -1
  15. package/lib/cli/architect.mjs +71 -784
  16. package/lib/cli/commands/init.mjs +32 -98
  17. package/lib/cli/commands/optimize.mjs +0 -4
  18. package/lib/cli/commands/upgrade.mjs +2 -5
  19. package/lib/cli/compiler.mjs +3 -11
  20. package/lib/cli/constants.mjs +3 -73
  21. package/lib/cli/detector/design-evidence.mjs +427 -0
  22. package/lib/cli/detector.mjs +13 -116
  23. package/lib/cli/init-options.mjs +0 -118
  24. package/lib/cli/project-scaffolder/constants.mjs +67 -0
  25. package/lib/cli/project-scaffolder/design-contract.mjs +554 -0
  26. package/lib/cli/project-scaffolder/discovery.mjs +315 -0
  27. package/lib/cli/project-scaffolder/prompt-builders.mjs +196 -0
  28. package/lib/cli/project-scaffolder/storage.mjs +154 -0
  29. package/lib/cli/project-scaffolder.mjs +32 -1160
  30. package/lib/cli/utils.mjs +2 -11
  31. package/package.json +1 -1
  32. package/scripts/frontend-usability-audit.mjs +53 -0
  33. package/scripts/validate/config.mjs +401 -0
  34. package/scripts/validate/coverage-checks.mjs +429 -0
  35. package/scripts/validate.mjs +44 -754
  36. package/lib/cli/init-architecture-flow.mjs +0 -233
  37. package/lib/cli/profile-packs.mjs +0 -108
@@ -0,0 +1,427 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const FRONTEND_SCAN_DIRECTORY_NAMES = ['src', 'app', 'pages', 'components', 'styles'];
5
+ const FRONTEND_SCAN_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass']);
6
+ export const FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage']);
7
+ const FRONTEND_FILE_SCAN_LIMIT = 200;
8
+ const FRONTEND_FILE_SIZE_LIMIT_BYTES = 200_000;
9
+ const DESIGN_EVIDENCE_SAMPLE_LIMIT = 12;
10
+ const COLOR_PATTERN = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)/g;
11
+ const PROP_DRILLING_PATTERN = /<[A-Z][A-Za-z0-9_.:-]*(?:\s+[A-Za-z0-9_:-]+=\{[^}]+\}){5,}/g;
12
+ const MEDIA_QUERY_PATTERN = /@media\b/g;
13
+ const TAILWIND_BREAKPOINT_PATTERN = /\b(?:sm|md|lg|xl|2xl):/g;
14
+ const ARBITRARY_BREAKPOINT_PATTERN = /\b(?:min|max)-\[[^\]]+\]:/g;
15
+ const CSS_VARIABLE_DEFINITION_PATTERN = /--([a-zA-Z0-9-_]+)\s*:/g;
16
+ const CSS_VARIABLE_REFERENCE_PATTERN = /var\(--([a-zA-Z0-9-_]+)\)/g;
17
+ const RAW_SPACING_PATTERN = /\b(?:margin|padding|gap|column-gap|row-gap|min-width|max-width|min-height|max-height|width|height|top|right|bottom|left|inset|spaceBetween|paddingInline|paddingBlock|marginInline|marginBlock|gapX|gapY|spaceX|spaceY)\b[^;\n:]*[:=]\s*['"`{(]*(-?[0-9.]+(?:px|rem|em|vh|vw))/gi;
18
+ const RAW_RADIUS_PATTERN = /\b(?:border-radius|borderRadius)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em)|9999px)/gi;
19
+ const RAW_SHADOW_PATTERN = /\b(?:box-shadow|boxShadow)\b[^;\n:]*[:=]\s*['"`{(]*([^;\n}]+)/gi;
20
+ const FONT_FAMILY_PATTERN = /\b(?:font-family|fontFamily)\b[^;\n:]*[:=]\s*['"`{(]*([^;\n}]+)/gi;
21
+ const FONT_SIZE_PATTERN = /\b(?:font-size|fontSize)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em))/gi;
22
+ const LINE_HEIGHT_PATTERN = /\b(?:line-height|lineHeight)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em)?)/gi;
23
+ const LETTER_SPACING_PATTERN = /\b(?:letter-spacing|letterSpacing)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.-]+(?:px|rem|em))/gi;
24
+ const TRANSITION_PATTERN = /\btransition(?:-[a-z]+)?\b/g;
25
+ const ANIMATION_PATTERN = /\banimation(?:-[a-z]+)?\b/g;
26
+ const DURATION_PATTERN = /\b\d+(?:\.\d+)?m?s\b/g;
27
+ const MEDIA_WIDTH_PATTERN = /\((?:min|max)-width:\s*([0-9.]+(?:px|rem|em))\)/g;
28
+ const TAILWIND_UTILITY_PATTERN = /\b([a-z]+(?:-[a-z]+)?)-[^\s"'`{}()<>]+/g;
29
+
30
+ async function collectFrontendSourceFilePaths(directoryPath, collectedFilePaths = []) {
31
+ if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
32
+ return collectedFilePaths;
33
+ }
34
+
35
+ let directoryEntries;
36
+ try {
37
+ directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
38
+ } catch {
39
+ return collectedFilePaths;
40
+ }
41
+
42
+ for (const directoryEntry of directoryEntries) {
43
+ if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
44
+ break;
45
+ }
46
+
47
+ if (directoryEntry.isDirectory()) {
48
+ if (FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES.has(directoryEntry.name)) {
49
+ continue;
50
+ }
51
+
52
+ await collectFrontendSourceFilePaths(path.join(directoryPath, directoryEntry.name), collectedFilePaths);
53
+ continue;
54
+ }
55
+
56
+ const fileExtension = path.extname(directoryEntry.name).toLowerCase();
57
+ if (FRONTEND_SCAN_FILE_EXTENSIONS.has(fileExtension)) {
58
+ collectedFilePaths.push(path.join(directoryPath, directoryEntry.name));
59
+ }
60
+ }
61
+
62
+ return collectedFilePaths;
63
+ }
64
+
65
+ function countPatternMatches(sourceText, pattern) {
66
+ return Array.from(sourceText.matchAll(pattern)).length;
67
+ }
68
+
69
+ function pushSampleValue(targetSamples, value, targetSet) {
70
+ const normalizedValue = String(value || '').trim();
71
+ if (!normalizedValue || targetSet.has(normalizedValue)) {
72
+ return;
73
+ }
74
+
75
+ targetSet.add(normalizedValue);
76
+ if (targetSamples.length < DESIGN_EVIDENCE_SAMPLE_LIMIT) {
77
+ targetSamples.push(normalizedValue);
78
+ }
79
+ }
80
+
81
+ function categorizeCssVariable(variableName) {
82
+ const normalizedVariableName = String(variableName || '').trim().toLowerCase();
83
+
84
+ if (/(color|surface|accent|bg|text|border|fill|stroke|ink|tone)/.test(normalizedVariableName)) {
85
+ return 'color';
86
+ }
87
+
88
+ if (/(space|gap|padding|margin|size|width|height|inset)/.test(normalizedVariableName)) {
89
+ return 'spacing';
90
+ }
91
+
92
+ if (/(radius|rounded|corner)/.test(normalizedVariableName)) {
93
+ return 'radius';
94
+ }
95
+
96
+ if (/(shadow|elevation)/.test(normalizedVariableName)) {
97
+ return 'shadow';
98
+ }
99
+
100
+ if (/(font|type|line|letter|tracking|leading)/.test(normalizedVariableName)) {
101
+ return 'typography';
102
+ }
103
+
104
+ if (/(motion|duration|easing|ease|animation|transition)/.test(normalizedVariableName)) {
105
+ return 'motion';
106
+ }
107
+
108
+ return 'other';
109
+ }
110
+
111
+ function inferColorKind(colorValue) {
112
+ if (/^#/i.test(colorValue)) {
113
+ return 'hex';
114
+ }
115
+
116
+ if (/^rgba?\(/i.test(colorValue)) {
117
+ return 'rgb';
118
+ }
119
+
120
+ if (/^hsla?\(/i.test(colorValue)) {
121
+ return 'hsl';
122
+ }
123
+
124
+ if (/^oklch\(/i.test(colorValue)) {
125
+ return 'oklch';
126
+ }
127
+
128
+ return 'other';
129
+ }
130
+
131
+ function incrementCountMap(countMap, key) {
132
+ countMap[key] = (countMap[key] || 0) + 1;
133
+ }
134
+
135
+ function createDesignEvidenceSummary(scanRootRelativePaths) {
136
+ return {
137
+ summaryVersion: 'v1',
138
+ source: 'lightweight-static-scan',
139
+ scanRootRelativePaths,
140
+ scannedFileCount: 0,
141
+ cssVariables: {
142
+ definitionCount: 0,
143
+ referenceCount: 0,
144
+ sampleNames: [],
145
+ categoryCounts: {
146
+ color: 0,
147
+ spacing: 0,
148
+ radius: 0,
149
+ shadow: 0,
150
+ typography: 0,
151
+ motion: 0,
152
+ other: 0,
153
+ },
154
+ },
155
+ colors: {
156
+ hardcodedCount: 0,
157
+ kindCounts: {
158
+ hex: 0,
159
+ rgb: 0,
160
+ hsl: 0,
161
+ oklch: 0,
162
+ other: 0,
163
+ },
164
+ sampleValues: [],
165
+ },
166
+ spacing: {
167
+ rawValueCount: 0,
168
+ sampleValues: [],
169
+ },
170
+ radius: {
171
+ rawValueCount: 0,
172
+ sampleValues: [],
173
+ },
174
+ shadow: {
175
+ rawValueCount: 0,
176
+ sampleValues: [],
177
+ },
178
+ typography: {
179
+ fontFamilyCount: 0,
180
+ fontSizeCount: 0,
181
+ lineHeightCount: 0,
182
+ letterSpacingCount: 0,
183
+ fontFamilySamples: [],
184
+ fontSizeSamples: [],
185
+ lineHeightSamples: [],
186
+ letterSpacingSamples: [],
187
+ },
188
+ motion: {
189
+ transitionCount: 0,
190
+ animationCount: 0,
191
+ durationCount: 0,
192
+ durationSamples: [],
193
+ },
194
+ tailwind: {
195
+ breakpointUsageCount: 0,
196
+ arbitraryBreakpointCount: 0,
197
+ utilityFamilyCounts: {},
198
+ utilityFamilySamples: [],
199
+ },
200
+ componentInventory: {
201
+ componentFileCount: 0,
202
+ pageFileCount: 0,
203
+ layoutFileCount: 0,
204
+ surfaceFileSamples: [],
205
+ },
206
+ tokenBypassSignals: {
207
+ hardcodedColorCount: 0,
208
+ rawSpacingCount: 0,
209
+ rawRadiusCount: 0,
210
+ rawShadowCount: 0,
211
+ },
212
+ };
213
+ }
214
+
215
+ function registerSurfaceFile(summary, targetDirectoryPath, scannedFilePath, seenSurfaceFiles) {
216
+ const relativeFilePath = path.relative(targetDirectoryPath, scannedFilePath).replace(/\\/g, '/');
217
+ const normalizedBaseName = path.basename(scannedFilePath, path.extname(scannedFilePath)).toLowerCase();
218
+ const looksLikeComponent = /[A-Z]/.test(path.basename(scannedFilePath, path.extname(scannedFilePath)))
219
+ || relativeFilePath.includes('/components/')
220
+ || relativeFilePath.startsWith('components/');
221
+ const looksLikePage = normalizedBaseName === 'page'
222
+ || normalizedBaseName === 'index'
223
+ || relativeFilePath.includes('/pages/')
224
+ || relativeFilePath.startsWith('pages/')
225
+ || relativeFilePath.includes('/app/');
226
+ const looksLikeLayout = normalizedBaseName === 'layout' || relativeFilePath.includes('/layouts/');
227
+
228
+ if (looksLikeComponent) {
229
+ summary.componentInventory.componentFileCount += 1;
230
+ }
231
+
232
+ if (looksLikePage) {
233
+ summary.componentInventory.pageFileCount += 1;
234
+ }
235
+
236
+ if (looksLikeLayout) {
237
+ summary.componentInventory.layoutFileCount += 1;
238
+ }
239
+
240
+ if ((looksLikeComponent || looksLikePage || looksLikeLayout) && !seenSurfaceFiles.has(relativeFilePath)) {
241
+ seenSurfaceFiles.add(relativeFilePath);
242
+ if (summary.componentInventory.surfaceFileSamples.length < DESIGN_EVIDENCE_SAMPLE_LIMIT) {
243
+ summary.componentInventory.surfaceFileSamples.push(relativeFilePath);
244
+ }
245
+ }
246
+ }
247
+
248
+ function collectSampleMatches(sourceText, pattern, targetSamples, targetSet, transform = (match) => match[1] || match[0]) {
249
+ for (const match of sourceText.matchAll(pattern)) {
250
+ pushSampleValue(targetSamples, transform(match), targetSet);
251
+ }
252
+ }
253
+
254
+ export async function collectFrontendDesignEvidence({
255
+ targetDirectoryPath,
256
+ markerNames,
257
+ scanRootDirectoryPaths = [],
258
+ }) {
259
+ const candidateDirectoryPaths = FRONTEND_SCAN_DIRECTORY_NAMES
260
+ .filter((directoryName) => markerNames.has(directoryName))
261
+ .map((directoryName) => path.join(targetDirectoryPath, directoryName));
262
+ const explicitScanRootDirectoryPaths = Array.isArray(scanRootDirectoryPaths)
263
+ ? scanRootDirectoryPaths.filter((scanRootDirectoryPath) => typeof scanRootDirectoryPath === 'string' && scanRootDirectoryPath.trim().length > 0)
264
+ : [];
265
+ const resolvedCandidateDirectoryPaths = explicitScanRootDirectoryPaths.length > 0
266
+ ? Array.from(new Set(explicitScanRootDirectoryPaths))
267
+ : candidateDirectoryPaths.length > 0
268
+ ? candidateDirectoryPaths
269
+ : [targetDirectoryPath];
270
+ const scannedFilePaths = [];
271
+ const scanRootRelativePaths = resolvedCandidateDirectoryPaths
272
+ .map((candidateDirectoryPath) => path.relative(targetDirectoryPath, candidateDirectoryPath).replace(/\\/g, '/') || '.');
273
+ const designEvidenceSummary = createDesignEvidenceSummary(scanRootRelativePaths);
274
+ const cssVariableSamples = new Set();
275
+ const colorSamples = new Set();
276
+ const spacingSamples = new Set();
277
+ const radiusSamples = new Set();
278
+ const shadowSamples = new Set();
279
+ const fontFamilySamples = new Set();
280
+ const fontSizeSamples = new Set();
281
+ const lineHeightSamples = new Set();
282
+ const letterSpacingSamples = new Set();
283
+ const durationSamples = new Set();
284
+ const utilityFamilySamples = new Set();
285
+ const seenSurfaceFiles = new Set();
286
+ let hardcodedColorCount = 0;
287
+ let propDrillingCandidateCount = 0;
288
+ let mediaQueryCount = 0;
289
+ let tailwindBreakpointUsageCount = 0;
290
+ let arbitraryBreakpointCount = 0;
291
+ const uniqueMediaWidths = new Set();
292
+
293
+ for (const candidateDirectoryPath of resolvedCandidateDirectoryPaths) {
294
+ await collectFrontendSourceFilePaths(candidateDirectoryPath, scannedFilePaths);
295
+ if (scannedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
296
+ break;
297
+ }
298
+ }
299
+
300
+ designEvidenceSummary.scannedFileCount = scannedFilePaths.length;
301
+
302
+ for (const scannedFilePath of scannedFilePaths) {
303
+ let sourceText;
304
+
305
+ try {
306
+ const fileStat = await fs.stat(scannedFilePath);
307
+ if (fileStat.size > FRONTEND_FILE_SIZE_LIMIT_BYTES) {
308
+ continue;
309
+ }
310
+
311
+ sourceText = await fs.readFile(scannedFilePath, 'utf8');
312
+ } catch {
313
+ continue;
314
+ }
315
+
316
+ registerSurfaceFile(designEvidenceSummary, targetDirectoryPath, scannedFilePath, seenSurfaceFiles);
317
+
318
+ for (const cssVariableMatch of sourceText.matchAll(CSS_VARIABLE_DEFINITION_PATTERN)) {
319
+ designEvidenceSummary.cssVariables.definitionCount += 1;
320
+ const variableName = cssVariableMatch[1];
321
+ const categoryKey = categorizeCssVariable(variableName);
322
+ incrementCountMap(designEvidenceSummary.cssVariables.categoryCounts, categoryKey);
323
+ pushSampleValue(designEvidenceSummary.cssVariables.sampleNames, variableName, cssVariableSamples);
324
+ }
325
+
326
+ for (const cssVariableReferenceMatch of sourceText.matchAll(CSS_VARIABLE_REFERENCE_PATTERN)) {
327
+ designEvidenceSummary.cssVariables.referenceCount += 1;
328
+ pushSampleValue(
329
+ designEvidenceSummary.cssVariables.sampleNames,
330
+ cssVariableReferenceMatch[1],
331
+ cssVariableSamples
332
+ );
333
+ }
334
+
335
+ for (const colorMatch of sourceText.matchAll(COLOR_PATTERN)) {
336
+ const colorValue = colorMatch[0];
337
+ hardcodedColorCount += 1;
338
+ designEvidenceSummary.colors.hardcodedCount += 1;
339
+ incrementCountMap(designEvidenceSummary.colors.kindCounts, inferColorKind(colorValue));
340
+ pushSampleValue(designEvidenceSummary.colors.sampleValues, colorValue, colorSamples);
341
+ }
342
+
343
+ propDrillingCandidateCount += countPatternMatches(sourceText, PROP_DRILLING_PATTERN);
344
+ mediaQueryCount += countPatternMatches(sourceText, MEDIA_QUERY_PATTERN);
345
+ tailwindBreakpointUsageCount += countPatternMatches(sourceText, TAILWIND_BREAKPOINT_PATTERN);
346
+ arbitraryBreakpointCount += countPatternMatches(sourceText, ARBITRARY_BREAKPOINT_PATTERN);
347
+ designEvidenceSummary.tailwind.breakpointUsageCount += countPatternMatches(sourceText, TAILWIND_BREAKPOINT_PATTERN);
348
+ designEvidenceSummary.tailwind.arbitraryBreakpointCount += countPatternMatches(sourceText, ARBITRARY_BREAKPOINT_PATTERN);
349
+
350
+ for (const rawSpacingMatch of sourceText.matchAll(RAW_SPACING_PATTERN)) {
351
+ designEvidenceSummary.spacing.rawValueCount += 1;
352
+ pushSampleValue(designEvidenceSummary.spacing.sampleValues, rawSpacingMatch[1], spacingSamples);
353
+ }
354
+
355
+ for (const rawRadiusMatch of sourceText.matchAll(RAW_RADIUS_PATTERN)) {
356
+ designEvidenceSummary.radius.rawValueCount += 1;
357
+ pushSampleValue(designEvidenceSummary.radius.sampleValues, rawRadiusMatch[1], radiusSamples);
358
+ }
359
+
360
+ for (const rawShadowMatch of sourceText.matchAll(RAW_SHADOW_PATTERN)) {
361
+ designEvidenceSummary.shadow.rawValueCount += 1;
362
+ pushSampleValue(designEvidenceSummary.shadow.sampleValues, rawShadowMatch[1], shadowSamples);
363
+ }
364
+
365
+ for (const fontFamilyMatch of sourceText.matchAll(FONT_FAMILY_PATTERN)) {
366
+ designEvidenceSummary.typography.fontFamilyCount += 1;
367
+ pushSampleValue(designEvidenceSummary.typography.fontFamilySamples, fontFamilyMatch[1], fontFamilySamples);
368
+ }
369
+
370
+ for (const fontSizeMatch of sourceText.matchAll(FONT_SIZE_PATTERN)) {
371
+ designEvidenceSummary.typography.fontSizeCount += 1;
372
+ pushSampleValue(designEvidenceSummary.typography.fontSizeSamples, fontSizeMatch[1], fontSizeSamples);
373
+ }
374
+
375
+ for (const lineHeightMatch of sourceText.matchAll(LINE_HEIGHT_PATTERN)) {
376
+ designEvidenceSummary.typography.lineHeightCount += 1;
377
+ pushSampleValue(designEvidenceSummary.typography.lineHeightSamples, lineHeightMatch[1], lineHeightSamples);
378
+ }
379
+
380
+ for (const letterSpacingMatch of sourceText.matchAll(LETTER_SPACING_PATTERN)) {
381
+ designEvidenceSummary.typography.letterSpacingCount += 1;
382
+ pushSampleValue(
383
+ designEvidenceSummary.typography.letterSpacingSamples,
384
+ letterSpacingMatch[1],
385
+ letterSpacingSamples
386
+ );
387
+ }
388
+
389
+ designEvidenceSummary.motion.transitionCount += countPatternMatches(sourceText, TRANSITION_PATTERN);
390
+ designEvidenceSummary.motion.animationCount += countPatternMatches(sourceText, ANIMATION_PATTERN);
391
+
392
+ for (const durationMatch of sourceText.matchAll(DURATION_PATTERN)) {
393
+ designEvidenceSummary.motion.durationCount += 1;
394
+ pushSampleValue(designEvidenceSummary.motion.durationSamples, durationMatch[0], durationSamples);
395
+ }
396
+
397
+ for (const mediaWidthMatch of sourceText.matchAll(MEDIA_WIDTH_PATTERN)) {
398
+ uniqueMediaWidths.add(mediaWidthMatch[1]);
399
+ }
400
+
401
+ for (const utilityMatch of sourceText.matchAll(TAILWIND_UTILITY_PATTERN)) {
402
+ const utilityFamily = utilityMatch[1];
403
+ incrementCountMap(designEvidenceSummary.tailwind.utilityFamilyCounts, utilityFamily);
404
+ pushSampleValue(designEvidenceSummary.tailwind.utilityFamilySamples, utilityFamily, utilityFamilySamples);
405
+ }
406
+ }
407
+
408
+ designEvidenceSummary.tokenBypassSignals = {
409
+ hardcodedColorCount: designEvidenceSummary.colors.hardcodedCount,
410
+ rawSpacingCount: designEvidenceSummary.spacing.rawValueCount,
411
+ rawRadiusCount: designEvidenceSummary.radius.rawValueCount,
412
+ rawShadowCount: designEvidenceSummary.shadow.rawValueCount,
413
+ };
414
+
415
+ return {
416
+ frontendEvidenceMetrics: {
417
+ scannedFileCount: scannedFilePaths.length,
418
+ hardcodedColorCount,
419
+ propDrillingCandidateCount,
420
+ mediaQueryCount,
421
+ tailwindBreakpointUsageCount,
422
+ arbitraryBreakpointCount,
423
+ uniqueMediaWidthCount: uniqueMediaWidths.size,
424
+ },
425
+ designEvidenceSummary,
426
+ };
427
+ }
@@ -6,13 +6,12 @@ import fs from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
 
8
8
  import { BLUEPRINT_RECOMMENDATIONS } from './constants.mjs';
9
+ import {
10
+ collectFrontendDesignEvidence,
11
+ FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
12
+ } from './detector/design-evidence.mjs';
9
13
  import { toTitleCase } from './utils.mjs';
10
14
 
11
- const FRONTEND_SCAN_DIRECTORY_NAMES = ['src', 'app', 'pages', 'components', 'styles'];
12
- const FRONTEND_SCAN_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass']);
13
- const FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage']);
14
- const FRONTEND_FILE_SCAN_LIMIT = 200;
15
- const FRONTEND_FILE_SIZE_LIMIT_BYTES = 200_000;
16
15
  const WORKSPACE_SCAN_MAX_DEPTH = 3;
17
16
  const WORKSPACE_SCAN_MAX_DIRECTORIES = 120;
18
17
  const WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES = new Set([
@@ -314,115 +313,6 @@ async function collectNestedWorkspaceProjects(targetDirectoryPath) {
314
313
  return nestedWorkspaceProjects;
315
314
  }
316
315
 
317
- async function collectFrontendSourceFilePaths(directoryPath, collectedFilePaths = []) {
318
- if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
319
- return collectedFilePaths;
320
- }
321
-
322
- let directoryEntries;
323
- try {
324
- directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
325
- } catch {
326
- return collectedFilePaths;
327
- }
328
-
329
- for (const directoryEntry of directoryEntries) {
330
- if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
331
- break;
332
- }
333
-
334
- if (directoryEntry.isDirectory()) {
335
- if (FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES.has(directoryEntry.name)) {
336
- continue;
337
- }
338
-
339
- await collectFrontendSourceFilePaths(path.join(directoryPath, directoryEntry.name), collectedFilePaths);
340
- continue;
341
- }
342
-
343
- const fileExtension = path.extname(directoryEntry.name).toLowerCase();
344
- if (FRONTEND_SCAN_FILE_EXTENSIONS.has(fileExtension)) {
345
- collectedFilePaths.push(path.join(directoryPath, directoryEntry.name));
346
- }
347
- }
348
-
349
- return collectedFilePaths;
350
- }
351
-
352
- function countPatternMatches(sourceText, pattern) {
353
- return Array.from(sourceText.matchAll(pattern)).length;
354
- }
355
-
356
- async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, scanRootDirectoryPaths = []) {
357
- const candidateDirectoryPaths = FRONTEND_SCAN_DIRECTORY_NAMES
358
- .filter((directoryName) => markerNames.has(directoryName))
359
- .map((directoryName) => path.join(targetDirectoryPath, directoryName));
360
- const explicitScanRootDirectoryPaths = Array.isArray(scanRootDirectoryPaths)
361
- ? scanRootDirectoryPaths.filter((scanRootDirectoryPath) => typeof scanRootDirectoryPath === 'string' && scanRootDirectoryPath.trim().length > 0)
362
- : [];
363
- const resolvedCandidateDirectoryPaths = explicitScanRootDirectoryPaths.length > 0
364
- ? Array.from(new Set(explicitScanRootDirectoryPaths))
365
- : candidateDirectoryPaths.length > 0
366
- ? candidateDirectoryPaths
367
- : [targetDirectoryPath];
368
- const scannedFilePaths = [];
369
-
370
- for (const candidateDirectoryPath of resolvedCandidateDirectoryPaths) {
371
- await collectFrontendSourceFilePaths(candidateDirectoryPath, scannedFilePaths);
372
- if (scannedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
373
- break;
374
- }
375
- }
376
-
377
- let hardcodedColorCount = 0;
378
- let propDrillingCandidateCount = 0;
379
- let mediaQueryCount = 0;
380
- let tailwindBreakpointUsageCount = 0;
381
- let arbitraryBreakpointCount = 0;
382
- const uniqueMediaWidths = new Set();
383
-
384
- for (const scannedFilePath of scannedFilePaths) {
385
- let sourceText;
386
-
387
- try {
388
- const fileStat = await fs.stat(scannedFilePath);
389
- if (fileStat.size > FRONTEND_FILE_SIZE_LIMIT_BYTES) {
390
- continue;
391
- }
392
-
393
- sourceText = await fs.readFile(scannedFilePath, 'utf8');
394
- } catch {
395
- continue;
396
- }
397
-
398
- hardcodedColorCount += countPatternMatches(
399
- sourceText,
400
- /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)/g
401
- );
402
- propDrillingCandidateCount += countPatternMatches(
403
- sourceText,
404
- /<[A-Z][A-Za-z0-9_.:-]*(?:\s+[A-Za-z0-9_:-]+=\{[^}]+\}){5,}/g
405
- );
406
- mediaQueryCount += countPatternMatches(sourceText, /@media\b/g);
407
- tailwindBreakpointUsageCount += countPatternMatches(sourceText, /\b(?:sm|md|lg|xl|2xl):/g);
408
- arbitraryBreakpointCount += countPatternMatches(sourceText, /\b(?:min|max)-\[[^\]]+\]:/g);
409
-
410
- for (const mediaWidthMatch of sourceText.matchAll(/\((?:min|max)-width:\s*([0-9.]+(?:px|rem|em))\)/g)) {
411
- uniqueMediaWidths.add(mediaWidthMatch[1]);
412
- }
413
- }
414
-
415
- return {
416
- scannedFileCount: scannedFilePaths.length,
417
- hardcodedColorCount,
418
- propDrillingCandidateCount,
419
- mediaQueryCount,
420
- tailwindBreakpointUsageCount,
421
- arbitraryBreakpointCount,
422
- uniqueMediaWidthCount: uniqueMediaWidths.size,
423
- };
424
- }
425
-
426
316
  function analyzeUiSignalsForMarkerSet(markerNames, packageManifest, sourceLabel = null) {
427
317
  const detectedUiMarkers = DIRECT_UI_MARKER_NAMES.filter((markerName) => markerNames.has(markerName));
428
318
  const dependencySource = {
@@ -667,9 +557,15 @@ export async function detectUiScopeSignals({
667
557
  )
668
558
  ? nestedUiSignals.map((nestedUiSignal) => nestedUiSignal.directoryPath)
669
559
  : [];
670
- const frontendEvidenceMetrics = isUiScopeLikely
671
- ? await collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, frontendScanRootDirectoryPaths)
560
+ const designEvidence = isUiScopeLikely
561
+ ? await collectFrontendDesignEvidence({
562
+ targetDirectoryPath,
563
+ markerNames,
564
+ scanRootDirectoryPaths: frontendScanRootDirectoryPaths,
565
+ })
672
566
  : null;
567
+ const frontendEvidenceMetrics = designEvidence?.frontendEvidenceMetrics || null;
568
+ const designEvidenceSummary = designEvidence?.designEvidenceSummary || null;
673
569
 
674
570
  return {
675
571
  isUiScopeLikely,
@@ -677,6 +573,7 @@ export async function detectUiScopeSignals({
677
573
  detectedUiMarkers,
678
574
  detectedUiDependencies,
679
575
  frontendEvidenceMetrics,
576
+ designEvidenceSummary,
680
577
  packageManifest: preferredUiWorkspaceEntry?.packageManifest || resolvedPackageManifest,
681
578
  workspaceUiEntries: nestedUiSignals.map((nestedUiSignal) => ({
682
579
  relativePath: nestedUiSignal.relativePath,