@ryuenn3123/agentic-senior-core 3.0.11 → 3.0.12
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 +7 -1
- package/.agent-context/state/memory-continuity-benchmark.json +1 -1
- package/.cursorrules +1 -1
- package/.windsurfrules +1 -1
- package/lib/cli/commands/init.mjs +2 -0
- package/lib/cli/commands/upgrade.mjs +5 -0
- package/lib/cli/compiler.mjs +9 -0
- package/lib/cli/constants.mjs +1 -0
- package/lib/cli/detector.mjs +422 -90
- package/lib/cli/project-scaffolder.mjs +122 -8
- package/package.json +1 -1
- package/scripts/frontend-usability-audit.mjs +2 -0
- package/scripts/mcp-server.mjs +200 -118
- package/scripts/ui-design-judge.mjs +3 -2
- package/scripts/validate.mjs +4 -0
package/lib/cli/detector.mjs
CHANGED
|
@@ -13,6 +13,131 @@ const FRONTEND_SCAN_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.v
|
|
|
13
13
|
const FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage']);
|
|
14
14
|
const FRONTEND_FILE_SCAN_LIMIT = 200;
|
|
15
15
|
const FRONTEND_FILE_SIZE_LIMIT_BYTES = 200_000;
|
|
16
|
+
const WORKSPACE_SCAN_MAX_DEPTH = 3;
|
|
17
|
+
const WORKSPACE_SCAN_MAX_DIRECTORIES = 120;
|
|
18
|
+
const WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES = new Set([
|
|
19
|
+
...FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
|
|
20
|
+
'.agent-context',
|
|
21
|
+
'.agents',
|
|
22
|
+
'.cursor',
|
|
23
|
+
'.gemini',
|
|
24
|
+
'.github',
|
|
25
|
+
'.idea',
|
|
26
|
+
'.vscode',
|
|
27
|
+
'.zed',
|
|
28
|
+
]);
|
|
29
|
+
const WORKSPACE_CONTAINER_DIRECTORY_NAMES = new Set([
|
|
30
|
+
'admin',
|
|
31
|
+
'admins',
|
|
32
|
+
'api',
|
|
33
|
+
'apis',
|
|
34
|
+
'app',
|
|
35
|
+
'apps',
|
|
36
|
+
'backend',
|
|
37
|
+
'backends',
|
|
38
|
+
'client',
|
|
39
|
+
'clients',
|
|
40
|
+
'dashboard',
|
|
41
|
+
'dashboards',
|
|
42
|
+
'frontend',
|
|
43
|
+
'frontends',
|
|
44
|
+
'mobile',
|
|
45
|
+
'mobiles',
|
|
46
|
+
'package',
|
|
47
|
+
'packages',
|
|
48
|
+
'pkg',
|
|
49
|
+
'server',
|
|
50
|
+
'servers',
|
|
51
|
+
'service',
|
|
52
|
+
'services',
|
|
53
|
+
'site',
|
|
54
|
+
'sites',
|
|
55
|
+
'ui',
|
|
56
|
+
'web',
|
|
57
|
+
'worker',
|
|
58
|
+
'workers',
|
|
59
|
+
]);
|
|
60
|
+
const WORKSPACE_ROOT_MARKER_FILE_NAMES = new Set([
|
|
61
|
+
'lerna.json',
|
|
62
|
+
'nx.json',
|
|
63
|
+
'pnpm-workspace.yaml',
|
|
64
|
+
'turbo.json',
|
|
65
|
+
]);
|
|
66
|
+
const DIRECT_UI_MARKER_NAMES = [
|
|
67
|
+
'src',
|
|
68
|
+
'next.config.js',
|
|
69
|
+
'next.config.mjs',
|
|
70
|
+
'next.config.ts',
|
|
71
|
+
'tailwind.config.js',
|
|
72
|
+
'tailwind.config.mjs',
|
|
73
|
+
'tailwind.config.ts',
|
|
74
|
+
'vite.config.js',
|
|
75
|
+
'vite.config.mjs',
|
|
76
|
+
'vite.config.ts',
|
|
77
|
+
'react-native.config.js',
|
|
78
|
+
'app',
|
|
79
|
+
'pages',
|
|
80
|
+
'components',
|
|
81
|
+
'public',
|
|
82
|
+
'styles',
|
|
83
|
+
'android',
|
|
84
|
+
'ios',
|
|
85
|
+
'index.html',
|
|
86
|
+
];
|
|
87
|
+
const PROJECT_MARKER_FILE_NAMES = new Set([
|
|
88
|
+
'Cargo.toml',
|
|
89
|
+
'Gemfile',
|
|
90
|
+
'build.gradle',
|
|
91
|
+
'build.gradle.kts',
|
|
92
|
+
'composer.json',
|
|
93
|
+
'go.mod',
|
|
94
|
+
'package.json',
|
|
95
|
+
'pom.xml',
|
|
96
|
+
'pubspec.yaml',
|
|
97
|
+
'pyproject.toml',
|
|
98
|
+
'react-native.config.js',
|
|
99
|
+
'requirements.txt',
|
|
100
|
+
'tsconfig.json',
|
|
101
|
+
...DIRECT_UI_MARKER_NAMES,
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
function looksLikeWorkspaceSearchCandidate(directoryName) {
|
|
105
|
+
const normalizedDirectoryName = String(directoryName || '').trim().toLowerCase();
|
|
106
|
+
|
|
107
|
+
if (!normalizedDirectoryName) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(normalizedDirectoryName)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [
|
|
116
|
+
'admin',
|
|
117
|
+
'api',
|
|
118
|
+
'app',
|
|
119
|
+
'backend',
|
|
120
|
+
'client',
|
|
121
|
+
'dashboard',
|
|
122
|
+
'frontend',
|
|
123
|
+
'mobile',
|
|
124
|
+
'package',
|
|
125
|
+
'server',
|
|
126
|
+
'service',
|
|
127
|
+
'site',
|
|
128
|
+
'ui',
|
|
129
|
+
'web',
|
|
130
|
+
'worker',
|
|
131
|
+
].some((keyword) => normalizedDirectoryName.includes(keyword));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasProjectMarkers(markerNames) {
|
|
135
|
+
return Array.from(markerNames).some((markerName) => (
|
|
136
|
+
PROJECT_MARKER_FILE_NAMES.has(markerName)
|
|
137
|
+
|| markerName.endsWith('.csproj')
|
|
138
|
+
|| markerName.endsWith('.sln')
|
|
139
|
+
));
|
|
140
|
+
}
|
|
16
141
|
|
|
17
142
|
export async function collectProjectMarkers(targetDirectoryPath) {
|
|
18
143
|
const markerNames = new Set();
|
|
@@ -40,6 +165,133 @@ async function readPackageJsonIfExists(targetDirectoryPath) {
|
|
|
40
165
|
}
|
|
41
166
|
}
|
|
42
167
|
|
|
168
|
+
async function readDirectoryEntries(directoryPath) {
|
|
169
|
+
try {
|
|
170
|
+
return await fs.readdir(directoryPath, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function collectNestedWorkspaceProjects(targetDirectoryPath) {
|
|
177
|
+
const rootDirectoryEntries = await readDirectoryEntries(targetDirectoryPath);
|
|
178
|
+
const rootMarkerNames = new Set(rootDirectoryEntries.map((directoryEntry) => directoryEntry.name));
|
|
179
|
+
const rootLooksLikeWorkspace = Array.from(rootMarkerNames).some((markerName) => (
|
|
180
|
+
WORKSPACE_ROOT_MARKER_FILE_NAMES.has(markerName)
|
|
181
|
+
|| looksLikeWorkspaceSearchCandidate(markerName)
|
|
182
|
+
));
|
|
183
|
+
const nestedWorkspaceProjects = [];
|
|
184
|
+
const queuedWorkspacePaths = new Set();
|
|
185
|
+
const workspaceQueue = [];
|
|
186
|
+
let scannedDirectoryCount = 0;
|
|
187
|
+
|
|
188
|
+
for (const rootDirectoryEntry of rootDirectoryEntries) {
|
|
189
|
+
if (!rootDirectoryEntry.isDirectory()) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(rootDirectoryEntry.name)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const shouldInspectRootChild = rootLooksLikeWorkspace
|
|
198
|
+
|| looksLikeWorkspaceSearchCandidate(rootDirectoryEntry.name);
|
|
199
|
+
|
|
200
|
+
if (!shouldInspectRootChild) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rootChildDirectoryPath = path.join(targetDirectoryPath, rootDirectoryEntry.name);
|
|
205
|
+
const rootChildEntries = await readDirectoryEntries(rootChildDirectoryPath);
|
|
206
|
+
const rootChildMarkerNames = new Set(rootChildEntries.map((directoryEntry) => directoryEntry.name));
|
|
207
|
+
const rootChildRelativePath = rootDirectoryEntry.name.replace(/\\/g, '/');
|
|
208
|
+
|
|
209
|
+
workspaceQueue.push({
|
|
210
|
+
directoryPath: rootChildDirectoryPath,
|
|
211
|
+
relativePath: rootChildRelativePath,
|
|
212
|
+
markerNames: rootChildMarkerNames,
|
|
213
|
+
depth: 1,
|
|
214
|
+
underWorkspaceContainer: WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(rootDirectoryEntry.name.toLowerCase()),
|
|
215
|
+
});
|
|
216
|
+
queuedWorkspacePaths.add(rootChildRelativePath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
while (workspaceQueue.length > 0 && scannedDirectoryCount < WORKSPACE_SCAN_MAX_DIRECTORIES) {
|
|
220
|
+
const currentWorkspaceEntry = workspaceQueue.shift();
|
|
221
|
+
scannedDirectoryCount += 1;
|
|
222
|
+
|
|
223
|
+
const isProjectCandidate = hasProjectMarkers(currentWorkspaceEntry.markerNames);
|
|
224
|
+
const currentDirectoryName = path.basename(currentWorkspaceEntry.directoryPath).toLowerCase();
|
|
225
|
+
const isWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(currentDirectoryName);
|
|
226
|
+
|
|
227
|
+
if (isProjectCandidate) {
|
|
228
|
+
nestedWorkspaceProjects.push({
|
|
229
|
+
directoryPath: currentWorkspaceEntry.directoryPath,
|
|
230
|
+
relativePath: currentWorkspaceEntry.relativePath,
|
|
231
|
+
markerNames: currentWorkspaceEntry.markerNames,
|
|
232
|
+
packageManifest: currentWorkspaceEntry.markerNames.has('package.json')
|
|
233
|
+
? await readPackageJsonIfExists(currentWorkspaceEntry.directoryPath)
|
|
234
|
+
: null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (currentWorkspaceEntry.depth >= WORKSPACE_SCAN_MAX_DEPTH) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const shouldTraverseChildren = currentWorkspaceEntry.underWorkspaceContainer
|
|
243
|
+
|| isWorkspaceContainer
|
|
244
|
+
|| !isProjectCandidate;
|
|
245
|
+
|
|
246
|
+
if (!shouldTraverseChildren) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const childEntries = await readDirectoryEntries(currentWorkspaceEntry.directoryPath);
|
|
251
|
+
for (const childEntry of childEntries) {
|
|
252
|
+
if (!childEntry.isDirectory()) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(childEntry.name)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const childLooksRelevant = looksLikeWorkspaceSearchCandidate(childEntry.name);
|
|
261
|
+
if (!childLooksRelevant && !currentWorkspaceEntry.underWorkspaceContainer && !isWorkspaceContainer) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const childDirectoryPath = path.join(currentWorkspaceEntry.directoryPath, childEntry.name);
|
|
266
|
+
const childRelativePath = path.join(currentWorkspaceEntry.relativePath, childEntry.name).replace(/\\/g, '/');
|
|
267
|
+
|
|
268
|
+
if (queuedWorkspacePaths.has(childRelativePath)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const childDirectoryEntries = await readDirectoryEntries(childDirectoryPath);
|
|
273
|
+
const childMarkerNames = new Set(childDirectoryEntries.map((directoryEntry) => directoryEntry.name));
|
|
274
|
+
const childIsProjectCandidate = hasProjectMarkers(childMarkerNames);
|
|
275
|
+
const childIsWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(childEntry.name.toLowerCase());
|
|
276
|
+
|
|
277
|
+
if (!childIsProjectCandidate && !childIsWorkspaceContainer && !childLooksRelevant) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
workspaceQueue.push({
|
|
282
|
+
directoryPath: childDirectoryPath,
|
|
283
|
+
relativePath: childRelativePath,
|
|
284
|
+
markerNames: childMarkerNames,
|
|
285
|
+
depth: currentWorkspaceEntry.depth + 1,
|
|
286
|
+
underWorkspaceContainer: currentWorkspaceEntry.underWorkspaceContainer || isWorkspaceContainer || childIsWorkspaceContainer,
|
|
287
|
+
});
|
|
288
|
+
queuedWorkspacePaths.add(childRelativePath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return nestedWorkspaceProjects;
|
|
293
|
+
}
|
|
294
|
+
|
|
43
295
|
async function collectFrontendSourceFilePaths(directoryPath, collectedFilePaths = []) {
|
|
44
296
|
if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
|
|
45
297
|
return collectedFilePaths;
|
|
@@ -79,13 +331,18 @@ function countPatternMatches(sourceText, pattern) {
|
|
|
79
331
|
return Array.from(sourceText.matchAll(pattern)).length;
|
|
80
332
|
}
|
|
81
333
|
|
|
82
|
-
async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames) {
|
|
334
|
+
async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, scanRootDirectoryPaths = []) {
|
|
83
335
|
const candidateDirectoryPaths = FRONTEND_SCAN_DIRECTORY_NAMES
|
|
84
336
|
.filter((directoryName) => markerNames.has(directoryName))
|
|
85
337
|
.map((directoryName) => path.join(targetDirectoryPath, directoryName));
|
|
86
|
-
const
|
|
87
|
-
?
|
|
88
|
-
: [
|
|
338
|
+
const explicitScanRootDirectoryPaths = Array.isArray(scanRootDirectoryPaths)
|
|
339
|
+
? scanRootDirectoryPaths.filter((scanRootDirectoryPath) => typeof scanRootDirectoryPath === 'string' && scanRootDirectoryPath.trim().length > 0)
|
|
340
|
+
: [];
|
|
341
|
+
const resolvedCandidateDirectoryPaths = explicitScanRootDirectoryPaths.length > 0
|
|
342
|
+
? Array.from(new Set(explicitScanRootDirectoryPaths))
|
|
343
|
+
: candidateDirectoryPaths.length > 0
|
|
344
|
+
? candidateDirectoryPaths
|
|
345
|
+
: [targetDirectoryPath];
|
|
89
346
|
const scannedFilePaths = [];
|
|
90
347
|
|
|
91
348
|
for (const candidateDirectoryPath of resolvedCandidateDirectoryPaths) {
|
|
@@ -144,63 +401,11 @@ async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames)
|
|
|
144
401
|
};
|
|
145
402
|
}
|
|
146
403
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
selectedStackFileName,
|
|
150
|
-
selectedBlueprintFileName,
|
|
151
|
-
packageManifest = null,
|
|
152
|
-
projectScopeKey = null,
|
|
153
|
-
projectScopeSourceLabel = 'project scope',
|
|
154
|
-
}) {
|
|
155
|
-
const signalReasons = [];
|
|
156
|
-
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
157
|
-
const resolvedPackageManifest = packageManifest || await readPackageJsonIfExists(targetDirectoryPath);
|
|
158
|
-
|
|
159
|
-
const normalizedProjectScopeKey = String(projectScopeKey || '').trim().toLowerCase();
|
|
160
|
-
if (normalizedProjectScopeKey === 'frontend-only' || normalizedProjectScopeKey === 'both') {
|
|
161
|
-
signalReasons.push(`${projectScopeSourceLabel}: ${normalizedProjectScopeKey}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const selectedStackKey = String(selectedStackFileName || '').trim().toLowerCase();
|
|
165
|
-
if (selectedStackKey === 'react-native.md' || selectedStackKey === 'flutter.md') {
|
|
166
|
-
signalReasons.push(`selected stack implies UI runtime: ${selectedStackKey}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const selectedBlueprintKey = String(selectedBlueprintFileName || '').trim().toLowerCase();
|
|
170
|
-
if (selectedBlueprintKey.includes('frontend') || selectedBlueprintKey.includes('landing') || selectedBlueprintKey.includes('mobile-app')) {
|
|
171
|
-
signalReasons.push(`selected blueprint implies UI scope: ${selectedBlueprintKey}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const directUiMarkerNames = [
|
|
175
|
-
'src',
|
|
176
|
-
'next.config.js',
|
|
177
|
-
'next.config.mjs',
|
|
178
|
-
'next.config.ts',
|
|
179
|
-
'tailwind.config.js',
|
|
180
|
-
'tailwind.config.mjs',
|
|
181
|
-
'tailwind.config.ts',
|
|
182
|
-
'vite.config.js',
|
|
183
|
-
'vite.config.mjs',
|
|
184
|
-
'vite.config.ts',
|
|
185
|
-
'react-native.config.js',
|
|
186
|
-
'app',
|
|
187
|
-
'pages',
|
|
188
|
-
'components',
|
|
189
|
-
'public',
|
|
190
|
-
'styles',
|
|
191
|
-
'android',
|
|
192
|
-
'ios',
|
|
193
|
-
'index.html',
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
const detectedUiMarkers = directUiMarkerNames.filter((markerName) => markerNames.has(markerName));
|
|
197
|
-
if (detectedUiMarkers.length > 0) {
|
|
198
|
-
signalReasons.push(`ui markers: ${detectedUiMarkers.join(', ')}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
404
|
+
function analyzeUiSignalsForMarkerSet(markerNames, packageManifest, sourceLabel = null) {
|
|
405
|
+
const detectedUiMarkers = DIRECT_UI_MARKER_NAMES.filter((markerName) => markerNames.has(markerName));
|
|
201
406
|
const dependencySource = {
|
|
202
|
-
...(
|
|
203
|
-
...(
|
|
407
|
+
...(packageManifest?.dependencies || {}),
|
|
408
|
+
...(packageManifest?.devDependencies || {}),
|
|
204
409
|
};
|
|
205
410
|
const detectableUiDependencies = [
|
|
206
411
|
'next',
|
|
@@ -211,10 +416,6 @@ export async function detectUiScopeSignals({
|
|
|
211
416
|
'tailwindcss',
|
|
212
417
|
];
|
|
213
418
|
const detectedUiDependencies = detectableUiDependencies.filter((dependencyName) => dependencySource[dependencyName]);
|
|
214
|
-
if (detectedUiDependencies.length > 0) {
|
|
215
|
-
signalReasons.push(`ui dependencies: ${detectedUiDependencies.join(', ')}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
419
|
const hasStrongUiMarker = detectedUiMarkers.some((markerName) => (
|
|
219
420
|
markerName.startsWith('next.config')
|
|
220
421
|
|| markerName === 'react-native.config.js'
|
|
@@ -223,46 +424,63 @@ export async function detectUiScopeSignals({
|
|
|
223
424
|
));
|
|
224
425
|
const hasUiDependencies = detectedUiDependencies.length > 0;
|
|
225
426
|
const hasStructuralUiMarkers = detectedUiMarkers.length >= 2;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
:
|
|
427
|
+
const signalReasons = [];
|
|
428
|
+
const sourcePrefix = sourceLabel ? `${sourceLabel}: ` : '';
|
|
429
|
+
|
|
430
|
+
if (detectedUiMarkers.length > 0) {
|
|
431
|
+
signalReasons.push(`${sourcePrefix}ui markers: ${detectedUiMarkers.join(', ')}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (detectedUiDependencies.length > 0) {
|
|
435
|
+
signalReasons.push(`${sourcePrefix}ui dependencies: ${detectedUiDependencies.join(', ')}`);
|
|
436
|
+
}
|
|
231
437
|
|
|
232
438
|
return {
|
|
233
|
-
isUiScopeLikely,
|
|
234
439
|
signalReasons,
|
|
235
440
|
detectedUiMarkers,
|
|
236
441
|
detectedUiDependencies,
|
|
237
|
-
|
|
238
|
-
|
|
442
|
+
hasStrongUiMarker,
|
|
443
|
+
hasUiDependencies,
|
|
444
|
+
hasStructuralUiMarkers,
|
|
239
445
|
};
|
|
240
446
|
}
|
|
241
447
|
|
|
242
|
-
|
|
243
|
-
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
448
|
+
function collectStackDetectionCandidates(markerNames, evidencePrefix = null) {
|
|
244
449
|
const detectionCandidates = [];
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
if (
|
|
450
|
+
const withEvidencePrefix = (evidenceItem) => evidencePrefix ? `${evidencePrefix}: ${evidenceItem}` : evidenceItem;
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
markerNames.has('package.json')
|
|
454
|
+
|| markerNames.has('tsconfig.json')
|
|
455
|
+
|| markerNames.has('next.config.js')
|
|
456
|
+
|| markerNames.has('next.config.mjs')
|
|
457
|
+
|| markerNames.has('vite.config.js')
|
|
458
|
+
|| markerNames.has('vite.config.mjs')
|
|
459
|
+
|| markerNames.has('vite.config.ts')
|
|
460
|
+
) {
|
|
248
461
|
const evidence = [];
|
|
249
462
|
let confidenceScore = 0.7;
|
|
250
463
|
|
|
251
464
|
if (markerNames.has('package.json')) {
|
|
252
|
-
evidence.push('package.json');
|
|
465
|
+
evidence.push(withEvidencePrefix('package.json'));
|
|
253
466
|
confidenceScore += 0.12;
|
|
254
467
|
}
|
|
255
468
|
|
|
256
469
|
if (markerNames.has('tsconfig.json')) {
|
|
257
|
-
evidence.push('tsconfig.json');
|
|
470
|
+
evidence.push(withEvidencePrefix('tsconfig.json'));
|
|
258
471
|
confidenceScore += 0.12;
|
|
259
472
|
}
|
|
260
473
|
|
|
261
474
|
if (markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
|
|
262
|
-
evidence.push('Next.js config');
|
|
475
|
+
evidence.push(withEvidencePrefix('Next.js config'));
|
|
263
476
|
confidenceScore += 0.05;
|
|
264
477
|
}
|
|
265
478
|
|
|
479
|
+
if (markerNames.has('vite.config.js') || markerNames.has('vite.config.mjs') || markerNames.has('vite.config.ts')) {
|
|
480
|
+
evidence.push(withEvidencePrefix('Vite config'));
|
|
481
|
+
confidenceScore += 0.08;
|
|
482
|
+
}
|
|
483
|
+
|
|
266
484
|
detectionCandidates.push({
|
|
267
485
|
stackFileName: 'typescript.md',
|
|
268
486
|
confidenceScore: Math.min(confidenceScore, 0.97),
|
|
@@ -274,14 +492,16 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
274
492
|
detectionCandidates.push({
|
|
275
493
|
stackFileName: 'python.md',
|
|
276
494
|
confidenceScore: markerNames.has('pyproject.toml') ? 0.96 : 0.78,
|
|
277
|
-
evidence: markerNames.has('pyproject.toml')
|
|
495
|
+
evidence: markerNames.has('pyproject.toml')
|
|
496
|
+
? [withEvidencePrefix('pyproject.toml')]
|
|
497
|
+
: [withEvidencePrefix('requirements.txt')],
|
|
278
498
|
});
|
|
279
499
|
}
|
|
280
500
|
|
|
281
501
|
if (markerNames.has('pom.xml') || markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) {
|
|
282
502
|
const evidence = [];
|
|
283
|
-
if (markerNames.has('pom.xml')) evidence.push('pom.xml');
|
|
284
|
-
if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push('Gradle build file');
|
|
503
|
+
if (markerNames.has('pom.xml')) evidence.push(withEvidencePrefix('pom.xml'));
|
|
504
|
+
if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push(withEvidencePrefix('Gradle build file'));
|
|
285
505
|
detectionCandidates.push({
|
|
286
506
|
stackFileName: 'java.md',
|
|
287
507
|
confidenceScore: markerNames.has('pom.xml') ? 0.95 : 0.84,
|
|
@@ -293,7 +513,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
293
513
|
detectionCandidates.push({
|
|
294
514
|
stackFileName: 'php.md',
|
|
295
515
|
confidenceScore: 0.95,
|
|
296
|
-
evidence: ['composer.json'],
|
|
516
|
+
evidence: [withEvidencePrefix('composer.json')],
|
|
297
517
|
});
|
|
298
518
|
}
|
|
299
519
|
|
|
@@ -301,7 +521,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
301
521
|
detectionCandidates.push({
|
|
302
522
|
stackFileName: 'go.md',
|
|
303
523
|
confidenceScore: 0.96,
|
|
304
|
-
evidence: ['go.mod'],
|
|
524
|
+
evidence: [withEvidencePrefix('go.mod')],
|
|
305
525
|
});
|
|
306
526
|
}
|
|
307
527
|
|
|
@@ -309,7 +529,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
309
529
|
detectionCandidates.push({
|
|
310
530
|
stackFileName: 'rust.md',
|
|
311
531
|
confidenceScore: 0.96,
|
|
312
|
-
evidence: ['Cargo.toml'],
|
|
532
|
+
evidence: [withEvidencePrefix('Cargo.toml')],
|
|
313
533
|
});
|
|
314
534
|
}
|
|
315
535
|
|
|
@@ -317,7 +537,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
317
537
|
detectionCandidates.push({
|
|
318
538
|
stackFileName: 'ruby.md',
|
|
319
539
|
confidenceScore: 0.95,
|
|
320
|
-
evidence: ['Gemfile'],
|
|
540
|
+
evidence: [withEvidencePrefix('Gemfile')],
|
|
321
541
|
});
|
|
322
542
|
}
|
|
323
543
|
|
|
@@ -326,7 +546,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
326
546
|
detectionCandidates.push({
|
|
327
547
|
stackFileName: 'csharp.md',
|
|
328
548
|
confidenceScore: 0.95,
|
|
329
|
-
evidence: ['.sln or .csproj file'],
|
|
549
|
+
evidence: [withEvidencePrefix('.sln or .csproj file')],
|
|
330
550
|
});
|
|
331
551
|
}
|
|
332
552
|
|
|
@@ -334,7 +554,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
334
554
|
detectionCandidates.push({
|
|
335
555
|
stackFileName: 'react-native.md',
|
|
336
556
|
confidenceScore: 0.9,
|
|
337
|
-
evidence: ['package.json', 'mobile runtime markers'],
|
|
557
|
+
evidence: [withEvidencePrefix('package.json'), withEvidencePrefix('mobile runtime markers')],
|
|
338
558
|
});
|
|
339
559
|
}
|
|
340
560
|
|
|
@@ -342,10 +562,122 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
342
562
|
detectionCandidates.push({
|
|
343
563
|
stackFileName: 'flutter.md',
|
|
344
564
|
confidenceScore: 0.94,
|
|
345
|
-
evidence: ['pubspec.yaml'],
|
|
565
|
+
evidence: [withEvidencePrefix('pubspec.yaml')],
|
|
346
566
|
});
|
|
347
567
|
}
|
|
348
568
|
|
|
569
|
+
return detectionCandidates;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function detectUiScopeSignals({
|
|
573
|
+
targetDirectoryPath,
|
|
574
|
+
selectedStackFileName,
|
|
575
|
+
selectedBlueprintFileName,
|
|
576
|
+
packageManifest = null,
|
|
577
|
+
projectScopeKey = null,
|
|
578
|
+
projectScopeSourceLabel = 'project scope',
|
|
579
|
+
}) {
|
|
580
|
+
const signalReasons = [];
|
|
581
|
+
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
582
|
+
const resolvedPackageManifest = packageManifest || await readPackageJsonIfExists(targetDirectoryPath);
|
|
583
|
+
const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
|
|
584
|
+
|
|
585
|
+
const normalizedProjectScopeKey = String(projectScopeKey || '').trim().toLowerCase();
|
|
586
|
+
if (normalizedProjectScopeKey === 'frontend-only' || normalizedProjectScopeKey === 'both') {
|
|
587
|
+
signalReasons.push(`${projectScopeSourceLabel}: ${normalizedProjectScopeKey}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const selectedStackKey = String(selectedStackFileName || '').trim().toLowerCase();
|
|
591
|
+
if (selectedStackKey === 'react-native.md' || selectedStackKey === 'flutter.md') {
|
|
592
|
+
signalReasons.push(`selected stack implies UI runtime: ${selectedStackKey}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const selectedBlueprintKey = String(selectedBlueprintFileName || '').trim().toLowerCase();
|
|
596
|
+
if (selectedBlueprintKey.includes('frontend') || selectedBlueprintKey.includes('landing') || selectedBlueprintKey.includes('mobile-app')) {
|
|
597
|
+
signalReasons.push(`selected blueprint implies UI scope: ${selectedBlueprintKey}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const rootUiSignals = analyzeUiSignalsForMarkerSet(markerNames, resolvedPackageManifest);
|
|
601
|
+
signalReasons.push(...rootUiSignals.signalReasons);
|
|
602
|
+
|
|
603
|
+
const nestedUiSignals = nestedWorkspaceProjects
|
|
604
|
+
.map((nestedWorkspaceProject) => ({
|
|
605
|
+
...nestedWorkspaceProject,
|
|
606
|
+
...analyzeUiSignalsForMarkerSet(
|
|
607
|
+
nestedWorkspaceProject.markerNames,
|
|
608
|
+
nestedWorkspaceProject.packageManifest,
|
|
609
|
+
`workspace ${nestedWorkspaceProject.relativePath}`
|
|
610
|
+
),
|
|
611
|
+
}))
|
|
612
|
+
.filter((nestedWorkspaceProject) => nestedWorkspaceProject.signalReasons.length > 0);
|
|
613
|
+
|
|
614
|
+
for (const nestedUiSignal of nestedUiSignals) {
|
|
615
|
+
signalReasons.push(...nestedUiSignal.signalReasons);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const detectedUiMarkers = Array.from(new Set([
|
|
619
|
+
...rootUiSignals.detectedUiMarkers,
|
|
620
|
+
...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiMarkers),
|
|
621
|
+
]));
|
|
622
|
+
const detectedUiDependencies = Array.from(new Set([
|
|
623
|
+
...rootUiSignals.detectedUiDependencies,
|
|
624
|
+
...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiDependencies),
|
|
625
|
+
]));
|
|
626
|
+
|
|
627
|
+
const hasStrongUiMarker = rootUiSignals.hasStrongUiMarker
|
|
628
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStrongUiMarker);
|
|
629
|
+
const hasUiDependencies = rootUiSignals.hasUiDependencies
|
|
630
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasUiDependencies);
|
|
631
|
+
const hasStructuralUiMarkers = rootUiSignals.hasStructuralUiMarkers
|
|
632
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStructuralUiMarkers);
|
|
633
|
+
const isUiScopeLikely = signalReasons.length > 0
|
|
634
|
+
&& (hasStrongUiMarker || hasUiDependencies || hasStructuralUiMarkers || normalizedProjectScopeKey.length > 0);
|
|
635
|
+
const preferredUiWorkspaceEntry = nestedUiSignals.find((nestedUiSignal) => (
|
|
636
|
+
nestedUiSignal.hasStrongUiMarker
|
|
637
|
+
|| nestedUiSignal.hasUiDependencies
|
|
638
|
+
|| nestedUiSignal.hasStructuralUiMarkers
|
|
639
|
+
)) || null;
|
|
640
|
+
const frontendScanRootDirectoryPaths = (
|
|
641
|
+
!rootUiSignals.hasStrongUiMarker
|
|
642
|
+
&& !rootUiSignals.hasUiDependencies
|
|
643
|
+
&& !rootUiSignals.hasStructuralUiMarkers
|
|
644
|
+
&& nestedUiSignals.length > 0
|
|
645
|
+
)
|
|
646
|
+
? nestedUiSignals.map((nestedUiSignal) => nestedUiSignal.directoryPath)
|
|
647
|
+
: [];
|
|
648
|
+
const frontendEvidenceMetrics = isUiScopeLikely
|
|
649
|
+
? await collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, frontendScanRootDirectoryPaths)
|
|
650
|
+
: null;
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
isUiScopeLikely,
|
|
654
|
+
signalReasons,
|
|
655
|
+
detectedUiMarkers,
|
|
656
|
+
detectedUiDependencies,
|
|
657
|
+
frontendEvidenceMetrics,
|
|
658
|
+
packageManifest: preferredUiWorkspaceEntry?.packageManifest || resolvedPackageManifest,
|
|
659
|
+
workspaceUiEntries: nestedUiSignals.map((nestedUiSignal) => ({
|
|
660
|
+
relativePath: nestedUiSignal.relativePath,
|
|
661
|
+
detectedUiMarkers: nestedUiSignal.detectedUiMarkers,
|
|
662
|
+
detectedUiDependencies: nestedUiSignal.detectedUiDependencies,
|
|
663
|
+
})),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export async function detectProjectContext(targetDirectoryPath) {
|
|
668
|
+
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
669
|
+
const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
|
|
670
|
+
const detectionCandidates = [
|
|
671
|
+
...collectStackDetectionCandidates(markerNames),
|
|
672
|
+
...nestedWorkspaceProjects.flatMap((nestedWorkspaceProject) => (
|
|
673
|
+
collectStackDetectionCandidates(
|
|
674
|
+
nestedWorkspaceProject.markerNames,
|
|
675
|
+
nestedWorkspaceProject.relativePath
|
|
676
|
+
)
|
|
677
|
+
)),
|
|
678
|
+
];
|
|
679
|
+
const hasExistingProjectFiles = markerNames.size > 0;
|
|
680
|
+
|
|
349
681
|
if (detectionCandidates.length === 0) {
|
|
350
682
|
return {
|
|
351
683
|
hasExistingProjectFiles,
|