@ryuenn3123/agentic-senior-core 3.0.50 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.agent-context/review-checklists/pr-checklist.md +1 -0
  2. package/.agent-context/rules/api-docs.md +63 -47
  3. package/.agent-context/rules/architecture.md +133 -120
  4. package/.agent-context/rules/database-design.md +36 -18
  5. package/.agent-context/rules/docker-runtime.md +66 -43
  6. package/.agent-context/rules/efficiency-vs-hype.md +38 -17
  7. package/.agent-context/rules/error-handling.md +35 -16
  8. package/.agent-context/rules/event-driven.md +35 -18
  9. package/.agent-context/rules/frontend-architecture.md +103 -76
  10. package/.agent-context/rules/git-workflow.md +81 -197
  11. package/.agent-context/rules/microservices.md +42 -41
  12. package/.agent-context/rules/naming-conv.md +27 -8
  13. package/.agent-context/rules/performance.md +32 -12
  14. package/.agent-context/rules/realtime.md +26 -9
  15. package/.agent-context/rules/security.md +39 -20
  16. package/.agent-context/rules/testing.md +36 -16
  17. package/AGENTS.md +9 -9
  18. package/README.md +10 -1
  19. package/lib/cli/commands/init.mjs +1 -0
  20. package/lib/cli/compiler.mjs +1 -0
  21. package/lib/cli/detector/constants.mjs +135 -0
  22. package/lib/cli/detector/design-evidence/collector.mjs +256 -0
  23. package/lib/cli/detector/design-evidence/constants.mjs +39 -0
  24. package/lib/cli/detector/design-evidence/file-traversal.mjs +83 -0
  25. package/lib/cli/detector/design-evidence/structured-attribute-evidence.mjs +117 -0
  26. package/lib/cli/detector/design-evidence/summary.mjs +109 -0
  27. package/lib/cli/detector/design-evidence/utility-helpers.mjs +122 -0
  28. package/lib/cli/detector/design-evidence.mjs +25 -610
  29. package/lib/cli/detector/stack-detection.mjs +243 -0
  30. package/lib/cli/detector/ui-signals.mjs +150 -0
  31. package/lib/cli/detector/workspace-scan.mjs +177 -0
  32. package/lib/cli/detector.mjs +20 -688
  33. package/lib/cli/memory-continuity.mjs +1 -0
  34. package/lib/cli/project-scaffolder/design-contract/sections/audits.mjs +96 -0
  35. package/lib/cli/project-scaffolder/design-contract/sections/conceptual-anchor.mjs +116 -0
  36. package/lib/cli/project-scaffolder/design-contract/sections/execution-handoff.mjs +211 -0
  37. package/lib/cli/project-scaffolder/design-contract/seed-signals.mjs +79 -0
  38. package/lib/cli/project-scaffolder/design-contract/signal-vocab.mjs +64 -0
  39. package/lib/cli/project-scaffolder/design-contract/validation/anchor-validators.mjs +222 -0
  40. package/lib/cli/project-scaffolder/design-contract/validation/audit-validators.mjs +117 -0
  41. package/lib/cli/project-scaffolder/design-contract/validation/completeness.mjs +83 -0
  42. package/lib/cli/project-scaffolder/design-contract/validation/execution-validators.mjs +328 -0
  43. package/lib/cli/project-scaffolder/design-contract/validation/helpers.mjs +8 -0
  44. package/lib/cli/project-scaffolder/design-contract/validation/structural-validators.mjs +79 -0
  45. package/lib/cli/project-scaffolder/design-contract/validation/system-validators.mjs +256 -0
  46. package/lib/cli/project-scaffolder/design-contract/validation.mjs +59 -896
  47. package/lib/cli/project-scaffolder/design-contract.mjs +147 -557
  48. package/mcp.json +30 -9
  49. package/package.json +17 -2
  50. package/scripts/audit-cache-layer-contract.mjs +258 -0
  51. package/scripts/audit-caching-scope-hygiene.mjs +263 -0
  52. package/scripts/audit-file-size.mjs +219 -0
  53. package/scripts/audit-reflection-citations.mjs +163 -0
  54. package/scripts/audit-release-bundle.mjs +170 -0
  55. package/scripts/audit-rule-id-uniqueness.mjs +313 -0
  56. package/scripts/benchmark-evidence-bundle.mjs +1 -0
  57. package/scripts/build-release-benchmark-bundle.mjs +204 -0
  58. package/scripts/context-triggered-audit.mjs +1 -0
  59. package/scripts/documentation-boundary-audit.mjs +1 -0
  60. package/scripts/explain-on-demand-audit.mjs +2 -1
  61. package/scripts/frontend-usability-audit.mjs +10 -10
  62. package/scripts/llm-judge/checklist-loader.mjs +45 -0
  63. package/scripts/llm-judge/constants.mjs +66 -0
  64. package/scripts/llm-judge/diff-collection.mjs +74 -0
  65. package/scripts/llm-judge/prompting.mjs +78 -0
  66. package/scripts/llm-judge/providers.mjs +111 -0
  67. package/scripts/llm-judge/verdict.mjs +134 -0
  68. package/scripts/llm-judge.mjs +21 -482
  69. package/scripts/mcp-server/tool-registry.mjs +55 -0
  70. package/scripts/mcp-server/tools.mjs +137 -1
  71. package/scripts/migrate-rule-format/id-prefix-table.mjs +37 -0
  72. package/scripts/migrate-rule-format/parse-legacy.mjs +180 -0
  73. package/scripts/migrate-rule-format/render-new.mjs +169 -0
  74. package/scripts/migrate-rule-format/roundtrip-validate.mjs +89 -0
  75. package/scripts/migrate-rule-format.mjs +192 -0
  76. package/scripts/release-gate/constants.mjs +1 -1
  77. package/scripts/release-gate/static-checks.mjs +1 -1
  78. package/scripts/rules-guardian-audit.mjs +5 -2
  79. package/scripts/single-source-lazy-loading-audit.mjs +2 -1
  80. package/scripts/ui-design-judge/git-input.mjs +3 -0
  81. package/scripts/validate/config.mjs +3 -2
  82. package/scripts/validate/coverage-checks.mjs +1 -1
  83. package/scripts/validate.mjs +93 -1
@@ -1,691 +1,23 @@
1
1
  /**
2
- * Stack Detector — Project context auto-detection.
3
- * Depends on: constants.mjs, utils.mjs
2
+ * Stack Detector — project context auto-detection. Aggregator that re-exports
3
+ * the public surface of the detector subsystem. Implementation lives under
4
+ * lib/cli/detector/* split per concern: workspace scan, UI signal analysis,
5
+ * and stack detection scoring.
6
+ *
7
+ * Public exports:
8
+ * collectProjectMarkers — read top-level markers in a directory
9
+ * detectProjectContext — top-level stack detection with ranked candidates
10
+ * detectUiScopeSignals — UI scope detection plus design evidence scan;
11
+ * returns frontendEvidenceMetrics and
12
+ * designEvidenceSummary when UI scope is detected
13
+ * buildDetectionSummary — human-readable detection summary text
14
+ * formatDetectionCandidates — formatter for ranked detection candidates
4
15
  */
5
- import fs from 'node:fs/promises';
6
- import path from 'node:path';
7
16
 
8
- import {
9
- collectFrontendDesignEvidence,
10
- FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
11
- } from './detector/design-evidence.mjs';
12
- import { toTitleCase } from './utils.mjs';
13
-
14
- const WORKSPACE_SCAN_MAX_DEPTH = 3;
15
- const WORKSPACE_SCAN_MAX_DIRECTORIES = 120;
16
- const WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES = new Set([
17
- ...FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
18
- '.agent-context',
19
- '.agents',
20
- '.cursor',
21
- '.gemini',
22
- '.github',
23
- '.idea',
24
- '.vscode',
25
- '.windsurf',
26
- '.zed',
27
- ]);
28
- const WORKSPACE_CONTAINER_DIRECTORY_NAMES = new Set([
29
- 'admin',
30
- 'admins',
31
- 'api',
32
- 'apis',
33
- 'app',
34
- 'apps',
35
- 'backend',
36
- 'backends',
37
- 'client',
38
- 'clients',
39
- 'dashboard',
40
- 'dashboards',
41
- 'frontend',
42
- 'frontends',
43
- 'mobile',
44
- 'mobiles',
45
- 'package',
46
- 'packages',
47
- 'pkg',
48
- 'server',
49
- 'servers',
50
- 'service',
51
- 'services',
52
- 'site',
53
- 'sites',
54
- 'ui',
55
- 'web',
56
- 'worker',
57
- 'workers',
58
- ]);
59
- const WORKSPACE_ROOT_MARKER_FILE_NAMES = new Set([
60
- 'lerna.json',
61
- 'nx.json',
62
- 'pnpm-workspace.yaml',
63
- 'turbo.json',
64
- ]);
65
- const DIRECT_UI_MARKER_NAMES = [
66
- 'src',
67
- 'next.config.js',
68
- 'next.config.mjs',
69
- 'next.config.ts',
70
- 'tailwind.config.js',
71
- 'tailwind.config.mjs',
72
- 'tailwind.config.ts',
73
- 'vite.config.js',
74
- 'vite.config.mjs',
75
- 'vite.config.ts',
76
- 'react-native.config.js',
77
- 'app',
78
- 'pages',
79
- 'components',
80
- 'public',
81
- 'styles',
82
- 'android',
83
- 'ios',
84
- 'index.html',
85
- ];
86
- const PROJECT_MARKER_FILE_NAMES = new Set([
87
- 'Cargo.toml',
88
- 'Gemfile',
89
- 'build.gradle',
90
- 'build.gradle.kts',
91
- 'composer.json',
92
- 'go.mod',
93
- 'package.json',
94
- 'pom.xml',
95
- 'pubspec.yaml',
96
- 'pyproject.toml',
97
- 'react-native.config.js',
98
- 'requirements.txt',
99
- 'tsconfig.json',
100
- ...DIRECT_UI_MARKER_NAMES,
101
- ]);
102
- const INTERNAL_GOVERNANCE_SURFACE_NAMES = new Set([
103
- '.agent-context',
104
- '.agent-instructions.md',
105
- '.agentic-backup',
106
- '.agents',
107
- '.clauderc',
108
- '.cursorrules',
109
- '.cursor',
110
- '.gemini',
111
- '.github',
112
- '.instructions.md',
113
- '.vscode',
114
- '.windsurf',
115
- '.windsurfrules',
116
- '.zed',
117
- 'AGENTS.md',
118
- 'CLAUDE.md',
119
- 'GEMINI.md',
120
- 'mcp.json',
121
- ]);
122
-
123
- function looksLikeWorkspaceSearchCandidate(directoryName) {
124
- const normalizedDirectoryName = String(directoryName || '').trim().toLowerCase();
125
-
126
- if (!normalizedDirectoryName) {
127
- return false;
128
- }
129
-
130
- if (WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(normalizedDirectoryName)) {
131
- return true;
132
- }
133
-
134
- return [
135
- 'admin',
136
- 'api',
137
- 'app',
138
- 'backend',
139
- 'client',
140
- 'dashboard',
141
- 'frontend',
142
- 'mobile',
143
- 'package',
144
- 'server',
145
- 'service',
146
- 'site',
147
- 'ui',
148
- 'web',
149
- 'worker',
150
- ].some((keyword) => normalizedDirectoryName.includes(keyword));
151
- }
152
-
153
- function hasProjectMarkers(markerNames) {
154
- return Array.from(markerNames).some((markerName) => (
155
- PROJECT_MARKER_FILE_NAMES.has(markerName)
156
- || markerName.endsWith('.csproj')
157
- || markerName.endsWith('.sln')
158
- ));
159
- }
160
-
161
- export async function collectProjectMarkers(targetDirectoryPath) {
162
- const markerNames = new Set();
163
- const directoryEntries = await fs.readdir(targetDirectoryPath, { withFileTypes: true });
164
-
165
- for (const directoryEntry of directoryEntries) {
166
- if (directoryEntry.name === '.git' || directoryEntry.name === 'node_modules') {
167
- continue;
168
- }
169
-
170
- if (INTERNAL_GOVERNANCE_SURFACE_NAMES.has(directoryEntry.name)) {
171
- continue;
172
- }
173
-
174
- markerNames.add(directoryEntry.name);
175
- }
176
-
177
- return markerNames;
178
- }
179
-
180
- async function readPackageJsonIfExists(targetDirectoryPath) {
181
- const packageJsonPath = path.join(targetDirectoryPath, 'package.json');
182
-
183
- try {
184
- const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
185
- return JSON.parse(packageJsonContent);
186
- } catch {
187
- return null;
188
- }
189
- }
190
-
191
- async function readDirectoryEntries(directoryPath) {
192
- try {
193
- return await fs.readdir(directoryPath, { withFileTypes: true });
194
- } catch {
195
- return [];
196
- }
197
- }
198
-
199
- async function collectNestedWorkspaceProjects(targetDirectoryPath) {
200
- const rootDirectoryEntries = await readDirectoryEntries(targetDirectoryPath);
201
- const rootMarkerNames = new Set(rootDirectoryEntries.map((directoryEntry) => directoryEntry.name));
202
- const rootLooksLikeWorkspace = Array.from(rootMarkerNames).some((markerName) => (
203
- WORKSPACE_ROOT_MARKER_FILE_NAMES.has(markerName)
204
- || looksLikeWorkspaceSearchCandidate(markerName)
205
- ));
206
- const nestedWorkspaceProjects = [];
207
- const queuedWorkspacePaths = new Set();
208
- const workspaceQueue = [];
209
- let scannedDirectoryCount = 0;
210
-
211
- for (const rootDirectoryEntry of rootDirectoryEntries) {
212
- if (!rootDirectoryEntry.isDirectory()) {
213
- continue;
214
- }
215
-
216
- if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(rootDirectoryEntry.name)) {
217
- continue;
218
- }
219
-
220
- const shouldInspectRootChild = rootLooksLikeWorkspace
221
- || looksLikeWorkspaceSearchCandidate(rootDirectoryEntry.name);
222
-
223
- if (!shouldInspectRootChild) {
224
- continue;
225
- }
226
-
227
- const rootChildDirectoryPath = path.join(targetDirectoryPath, rootDirectoryEntry.name);
228
- const rootChildEntries = await readDirectoryEntries(rootChildDirectoryPath);
229
- const rootChildMarkerNames = new Set(rootChildEntries.map((directoryEntry) => directoryEntry.name));
230
- const rootChildRelativePath = rootDirectoryEntry.name.replace(/\\/g, '/');
231
-
232
- workspaceQueue.push({
233
- directoryPath: rootChildDirectoryPath,
234
- relativePath: rootChildRelativePath,
235
- markerNames: rootChildMarkerNames,
236
- depth: 1,
237
- underWorkspaceContainer: WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(rootDirectoryEntry.name.toLowerCase()),
238
- });
239
- queuedWorkspacePaths.add(rootChildRelativePath);
240
- }
241
-
242
- while (workspaceQueue.length > 0 && scannedDirectoryCount < WORKSPACE_SCAN_MAX_DIRECTORIES) {
243
- const currentWorkspaceEntry = workspaceQueue.shift();
244
- scannedDirectoryCount += 1;
245
-
246
- const isProjectCandidate = hasProjectMarkers(currentWorkspaceEntry.markerNames);
247
- const currentDirectoryName = path.basename(currentWorkspaceEntry.directoryPath).toLowerCase();
248
- const isWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(currentDirectoryName);
249
-
250
- if (isProjectCandidate) {
251
- nestedWorkspaceProjects.push({
252
- directoryPath: currentWorkspaceEntry.directoryPath,
253
- relativePath: currentWorkspaceEntry.relativePath,
254
- markerNames: currentWorkspaceEntry.markerNames,
255
- packageManifest: currentWorkspaceEntry.markerNames.has('package.json')
256
- ? await readPackageJsonIfExists(currentWorkspaceEntry.directoryPath)
257
- : null,
258
- });
259
- }
260
-
261
- if (currentWorkspaceEntry.depth >= WORKSPACE_SCAN_MAX_DEPTH) {
262
- continue;
263
- }
264
-
265
- const shouldTraverseChildren = currentWorkspaceEntry.underWorkspaceContainer
266
- || isWorkspaceContainer
267
- || !isProjectCandidate;
268
-
269
- if (!shouldTraverseChildren) {
270
- continue;
271
- }
272
-
273
- const childEntries = await readDirectoryEntries(currentWorkspaceEntry.directoryPath);
274
- for (const childEntry of childEntries) {
275
- if (!childEntry.isDirectory()) {
276
- continue;
277
- }
278
-
279
- if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(childEntry.name)) {
280
- continue;
281
- }
282
-
283
- const childLooksRelevant = looksLikeWorkspaceSearchCandidate(childEntry.name);
284
- if (!childLooksRelevant && !currentWorkspaceEntry.underWorkspaceContainer && !isWorkspaceContainer) {
285
- continue;
286
- }
287
-
288
- const childDirectoryPath = path.join(currentWorkspaceEntry.directoryPath, childEntry.name);
289
- const childRelativePath = path.join(currentWorkspaceEntry.relativePath, childEntry.name).replace(/\\/g, '/');
290
-
291
- if (queuedWorkspacePaths.has(childRelativePath)) {
292
- continue;
293
- }
294
-
295
- const childDirectoryEntries = await readDirectoryEntries(childDirectoryPath);
296
- const childMarkerNames = new Set(childDirectoryEntries.map((directoryEntry) => directoryEntry.name));
297
- const childIsProjectCandidate = hasProjectMarkers(childMarkerNames);
298
- const childIsWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(childEntry.name.toLowerCase());
299
-
300
- if (!childIsProjectCandidate && !childIsWorkspaceContainer && !childLooksRelevant) {
301
- continue;
302
- }
303
-
304
- workspaceQueue.push({
305
- directoryPath: childDirectoryPath,
306
- relativePath: childRelativePath,
307
- markerNames: childMarkerNames,
308
- depth: currentWorkspaceEntry.depth + 1,
309
- underWorkspaceContainer: currentWorkspaceEntry.underWorkspaceContainer || isWorkspaceContainer || childIsWorkspaceContainer,
310
- });
311
- queuedWorkspacePaths.add(childRelativePath);
312
- }
313
- }
314
-
315
- return nestedWorkspaceProjects;
316
- }
317
-
318
- function analyzeUiSignalsForMarkerSet(markerNames, packageManifest, sourceLabel = null) {
319
- const detectedUiMarkers = DIRECT_UI_MARKER_NAMES.filter((markerName) => markerNames.has(markerName));
320
- const dependencySource = {
321
- ...(packageManifest?.dependencies || {}),
322
- ...(packageManifest?.devDependencies || {}),
323
- };
324
- const detectableUiDependencies = [
325
- 'next',
326
- 'react',
327
- 'react-dom',
328
- 'react-native',
329
- 'expo',
330
- 'tailwindcss',
331
- ];
332
- const detectedUiDependencies = detectableUiDependencies.filter((dependencyName) => dependencySource[dependencyName]);
333
- const hasStrongUiMarker = detectedUiMarkers.some((markerName) => (
334
- markerName.startsWith('next.config')
335
- || markerName === 'react-native.config.js'
336
- || markerName === 'android'
337
- || markerName === 'ios'
338
- ));
339
- const hasUiDependencies = detectedUiDependencies.length > 0;
340
- const hasStructuralUiMarkers = detectedUiMarkers.length >= 2;
341
- const signalReasons = [];
342
- const sourcePrefix = sourceLabel ? `${sourceLabel}: ` : '';
343
-
344
- if (detectedUiMarkers.length > 0) {
345
- signalReasons.push(`${sourcePrefix}ui markers: ${detectedUiMarkers.join(', ')}`);
346
- }
347
-
348
- if (detectedUiDependencies.length > 0) {
349
- signalReasons.push(`${sourcePrefix}ui dependencies: ${detectedUiDependencies.join(', ')}`);
350
- }
351
-
352
- return {
353
- signalReasons,
354
- detectedUiMarkers,
355
- detectedUiDependencies,
356
- hasStrongUiMarker,
357
- hasUiDependencies,
358
- hasStructuralUiMarkers,
359
- };
360
- }
361
-
362
- function collectStackDetectionCandidates(markerNames, evidencePrefix = null) {
363
- const detectionCandidates = [];
364
- const withEvidencePrefix = (evidenceItem) => evidencePrefix ? `${evidencePrefix}: ${evidenceItem}` : evidenceItem;
365
-
366
- if (
367
- markerNames.has('package.json')
368
- || markerNames.has('tsconfig.json')
369
- || markerNames.has('next.config.js')
370
- || markerNames.has('next.config.mjs')
371
- || markerNames.has('vite.config.js')
372
- || markerNames.has('vite.config.mjs')
373
- || markerNames.has('vite.config.ts')
374
- ) {
375
- const evidence = [];
376
- let confidenceScore = 0.7;
377
-
378
- if (markerNames.has('package.json')) {
379
- evidence.push(withEvidencePrefix('package.json'));
380
- confidenceScore += 0.12;
381
- }
382
-
383
- if (markerNames.has('tsconfig.json')) {
384
- evidence.push(withEvidencePrefix('tsconfig.json'));
385
- confidenceScore += 0.12;
386
- }
387
-
388
- if (markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
389
- evidence.push(withEvidencePrefix('Next.js config'));
390
- confidenceScore += 0.05;
391
- }
392
-
393
- if (markerNames.has('vite.config.js') || markerNames.has('vite.config.mjs') || markerNames.has('vite.config.ts')) {
394
- evidence.push(withEvidencePrefix('Vite config'));
395
- confidenceScore += 0.08;
396
- }
397
-
398
- detectionCandidates.push({
399
- stackFileName: 'typescript.md',
400
- confidenceScore: Math.min(confidenceScore, 0.97),
401
- evidence,
402
- });
403
- }
404
-
405
- if (markerNames.has('pyproject.toml') || markerNames.has('requirements.txt')) {
406
- detectionCandidates.push({
407
- stackFileName: 'python.md',
408
- confidenceScore: markerNames.has('pyproject.toml') ? 0.96 : 0.78,
409
- evidence: markerNames.has('pyproject.toml')
410
- ? [withEvidencePrefix('pyproject.toml')]
411
- : [withEvidencePrefix('requirements.txt')],
412
- });
413
- }
414
-
415
- if (markerNames.has('pom.xml') || markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) {
416
- const evidence = [];
417
- if (markerNames.has('pom.xml')) evidence.push(withEvidencePrefix('pom.xml'));
418
- if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push(withEvidencePrefix('Gradle build file'));
419
- detectionCandidates.push({
420
- stackFileName: 'java.md',
421
- confidenceScore: markerNames.has('pom.xml') ? 0.95 : 0.84,
422
- evidence,
423
- });
424
- }
425
-
426
- if (markerNames.has('composer.json')) {
427
- detectionCandidates.push({
428
- stackFileName: 'php.md',
429
- confidenceScore: 0.95,
430
- evidence: [withEvidencePrefix('composer.json')],
431
- });
432
- }
433
-
434
- if (markerNames.has('go.mod')) {
435
- detectionCandidates.push({
436
- stackFileName: 'go.md',
437
- confidenceScore: 0.96,
438
- evidence: [withEvidencePrefix('go.mod')],
439
- });
440
- }
441
-
442
- if (markerNames.has('Cargo.toml')) {
443
- detectionCandidates.push({
444
- stackFileName: 'rust.md',
445
- confidenceScore: 0.96,
446
- evidence: [withEvidencePrefix('Cargo.toml')],
447
- });
448
- }
449
-
450
- if (markerNames.has('Gemfile')) {
451
- detectionCandidates.push({
452
- stackFileName: 'ruby.md',
453
- confidenceScore: 0.95,
454
- evidence: [withEvidencePrefix('Gemfile')],
455
- });
456
- }
457
-
458
- const hasDotNetMarker = Array.from(markerNames).some((markerName) => markerName.endsWith('.sln') || markerName.endsWith('.csproj'));
459
- if (hasDotNetMarker) {
460
- detectionCandidates.push({
461
- stackFileName: 'csharp.md',
462
- confidenceScore: 0.95,
463
- evidence: [withEvidencePrefix('.sln or .csproj file')],
464
- });
465
- }
466
-
467
- if (markerNames.has('package.json') && (markerNames.has('android') || markerNames.has('ios') || markerNames.has('react-native.config.js'))) {
468
- detectionCandidates.push({
469
- stackFileName: 'react-native.md',
470
- confidenceScore: 0.9,
471
- evidence: [withEvidencePrefix('package.json'), withEvidencePrefix('mobile runtime markers')],
472
- });
473
- }
474
-
475
- if (markerNames.has('pubspec.yaml')) {
476
- detectionCandidates.push({
477
- stackFileName: 'flutter.md',
478
- confidenceScore: 0.94,
479
- evidence: [withEvidencePrefix('pubspec.yaml')],
480
- });
481
- }
482
-
483
- return detectionCandidates;
484
- }
485
-
486
- export async function detectUiScopeSignals({
487
- targetDirectoryPath,
488
- selectedStackFileName,
489
- selectedBlueprintFileName,
490
- packageManifest = null,
491
- projectScopeKey = null,
492
- projectScopeSourceLabel = 'project scope',
493
- }) {
494
- const signalReasons = [];
495
- const markerNames = await collectProjectMarkers(targetDirectoryPath);
496
- const resolvedPackageManifest = packageManifest || await readPackageJsonIfExists(targetDirectoryPath);
497
- const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
498
-
499
- const normalizedProjectScopeKey = String(projectScopeKey || '').trim().toLowerCase();
500
- if (normalizedProjectScopeKey === 'frontend-only' || normalizedProjectScopeKey === 'both') {
501
- signalReasons.push(`${projectScopeSourceLabel}: ${normalizedProjectScopeKey}`);
502
- }
503
-
504
- const selectedStackKey = String(selectedStackFileName || '').trim().toLowerCase();
505
- if (selectedStackKey === 'react-native.md' || selectedStackKey === 'flutter.md') {
506
- signalReasons.push(`selected stack implies UI runtime: ${selectedStackKey}`);
507
- }
508
-
509
- const selectedBlueprintKey = String(selectedBlueprintFileName || '').trim().toLowerCase();
510
- if (selectedBlueprintKey.includes('frontend') || selectedBlueprintKey.includes('landing') || selectedBlueprintKey.includes('mobile-app')) {
511
- signalReasons.push(`selected blueprint implies UI scope: ${selectedBlueprintKey}`);
512
- }
513
-
514
- const rootUiSignals = analyzeUiSignalsForMarkerSet(markerNames, resolvedPackageManifest);
515
- signalReasons.push(...rootUiSignals.signalReasons);
516
-
517
- const nestedUiSignals = nestedWorkspaceProjects
518
- .map((nestedWorkspaceProject) => ({
519
- ...nestedWorkspaceProject,
520
- ...analyzeUiSignalsForMarkerSet(
521
- nestedWorkspaceProject.markerNames,
522
- nestedWorkspaceProject.packageManifest,
523
- `workspace ${nestedWorkspaceProject.relativePath}`
524
- ),
525
- }))
526
- .filter((nestedWorkspaceProject) => nestedWorkspaceProject.signalReasons.length > 0);
527
-
528
- for (const nestedUiSignal of nestedUiSignals) {
529
- signalReasons.push(...nestedUiSignal.signalReasons);
530
- }
531
-
532
- const detectedUiMarkers = Array.from(new Set([
533
- ...rootUiSignals.detectedUiMarkers,
534
- ...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiMarkers),
535
- ]));
536
- const detectedUiDependencies = Array.from(new Set([
537
- ...rootUiSignals.detectedUiDependencies,
538
- ...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiDependencies),
539
- ]));
540
-
541
- const hasStrongUiMarker = rootUiSignals.hasStrongUiMarker
542
- || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStrongUiMarker);
543
- const hasUiDependencies = rootUiSignals.hasUiDependencies
544
- || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasUiDependencies);
545
- const hasStructuralUiMarkers = rootUiSignals.hasStructuralUiMarkers
546
- || nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStructuralUiMarkers);
547
- const isUiScopeLikely = signalReasons.length > 0
548
- && (hasStrongUiMarker || hasUiDependencies || hasStructuralUiMarkers || normalizedProjectScopeKey.length > 0);
549
- const preferredUiWorkspaceEntry = nestedUiSignals.find((nestedUiSignal) => (
550
- nestedUiSignal.hasStrongUiMarker
551
- || nestedUiSignal.hasUiDependencies
552
- || nestedUiSignal.hasStructuralUiMarkers
553
- )) || null;
554
- const frontendScanRootDirectoryPaths = (
555
- !rootUiSignals.hasStrongUiMarker
556
- && !rootUiSignals.hasUiDependencies
557
- && !rootUiSignals.hasStructuralUiMarkers
558
- && nestedUiSignals.length > 0
559
- )
560
- ? nestedUiSignals.map((nestedUiSignal) => nestedUiSignal.directoryPath)
561
- : [];
562
- const designEvidence = isUiScopeLikely
563
- ? await collectFrontendDesignEvidence({
564
- targetDirectoryPath,
565
- markerNames,
566
- scanRootDirectoryPaths: frontendScanRootDirectoryPaths,
567
- })
568
- : null;
569
- const frontendEvidenceMetrics = designEvidence?.frontendEvidenceMetrics || null;
570
- const designEvidenceSummary = designEvidence?.designEvidenceSummary || null;
571
-
572
- return {
573
- isUiScopeLikely,
574
- signalReasons,
575
- detectedUiMarkers,
576
- detectedUiDependencies,
577
- frontendEvidenceMetrics,
578
- designEvidenceSummary,
579
- packageManifest: preferredUiWorkspaceEntry?.packageManifest || resolvedPackageManifest,
580
- workspaceUiEntries: nestedUiSignals.map((nestedUiSignal) => ({
581
- relativePath: nestedUiSignal.relativePath,
582
- detectedUiMarkers: nestedUiSignal.detectedUiMarkers,
583
- detectedUiDependencies: nestedUiSignal.detectedUiDependencies,
584
- })),
585
- };
586
- }
587
-
588
- export async function detectProjectContext(targetDirectoryPath) {
589
- const markerNames = await collectProjectMarkers(targetDirectoryPath);
590
- const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
591
- const detectionCandidates = [
592
- ...collectStackDetectionCandidates(markerNames),
593
- ...nestedWorkspaceProjects.flatMap((nestedWorkspaceProject) => (
594
- collectStackDetectionCandidates(
595
- nestedWorkspaceProject.markerNames,
596
- nestedWorkspaceProject.relativePath
597
- )
598
- )),
599
- ];
600
- const hasExistingProjectFiles = markerNames.size > 0;
601
-
602
- if (detectionCandidates.length === 0) {
603
- return {
604
- hasExistingProjectFiles,
605
- detectedStackFileName: null,
606
- secondaryStackFileNames: [],
607
- detectedBlueprintFileName: null,
608
- confidenceLabel: null,
609
- confidenceScore: 0,
610
- confidenceGap: 0,
611
- detectionReasoning: 'No known project markers were detected.',
612
- rankedCandidates: [],
613
- evidence: [],
614
- };
615
- }
616
-
617
- detectionCandidates.sort((leftCandidate, rightCandidate) => rightCandidate.confidenceScore - leftCandidate.confidenceScore);
618
- const strongestCandidate = detectionCandidates[0];
619
- const secondStrongestCandidate = detectionCandidates[1];
620
- const confidenceGap = secondStrongestCandidate
621
- ? Number((strongestCandidate.confidenceScore - secondStrongestCandidate.confidenceScore).toFixed(2))
622
- : Number(strongestCandidate.confidenceScore.toFixed(2));
623
- const isAmbiguous = secondStrongestCandidate
624
- && confidenceGap < 0.08;
625
- const confidenceLabel = strongestCandidate.confidenceScore >= 0.9
626
- ? 'high'
627
- : strongestCandidate.confidenceScore >= 0.78
628
- ? 'medium'
629
- : 'low';
630
- const evidence = isAmbiguous
631
- ? [...strongestCandidate.evidence, `multiple stack signals detected`]
632
- : strongestCandidate.evidence;
633
- const rankedCandidates = detectionCandidates.slice(0, 3).map((detectionCandidate) => ({
634
- stackFileName: detectionCandidate.stackFileName,
635
- confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
636
- evidence: detectionCandidate.evidence,
637
- }));
638
- const secondaryStackFileNames = rankedCandidates
639
- .slice(1)
640
- .filter((rankedCandidate) => (strongestCandidate.confidenceScore - rankedCandidate.confidenceScore) < 0.08)
641
- .map((rankedCandidate) => rankedCandidate.stackFileName);
642
- const detectionReasoning = isAmbiguous
643
- ? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
644
- : `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(', ') || 'none'}.`;
645
-
646
- return {
647
- hasExistingProjectFiles,
648
- detectedStackFileName: strongestCandidate.stackFileName,
649
- secondaryStackFileNames,
650
- detectedBlueprintFileName: null,
651
- confidenceLabel,
652
- confidenceScore: strongestCandidate.confidenceScore,
653
- confidenceGap,
654
- detectionReasoning,
655
- rankedCandidates,
656
- evidence,
657
- };
658
- }
659
-
660
- export function buildDetectionSummary(projectDetection) {
661
- if (!projectDetection.detectedStackFileName) {
662
- return 'I did not find enough stack markers to auto-detect this project confidently.';
663
- }
664
-
665
- const readableEvidence = projectDetection.evidence.length > 0
666
- ? projectDetection.evidence.join(', ')
667
- : 'basic project markers';
668
-
669
- const confidenceGapSummary = typeof projectDetection.confidenceGap === 'number'
670
- ? ` Confidence gap: ${projectDetection.confidenceGap}.`
671
- : '';
672
-
673
- const secondaryStacksSummary = projectDetection.secondaryStackFileNames?.length
674
- ? ` Secondary stack signals: ${projectDetection.secondaryStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ')}.`
675
- : '';
676
-
677
- return `This folder looks like ${toTitleCase(projectDetection.detectedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}${secondaryStacksSummary}`;
678
- }
679
-
680
- export function formatDetectionCandidates(rankedCandidates) {
681
- if (!rankedCandidates?.length) {
682
- return 'No ranked candidates available.';
683
- }
684
-
685
- return rankedCandidates
686
- .map((candidate, candidateIndex) => {
687
- const evidenceSummary = candidate.evidence?.length ? candidate.evidence.join(', ') : 'no direct markers';
688
- return `${candidateIndex + 1}. ${toTitleCase(candidate.stackFileName)} (score ${candidate.confidenceScore}) via ${evidenceSummary}`;
689
- })
690
- .join('\n');
691
- }
17
+ export { collectProjectMarkers } from './detector/workspace-scan.mjs';
18
+ export { detectUiScopeSignals } from './detector/ui-signals.mjs';
19
+ export {
20
+ buildDetectionSummary,
21
+ detectProjectContext,
22
+ formatDetectionCandidates,
23
+ } from './detector/stack-detection.mjs';