@ryuenn3123/agentic-senior-core 2.0.24 → 2.0.26
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/review-checklists/frontend-excellence-rubric.md +54 -0
- package/.agent-context/review-checklists/frontend-skill-parity.md +1 -0
- package/.agent-context/review-checklists/frontend-usability.md +1 -0
- package/.agent-context/rules/docker-runtime.md +29 -0
- package/.agent-context/skills/frontend/README.md +1 -0
- package/.agent-context/skills/frontend.md +4 -0
- package/.cursorrules +1 -1
- package/.github/workflows/benchmark-detection.yml +8 -1
- package/.windsurfrules +1 -1
- package/README.md +7 -0
- package/lib/cli/commands/init.mjs +358 -16
- package/lib/cli/commands/optimize.mjs +12 -0
- package/lib/cli/commands/upgrade.mjs +30 -1
- package/lib/cli/compiler.mjs +55 -1
- package/lib/cli/constants.mjs +83 -0
- package/lib/cli/detector.mjs +11 -1
- package/lib/cli/project-scaffolder.mjs +183 -2
- package/lib/cli/skill-selector.mjs +60 -38
- package/lib/cli/templates/architecture-decision-record.md.tmpl +39 -0
- package/lib/cli/templates/flow-overview.md.tmpl +12 -0
- package/lib/cli/templates/project-brief.md.id.tmpl +2 -0
- package/lib/cli/templates/project-brief.md.tmpl +26 -0
- package/lib/cli/utils.mjs +2 -1
- package/package.json +1 -1
- package/scripts/frontend-usability-audit.mjs +21 -0
- package/scripts/release-gate.mjs +108 -0
- package/scripts/validate.mjs +2 -0
|
@@ -119,7 +119,17 @@ export async function runOptimizeCommand(targetDirectoryArgument, optimizeOption
|
|
|
119
119
|
? onboardingReport.selectedProfile
|
|
120
120
|
: 'balanced';
|
|
121
121
|
const selectedStackFileName = normalizeMarkdownFileName(onboardingReport.selectedStack, 'typescript.md');
|
|
122
|
+
const selectedAdditionalStackFileNames = Array.isArray(onboardingReport.selectedAdditionalStacks)
|
|
123
|
+
? onboardingReport.selectedAdditionalStacks
|
|
124
|
+
.map((stackFileName) => normalizeMarkdownFileName(stackFileName, ''))
|
|
125
|
+
.filter((stackFileName) => stackFileName && stackFileName !== selectedStackFileName)
|
|
126
|
+
: [];
|
|
122
127
|
const selectedBlueprintFileName = normalizeMarkdownFileName(onboardingReport.selectedBlueprint, 'api-nextjs.md');
|
|
128
|
+
const selectedAdditionalBlueprintFileNames = Array.isArray(onboardingReport.selectedAdditionalBlueprints)
|
|
129
|
+
? onboardingReport.selectedAdditionalBlueprints
|
|
130
|
+
.map((blueprintFileName) => normalizeMarkdownFileName(blueprintFileName, ''))
|
|
131
|
+
.filter((blueprintFileName) => blueprintFileName && blueprintFileName !== selectedBlueprintFileName)
|
|
132
|
+
: [];
|
|
123
133
|
const includeCiGuardrails = typeof onboardingReport.ciGuardrailsEnabled === 'boolean'
|
|
124
134
|
? onboardingReport.ciGuardrailsEnabled
|
|
125
135
|
: true;
|
|
@@ -128,7 +138,9 @@ export async function runOptimizeCommand(targetDirectoryArgument, optimizeOption
|
|
|
128
138
|
targetDirectoryPath: resolvedTargetDirectoryPath,
|
|
129
139
|
selectedProfileName,
|
|
130
140
|
selectedStackFileName,
|
|
141
|
+
selectedAdditionalStackFileNames,
|
|
131
142
|
selectedBlueprintFileName,
|
|
143
|
+
selectedAdditionalBlueprintFileNames,
|
|
132
144
|
includeCiGuardrails,
|
|
133
145
|
});
|
|
134
146
|
|
|
@@ -119,17 +119,35 @@ export async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions
|
|
|
119
119
|
? existingOnboardingReport.selectedStack
|
|
120
120
|
: projectDetection.recommendedStackFileName || 'typescript.md';
|
|
121
121
|
|
|
122
|
+
const selectedAdditionalStackFileNames = Array.isArray(existingOnboardingReport?.selectedAdditionalStacks)
|
|
123
|
+
? Array.from(new Set(existingOnboardingReport.selectedAdditionalStacks.filter(
|
|
124
|
+
(stackFileName) => stackFileNames.includes(stackFileName) && stackFileName !== selectedStackFileName
|
|
125
|
+
)))
|
|
126
|
+
: Array.from(new Set((projectDetection.secondaryStackFileNames || []).filter(
|
|
127
|
+
(stackFileName) => stackFileNames.includes(stackFileName) && stackFileName !== selectedStackFileName
|
|
128
|
+
)));
|
|
129
|
+
|
|
122
130
|
const selectedBlueprintFileName = blueprintFileNames.includes(existingOnboardingReport?.selectedBlueprint)
|
|
123
131
|
? existingOnboardingReport.selectedBlueprint
|
|
124
132
|
: BLUEPRINT_RECOMMENDATIONS[selectedStackFileName] || 'api-nextjs.md';
|
|
125
133
|
|
|
134
|
+
const selectedAdditionalBlueprintFileNames = Array.isArray(existingOnboardingReport?.selectedAdditionalBlueprints)
|
|
135
|
+
? Array.from(new Set(existingOnboardingReport.selectedAdditionalBlueprints.filter(
|
|
136
|
+
(blueprintFileName) => blueprintFileNames.includes(blueprintFileName) && blueprintFileName !== selectedBlueprintFileName
|
|
137
|
+
)))
|
|
138
|
+
: Array.from(new Set(selectedAdditionalStackFileNames
|
|
139
|
+
.map((stackFileName) => BLUEPRINT_RECOMMENDATIONS[stackFileName] || null)
|
|
140
|
+
.filter((blueprintFileName) => blueprintFileName && blueprintFileName !== selectedBlueprintFileName)));
|
|
141
|
+
|
|
126
142
|
const includeCiGuardrails = typeof existingOnboardingReport?.ciGuardrailsEnabled === 'boolean'
|
|
127
143
|
? existingOnboardingReport.ciGuardrailsEnabled
|
|
128
144
|
: true;
|
|
129
145
|
|
|
130
146
|
const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(
|
|
131
147
|
selectedStackFileName,
|
|
132
|
-
selectedBlueprintFileName
|
|
148
|
+
selectedBlueprintFileName,
|
|
149
|
+
selectedAdditionalStackFileNames,
|
|
150
|
+
selectedAdditionalBlueprintFileNames
|
|
133
151
|
);
|
|
134
152
|
const compatibilityWarnings = await evaluateSkillDomainCompatibility(
|
|
135
153
|
resolvedTargetDirectoryPath,
|
|
@@ -154,7 +172,9 @@ export async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions
|
|
|
154
172
|
targetDirectoryPath: resolvedTargetDirectoryPath,
|
|
155
173
|
selectedProfileName,
|
|
156
174
|
selectedStackFileName,
|
|
175
|
+
selectedAdditionalStackFileNames,
|
|
157
176
|
selectedBlueprintFileName,
|
|
177
|
+
selectedAdditionalBlueprintFileNames,
|
|
158
178
|
includeCiGuardrails,
|
|
159
179
|
});
|
|
160
180
|
|
|
@@ -166,7 +186,13 @@ export async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions
|
|
|
166
186
|
console.log(`- Target directory: ${resolvedTargetDirectoryPath}`);
|
|
167
187
|
console.log(`- Profile: ${toTitleCase(selectedProfileName)}`);
|
|
168
188
|
console.log(`- Stack: ${toTitleCase(selectedStackFileName)}`);
|
|
189
|
+
if (selectedAdditionalStackFileNames.length > 0) {
|
|
190
|
+
console.log(`- Additional stacks: ${selectedAdditionalStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ')}`);
|
|
191
|
+
}
|
|
169
192
|
console.log(`- Blueprint: ${toTitleCase(selectedBlueprintFileName)}`);
|
|
193
|
+
if (selectedAdditionalBlueprintFileNames.length > 0) {
|
|
194
|
+
console.log(`- Additional blueprints: ${selectedAdditionalBlueprintFileNames.map((blueprintFileName) => toTitleCase(blueprintFileName)).join(', ')}`);
|
|
195
|
+
}
|
|
170
196
|
console.log(`- CI/CD guardrails: ${includeCiGuardrails ? 'enabled' : 'disabled'}`);
|
|
171
197
|
console.log(`- Existing rules lines: ${currentRuleLineCount}`);
|
|
172
198
|
console.log(`- Planned rules lines: ${plannedRuleLineCount}`);
|
|
@@ -213,12 +239,15 @@ export async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions
|
|
|
213
239
|
selectedProfileName,
|
|
214
240
|
selectedProfilePack: existingOnboardingReport?.selectedProfilePack || null,
|
|
215
241
|
selectedStackFileName,
|
|
242
|
+
selectedAdditionalStackFileNames,
|
|
216
243
|
selectedBlueprintFileName,
|
|
244
|
+
selectedAdditionalBlueprintFileNames,
|
|
217
245
|
includeCiGuardrails,
|
|
218
246
|
setupDurationMs,
|
|
219
247
|
projectDetection,
|
|
220
248
|
selectedSkillDomains: selectedSkillDomainNames,
|
|
221
249
|
compatibilityWarnings,
|
|
250
|
+
runtimeEnvironment: existingOnboardingReport?.runtimeEnvironment || null,
|
|
222
251
|
operationMode: 'upgrade',
|
|
223
252
|
});
|
|
224
253
|
|
package/lib/cli/compiler.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CLI_VERSION,
|
|
10
10
|
POLICY_FILE_NAME,
|
|
11
11
|
SKILL_PLATFORM_INDEX_PATH,
|
|
12
|
+
BLUEPRINT_RECOMMENDATIONS,
|
|
12
13
|
} from './constants.mjs';
|
|
13
14
|
|
|
14
15
|
import {
|
|
@@ -38,12 +39,15 @@ export async function writeOnboardingReport({
|
|
|
38
39
|
selectedProfilePack,
|
|
39
40
|
selectedPreset,
|
|
40
41
|
selectedStackFileName,
|
|
42
|
+
selectedAdditionalStackFileNames = [],
|
|
41
43
|
selectedBlueprintFileName,
|
|
44
|
+
selectedAdditionalBlueprintFileNames = [],
|
|
42
45
|
includeCiGuardrails,
|
|
43
46
|
setupDurationMs,
|
|
44
47
|
projectDetection,
|
|
45
48
|
selectedSkillDomains = [],
|
|
46
49
|
compatibilityWarnings = [],
|
|
50
|
+
runtimeEnvironment = null,
|
|
47
51
|
operationMode = 'init',
|
|
48
52
|
}) {
|
|
49
53
|
const onboardingReportPath = path.join(targetDirectoryPath, '.agent-context', 'state', 'onboarding-report.json');
|
|
@@ -60,14 +64,23 @@ export async function writeOnboardingReport({
|
|
|
60
64
|
: null,
|
|
61
65
|
selectedPreset,
|
|
62
66
|
selectedStack: selectedStackFileName,
|
|
67
|
+
selectedAdditionalStacks: selectedAdditionalStackFileNames,
|
|
63
68
|
selectedBlueprint: selectedBlueprintFileName,
|
|
69
|
+
selectedAdditionalBlueprints: selectedAdditionalBlueprintFileNames,
|
|
64
70
|
ciGuardrailsEnabled: includeCiGuardrails,
|
|
65
71
|
setupDurationMs,
|
|
66
72
|
selectedSkillDomains,
|
|
67
73
|
compatibilityWarnings,
|
|
74
|
+
runtimeEnvironment,
|
|
68
75
|
autoDetection: {
|
|
69
76
|
recommendedStack: projectDetection.recommendedStackFileName,
|
|
77
|
+
recommendedAdditionalStacks: projectDetection.secondaryStackFileNames || [],
|
|
70
78
|
recommendedBlueprint: projectDetection.recommendedBlueprintFileName,
|
|
79
|
+
recommendedAdditionalBlueprints: Array.isArray(projectDetection.secondaryStackFileNames)
|
|
80
|
+
? projectDetection.secondaryStackFileNames
|
|
81
|
+
.map((secondaryStackFileName) => BLUEPRINT_RECOMMENDATIONS[secondaryStackFileName] || null)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
: [],
|
|
71
84
|
confidenceLabel: projectDetection.confidenceLabel,
|
|
72
85
|
confidenceScore: projectDetection.confidenceScore,
|
|
73
86
|
confidenceGap: projectDetection.confidenceGap,
|
|
@@ -94,7 +107,9 @@ export async function buildCompiledRulesContent({
|
|
|
94
107
|
targetDirectoryPath,
|
|
95
108
|
selectedProfileName,
|
|
96
109
|
selectedStackFileName,
|
|
110
|
+
selectedAdditionalStackFileNames = [],
|
|
97
111
|
selectedBlueprintFileName,
|
|
112
|
+
selectedAdditionalBlueprintFileNames = [],
|
|
98
113
|
includeCiGuardrails,
|
|
99
114
|
}) {
|
|
100
115
|
const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
|
|
@@ -102,7 +117,20 @@ export async function buildCompiledRulesContent({
|
|
|
102
117
|
const selectedStacksDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'stacks');
|
|
103
118
|
const selectedBlueprintsDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'blueprints');
|
|
104
119
|
const skillPlatformIndex = JSON.parse(await fs.readFile(SKILL_PLATFORM_INDEX_PATH, 'utf8'));
|
|
105
|
-
const
|
|
120
|
+
const normalizedAdditionalStackFileNames = Array.isArray(selectedAdditionalStackFileNames)
|
|
121
|
+
? Array.from(new Set(selectedAdditionalStackFileNames.filter((stackFileName) => stackFileName && stackFileName !== selectedStackFileName)))
|
|
122
|
+
: [];
|
|
123
|
+
const normalizedAdditionalBlueprintFileNames = Array.isArray(selectedAdditionalBlueprintFileNames)
|
|
124
|
+
? Array.from(new Set(selectedAdditionalBlueprintFileNames.filter(
|
|
125
|
+
(blueprintFileName) => blueprintFileName && blueprintFileName !== selectedBlueprintFileName
|
|
126
|
+
)))
|
|
127
|
+
: [];
|
|
128
|
+
const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(
|
|
129
|
+
selectedStackFileName,
|
|
130
|
+
selectedBlueprintFileName,
|
|
131
|
+
normalizedAdditionalStackFileNames,
|
|
132
|
+
normalizedAdditionalBlueprintFileNames
|
|
133
|
+
);
|
|
106
134
|
|
|
107
135
|
const universalRuleFileNames = await collectFileNames(selectedRulesDirectoryPath);
|
|
108
136
|
const contextBlocks = [];
|
|
@@ -166,6 +194,16 @@ export async function buildCompiledRulesContent({
|
|
|
166
194
|
].join('\n')
|
|
167
195
|
);
|
|
168
196
|
|
|
197
|
+
if (normalizedAdditionalStackFileNames.length > 0) {
|
|
198
|
+
contextBlocks.push(
|
|
199
|
+
[
|
|
200
|
+
'## LAYER 2B: ADDITIONAL STACK PROFILES',
|
|
201
|
+
'This project uses multiple stacks. Load all additional stack profiles below:',
|
|
202
|
+
...normalizedAdditionalStackFileNames.map((stackFileName, stackIndex) => `${stackIndex + 1}. .agent-context/stacks/${stackFileName}`),
|
|
203
|
+
].join('\n')
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
169
207
|
const blueprintFilePath = path.join(selectedBlueprintsDirectoryPath, selectedBlueprintFileName);
|
|
170
208
|
const blueprintContent = await fs.readFile(blueprintFilePath, 'utf8');
|
|
171
209
|
const blueprintSummary = firstMarkdownHeading(blueprintContent, selectedBlueprintFileName);
|
|
@@ -178,6 +216,18 @@ export async function buildCompiledRulesContent({
|
|
|
178
216
|
].join('\n')
|
|
179
217
|
);
|
|
180
218
|
|
|
219
|
+
if (normalizedAdditionalBlueprintFileNames.length > 0) {
|
|
220
|
+
contextBlocks.push(
|
|
221
|
+
[
|
|
222
|
+
'## LAYER 3A: ADDITIONAL BLUEPRINT PROFILES',
|
|
223
|
+
'This project uses multiple architecture blueprints. Load all additional blueprint profiles below:',
|
|
224
|
+
...normalizedAdditionalBlueprintFileNames.map(
|
|
225
|
+
(blueprintFileName, blueprintIndex) => `${blueprintIndex + 1}. .agent-context/blueprints/${blueprintFileName}`
|
|
226
|
+
),
|
|
227
|
+
].join('\n')
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
181
231
|
if (includeCiGuardrails) {
|
|
182
232
|
contextBlocks.push(
|
|
183
233
|
[
|
|
@@ -300,7 +350,9 @@ export async function compileDynamicContext({
|
|
|
300
350
|
targetDirectoryPath,
|
|
301
351
|
selectedProfileName,
|
|
302
352
|
selectedStackFileName,
|
|
353
|
+
selectedAdditionalStackFileNames = [],
|
|
303
354
|
selectedBlueprintFileName,
|
|
355
|
+
selectedAdditionalBlueprintFileNames = [],
|
|
304
356
|
includeCiGuardrails,
|
|
305
357
|
}) {
|
|
306
358
|
const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
|
|
@@ -308,7 +360,9 @@ export async function compileDynamicContext({
|
|
|
308
360
|
targetDirectoryPath: resolvedTargetDirectoryPath,
|
|
309
361
|
selectedProfileName,
|
|
310
362
|
selectedStackFileName,
|
|
363
|
+
selectedAdditionalStackFileNames,
|
|
311
364
|
selectedBlueprintFileName,
|
|
365
|
+
selectedAdditionalBlueprintFileNames,
|
|
312
366
|
includeCiGuardrails,
|
|
313
367
|
});
|
|
314
368
|
|
package/lib/cli/constants.mjs
CHANGED
|
@@ -160,6 +160,89 @@ export const PROFILE_PRESETS = {
|
|
|
160
160
|
},
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
export const PROJECT_SCOPE_CHOICES = [
|
|
164
|
+
{
|
|
165
|
+
key: 'api-service',
|
|
166
|
+
label: 'API service',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'web-application',
|
|
170
|
+
label: 'Web application',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: 'mobile-app',
|
|
174
|
+
label: 'Mobile app',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'cli-tool',
|
|
178
|
+
label: 'CLI tool',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: 'library-sdk',
|
|
182
|
+
label: 'Library / SDK',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'other',
|
|
186
|
+
label: 'Other',
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
export const PROJECT_SCOPE_STACK_FILTERS = {
|
|
191
|
+
'api-service': ['typescript.md', 'python.md', 'go.md', 'java.md', 'php.md', 'csharp.md', 'ruby.md', 'rust.md'],
|
|
192
|
+
'web-application': ['typescript.md', 'python.md', 'go.md', 'java.md', 'php.md', 'csharp.md', 'ruby.md', 'rust.md'],
|
|
193
|
+
'mobile-app': ['react-native.md', 'flutter.md'],
|
|
194
|
+
'cli-tool': ['typescript.md', 'python.md', 'go.md', 'rust.md', 'ruby.md', 'java.md', 'csharp.md'],
|
|
195
|
+
'library-sdk': ['typescript.md', 'python.md', 'go.md', 'rust.md', 'java.md', 'csharp.md', 'php.md', 'ruby.md'],
|
|
196
|
+
other: null,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const WEB_FRONTEND_STACK_CANDIDATES = ['typescript.md'];
|
|
200
|
+
|
|
201
|
+
export const WEB_BACKEND_STACK_CANDIDATES = [
|
|
202
|
+
'typescript.md',
|
|
203
|
+
'python.md',
|
|
204
|
+
'go.md',
|
|
205
|
+
'java.md',
|
|
206
|
+
'php.md',
|
|
207
|
+
'csharp.md',
|
|
208
|
+
'ruby.md',
|
|
209
|
+
'rust.md',
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
export const WEB_FRONTEND_BLUEPRINT_CANDIDATES = [
|
|
213
|
+
'api-nextjs.md',
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
export const WEB_BACKEND_BLUEPRINT_CANDIDATES = [
|
|
217
|
+
'nestjs-logic.md',
|
|
218
|
+
'fastapi-service.md',
|
|
219
|
+
'go-service.md',
|
|
220
|
+
'spring-boot-api.md',
|
|
221
|
+
'laravel-api.md',
|
|
222
|
+
'aspnet-api.md',
|
|
223
|
+
'graphql-grpc-api.md',
|
|
224
|
+
'api-nextjs.md',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
export const RUNTIME_ENVIRONMENT_CHOICES = [
|
|
228
|
+
{
|
|
229
|
+
key: 'linux-wsl',
|
|
230
|
+
label: 'Linux / WSL',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
key: 'windows',
|
|
234
|
+
label: 'Windows',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: 'linux',
|
|
238
|
+
label: 'Linux',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: 'macos',
|
|
242
|
+
label: 'macOS',
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
|
|
163
246
|
export const entryPointFiles = [
|
|
164
247
|
'.cursorrules',
|
|
165
248
|
'.windsurfrules',
|
package/lib/cli/detector.mjs
CHANGED
|
@@ -133,6 +133,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
133
133
|
return {
|
|
134
134
|
hasExistingProjectFiles,
|
|
135
135
|
recommendedStackFileName: null,
|
|
136
|
+
secondaryStackFileNames: [],
|
|
136
137
|
recommendedBlueprintFileName: null,
|
|
137
138
|
confidenceLabel: null,
|
|
138
139
|
confidenceScore: 0,
|
|
@@ -164,6 +165,10 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
164
165
|
confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
|
|
165
166
|
evidence: detectionCandidate.evidence,
|
|
166
167
|
}));
|
|
168
|
+
const secondaryStackFileNames = rankedCandidates
|
|
169
|
+
.slice(1)
|
|
170
|
+
.filter((rankedCandidate) => (strongestCandidate.confidenceScore - rankedCandidate.confidenceScore) < 0.08)
|
|
171
|
+
.map((rankedCandidate) => rankedCandidate.stackFileName);
|
|
167
172
|
const detectionReasoning = isAmbiguous
|
|
168
173
|
? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
|
|
169
174
|
: `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(', ') || 'none'}.`;
|
|
@@ -171,6 +176,7 @@ export async function detectProjectContext(targetDirectoryPath) {
|
|
|
171
176
|
return {
|
|
172
177
|
hasExistingProjectFiles,
|
|
173
178
|
recommendedStackFileName: strongestCandidate.stackFileName,
|
|
179
|
+
secondaryStackFileNames,
|
|
174
180
|
recommendedBlueprintFileName: BLUEPRINT_RECOMMENDATIONS[strongestCandidate.stackFileName] || null,
|
|
175
181
|
confidenceLabel,
|
|
176
182
|
confidenceScore: strongestCandidate.confidenceScore,
|
|
@@ -194,7 +200,11 @@ export function buildDetectionSummary(projectDetection) {
|
|
|
194
200
|
? ` Confidence gap: ${projectDetection.confidenceGap}.`
|
|
195
201
|
: '';
|
|
196
202
|
|
|
197
|
-
|
|
203
|
+
const secondaryStacksSummary = projectDetection.secondaryStackFileNames?.length
|
|
204
|
+
? ` Secondary stack signals: ${projectDetection.secondaryStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ')}.`
|
|
205
|
+
: '';
|
|
206
|
+
|
|
207
|
+
return `This folder looks like ${toTitleCase(projectDetection.recommendedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}${secondaryStacksSummary}`;
|
|
198
208
|
}
|
|
199
209
|
|
|
200
210
|
export function formatDetectionCandidates(rankedCandidates) {
|
|
@@ -22,7 +22,7 @@ const PROJECT_DOC_FILE_NAMES = [
|
|
|
22
22
|
'flow-overview.md',
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
export const PROJECT_DOC_TEMPLATE_VERSION = '1.
|
|
25
|
+
export const PROJECT_DOC_TEMPLATE_VERSION = '1.2.0';
|
|
26
26
|
|
|
27
27
|
const DOMAIN_CHOICES = [
|
|
28
28
|
'API service',
|
|
@@ -50,6 +50,13 @@ const AUTH_CHOICES = [
|
|
|
50
50
|
'Other',
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
+
const DOCKER_STRATEGY_CHOICES = [
|
|
54
|
+
'No Docker (run services directly)',
|
|
55
|
+
'Docker for development only',
|
|
56
|
+
'Docker for production only',
|
|
57
|
+
'Docker for both development and production',
|
|
58
|
+
];
|
|
59
|
+
|
|
53
60
|
const DISCOVERY_MODE_CHOICES = [
|
|
54
61
|
'Quick mode (mostly choices, fastest)',
|
|
55
62
|
'Detailed mode (type your own answers)',
|
|
@@ -101,6 +108,100 @@ const FEATURE_PRESET_MAP = {
|
|
|
101
108
|
],
|
|
102
109
|
};
|
|
103
110
|
|
|
111
|
+
function parseBooleanLikeValue(rawValue) {
|
|
112
|
+
const normalizedValue = String(rawValue || '').trim().toLowerCase();
|
|
113
|
+
if (['true', 'yes', 'y', '1'].includes(normalizedValue)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (['false', 'no', 'n', '0'].includes(normalizedValue)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveDockerStrategy({ dockerStrategy, useDocker, useDockerDevelopment, useDockerProduction }) {
|
|
125
|
+
if (typeof dockerStrategy === 'string' && dockerStrategy.trim().length > 0) {
|
|
126
|
+
const normalizedDockerStrategy = dockerStrategy.trim().toLowerCase();
|
|
127
|
+
const directMatch = DOCKER_STRATEGY_CHOICES.find(
|
|
128
|
+
(dockerStrategyChoice) => dockerStrategyChoice.toLowerCase() === normalizedDockerStrategy
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (directMatch) {
|
|
132
|
+
return directMatch;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const normalizedUseDocker = typeof useDocker === 'boolean' ? useDocker : parseBooleanLikeValue(useDocker);
|
|
137
|
+
const normalizedUseDockerDevelopment = typeof useDockerDevelopment === 'boolean'
|
|
138
|
+
? useDockerDevelopment
|
|
139
|
+
: parseBooleanLikeValue(useDockerDevelopment);
|
|
140
|
+
const normalizedUseDockerProduction = typeof useDockerProduction === 'boolean'
|
|
141
|
+
? useDockerProduction
|
|
142
|
+
: parseBooleanLikeValue(useDockerProduction);
|
|
143
|
+
|
|
144
|
+
if (normalizedUseDocker === false) {
|
|
145
|
+
return DOCKER_STRATEGY_CHOICES[0];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction === true) {
|
|
149
|
+
return DOCKER_STRATEGY_CHOICES[3];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction !== true) {
|
|
153
|
+
return DOCKER_STRATEGY_CHOICES[1];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (normalizedUseDockerProduction === true && normalizedUseDockerDevelopment !== true) {
|
|
157
|
+
return DOCKER_STRATEGY_CHOICES[2];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (normalizedUseDocker === true) {
|
|
161
|
+
return DOCKER_STRATEGY_CHOICES[3];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return DOCKER_STRATEGY_CHOICES[0];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseDockerStrategy(dockerStrategy) {
|
|
168
|
+
const normalizedDockerStrategy = String(dockerStrategy || '').toLowerCase();
|
|
169
|
+
|
|
170
|
+
if (normalizedDockerStrategy.includes('both development and production')) {
|
|
171
|
+
return {
|
|
172
|
+
dockerStrategy: DOCKER_STRATEGY_CHOICES[3],
|
|
173
|
+
hasDocker: true,
|
|
174
|
+
useDockerDevelopment: true,
|
|
175
|
+
useDockerProduction: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (normalizedDockerStrategy.includes('development only')) {
|
|
180
|
+
return {
|
|
181
|
+
dockerStrategy: DOCKER_STRATEGY_CHOICES[1],
|
|
182
|
+
hasDocker: true,
|
|
183
|
+
useDockerDevelopment: true,
|
|
184
|
+
useDockerProduction: false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (normalizedDockerStrategy.includes('production only')) {
|
|
189
|
+
return {
|
|
190
|
+
dockerStrategy: DOCKER_STRATEGY_CHOICES[2],
|
|
191
|
+
hasDocker: true,
|
|
192
|
+
useDockerDevelopment: false,
|
|
193
|
+
useDockerProduction: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
dockerStrategy: DOCKER_STRATEGY_CHOICES[0],
|
|
199
|
+
hasDocker: false,
|
|
200
|
+
useDockerDevelopment: false,
|
|
201
|
+
useDockerProduction: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
async function askFeatureList(userInterface) {
|
|
105
206
|
console.log('\nList your key features (one per line, press Enter to finish):');
|
|
106
207
|
|
|
@@ -184,7 +285,15 @@ export async function runProjectDiscovery(userInterface, options = {}) {
|
|
|
184
285
|
}
|
|
185
286
|
|
|
186
287
|
if (!projectName) {
|
|
187
|
-
|
|
288
|
+
const projectNamePrompt = defaultProjectName
|
|
289
|
+
? `Project name (press Enter to use folder name: ${defaultProjectName}): `
|
|
290
|
+
: 'Project name: ';
|
|
291
|
+
|
|
292
|
+
projectName = (await userInterface.question(projectNamePrompt)).trim();
|
|
293
|
+
|
|
294
|
+
if (!projectName && defaultProjectName) {
|
|
295
|
+
projectName = defaultProjectName;
|
|
296
|
+
}
|
|
188
297
|
}
|
|
189
298
|
|
|
190
299
|
if (!projectName) {
|
|
@@ -245,6 +354,42 @@ export async function runProjectDiscovery(userInterface, options = {}) {
|
|
|
245
354
|
authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
|
|
246
355
|
}
|
|
247
356
|
|
|
357
|
+
let dockerStrategy = DOCKER_STRATEGY_CHOICES[0];
|
|
358
|
+
if (isQuickMode) {
|
|
359
|
+
dockerStrategy = await askChoice(
|
|
360
|
+
'Containerization strategy:',
|
|
361
|
+
DOCKER_STRATEGY_CHOICES,
|
|
362
|
+
userInterface
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
const useDocker = await askYesNo(
|
|
366
|
+
'Use Docker for this project (development and/or production)?',
|
|
367
|
+
userInterface,
|
|
368
|
+
false
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (useDocker) {
|
|
372
|
+
const useDockerDevelopment = await askYesNo(
|
|
373
|
+
'Use Docker for development workflow?',
|
|
374
|
+
userInterface,
|
|
375
|
+
true
|
|
376
|
+
);
|
|
377
|
+
const useDockerProduction = await askYesNo(
|
|
378
|
+
'Use Docker for production runtime/build?',
|
|
379
|
+
userInterface,
|
|
380
|
+
true
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
dockerStrategy = resolveDockerStrategy({
|
|
384
|
+
useDocker,
|
|
385
|
+
useDockerDevelopment,
|
|
386
|
+
useDockerProduction,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const parsedDockerStrategy = parseDockerStrategy(dockerStrategy);
|
|
392
|
+
|
|
248
393
|
let features = [];
|
|
249
394
|
if (isQuickMode) {
|
|
250
395
|
const selectedFeaturePreset = await askChoice(
|
|
@@ -289,6 +434,9 @@ export async function runProjectDiscovery(userInterface, options = {}) {
|
|
|
289
434
|
primaryDomain,
|
|
290
435
|
databaseChoice,
|
|
291
436
|
authStrategy,
|
|
437
|
+
dockerStrategy: parsedDockerStrategy.dockerStrategy,
|
|
438
|
+
useDockerDevelopment: parsedDockerStrategy.useDockerDevelopment,
|
|
439
|
+
useDockerProduction: parsedDockerStrategy.useDockerProduction,
|
|
292
440
|
features,
|
|
293
441
|
additionalContext,
|
|
294
442
|
};
|
|
@@ -335,6 +483,13 @@ export function resolveDocumentManifest(discoveryAnswers) {
|
|
|
335
483
|
export function buildTemplateContext(discoveryAnswers, initContext) {
|
|
336
484
|
const hasDatabase = !discoveryAnswers.databaseChoice.toLowerCase().startsWith('none');
|
|
337
485
|
const hasAuth = !discoveryAnswers.authStrategy.toLowerCase().startsWith('none');
|
|
486
|
+
const additionalStackFileNames = Array.isArray(initContext.additionalStackFileNames)
|
|
487
|
+
? initContext.additionalStackFileNames
|
|
488
|
+
: [];
|
|
489
|
+
const additionalBlueprintFileNames = Array.isArray(initContext.additionalBlueprintFileNames)
|
|
490
|
+
? initContext.additionalBlueprintFileNames
|
|
491
|
+
: [];
|
|
492
|
+
const parsedDockerStrategy = parseDockerStrategy(discoveryAnswers.dockerStrategy);
|
|
338
493
|
|
|
339
494
|
const baseUrlMap = {
|
|
340
495
|
'API service': 'http://localhost:3000',
|
|
@@ -354,14 +509,34 @@ export function buildTemplateContext(discoveryAnswers, initContext) {
|
|
|
354
509
|
additionalContext: discoveryAnswers.additionalContext,
|
|
355
510
|
stackFileName: initContext.stackFileName,
|
|
356
511
|
stackDisplayName: toTitleCase(initContext.stackFileName),
|
|
512
|
+
additionalStackFileNames,
|
|
513
|
+
additionalStackDisplayNames: additionalStackFileNames.length > 0
|
|
514
|
+
? additionalStackFileNames.map((stackFileName) => toTitleCase(stackFileName))
|
|
515
|
+
: null,
|
|
357
516
|
blueprintFileName: initContext.blueprintFileName,
|
|
358
517
|
blueprintDisplayName: toTitleCase(initContext.blueprintFileName),
|
|
518
|
+
additionalBlueprintFileNames,
|
|
519
|
+
additionalBlueprintDisplayNames: additionalBlueprintFileNames.length > 0
|
|
520
|
+
? additionalBlueprintFileNames.map((blueprintFileName) => toTitleCase(blueprintFileName))
|
|
521
|
+
: null,
|
|
522
|
+
runtimeEnvironmentKey: initContext.runtimeEnvironmentKey || 'linux',
|
|
523
|
+
runtimeEnvironmentLabel: initContext.runtimeEnvironmentLabel || 'Linux',
|
|
359
524
|
cliVersion: CLI_VERSION,
|
|
360
525
|
templateVersion: PROJECT_DOC_TEMPLATE_VERSION,
|
|
361
526
|
generatedAt: new Date().toISOString(),
|
|
362
527
|
generatedDate: new Date().toISOString().split('T')[0],
|
|
363
528
|
hasDatabase,
|
|
364
529
|
hasAuth,
|
|
530
|
+
hasDocker: parsedDockerStrategy.hasDocker,
|
|
531
|
+
dockerStrategy: parsedDockerStrategy.dockerStrategy,
|
|
532
|
+
useDockerDevelopment: parsedDockerStrategy.useDockerDevelopment,
|
|
533
|
+
useDockerProduction: parsedDockerStrategy.useDockerProduction,
|
|
534
|
+
dockerDevelopmentGuidance: parsedDockerStrategy.useDockerDevelopment
|
|
535
|
+
? '- Development containers are required: optimize for fast rebuilds, bind mounts, and debug-friendly startup.'
|
|
536
|
+
: '',
|
|
537
|
+
dockerProductionGuidance: parsedDockerStrategy.useDockerProduction
|
|
538
|
+
? '- Production containers are required: use multi-stage builds, non-root runtime, and minimal image footprint.'
|
|
539
|
+
: '',
|
|
365
540
|
baseUrl: baseUrlMap[discoveryAnswers.primaryDomain] || 'http://localhost:3000',
|
|
366
541
|
};
|
|
367
542
|
}
|
|
@@ -587,6 +762,12 @@ export async function loadProjectConfig(configFilePath) {
|
|
|
587
762
|
primaryDomain: configEntries.primaryDomain || configEntries.domain || 'API service',
|
|
588
763
|
databaseChoice: configEntries.databaseChoice || configEntries.database || 'None (stateless service)',
|
|
589
764
|
authStrategy: configEntries.authStrategy || configEntries.auth || 'None (public service)',
|
|
765
|
+
dockerStrategy: resolveDockerStrategy({
|
|
766
|
+
dockerStrategy: configEntries.dockerStrategy || configEntries.containerStrategy,
|
|
767
|
+
useDocker: configEntries.useDocker,
|
|
768
|
+
useDockerDevelopment: configEntries.useDockerDevelopment || configEntries.dockerDevelopment,
|
|
769
|
+
useDockerProduction: configEntries.useDockerProduction || configEntries.dockerProduction,
|
|
770
|
+
}),
|
|
590
771
|
features: Array.isArray(configEntries.features) ? configEntries.features : [],
|
|
591
772
|
additionalContext: configEntries.additionalContext || configEntries.context || 'No additional context provided.',
|
|
592
773
|
docsLang: configEntries.docsLang || configEntries.docsLanguage || 'en',
|