@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.
- package/.agent-context/prompts/bootstrap-design.md +30 -16
- package/.agent-context/prompts/init-project.md +4 -0
- package/.agent-context/rules/architecture.md +13 -0
- package/.agent-context/rules/docker-runtime.md +12 -0
- package/.agent-context/rules/efficiency-vs-hype.md +17 -6
- package/.agent-context/rules/frontend-architecture.md +5 -0
- package/.agent-context/state/memory-continuity-benchmark.json +1 -1
- package/.agent-context/state/onboarding-report.json +0 -1
- package/.cursorrules +66 -29
- package/.gemini/instructions.md +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.instructions.md +4 -3
- package/.windsurfrules +66 -29
- package/AGENTS.md +1 -1
- package/lib/cli/architect.mjs +71 -784
- package/lib/cli/commands/init.mjs +32 -98
- package/lib/cli/commands/optimize.mjs +0 -4
- package/lib/cli/commands/upgrade.mjs +2 -5
- package/lib/cli/compiler.mjs +3 -11
- package/lib/cli/constants.mjs +3 -73
- package/lib/cli/detector/design-evidence.mjs +427 -0
- package/lib/cli/detector.mjs +13 -116
- package/lib/cli/init-options.mjs +0 -118
- package/lib/cli/project-scaffolder/constants.mjs +67 -0
- package/lib/cli/project-scaffolder/design-contract.mjs +554 -0
- package/lib/cli/project-scaffolder/discovery.mjs +315 -0
- package/lib/cli/project-scaffolder/prompt-builders.mjs +196 -0
- package/lib/cli/project-scaffolder/storage.mjs +154 -0
- package/lib/cli/project-scaffolder.mjs +32 -1160
- package/lib/cli/utils.mjs +2 -11
- package/package.json +1 -1
- package/scripts/frontend-usability-audit.mjs +53 -0
- package/scripts/validate/config.mjs +401 -0
- package/scripts/validate/coverage-checks.mjs +429 -0
- package/scripts/validate.mjs +44 -754
- package/lib/cli/init-architecture-flow.mjs +0 -233
- 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
|
+
}
|
package/lib/cli/detector.mjs
CHANGED
|
@@ -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
|
|
671
|
-
? await
|
|
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,
|