@ryuenn3123/agentic-senior-core 3.0.11 → 3.0.13

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,149 @@ 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
+ const INTERNAL_GOVERNANCE_SURFACE_NAMES = new Set([
104
+ '.agent-context',
105
+ '.agent-instructions.md',
106
+ '.agent-override.md',
107
+ '.agentic-backup',
108
+ '.agents',
109
+ '.clauderc',
110
+ '.cursorrules',
111
+ '.cursor',
112
+ '.gemini',
113
+ '.github',
114
+ '.instructions.md',
115
+ '.vscode',
116
+ '.windsurfrules',
117
+ '.zed',
118
+ 'AGENTS.md',
119
+ 'mcp.json',
120
+ ]);
121
+
122
+ function looksLikeWorkspaceSearchCandidate(directoryName) {
123
+ const normalizedDirectoryName = String(directoryName || '').trim().toLowerCase();
124
+
125
+ if (!normalizedDirectoryName) {
126
+ return false;
127
+ }
128
+
129
+ if (WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(normalizedDirectoryName)) {
130
+ return true;
131
+ }
132
+
133
+ return [
134
+ 'admin',
135
+ 'api',
136
+ 'app',
137
+ 'backend',
138
+ 'client',
139
+ 'dashboard',
140
+ 'frontend',
141
+ 'mobile',
142
+ 'package',
143
+ 'server',
144
+ 'service',
145
+ 'site',
146
+ 'ui',
147
+ 'web',
148
+ 'worker',
149
+ ].some((keyword) => normalizedDirectoryName.includes(keyword));
150
+ }
151
+
152
+ function hasProjectMarkers(markerNames) {
153
+ return Array.from(markerNames).some((markerName) => (
154
+ PROJECT_MARKER_FILE_NAMES.has(markerName)
155
+ || markerName.endsWith('.csproj')
156
+ || markerName.endsWith('.sln')
157
+ ));
158
+ }
16
159
 
17
160
  export async function collectProjectMarkers(targetDirectoryPath) {
18
161
  const markerNames = new Set();
@@ -23,6 +166,10 @@ export async function collectProjectMarkers(targetDirectoryPath) {
23
166
  continue;
24
167
  }
25
168
 
169
+ if (INTERNAL_GOVERNANCE_SURFACE_NAMES.has(directoryEntry.name)) {
170
+ continue;
171
+ }
172
+
26
173
  markerNames.add(directoryEntry.name);
27
174
  }
28
175
 
@@ -40,6 +187,133 @@ async function readPackageJsonIfExists(targetDirectoryPath) {
40
187
  }
41
188
  }
42
189
 
190
+ async function readDirectoryEntries(directoryPath) {
191
+ try {
192
+ return await fs.readdir(directoryPath, { withFileTypes: true });
193
+ } catch {
194
+ return [];
195
+ }
196
+ }
197
+
198
+ async function collectNestedWorkspaceProjects(targetDirectoryPath) {
199
+ const rootDirectoryEntries = await readDirectoryEntries(targetDirectoryPath);
200
+ const rootMarkerNames = new Set(rootDirectoryEntries.map((directoryEntry) => directoryEntry.name));
201
+ const rootLooksLikeWorkspace = Array.from(rootMarkerNames).some((markerName) => (
202
+ WORKSPACE_ROOT_MARKER_FILE_NAMES.has(markerName)
203
+ || looksLikeWorkspaceSearchCandidate(markerName)
204
+ ));
205
+ const nestedWorkspaceProjects = [];
206
+ const queuedWorkspacePaths = new Set();
207
+ const workspaceQueue = [];
208
+ let scannedDirectoryCount = 0;
209
+
210
+ for (const rootDirectoryEntry of rootDirectoryEntries) {
211
+ if (!rootDirectoryEntry.isDirectory()) {
212
+ continue;
213
+ }
214
+
215
+ if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(rootDirectoryEntry.name)) {
216
+ continue;
217
+ }
218
+
219
+ const shouldInspectRootChild = rootLooksLikeWorkspace
220
+ || looksLikeWorkspaceSearchCandidate(rootDirectoryEntry.name);
221
+
222
+ if (!shouldInspectRootChild) {
223
+ continue;
224
+ }
225
+
226
+ const rootChildDirectoryPath = path.join(targetDirectoryPath, rootDirectoryEntry.name);
227
+ const rootChildEntries = await readDirectoryEntries(rootChildDirectoryPath);
228
+ const rootChildMarkerNames = new Set(rootChildEntries.map((directoryEntry) => directoryEntry.name));
229
+ const rootChildRelativePath = rootDirectoryEntry.name.replace(/\\/g, '/');
230
+
231
+ workspaceQueue.push({
232
+ directoryPath: rootChildDirectoryPath,
233
+ relativePath: rootChildRelativePath,
234
+ markerNames: rootChildMarkerNames,
235
+ depth: 1,
236
+ underWorkspaceContainer: WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(rootDirectoryEntry.name.toLowerCase()),
237
+ });
238
+ queuedWorkspacePaths.add(rootChildRelativePath);
239
+ }
240
+
241
+ while (workspaceQueue.length > 0 && scannedDirectoryCount < WORKSPACE_SCAN_MAX_DIRECTORIES) {
242
+ const currentWorkspaceEntry = workspaceQueue.shift();
243
+ scannedDirectoryCount += 1;
244
+
245
+ const isProjectCandidate = hasProjectMarkers(currentWorkspaceEntry.markerNames);
246
+ const currentDirectoryName = path.basename(currentWorkspaceEntry.directoryPath).toLowerCase();
247
+ const isWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(currentDirectoryName);
248
+
249
+ if (isProjectCandidate) {
250
+ nestedWorkspaceProjects.push({
251
+ directoryPath: currentWorkspaceEntry.directoryPath,
252
+ relativePath: currentWorkspaceEntry.relativePath,
253
+ markerNames: currentWorkspaceEntry.markerNames,
254
+ packageManifest: currentWorkspaceEntry.markerNames.has('package.json')
255
+ ? await readPackageJsonIfExists(currentWorkspaceEntry.directoryPath)
256
+ : null,
257
+ });
258
+ }
259
+
260
+ if (currentWorkspaceEntry.depth >= WORKSPACE_SCAN_MAX_DEPTH) {
261
+ continue;
262
+ }
263
+
264
+ const shouldTraverseChildren = currentWorkspaceEntry.underWorkspaceContainer
265
+ || isWorkspaceContainer
266
+ || !isProjectCandidate;
267
+
268
+ if (!shouldTraverseChildren) {
269
+ continue;
270
+ }
271
+
272
+ const childEntries = await readDirectoryEntries(currentWorkspaceEntry.directoryPath);
273
+ for (const childEntry of childEntries) {
274
+ if (!childEntry.isDirectory()) {
275
+ continue;
276
+ }
277
+
278
+ if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(childEntry.name)) {
279
+ continue;
280
+ }
281
+
282
+ const childLooksRelevant = looksLikeWorkspaceSearchCandidate(childEntry.name);
283
+ if (!childLooksRelevant && !currentWorkspaceEntry.underWorkspaceContainer && !isWorkspaceContainer) {
284
+ continue;
285
+ }
286
+
287
+ const childDirectoryPath = path.join(currentWorkspaceEntry.directoryPath, childEntry.name);
288
+ const childRelativePath = path.join(currentWorkspaceEntry.relativePath, childEntry.name).replace(/\\/g, '/');
289
+
290
+ if (queuedWorkspacePaths.has(childRelativePath)) {
291
+ continue;
292
+ }
293
+
294
+ const childDirectoryEntries = await readDirectoryEntries(childDirectoryPath);
295
+ const childMarkerNames = new Set(childDirectoryEntries.map((directoryEntry) => directoryEntry.name));
296
+ const childIsProjectCandidate = hasProjectMarkers(childMarkerNames);
297
+ const childIsWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(childEntry.name.toLowerCase());
298
+
299
+ if (!childIsProjectCandidate && !childIsWorkspaceContainer && !childLooksRelevant) {
300
+ continue;
301
+ }
302
+
303
+ workspaceQueue.push({
304
+ directoryPath: childDirectoryPath,
305
+ relativePath: childRelativePath,
306
+ markerNames: childMarkerNames,
307
+ depth: currentWorkspaceEntry.depth + 1,
308
+ underWorkspaceContainer: currentWorkspaceEntry.underWorkspaceContainer || isWorkspaceContainer || childIsWorkspaceContainer,
309
+ });
310
+ queuedWorkspacePaths.add(childRelativePath);
311
+ }
312
+ }
313
+
314
+ return nestedWorkspaceProjects;
315
+ }
316
+
43
317
  async function collectFrontendSourceFilePaths(directoryPath, collectedFilePaths = []) {
44
318
  if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
45
319
  return collectedFilePaths;
@@ -79,13 +353,18 @@ function countPatternMatches(sourceText, pattern) {
79
353
  return Array.from(sourceText.matchAll(pattern)).length;
80
354
  }
81
355
 
82
- async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames) {
356
+ async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, scanRootDirectoryPaths = []) {
83
357
  const candidateDirectoryPaths = FRONTEND_SCAN_DIRECTORY_NAMES
84
358
  .filter((directoryName) => markerNames.has(directoryName))
85
359
  .map((directoryName) => path.join(targetDirectoryPath, directoryName));
86
- const resolvedCandidateDirectoryPaths = candidateDirectoryPaths.length > 0
87
- ? candidateDirectoryPaths
88
- : [targetDirectoryPath];
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];
89
368
  const scannedFilePaths = [];
90
369
 
91
370
  for (const candidateDirectoryPath of resolvedCandidateDirectoryPaths) {
@@ -144,63 +423,11 @@ async function collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames)
144
423
  };
145
424
  }
146
425
 
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
-
426
+ function analyzeUiSignalsForMarkerSet(markerNames, packageManifest, sourceLabel = null) {
427
+ const detectedUiMarkers = DIRECT_UI_MARKER_NAMES.filter((markerName) => markerNames.has(markerName));
201
428
  const dependencySource = {
202
- ...(resolvedPackageManifest?.dependencies || {}),
203
- ...(resolvedPackageManifest?.devDependencies || {}),
429
+ ...(packageManifest?.dependencies || {}),
430
+ ...(packageManifest?.devDependencies || {}),
204
431
  };
205
432
  const detectableUiDependencies = [
206
433
  'next',
@@ -211,10 +438,6 @@ export async function detectUiScopeSignals({
211
438
  'tailwindcss',
212
439
  ];
213
440
  const detectedUiDependencies = detectableUiDependencies.filter((dependencyName) => dependencySource[dependencyName]);
214
- if (detectedUiDependencies.length > 0) {
215
- signalReasons.push(`ui dependencies: ${detectedUiDependencies.join(', ')}`);
216
- }
217
-
218
441
  const hasStrongUiMarker = detectedUiMarkers.some((markerName) => (
219
442
  markerName.startsWith('next.config')
220
443
  || markerName === 'react-native.config.js'
@@ -223,46 +446,63 @@ export async function detectUiScopeSignals({
223
446
  ));
224
447
  const hasUiDependencies = detectedUiDependencies.length > 0;
225
448
  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;
449
+ const signalReasons = [];
450
+ const sourcePrefix = sourceLabel ? `${sourceLabel}: ` : '';
451
+
452
+ if (detectedUiMarkers.length > 0) {
453
+ signalReasons.push(`${sourcePrefix}ui markers: ${detectedUiMarkers.join(', ')}`);
454
+ }
455
+
456
+ if (detectedUiDependencies.length > 0) {
457
+ signalReasons.push(`${sourcePrefix}ui dependencies: ${detectedUiDependencies.join(', ')}`);
458
+ }
231
459
 
232
460
  return {
233
- isUiScopeLikely,
234
461
  signalReasons,
235
462
  detectedUiMarkers,
236
463
  detectedUiDependencies,
237
- frontendEvidenceMetrics,
238
- packageManifest: resolvedPackageManifest,
464
+ hasStrongUiMarker,
465
+ hasUiDependencies,
466
+ hasStructuralUiMarkers,
239
467
  };
240
468
  }
241
469
 
242
- export async function detectProjectContext(targetDirectoryPath) {
243
- const markerNames = await collectProjectMarkers(targetDirectoryPath);
470
+ function collectStackDetectionCandidates(markerNames, evidencePrefix = null) {
244
471
  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')) {
472
+ const withEvidencePrefix = (evidenceItem) => evidencePrefix ? `${evidencePrefix}: ${evidenceItem}` : evidenceItem;
473
+
474
+ if (
475
+ markerNames.has('package.json')
476
+ || markerNames.has('tsconfig.json')
477
+ || markerNames.has('next.config.js')
478
+ || markerNames.has('next.config.mjs')
479
+ || markerNames.has('vite.config.js')
480
+ || markerNames.has('vite.config.mjs')
481
+ || markerNames.has('vite.config.ts')
482
+ ) {
248
483
  const evidence = [];
249
484
  let confidenceScore = 0.7;
250
485
 
251
486
  if (markerNames.has('package.json')) {
252
- evidence.push('package.json');
487
+ evidence.push(withEvidencePrefix('package.json'));
253
488
  confidenceScore += 0.12;
254
489
  }
255
490
 
256
491
  if (markerNames.has('tsconfig.json')) {
257
- evidence.push('tsconfig.json');
492
+ evidence.push(withEvidencePrefix('tsconfig.json'));
258
493
  confidenceScore += 0.12;
259
494
  }
260
495
 
261
496
  if (markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
262
- evidence.push('Next.js config');
497
+ evidence.push(withEvidencePrefix('Next.js config'));
263
498
  confidenceScore += 0.05;
264
499
  }
265
500
 
501
+ if (markerNames.has('vite.config.js') || markerNames.has('vite.config.mjs') || markerNames.has('vite.config.ts')) {
502
+ evidence.push(withEvidencePrefix('Vite config'));
503
+ confidenceScore += 0.08;
504
+ }
505
+
266
506
  detectionCandidates.push({
267
507
  stackFileName: 'typescript.md',
268
508
  confidenceScore: Math.min(confidenceScore, 0.97),
@@ -274,14 +514,16 @@ export async function detectProjectContext(targetDirectoryPath) {
274
514
  detectionCandidates.push({
275
515
  stackFileName: 'python.md',
276
516
  confidenceScore: markerNames.has('pyproject.toml') ? 0.96 : 0.78,
277
- evidence: markerNames.has('pyproject.toml') ? ['pyproject.toml'] : ['requirements.txt'],
517
+ evidence: markerNames.has('pyproject.toml')
518
+ ? [withEvidencePrefix('pyproject.toml')]
519
+ : [withEvidencePrefix('requirements.txt')],
278
520
  });
279
521
  }
280
522
 
281
523
  if (markerNames.has('pom.xml') || markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) {
282
524
  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');
525
+ if (markerNames.has('pom.xml')) evidence.push(withEvidencePrefix('pom.xml'));
526
+ if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push(withEvidencePrefix('Gradle build file'));
285
527
  detectionCandidates.push({
286
528
  stackFileName: 'java.md',
287
529
  confidenceScore: markerNames.has('pom.xml') ? 0.95 : 0.84,
@@ -293,7 +535,7 @@ export async function detectProjectContext(targetDirectoryPath) {
293
535
  detectionCandidates.push({
294
536
  stackFileName: 'php.md',
295
537
  confidenceScore: 0.95,
296
- evidence: ['composer.json'],
538
+ evidence: [withEvidencePrefix('composer.json')],
297
539
  });
298
540
  }
299
541
 
@@ -301,7 +543,7 @@ export async function detectProjectContext(targetDirectoryPath) {
301
543
  detectionCandidates.push({
302
544
  stackFileName: 'go.md',
303
545
  confidenceScore: 0.96,
304
- evidence: ['go.mod'],
546
+ evidence: [withEvidencePrefix('go.mod')],
305
547
  });
306
548
  }
307
549
 
@@ -309,7 +551,7 @@ export async function detectProjectContext(targetDirectoryPath) {
309
551
  detectionCandidates.push({
310
552
  stackFileName: 'rust.md',
311
553
  confidenceScore: 0.96,
312
- evidence: ['Cargo.toml'],
554
+ evidence: [withEvidencePrefix('Cargo.toml')],
313
555
  });
314
556
  }
315
557
 
@@ -317,7 +559,7 @@ export async function detectProjectContext(targetDirectoryPath) {
317
559
  detectionCandidates.push({
318
560
  stackFileName: 'ruby.md',
319
561
  confidenceScore: 0.95,
320
- evidence: ['Gemfile'],
562
+ evidence: [withEvidencePrefix('Gemfile')],
321
563
  });
322
564
  }
323
565
 
@@ -326,7 +568,7 @@ export async function detectProjectContext(targetDirectoryPath) {
326
568
  detectionCandidates.push({
327
569
  stackFileName: 'csharp.md',
328
570
  confidenceScore: 0.95,
329
- evidence: ['.sln or .csproj file'],
571
+ evidence: [withEvidencePrefix('.sln or .csproj file')],
330
572
  });
331
573
  }
332
574
 
@@ -334,7 +576,7 @@ export async function detectProjectContext(targetDirectoryPath) {
334
576
  detectionCandidates.push({
335
577
  stackFileName: 'react-native.md',
336
578
  confidenceScore: 0.9,
337
- evidence: ['package.json', 'mobile runtime markers'],
579
+ evidence: [withEvidencePrefix('package.json'), withEvidencePrefix('mobile runtime markers')],
338
580
  });
339
581
  }
340
582
 
@@ -342,10 +584,122 @@ export async function detectProjectContext(targetDirectoryPath) {
342
584
  detectionCandidates.push({
343
585
  stackFileName: 'flutter.md',
344
586
  confidenceScore: 0.94,
345
- evidence: ['pubspec.yaml'],
587
+ evidence: [withEvidencePrefix('pubspec.yaml')],
346
588
  });
347
589
  }
348
590
 
591
+ return detectionCandidates;
592
+ }
593
+
594
+ export async function detectUiScopeSignals({
595
+ targetDirectoryPath,
596
+ selectedStackFileName,
597
+ selectedBlueprintFileName,
598
+ packageManifest = null,
599
+ projectScopeKey = null,
600
+ projectScopeSourceLabel = 'project scope',
601
+ }) {
602
+ const signalReasons = [];
603
+ const markerNames = await collectProjectMarkers(targetDirectoryPath);
604
+ const resolvedPackageManifest = packageManifest || await readPackageJsonIfExists(targetDirectoryPath);
605
+ const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
606
+
607
+ const normalizedProjectScopeKey = String(projectScopeKey || '').trim().toLowerCase();
608
+ if (normalizedProjectScopeKey === 'frontend-only' || normalizedProjectScopeKey === 'both') {
609
+ signalReasons.push(`${projectScopeSourceLabel}: ${normalizedProjectScopeKey}`);
610
+ }
611
+
612
+ const selectedStackKey = String(selectedStackFileName || '').trim().toLowerCase();
613
+ if (selectedStackKey === 'react-native.md' || selectedStackKey === 'flutter.md') {
614
+ signalReasons.push(`selected stack implies UI runtime: ${selectedStackKey}`);
615
+ }
616
+
617
+ const selectedBlueprintKey = String(selectedBlueprintFileName || '').trim().toLowerCase();
618
+ if (selectedBlueprintKey.includes('frontend') || selectedBlueprintKey.includes('landing') || selectedBlueprintKey.includes('mobile-app')) {
619
+ signalReasons.push(`selected blueprint implies UI scope: ${selectedBlueprintKey}`);
620
+ }
621
+
622
+ const rootUiSignals = analyzeUiSignalsForMarkerSet(markerNames, resolvedPackageManifest);
623
+ signalReasons.push(...rootUiSignals.signalReasons);
624
+
625
+ const nestedUiSignals = nestedWorkspaceProjects
626
+ .map((nestedWorkspaceProject) => ({
627
+ ...nestedWorkspaceProject,
628
+ ...analyzeUiSignalsForMarkerSet(
629
+ nestedWorkspaceProject.markerNames,
630
+ nestedWorkspaceProject.packageManifest,
631
+ `workspace ${nestedWorkspaceProject.relativePath}`
632
+ ),
633
+ }))
634
+ .filter((nestedWorkspaceProject) => nestedWorkspaceProject.signalReasons.length > 0);
635
+
636
+ for (const nestedUiSignal of nestedUiSignals) {
637
+ signalReasons.push(...nestedUiSignal.signalReasons);
638
+ }
639
+
640
+ const detectedUiMarkers = Array.from(new Set([
641
+ ...rootUiSignals.detectedUiMarkers,
642
+ ...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiMarkers),
643
+ ]));
644
+ const detectedUiDependencies = Array.from(new Set([
645
+ ...rootUiSignals.detectedUiDependencies,
646
+ ...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiDependencies),
647
+ ]));
648
+
649
+ const hasStrongUiMarker = rootUiSignals.hasStrongUiMarker
650
+ || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStrongUiMarker);
651
+ const hasUiDependencies = rootUiSignals.hasUiDependencies
652
+ || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasUiDependencies);
653
+ const hasStructuralUiMarkers = rootUiSignals.hasStructuralUiMarkers
654
+ || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStructuralUiMarkers);
655
+ const isUiScopeLikely = signalReasons.length > 0
656
+ && (hasStrongUiMarker || hasUiDependencies || hasStructuralUiMarkers || normalizedProjectScopeKey.length > 0);
657
+ const preferredUiWorkspaceEntry = nestedUiSignals.find((nestedUiSignal) => (
658
+ nestedUiSignal.hasStrongUiMarker
659
+ || nestedUiSignal.hasUiDependencies
660
+ || nestedUiSignal.hasStructuralUiMarkers
661
+ )) || null;
662
+ const frontendScanRootDirectoryPaths = (
663
+ !rootUiSignals.hasStrongUiMarker
664
+ && !rootUiSignals.hasUiDependencies
665
+ && !rootUiSignals.hasStructuralUiMarkers
666
+ && nestedUiSignals.length > 0
667
+ )
668
+ ? nestedUiSignals.map((nestedUiSignal) => nestedUiSignal.directoryPath)
669
+ : [];
670
+ const frontendEvidenceMetrics = isUiScopeLikely
671
+ ? await collectFrontendEvidenceMetrics(targetDirectoryPath, markerNames, frontendScanRootDirectoryPaths)
672
+ : null;
673
+
674
+ return {
675
+ isUiScopeLikely,
676
+ signalReasons,
677
+ detectedUiMarkers,
678
+ detectedUiDependencies,
679
+ frontendEvidenceMetrics,
680
+ packageManifest: preferredUiWorkspaceEntry?.packageManifest || resolvedPackageManifest,
681
+ workspaceUiEntries: nestedUiSignals.map((nestedUiSignal) => ({
682
+ relativePath: nestedUiSignal.relativePath,
683
+ detectedUiMarkers: nestedUiSignal.detectedUiMarkers,
684
+ detectedUiDependencies: nestedUiSignal.detectedUiDependencies,
685
+ })),
686
+ };
687
+ }
688
+
689
+ export async function detectProjectContext(targetDirectoryPath) {
690
+ const markerNames = await collectProjectMarkers(targetDirectoryPath);
691
+ const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
692
+ const detectionCandidates = [
693
+ ...collectStackDetectionCandidates(markerNames),
694
+ ...nestedWorkspaceProjects.flatMap((nestedWorkspaceProject) => (
695
+ collectStackDetectionCandidates(
696
+ nestedWorkspaceProject.markerNames,
697
+ nestedWorkspaceProject.relativePath
698
+ )
699
+ )),
700
+ ];
701
+ const hasExistingProjectFiles = markerNames.size > 0;
702
+
349
703
  if (detectionCandidates.length === 0) {
350
704
  return {
351
705
  hasExistingProjectFiles,