@ryuenn3123/agentic-senior-core 3.0.49 → 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.
- package/.agent-context/prompts/bootstrap-design.md +2 -1
- package/.agent-context/review-checklists/pr-checklist.md +1 -0
- package/.agent-context/rules/api-docs.md +63 -45
- package/.agent-context/rules/architecture.md +133 -118
- package/.agent-context/rules/database-design.md +36 -16
- package/.agent-context/rules/docker-runtime.md +66 -43
- package/.agent-context/rules/efficiency-vs-hype.md +38 -17
- package/.agent-context/rules/error-handling.md +35 -14
- package/.agent-context/rules/event-driven.md +35 -18
- package/.agent-context/rules/frontend-architecture.md +103 -74
- package/.agent-context/rules/git-workflow.md +81 -197
- package/.agent-context/rules/microservices.md +42 -41
- package/.agent-context/rules/naming-conv.md +27 -6
- package/.agent-context/rules/performance.md +32 -10
- package/.agent-context/rules/realtime.md +26 -9
- package/.agent-context/rules/security.md +39 -19
- package/.agent-context/rules/testing.md +36 -15
- package/AGENTS.md +9 -9
- package/README.md +10 -1
- package/lib/cli/commands/init.mjs +1 -0
- package/lib/cli/compiler.mjs +1 -0
- package/lib/cli/detector/constants.mjs +135 -0
- package/lib/cli/detector/design-evidence/collector.mjs +256 -0
- package/lib/cli/detector/design-evidence/constants.mjs +39 -0
- package/lib/cli/detector/design-evidence/file-traversal.mjs +83 -0
- package/lib/cli/detector/design-evidence/structured-attribute-evidence.mjs +117 -0
- package/lib/cli/detector/design-evidence/summary.mjs +109 -0
- package/lib/cli/detector/design-evidence/utility-helpers.mjs +122 -0
- package/lib/cli/detector/design-evidence.mjs +25 -610
- package/lib/cli/detector/stack-detection.mjs +243 -0
- package/lib/cli/detector/ui-signals.mjs +150 -0
- package/lib/cli/detector/workspace-scan.mjs +177 -0
- package/lib/cli/detector.mjs +20 -688
- package/lib/cli/memory-continuity.mjs +1 -0
- package/lib/cli/project-scaffolder/design-contract/sections/audits.mjs +96 -0
- package/lib/cli/project-scaffolder/design-contract/sections/conceptual-anchor.mjs +116 -0
- package/lib/cli/project-scaffolder/design-contract/sections/execution-handoff.mjs +211 -0
- package/lib/cli/project-scaffolder/design-contract/seed-signals.mjs +79 -0
- package/lib/cli/project-scaffolder/design-contract/signal-vocab.mjs +64 -0
- package/lib/cli/project-scaffolder/design-contract/validation/anchor-validators.mjs +222 -0
- package/lib/cli/project-scaffolder/design-contract/validation/audit-validators.mjs +117 -0
- package/lib/cli/project-scaffolder/design-contract/validation/completeness.mjs +83 -0
- package/lib/cli/project-scaffolder/design-contract/validation/execution-validators.mjs +328 -0
- package/lib/cli/project-scaffolder/design-contract/validation/helpers.mjs +8 -0
- package/lib/cli/project-scaffolder/design-contract/validation/structural-validators.mjs +79 -0
- package/lib/cli/project-scaffolder/design-contract/validation/system-validators.mjs +256 -0
- package/lib/cli/project-scaffolder/design-contract/validation.mjs +59 -896
- package/lib/cli/project-scaffolder/design-contract.mjs +147 -557
- package/mcp.json +30 -9
- package/package.json +17 -2
- package/scripts/audit-cache-layer-contract.mjs +258 -0
- package/scripts/audit-caching-scope-hygiene.mjs +263 -0
- package/scripts/audit-file-size.mjs +219 -0
- package/scripts/audit-reflection-citations.mjs +163 -0
- package/scripts/audit-release-bundle.mjs +170 -0
- package/scripts/audit-rule-id-uniqueness.mjs +313 -0
- package/scripts/benchmark-evidence-bundle.mjs +1 -0
- package/scripts/build-release-benchmark-bundle.mjs +204 -0
- package/scripts/context-triggered-audit.mjs +1 -0
- package/scripts/documentation-boundary-audit.mjs +1 -0
- package/scripts/explain-on-demand-audit.mjs +2 -1
- package/scripts/frontend-usability-audit.mjs +10 -10
- package/scripts/llm-judge/checklist-loader.mjs +45 -0
- package/scripts/llm-judge/constants.mjs +66 -0
- package/scripts/llm-judge/diff-collection.mjs +74 -0
- package/scripts/llm-judge/prompting.mjs +78 -0
- package/scripts/llm-judge/providers.mjs +111 -0
- package/scripts/llm-judge/verdict.mjs +134 -0
- package/scripts/llm-judge.mjs +21 -482
- package/scripts/mcp-server/tool-registry.mjs +55 -0
- package/scripts/mcp-server/tools.mjs +137 -1
- package/scripts/migrate-rule-format/id-prefix-table.mjs +37 -0
- package/scripts/migrate-rule-format/parse-legacy.mjs +180 -0
- package/scripts/migrate-rule-format/render-new.mjs +169 -0
- package/scripts/migrate-rule-format/roundtrip-validate.mjs +89 -0
- package/scripts/migrate-rule-format.mjs +192 -0
- package/scripts/release-gate/constants.mjs +1 -1
- package/scripts/release-gate/static-checks.mjs +1 -1
- package/scripts/rules-guardian-audit.mjs +5 -2
- package/scripts/single-source-lazy-loading-audit.mjs +2 -1
- package/scripts/ui-design-judge/git-input.mjs +3 -0
- package/scripts/validate/config.mjs +3 -2
- package/scripts/validate/coverage-checks.mjs +1 -1
- package/scripts/validate.mjs +93 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack detection: scores known marker sets per language ecosystem, ranks them,
|
|
3
|
+
* and emits an explanation of what was chosen and why. Used by the CLI to seed
|
|
4
|
+
* detection summaries and ranked candidate lists.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { toTitleCase } from '../utils.mjs';
|
|
8
|
+
import { collectNestedWorkspaceProjects, collectProjectMarkers } from './workspace-scan.mjs';
|
|
9
|
+
|
|
10
|
+
export function collectStackDetectionCandidates(markerNames, evidencePrefix = null) {
|
|
11
|
+
const detectionCandidates = [];
|
|
12
|
+
const withEvidencePrefix = (evidenceItem) => evidencePrefix ? `${evidencePrefix}: ${evidenceItem}` : evidenceItem;
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
markerNames.has('package.json')
|
|
16
|
+
|| markerNames.has('tsconfig.json')
|
|
17
|
+
|| markerNames.has('next.config.js')
|
|
18
|
+
|| markerNames.has('next.config.mjs')
|
|
19
|
+
|| markerNames.has('vite.config.js')
|
|
20
|
+
|| markerNames.has('vite.config.mjs')
|
|
21
|
+
|| markerNames.has('vite.config.ts')
|
|
22
|
+
) {
|
|
23
|
+
const evidence = [];
|
|
24
|
+
let confidenceScore = 0.7;
|
|
25
|
+
|
|
26
|
+
if (markerNames.has('package.json')) {
|
|
27
|
+
evidence.push(withEvidencePrefix('package.json'));
|
|
28
|
+
confidenceScore += 0.12;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (markerNames.has('tsconfig.json')) {
|
|
32
|
+
evidence.push(withEvidencePrefix('tsconfig.json'));
|
|
33
|
+
confidenceScore += 0.12;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
|
|
37
|
+
evidence.push(withEvidencePrefix('Next.js config'));
|
|
38
|
+
confidenceScore += 0.05;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (markerNames.has('vite.config.js') || markerNames.has('vite.config.mjs') || markerNames.has('vite.config.ts')) {
|
|
42
|
+
evidence.push(withEvidencePrefix('Vite config'));
|
|
43
|
+
confidenceScore += 0.08;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
detectionCandidates.push({
|
|
47
|
+
stackFileName: 'typescript.md',
|
|
48
|
+
confidenceScore: Math.min(confidenceScore, 0.97),
|
|
49
|
+
evidence,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (markerNames.has('pyproject.toml') || markerNames.has('requirements.txt')) {
|
|
54
|
+
detectionCandidates.push({
|
|
55
|
+
stackFileName: 'python.md',
|
|
56
|
+
confidenceScore: markerNames.has('pyproject.toml') ? 0.96 : 0.78,
|
|
57
|
+
evidence: markerNames.has('pyproject.toml')
|
|
58
|
+
? [withEvidencePrefix('pyproject.toml')]
|
|
59
|
+
: [withEvidencePrefix('requirements.txt')],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (markerNames.has('pom.xml') || markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) {
|
|
64
|
+
const evidence = [];
|
|
65
|
+
if (markerNames.has('pom.xml')) evidence.push(withEvidencePrefix('pom.xml'));
|
|
66
|
+
if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push(withEvidencePrefix('Gradle build file'));
|
|
67
|
+
detectionCandidates.push({
|
|
68
|
+
stackFileName: 'java.md',
|
|
69
|
+
confidenceScore: markerNames.has('pom.xml') ? 0.95 : 0.84,
|
|
70
|
+
evidence,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (markerNames.has('composer.json')) {
|
|
75
|
+
detectionCandidates.push({
|
|
76
|
+
stackFileName: 'php.md',
|
|
77
|
+
confidenceScore: 0.95,
|
|
78
|
+
evidence: [withEvidencePrefix('composer.json')],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (markerNames.has('go.mod')) {
|
|
83
|
+
detectionCandidates.push({
|
|
84
|
+
stackFileName: 'go.md',
|
|
85
|
+
confidenceScore: 0.96,
|
|
86
|
+
evidence: [withEvidencePrefix('go.mod')],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (markerNames.has('Cargo.toml')) {
|
|
91
|
+
detectionCandidates.push({
|
|
92
|
+
stackFileName: 'rust.md',
|
|
93
|
+
confidenceScore: 0.96,
|
|
94
|
+
evidence: [withEvidencePrefix('Cargo.toml')],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (markerNames.has('Gemfile')) {
|
|
99
|
+
detectionCandidates.push({
|
|
100
|
+
stackFileName: 'ruby.md',
|
|
101
|
+
confidenceScore: 0.95,
|
|
102
|
+
evidence: [withEvidencePrefix('Gemfile')],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasDotNetMarker = Array.from(markerNames).some(
|
|
107
|
+
(markerName) => markerName.endsWith('.sln') || markerName.endsWith('.csproj'),
|
|
108
|
+
);
|
|
109
|
+
if (hasDotNetMarker) {
|
|
110
|
+
detectionCandidates.push({
|
|
111
|
+
stackFileName: 'csharp.md',
|
|
112
|
+
confidenceScore: 0.95,
|
|
113
|
+
evidence: [withEvidencePrefix('.sln or .csproj file')],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
markerNames.has('package.json')
|
|
119
|
+
&& (markerNames.has('android') || markerNames.has('ios') || markerNames.has('react-native.config.js'))
|
|
120
|
+
) {
|
|
121
|
+
detectionCandidates.push({
|
|
122
|
+
stackFileName: 'react-native.md',
|
|
123
|
+
confidenceScore: 0.9,
|
|
124
|
+
evidence: [withEvidencePrefix('package.json'), withEvidencePrefix('mobile runtime markers')],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (markerNames.has('pubspec.yaml')) {
|
|
129
|
+
detectionCandidates.push({
|
|
130
|
+
stackFileName: 'flutter.md',
|
|
131
|
+
confidenceScore: 0.94,
|
|
132
|
+
evidence: [withEvidencePrefix('pubspec.yaml')],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return detectionCandidates;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function detectProjectContext(targetDirectoryPath) {
|
|
140
|
+
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
141
|
+
const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
|
|
142
|
+
const detectionCandidates = [
|
|
143
|
+
...collectStackDetectionCandidates(markerNames),
|
|
144
|
+
...nestedWorkspaceProjects.flatMap((nestedWorkspaceProject) => (
|
|
145
|
+
collectStackDetectionCandidates(
|
|
146
|
+
nestedWorkspaceProject.markerNames,
|
|
147
|
+
nestedWorkspaceProject.relativePath,
|
|
148
|
+
)
|
|
149
|
+
)),
|
|
150
|
+
];
|
|
151
|
+
const hasExistingProjectFiles = markerNames.size > 0;
|
|
152
|
+
|
|
153
|
+
if (detectionCandidates.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
hasExistingProjectFiles,
|
|
156
|
+
detectedStackFileName: null,
|
|
157
|
+
secondaryStackFileNames: [],
|
|
158
|
+
detectedBlueprintFileName: null,
|
|
159
|
+
confidenceLabel: null,
|
|
160
|
+
confidenceScore: 0,
|
|
161
|
+
confidenceGap: 0,
|
|
162
|
+
detectionReasoning: 'No known project markers were detected.',
|
|
163
|
+
rankedCandidates: [],
|
|
164
|
+
evidence: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
detectionCandidates.sort(
|
|
169
|
+
(leftCandidate, rightCandidate) => rightCandidate.confidenceScore - leftCandidate.confidenceScore,
|
|
170
|
+
);
|
|
171
|
+
const strongestCandidate = detectionCandidates[0];
|
|
172
|
+
const secondStrongestCandidate = detectionCandidates[1];
|
|
173
|
+
const confidenceGap = secondStrongestCandidate
|
|
174
|
+
? Number((strongestCandidate.confidenceScore - secondStrongestCandidate.confidenceScore).toFixed(2))
|
|
175
|
+
: Number(strongestCandidate.confidenceScore.toFixed(2));
|
|
176
|
+
const isAmbiguous = secondStrongestCandidate && confidenceGap < 0.08;
|
|
177
|
+
const confidenceLabel = strongestCandidate.confidenceScore >= 0.9
|
|
178
|
+
? 'high'
|
|
179
|
+
: strongestCandidate.confidenceScore >= 0.78
|
|
180
|
+
? 'medium'
|
|
181
|
+
: 'low';
|
|
182
|
+
const evidence = isAmbiguous
|
|
183
|
+
? [...strongestCandidate.evidence, 'multiple stack signals detected']
|
|
184
|
+
: strongestCandidate.evidence;
|
|
185
|
+
const rankedCandidates = detectionCandidates.slice(0, 3).map((detectionCandidate) => ({
|
|
186
|
+
stackFileName: detectionCandidate.stackFileName,
|
|
187
|
+
confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
|
|
188
|
+
evidence: detectionCandidate.evidence,
|
|
189
|
+
}));
|
|
190
|
+
const secondaryStackFileNames = rankedCandidates
|
|
191
|
+
.slice(1)
|
|
192
|
+
.filter((rankedCandidate) => (strongestCandidate.confidenceScore - rankedCandidate.confidenceScore) < 0.08)
|
|
193
|
+
.map((rankedCandidate) => rankedCandidate.stackFileName);
|
|
194
|
+
const detectionReasoning = isAmbiguous
|
|
195
|
+
? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
|
|
196
|
+
: `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(', ') || 'none'}.`;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
hasExistingProjectFiles,
|
|
200
|
+
detectedStackFileName: strongestCandidate.stackFileName,
|
|
201
|
+
secondaryStackFileNames,
|
|
202
|
+
detectedBlueprintFileName: null,
|
|
203
|
+
confidenceLabel,
|
|
204
|
+
confidenceScore: strongestCandidate.confidenceScore,
|
|
205
|
+
confidenceGap,
|
|
206
|
+
detectionReasoning,
|
|
207
|
+
rankedCandidates,
|
|
208
|
+
evidence,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildDetectionSummary(projectDetection) {
|
|
213
|
+
if (!projectDetection.detectedStackFileName) {
|
|
214
|
+
return 'I did not find enough stack markers to auto-detect this project confidently.';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const readableEvidence = projectDetection.evidence.length > 0
|
|
218
|
+
? projectDetection.evidence.join(', ')
|
|
219
|
+
: 'basic project markers';
|
|
220
|
+
|
|
221
|
+
const confidenceGapSummary = typeof projectDetection.confidenceGap === 'number'
|
|
222
|
+
? ` Confidence gap: ${projectDetection.confidenceGap}.`
|
|
223
|
+
: '';
|
|
224
|
+
|
|
225
|
+
const secondaryStacksSummary = projectDetection.secondaryStackFileNames?.length
|
|
226
|
+
? ` Secondary stack signals: ${projectDetection.secondaryStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ')}.`
|
|
227
|
+
: '';
|
|
228
|
+
|
|
229
|
+
return `This folder looks like ${toTitleCase(projectDetection.detectedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}${secondaryStacksSummary}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function formatDetectionCandidates(rankedCandidates) {
|
|
233
|
+
if (!rankedCandidates?.length) {
|
|
234
|
+
return 'No ranked candidates available.';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return rankedCandidates
|
|
238
|
+
.map((candidate, candidateIndex) => {
|
|
239
|
+
const evidenceSummary = candidate.evidence?.length ? candidate.evidence.join(', ') : 'no direct markers';
|
|
240
|
+
return `${candidateIndex + 1}. ${toTitleCase(candidate.stackFileName)} (score ${candidate.confidenceScore}) via ${evidenceSummary}`;
|
|
241
|
+
})
|
|
242
|
+
.join('\n');
|
|
243
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI scope detection: combines marker sets, package.json dependencies, nested
|
|
3
|
+
* workspace evidence, and explicit user scope keys to decide whether the
|
|
4
|
+
* current target directory is a UI surface. Triggers the lightweight design
|
|
5
|
+
* evidence scan when the answer is yes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { collectFrontendDesignEvidence } from './design-evidence.mjs';
|
|
9
|
+
import { DIRECT_UI_MARKER_NAMES } from './constants.mjs';
|
|
10
|
+
import { collectNestedWorkspaceProjects, collectProjectMarkers, readPackageJsonIfExists } from './workspace-scan.mjs';
|
|
11
|
+
|
|
12
|
+
const UI_DEPENDENCY_NAMES = ['next', 'react', 'react-dom', 'react-native', 'expo', 'tailwindcss'];
|
|
13
|
+
|
|
14
|
+
function analyzeUiSignalsForMarkerSet(markerNames, packageManifest, sourceLabel = null) {
|
|
15
|
+
const detectedUiMarkers = DIRECT_UI_MARKER_NAMES.filter((markerName) => markerNames.has(markerName));
|
|
16
|
+
const dependencySource = {
|
|
17
|
+
...(packageManifest?.dependencies || {}),
|
|
18
|
+
...(packageManifest?.devDependencies || {}),
|
|
19
|
+
};
|
|
20
|
+
const detectedUiDependencies = UI_DEPENDENCY_NAMES.filter((dependencyName) => dependencySource[dependencyName]);
|
|
21
|
+
const hasStrongUiMarker = detectedUiMarkers.some((markerName) => (
|
|
22
|
+
markerName.startsWith('next.config')
|
|
23
|
+
|| markerName === 'react-native.config.js'
|
|
24
|
+
|| markerName === 'android'
|
|
25
|
+
|| markerName === 'ios'
|
|
26
|
+
));
|
|
27
|
+
const hasUiDependencies = detectedUiDependencies.length > 0;
|
|
28
|
+
const hasStructuralUiMarkers = detectedUiMarkers.length >= 2;
|
|
29
|
+
const signalReasons = [];
|
|
30
|
+
const sourcePrefix = sourceLabel ? `${sourceLabel}: ` : '';
|
|
31
|
+
|
|
32
|
+
if (detectedUiMarkers.length > 0) {
|
|
33
|
+
signalReasons.push(`${sourcePrefix}ui markers: ${detectedUiMarkers.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (detectedUiDependencies.length > 0) {
|
|
37
|
+
signalReasons.push(`${sourcePrefix}ui dependencies: ${detectedUiDependencies.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
signalReasons,
|
|
42
|
+
detectedUiMarkers,
|
|
43
|
+
detectedUiDependencies,
|
|
44
|
+
hasStrongUiMarker,
|
|
45
|
+
hasUiDependencies,
|
|
46
|
+
hasStructuralUiMarkers,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function detectUiScopeSignals({
|
|
51
|
+
targetDirectoryPath,
|
|
52
|
+
selectedStackFileName,
|
|
53
|
+
selectedBlueprintFileName,
|
|
54
|
+
packageManifest = null,
|
|
55
|
+
projectScopeKey = null,
|
|
56
|
+
projectScopeSourceLabel = 'project scope',
|
|
57
|
+
}) {
|
|
58
|
+
const signalReasons = [];
|
|
59
|
+
const markerNames = await collectProjectMarkers(targetDirectoryPath);
|
|
60
|
+
const resolvedPackageManifest = packageManifest || await readPackageJsonIfExists(targetDirectoryPath);
|
|
61
|
+
const nestedWorkspaceProjects = await collectNestedWorkspaceProjects(targetDirectoryPath);
|
|
62
|
+
|
|
63
|
+
const normalizedProjectScopeKey = String(projectScopeKey || '').trim().toLowerCase();
|
|
64
|
+
if (normalizedProjectScopeKey === 'frontend-only' || normalizedProjectScopeKey === 'both') {
|
|
65
|
+
signalReasons.push(`${projectScopeSourceLabel}: ${normalizedProjectScopeKey}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const selectedStackKey = String(selectedStackFileName || '').trim().toLowerCase();
|
|
69
|
+
if (selectedStackKey === 'react-native.md' || selectedStackKey === 'flutter.md') {
|
|
70
|
+
signalReasons.push(`selected stack implies UI runtime: ${selectedStackKey}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selectedBlueprintKey = String(selectedBlueprintFileName || '').trim().toLowerCase();
|
|
74
|
+
if (selectedBlueprintKey.includes('frontend') || selectedBlueprintKey.includes('landing') || selectedBlueprintKey.includes('mobile-app')) {
|
|
75
|
+
signalReasons.push(`selected blueprint implies UI scope: ${selectedBlueprintKey}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rootUiSignals = analyzeUiSignalsForMarkerSet(markerNames, resolvedPackageManifest);
|
|
79
|
+
signalReasons.push(...rootUiSignals.signalReasons);
|
|
80
|
+
|
|
81
|
+
const nestedUiSignals = nestedWorkspaceProjects
|
|
82
|
+
.map((nestedWorkspaceProject) => ({
|
|
83
|
+
...nestedWorkspaceProject,
|
|
84
|
+
...analyzeUiSignalsForMarkerSet(
|
|
85
|
+
nestedWorkspaceProject.markerNames,
|
|
86
|
+
nestedWorkspaceProject.packageManifest,
|
|
87
|
+
`workspace ${nestedWorkspaceProject.relativePath}`,
|
|
88
|
+
),
|
|
89
|
+
}))
|
|
90
|
+
.filter((nestedWorkspaceProject) => nestedWorkspaceProject.signalReasons.length > 0);
|
|
91
|
+
|
|
92
|
+
for (const nestedUiSignal of nestedUiSignals) {
|
|
93
|
+
signalReasons.push(...nestedUiSignal.signalReasons);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const detectedUiMarkers = Array.from(new Set([
|
|
97
|
+
...rootUiSignals.detectedUiMarkers,
|
|
98
|
+
...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiMarkers),
|
|
99
|
+
]));
|
|
100
|
+
const detectedUiDependencies = Array.from(new Set([
|
|
101
|
+
...rootUiSignals.detectedUiDependencies,
|
|
102
|
+
...nestedUiSignals.flatMap((nestedUiSignal) => nestedUiSignal.detectedUiDependencies),
|
|
103
|
+
]));
|
|
104
|
+
|
|
105
|
+
const hasStrongUiMarker = rootUiSignals.hasStrongUiMarker
|
|
106
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStrongUiMarker);
|
|
107
|
+
const hasUiDependencies = rootUiSignals.hasUiDependencies
|
|
108
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasUiDependencies);
|
|
109
|
+
const hasStructuralUiMarkers = rootUiSignals.hasStructuralUiMarkers
|
|
110
|
+
|| nestedUiSignals.some((nestedUiSignal) => nestedUiSignal.hasStructuralUiMarkers);
|
|
111
|
+
const isUiScopeLikely = signalReasons.length > 0
|
|
112
|
+
&& (hasStrongUiMarker || hasUiDependencies || hasStructuralUiMarkers || normalizedProjectScopeKey.length > 0);
|
|
113
|
+
const preferredUiWorkspaceEntry = nestedUiSignals.find((nestedUiSignal) => (
|
|
114
|
+
nestedUiSignal.hasStrongUiMarker
|
|
115
|
+
|| nestedUiSignal.hasUiDependencies
|
|
116
|
+
|| nestedUiSignal.hasStructuralUiMarkers
|
|
117
|
+
)) || null;
|
|
118
|
+
const frontendScanRootDirectoryPaths = (
|
|
119
|
+
!rootUiSignals.hasStrongUiMarker
|
|
120
|
+
&& !rootUiSignals.hasUiDependencies
|
|
121
|
+
&& !rootUiSignals.hasStructuralUiMarkers
|
|
122
|
+
&& nestedUiSignals.length > 0
|
|
123
|
+
)
|
|
124
|
+
? nestedUiSignals.map((nestedUiSignal) => nestedUiSignal.directoryPath)
|
|
125
|
+
: [];
|
|
126
|
+
const designEvidence = isUiScopeLikely
|
|
127
|
+
? await collectFrontendDesignEvidence({
|
|
128
|
+
targetDirectoryPath,
|
|
129
|
+
markerNames,
|
|
130
|
+
scanRootDirectoryPaths: frontendScanRootDirectoryPaths,
|
|
131
|
+
})
|
|
132
|
+
: null;
|
|
133
|
+
const frontendEvidenceMetrics = designEvidence?.frontendEvidenceMetrics || null;
|
|
134
|
+
const designEvidenceSummary = designEvidence?.designEvidenceSummary || null;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
isUiScopeLikely,
|
|
138
|
+
signalReasons,
|
|
139
|
+
detectedUiMarkers,
|
|
140
|
+
detectedUiDependencies,
|
|
141
|
+
frontendEvidenceMetrics,
|
|
142
|
+
designEvidenceSummary,
|
|
143
|
+
packageManifest: preferredUiWorkspaceEntry?.packageManifest || resolvedPackageManifest,
|
|
144
|
+
workspaceUiEntries: nestedUiSignals.map((nestedUiSignal) => ({
|
|
145
|
+
relativePath: nestedUiSignal.relativePath,
|
|
146
|
+
detectedUiMarkers: nestedUiSignal.detectedUiMarkers,
|
|
147
|
+
detectedUiDependencies: nestedUiSignal.detectedUiDependencies,
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace traversal: from a target directory, find immediate project markers,
|
|
3
|
+
* locate nested project candidates inside monorepo containers, and read package
|
|
4
|
+
* manifests when present. The scan is bounded by max depth and a per-run
|
|
5
|
+
* directory budget so it terminates cleanly on huge repos.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
hasProjectMarkers,
|
|
13
|
+
INTERNAL_GOVERNANCE_SURFACE_NAMES,
|
|
14
|
+
looksLikeWorkspaceSearchCandidate,
|
|
15
|
+
WORKSPACE_CONTAINER_DIRECTORY_NAMES,
|
|
16
|
+
WORKSPACE_ROOT_MARKER_FILE_NAMES,
|
|
17
|
+
WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES,
|
|
18
|
+
WORKSPACE_SCAN_MAX_DEPTH,
|
|
19
|
+
WORKSPACE_SCAN_MAX_DIRECTORIES,
|
|
20
|
+
} from './constants.mjs';
|
|
21
|
+
|
|
22
|
+
export async function collectProjectMarkers(targetDirectoryPath) {
|
|
23
|
+
const markerNames = new Set();
|
|
24
|
+
const directoryEntries = await fs.readdir(targetDirectoryPath, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
for (const directoryEntry of directoryEntries) {
|
|
27
|
+
if (directoryEntry.name === '.git' || directoryEntry.name === 'node_modules') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (INTERNAL_GOVERNANCE_SURFACE_NAMES.has(directoryEntry.name)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
markerNames.add(directoryEntry.name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return markerNames;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readPackageJsonIfExists(targetDirectoryPath) {
|
|
42
|
+
const packageJsonPath = path.join(targetDirectoryPath, 'package.json');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
|
|
46
|
+
return JSON.parse(packageJsonContent);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readDirectoryEntries(directoryPath) {
|
|
53
|
+
try {
|
|
54
|
+
return await fs.readdir(directoryPath, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function collectNestedWorkspaceProjects(targetDirectoryPath) {
|
|
61
|
+
const rootDirectoryEntries = await readDirectoryEntries(targetDirectoryPath);
|
|
62
|
+
const rootMarkerNames = new Set(rootDirectoryEntries.map((directoryEntry) => directoryEntry.name));
|
|
63
|
+
const rootLooksLikeWorkspace = Array.from(rootMarkerNames).some((markerName) => (
|
|
64
|
+
WORKSPACE_ROOT_MARKER_FILE_NAMES.has(markerName)
|
|
65
|
+
|| looksLikeWorkspaceSearchCandidate(markerName)
|
|
66
|
+
));
|
|
67
|
+
const nestedWorkspaceProjects = [];
|
|
68
|
+
const queuedWorkspacePaths = new Set();
|
|
69
|
+
const workspaceQueue = [];
|
|
70
|
+
let scannedDirectoryCount = 0;
|
|
71
|
+
|
|
72
|
+
for (const rootDirectoryEntry of rootDirectoryEntries) {
|
|
73
|
+
if (!rootDirectoryEntry.isDirectory()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(rootDirectoryEntry.name)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const shouldInspectRootChild = rootLooksLikeWorkspace
|
|
82
|
+
|| looksLikeWorkspaceSearchCandidate(rootDirectoryEntry.name);
|
|
83
|
+
|
|
84
|
+
if (!shouldInspectRootChild) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rootChildDirectoryPath = path.join(targetDirectoryPath, rootDirectoryEntry.name);
|
|
89
|
+
const rootChildEntries = await readDirectoryEntries(rootChildDirectoryPath);
|
|
90
|
+
const rootChildMarkerNames = new Set(rootChildEntries.map((directoryEntry) => directoryEntry.name));
|
|
91
|
+
const rootChildRelativePath = rootDirectoryEntry.name.replace(/\\/g, '/');
|
|
92
|
+
|
|
93
|
+
workspaceQueue.push({
|
|
94
|
+
directoryPath: rootChildDirectoryPath,
|
|
95
|
+
relativePath: rootChildRelativePath,
|
|
96
|
+
markerNames: rootChildMarkerNames,
|
|
97
|
+
depth: 1,
|
|
98
|
+
underWorkspaceContainer: WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(rootDirectoryEntry.name.toLowerCase()),
|
|
99
|
+
});
|
|
100
|
+
queuedWorkspacePaths.add(rootChildRelativePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
while (workspaceQueue.length > 0 && scannedDirectoryCount < WORKSPACE_SCAN_MAX_DIRECTORIES) {
|
|
104
|
+
const currentWorkspaceEntry = workspaceQueue.shift();
|
|
105
|
+
scannedDirectoryCount += 1;
|
|
106
|
+
|
|
107
|
+
const isProjectCandidate = hasProjectMarkers(currentWorkspaceEntry.markerNames);
|
|
108
|
+
const currentDirectoryName = path.basename(currentWorkspaceEntry.directoryPath).toLowerCase();
|
|
109
|
+
const isWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(currentDirectoryName);
|
|
110
|
+
|
|
111
|
+
if (isProjectCandidate) {
|
|
112
|
+
nestedWorkspaceProjects.push({
|
|
113
|
+
directoryPath: currentWorkspaceEntry.directoryPath,
|
|
114
|
+
relativePath: currentWorkspaceEntry.relativePath,
|
|
115
|
+
markerNames: currentWorkspaceEntry.markerNames,
|
|
116
|
+
packageManifest: currentWorkspaceEntry.markerNames.has('package.json')
|
|
117
|
+
? await readPackageJsonIfExists(currentWorkspaceEntry.directoryPath)
|
|
118
|
+
: null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (currentWorkspaceEntry.depth >= WORKSPACE_SCAN_MAX_DEPTH) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const shouldTraverseChildren = currentWorkspaceEntry.underWorkspaceContainer
|
|
127
|
+
|| isWorkspaceContainer
|
|
128
|
+
|| !isProjectCandidate;
|
|
129
|
+
|
|
130
|
+
if (!shouldTraverseChildren) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const childEntries = await readDirectoryEntries(currentWorkspaceEntry.directoryPath);
|
|
135
|
+
for (const childEntry of childEntries) {
|
|
136
|
+
if (!childEntry.isDirectory()) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES.has(childEntry.name)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const childLooksRelevant = looksLikeWorkspaceSearchCandidate(childEntry.name);
|
|
145
|
+
if (!childLooksRelevant && !currentWorkspaceEntry.underWorkspaceContainer && !isWorkspaceContainer) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const childDirectoryPath = path.join(currentWorkspaceEntry.directoryPath, childEntry.name);
|
|
150
|
+
const childRelativePath = path.join(currentWorkspaceEntry.relativePath, childEntry.name).replace(/\\/g, '/');
|
|
151
|
+
|
|
152
|
+
if (queuedWorkspacePaths.has(childRelativePath)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const childDirectoryEntries = await readDirectoryEntries(childDirectoryPath);
|
|
157
|
+
const childMarkerNames = new Set(childDirectoryEntries.map((directoryEntry) => directoryEntry.name));
|
|
158
|
+
const childIsProjectCandidate = hasProjectMarkers(childMarkerNames);
|
|
159
|
+
const childIsWorkspaceContainer = WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(childEntry.name.toLowerCase());
|
|
160
|
+
|
|
161
|
+
if (!childIsProjectCandidate && !childIsWorkspaceContainer && !childLooksRelevant) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
workspaceQueue.push({
|
|
166
|
+
directoryPath: childDirectoryPath,
|
|
167
|
+
relativePath: childRelativePath,
|
|
168
|
+
markerNames: childMarkerNames,
|
|
169
|
+
depth: currentWorkspaceEntry.depth + 1,
|
|
170
|
+
underWorkspaceContainer: currentWorkspaceEntry.underWorkspaceContainer || isWorkspaceContainer || childIsWorkspaceContainer,
|
|
171
|
+
});
|
|
172
|
+
queuedWorkspacePaths.add(childRelativePath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return nestedWorkspaceProjects;
|
|
177
|
+
}
|