@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.
@@ -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 resolvedCandidateDirectoryPaths = candidateDirectoryPaths.length > 0
87
- ? candidateDirectoryPaths
88
- : [targetDirectoryPath];
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
- export async function detectUiScopeSignals({
148
- targetDirectoryPath,
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
- ...(resolvedPackageManifest?.dependencies || {}),
203
- ...(resolvedPackageManifest?.devDependencies || {}),
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 isUiScopeLikely = signalReasons.length > 0
227
- && (hasStrongUiMarker || hasUiDependencies || hasStructuralUiMarkers || normalizedProjectScopeKey.length > 0);
228
- const frontendEvidenceMetrics = isUiScopeLikely
229
- ? await collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames)
230
- : null;
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
- frontendEvidenceMetrics,
238
- packageManifest: resolvedPackageManifest,
442
+ hasStrongUiMarker,
443
+ hasUiDependencies,
444
+ hasStructuralUiMarkers,
239
445
  };
240
446
  }
241
447
 
242
- export async function detectProjectContext(targetDirectoryPath) {
243
- const markerNames = await collectProjectMarkers(targetDirectoryPath);
448
+ function collectStackDetectionCandidates(markerNames, evidencePrefix = null) {
244
449
  const detectionCandidates = [];
245
- const hasExistingProjectFiles = markerNames.size > 0;
246
-
247
- if (markerNames.has('package.json') || markerNames.has('tsconfig.json') || markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
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') ? ['pyproject.toml'] : ['requirements.txt'],
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,