@ryuenn3123/agentic-senior-core 3.0.15 → 3.0.16

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.
@@ -0,0 +1,315 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import { askChoice, askYesNo } from '../utils.mjs';
4
+ import {
5
+ ARCHITECTURE_STYLE_CHOICES,
6
+ AUTH_CHOICES,
7
+ DATABASE_CHOICES,
8
+ DOCKER_STRATEGY_CHOICES,
9
+ DOMAIN_CHOICES,
10
+ SUPPORTED_DOC_LANGUAGES,
11
+ } from './constants.mjs';
12
+
13
+ function parseBooleanLikeValue(rawValue) {
14
+ const normalizedValue = String(rawValue || '').trim().toLowerCase();
15
+ if (['true', 'yes', 'y', '1'].includes(normalizedValue)) {
16
+ return true;
17
+ }
18
+
19
+ if (['false', 'no', 'n', '0'].includes(normalizedValue)) {
20
+ return false;
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ function resolveDockerStrategy({ dockerStrategy, useDocker, useDockerDevelopment, useDockerProduction }) {
27
+ if (typeof dockerStrategy === 'string' && dockerStrategy.trim().length > 0) {
28
+ const normalizedDockerStrategy = dockerStrategy.trim().toLowerCase();
29
+ const directMatch = DOCKER_STRATEGY_CHOICES.find(
30
+ (dockerStrategyChoice) => dockerStrategyChoice.toLowerCase() === normalizedDockerStrategy
31
+ );
32
+
33
+ if (directMatch) {
34
+ return directMatch;
35
+ }
36
+ }
37
+
38
+ const normalizedUseDocker = typeof useDocker === 'boolean' ? useDocker : parseBooleanLikeValue(useDocker);
39
+ const normalizedUseDockerDevelopment = typeof useDockerDevelopment === 'boolean'
40
+ ? useDockerDevelopment
41
+ : parseBooleanLikeValue(useDockerDevelopment);
42
+ const normalizedUseDockerProduction = typeof useDockerProduction === 'boolean'
43
+ ? useDockerProduction
44
+ : parseBooleanLikeValue(useDockerProduction);
45
+
46
+ if (normalizedUseDocker === false) {
47
+ return DOCKER_STRATEGY_CHOICES[0];
48
+ }
49
+
50
+ if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction === true) {
51
+ return DOCKER_STRATEGY_CHOICES[3];
52
+ }
53
+
54
+ if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction !== true) {
55
+ return DOCKER_STRATEGY_CHOICES[1];
56
+ }
57
+
58
+ if (normalizedUseDockerProduction === true && normalizedUseDockerDevelopment !== true) {
59
+ return DOCKER_STRATEGY_CHOICES[2];
60
+ }
61
+
62
+ if (normalizedUseDocker === true) {
63
+ return DOCKER_STRATEGY_CHOICES[3];
64
+ }
65
+
66
+ return DOCKER_STRATEGY_CHOICES[0];
67
+ }
68
+
69
+ function resolveArchitectureStyle(rawArchitectureStyle) {
70
+ const normalizedArchitectureStyle = String(rawArchitectureStyle || '').trim().toLowerCase();
71
+
72
+ if (!normalizedArchitectureStyle) {
73
+ return ARCHITECTURE_STYLE_CHOICES[0];
74
+ }
75
+
76
+ if (normalizedArchitectureStyle === 'monolith' || normalizedArchitectureStyle === 'modular monolith') {
77
+ return ARCHITECTURE_STYLE_CHOICES[0];
78
+ }
79
+
80
+ if (
81
+ normalizedArchitectureStyle === 'microservice'
82
+ || normalizedArchitectureStyle === 'microservices'
83
+ || normalizedArchitectureStyle === 'distributed'
84
+ || normalizedArchitectureStyle === 'distributed system'
85
+ || normalizedArchitectureStyle === 'microservice / distributed system'
86
+ ) {
87
+ return ARCHITECTURE_STYLE_CHOICES[1];
88
+ }
89
+
90
+ const directMatch = ARCHITECTURE_STYLE_CHOICES.find(
91
+ (architectureStyleChoice) => architectureStyleChoice.toLowerCase() === normalizedArchitectureStyle
92
+ );
93
+
94
+ return directMatch || ARCHITECTURE_STYLE_CHOICES[0];
95
+ }
96
+
97
+ async function askFeatureList(userInterface) {
98
+ console.log('\nList your key features (one per line, press Enter to finish):');
99
+
100
+ const features = [];
101
+ while (features.length < 10) {
102
+ const featureLine = (await userInterface.question(` Feature ${features.length + 1}: `)).trim();
103
+ if (!featureLine) {
104
+ break;
105
+ }
106
+
107
+ features.push(featureLine);
108
+ }
109
+
110
+ return features;
111
+ }
112
+
113
+ export function normalizeDocsLanguage(rawDocsLanguage = 'en') {
114
+ const normalizedDocsLanguage = String(rawDocsLanguage || 'en').trim().toLowerCase();
115
+ return SUPPORTED_DOC_LANGUAGES.has(normalizedDocsLanguage) ? normalizedDocsLanguage : null;
116
+ }
117
+
118
+ export async function runProjectDiscovery(userInterface, options = {}) {
119
+ console.log('\n--- Project Setup ---');
120
+ console.log('I will ask one focused set of questions to bootstrap project context and documentation.');
121
+ console.log('This helps AI agents understand your project before writing code.\n');
122
+ console.log('You can answer in your own language.');
123
+ console.log('CLI prompts stay in English, but non-English answers are fully supported.\n');
124
+
125
+ const defaultProjectName = (options.defaultProjectName || '').trim();
126
+ const defaultProjectDescription = String(options.defaultProjectDescription || '').trim();
127
+ const defaultIncludeCiGuardrails = typeof options.defaultIncludeCiGuardrails === 'boolean'
128
+ ? options.defaultIncludeCiGuardrails
129
+ : true;
130
+ const shouldAskForCiGuardrails = options.askForCiGuardrails !== false;
131
+
132
+ const projectNamePrompt = defaultProjectName
133
+ ? `Project name (press Enter to use folder name: ${defaultProjectName}): `
134
+ : 'Project name: ';
135
+
136
+ let projectName = (await userInterface.question(projectNamePrompt)).trim();
137
+
138
+ if (!projectName && defaultProjectName) {
139
+ projectName = defaultProjectName;
140
+ }
141
+
142
+ if (!projectName) {
143
+ throw new Error('Project name is required for documentation scaffolding.');
144
+ }
145
+
146
+ const projectDescriptionPrompt = defaultProjectDescription
147
+ ? `One-line description (press Enter to use: ${defaultProjectDescription}): `
148
+ : 'One-line description: ';
149
+
150
+ let projectDescription = (await userInterface.question(projectDescriptionPrompt)).trim();
151
+
152
+ if (!projectDescription) {
153
+ projectDescription = defaultProjectDescription || `A ${projectName} project.`;
154
+ }
155
+
156
+ const architectureStyle = await askChoice(
157
+ 'Project topology:',
158
+ ARCHITECTURE_STYLE_CHOICES,
159
+ userInterface
160
+ );
161
+
162
+ const includeCiGuardrails = shouldAskForCiGuardrails
163
+ ? await askYesNo(
164
+ 'Enable CI/CD quality checks (guardrails) and the LLM Judge policy?',
165
+ userInterface,
166
+ defaultIncludeCiGuardrails
167
+ )
168
+ : defaultIncludeCiGuardrails;
169
+
170
+ const domainSelection = await askChoice('Primary domain:', DOMAIN_CHOICES, userInterface);
171
+ let primaryDomain = domainSelection;
172
+ if (domainSelection === 'Other') {
173
+ primaryDomain = (await userInterface.question('Describe your domain: ')).trim() || 'Custom domain';
174
+ }
175
+
176
+ const databaseSelection = await askChoice('Database needs:', DATABASE_CHOICES, userInterface);
177
+ let databaseChoice = databaseSelection;
178
+ if (databaseSelection === 'Other') {
179
+ databaseChoice = (await userInterface.question('Describe your database setup: ')).trim() || 'Custom database';
180
+ }
181
+
182
+ const authSelection = await askChoice('Auth strategy:', AUTH_CHOICES, userInterface);
183
+ let authStrategy = authSelection;
184
+ if (authSelection === 'Other') {
185
+ authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
186
+ }
187
+
188
+ const dockerStrategy = await askChoice(
189
+ 'Containerization strategy:',
190
+ DOCKER_STRATEGY_CHOICES,
191
+ userInterface
192
+ );
193
+
194
+ const features = await askFeatureList(userInterface);
195
+
196
+ return {
197
+ projectName,
198
+ projectDescription,
199
+ architectureStyle,
200
+ includeCiGuardrails,
201
+ primaryDomain,
202
+ databaseChoice,
203
+ authStrategy,
204
+ dockerStrategy,
205
+ features,
206
+ additionalContext: 'No additional context provided.',
207
+ };
208
+ }
209
+
210
+ export function resolveProjectDocTargets(discoveryAnswers) {
211
+ const hasDatabase = !discoveryAnswers.databaseChoice.toLowerCase().startsWith('none');
212
+ const isApiOrWebDomain = ['API service', 'Web application'].includes(discoveryAnswers.primaryDomain)
213
+ || discoveryAnswers.primaryDomain.toLowerCase().includes('api')
214
+ || discoveryAnswers.primaryDomain.toLowerCase().includes('web');
215
+
216
+ const requiredDocFileNames = [
217
+ 'project-brief.md',
218
+ 'architecture-decision-record.md',
219
+ 'flow-overview.md',
220
+ ];
221
+
222
+ if (hasDatabase) {
223
+ requiredDocFileNames.push('database-schema.md');
224
+ }
225
+
226
+ if (isApiOrWebDomain) {
227
+ requiredDocFileNames.push('api-contract.md');
228
+ }
229
+
230
+ return { requiredDocFileNames };
231
+ }
232
+
233
+ export function buildSynthesisContext(_discoveryAnswers, initContext) {
234
+ const additionalStackFileNames = Array.isArray(initContext.additionalStackFileNames)
235
+ ? initContext.additionalStackFileNames
236
+ : [];
237
+ const additionalBlueprintFileNames = Array.isArray(initContext.additionalBlueprintFileNames)
238
+ ? initContext.additionalBlueprintFileNames
239
+ : [];
240
+
241
+ return {
242
+ stackFileName: initContext.stackFileName,
243
+ additionalStackFileNames,
244
+ blueprintFileName: initContext.blueprintFileName,
245
+ additionalBlueprintFileNames,
246
+ runtimeEnvironmentKey: initContext.runtimeEnvironmentKey || 'linux',
247
+ runtimeEnvironmentLabel: initContext.runtimeEnvironmentLabel || 'Linux',
248
+ };
249
+ }
250
+
251
+ export async function loadProjectConfig(configFilePath) {
252
+ const configContent = await fs.readFile(configFilePath, 'utf8');
253
+ const configLines = configContent.split(/\r?\n/);
254
+ const configEntries = {};
255
+ let currentKey = null;
256
+ let currentArrayValues = null;
257
+
258
+ for (const configLine of configLines) {
259
+ const trimmedLine = configLine.trim();
260
+
261
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
262
+ continue;
263
+ }
264
+
265
+ if (trimmedLine.startsWith('- ') && currentKey && currentArrayValues !== null) {
266
+ currentArrayValues.push(trimmedLine.slice(2).trim());
267
+ continue;
268
+ }
269
+
270
+ if (currentKey && currentArrayValues !== null) {
271
+ configEntries[currentKey] = currentArrayValues;
272
+ currentKey = null;
273
+ currentArrayValues = null;
274
+ }
275
+
276
+ const colonIndex = trimmedLine.indexOf(':');
277
+ if (colonIndex === -1) {
278
+ continue;
279
+ }
280
+
281
+ const entryKey = trimmedLine.slice(0, colonIndex).trim();
282
+ const entryValue = trimmedLine.slice(colonIndex + 1).trim();
283
+
284
+ if (!entryValue) {
285
+ currentKey = entryKey;
286
+ currentArrayValues = [];
287
+ continue;
288
+ }
289
+
290
+ configEntries[entryKey] = entryValue;
291
+ }
292
+
293
+ if (currentKey && currentArrayValues !== null) {
294
+ configEntries[currentKey] = currentArrayValues;
295
+ }
296
+
297
+ return {
298
+ projectName: configEntries.projectName || configEntries.name || '',
299
+ projectDescription: configEntries.projectDescription || configEntries.description || '',
300
+ architectureStyle: resolveArchitectureStyle(configEntries.architectureStyle || configEntries.topology || configEntries.serviceTopology),
301
+ includeCiGuardrails: parseBooleanLikeValue(configEntries.includeCiGuardrails) ?? parseBooleanLikeValue(configEntries.ci) ?? true,
302
+ primaryDomain: configEntries.primaryDomain || configEntries.domain || 'API service',
303
+ databaseChoice: configEntries.databaseChoice || configEntries.database || 'None (stateless service)',
304
+ authStrategy: configEntries.authStrategy || configEntries.auth || 'None (public service)',
305
+ dockerStrategy: resolveDockerStrategy({
306
+ dockerStrategy: configEntries.dockerStrategy || configEntries.containerStrategy,
307
+ useDocker: configEntries.useDocker,
308
+ useDockerDevelopment: configEntries.useDockerDevelopment || configEntries.dockerDevelopment,
309
+ useDockerProduction: configEntries.useDockerProduction || configEntries.dockerProduction,
310
+ }),
311
+ features: Array.isArray(configEntries.features) ? configEntries.features : [],
312
+ additionalContext: configEntries.additionalContext || configEntries.context || 'No additional context provided.',
313
+ docsLang: configEntries.docsLang || configEntries.docsLanguage || 'en',
314
+ };
315
+ }
@@ -0,0 +1,196 @@
1
+ import { toTitleCase } from '../utils.mjs';
2
+ import {
3
+ ARCHITECTURE_STYLE_CHOICES,
4
+ PROJECT_DOC_SYNTHESIS_PROMPT_VERSION,
5
+ } from './constants.mjs';
6
+ import { buildDesignIntentSeed } from './design-contract.mjs';
7
+
8
+ export function buildProjectContextBootstrapPrompt({
9
+ discoveryAnswers,
10
+ initContext,
11
+ expectedDocFileNames,
12
+ docsLanguage,
13
+ architectureRecommendation,
14
+ }) {
15
+ const featuresList = Array.isArray(discoveryAnswers.features) && discoveryAnswers.features.length > 0
16
+ ? discoveryAnswers.features.map((feature, featureIndex) => `${featureIndex + 1}. ${feature}`).join('\n')
17
+ : 'Derive the first concrete feature set from the project name, description, and domain. Do not invent arbitrary modules just to fill space.';
18
+
19
+ const expectedDocsList = expectedDocFileNames
20
+ .map((fileName, fileIndex) => `${fileIndex + 1}. docs/${fileName}`)
21
+ .join('\n');
22
+
23
+ const architectureSnapshot = architectureRecommendation
24
+ ? JSON.stringify({
25
+ briefType: architectureRecommendation.briefType,
26
+ stack: architectureRecommendation.recommendedStackFileName,
27
+ blueprint: architectureRecommendation.recommendedBlueprintFileName,
28
+ confidenceLabel: architectureRecommendation.confidenceLabel,
29
+ confidenceScore: architectureRecommendation.confidenceScore,
30
+ signalSummary: architectureRecommendation.signalSummary,
31
+ uncertaintyNotes: architectureRecommendation.uncertaintyNotes,
32
+ failureModes: architectureRecommendation.failureModes,
33
+ }, null, 2)
34
+ : 'null';
35
+
36
+ return [
37
+ '# Bootstrap Prompt: Dynamic Project Context Synthesis',
38
+ '',
39
+ `Protocol version: ${PROJECT_DOC_SYNTHESIS_PROMPT_VERSION}`,
40
+ '',
41
+ 'You are a Lead Solution Architect and Principal Engineer.',
42
+ 'Write project context docs from scratch (no template rendering, no placeholder boilerplate).',
43
+ '',
44
+ '## Mission',
45
+ `Create or update these files in ${docsLanguage.toUpperCase()} language:`,
46
+ expectedDocsList,
47
+ '',
48
+ '## Hard Rules',
49
+ '1. No copy-paste from external prose.',
50
+ '2. Every major section must explain rationale and tradeoffs.',
51
+ '3. Keep stack, database, and auth aligned with the project constraints below unless user explicitly requests migration.',
52
+ '4. Output must be implementation-ready for engineers, not generic textbook explanation.',
53
+ '5. For any ecosystem or technology claim, perform live web research and include citation metadata (source + fetchedAt timestamp) rather than relying on offline heuristics.',
54
+ '6. Write for native English speakers at an 8th-grade reading level. Use clear, direct, plain language.',
55
+ '7. Avoid emoji, AI cliches, buzzwords, academic phrasing, padding, and generic filler.',
56
+ '8. Separate confirmed facts from assumptions explicitly. When context is incomplete, add an `Assumptions to Validate` section and a `Next Validation Action` line.',
57
+ '9. If user inputs conflict with repo evidence, call out the conflict and choose the safer interpretation instead of silently forcing a generic answer.',
58
+ '10. Do not invent modules or architecture layers only to make the docs look complete.',
59
+ '11. Prefer the latest stable compatible framework and package versions. If an official framework setup flow yields newer, better-supported defaults than manual package assembly, prefer that path.',
60
+ '12. Treat project topology as a design constraint: if the project is a monolith, explain why that shape is sufficient; if it is a microservice or distributed system, explain the service boundary logic and why the split is justified.',
61
+ '',
62
+ '## Project Inputs',
63
+ `- Project name: ${discoveryAnswers.projectName}`,
64
+ `- Project description: ${discoveryAnswers.projectDescription}`,
65
+ `- Project topology: ${discoveryAnswers.architectureStyle || ARCHITECTURE_STYLE_CHOICES[0]}`,
66
+ `- Primary domain: ${discoveryAnswers.primaryDomain}`,
67
+ `- Database strategy: ${discoveryAnswers.databaseChoice}`,
68
+ `- Auth strategy: ${discoveryAnswers.authStrategy}`,
69
+ `- Docker strategy: ${discoveryAnswers.dockerStrategy}`,
70
+ `- Runtime environment: ${initContext.runtimeEnvironmentLabel || initContext.runtimeEnvironmentKey || 'Linux'}`,
71
+ `- Selected stack: ${toTitleCase(initContext.stackFileName)}`,
72
+ `- Selected blueprint: ${toTitleCase(initContext.blueprintFileName)}`,
73
+ `- Additional stacks: ${Array.isArray(initContext.additionalStackFileNames) && initContext.additionalStackFileNames.length > 0 ? initContext.additionalStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ') : 'none'}`,
74
+ `- Additional blueprints: ${Array.isArray(initContext.additionalBlueprintFileNames) && initContext.additionalBlueprintFileNames.length > 0 ? initContext.additionalBlueprintFileNames.map((blueprintFileName) => toTitleCase(blueprintFileName)).join(', ') : 'none'}`,
75
+ '',
76
+ '## Key Features',
77
+ featuresList,
78
+ '',
79
+ '## Additional Context',
80
+ discoveryAnswers.additionalContext || 'No additional context provided.',
81
+ '',
82
+ '## Architecture Brief Snapshot (for grounding)',
83
+ '```json',
84
+ architectureSnapshot,
85
+ '```',
86
+ '',
87
+ '## Required Execution',
88
+ '1. Create all required docs files listed above with complete Markdown content.',
89
+ '2. Make the docs adaptive to the real repo and prompt context. These are living references, not frozen templates.',
90
+ '3. In docs/project-brief.md and docs/architecture-decision-record.md, include explicit sections for confirmed facts, assumptions to validate, and next validation actions whenever context is incomplete.',
91
+ '4. Keep content original, specific to this project, and actionable for implementation.',
92
+ '5. After writing docs, continue coding tasks using these docs as living project context.',
93
+ '',
94
+ ].join('\n');
95
+ }
96
+
97
+ export function buildDesignBootstrapPrompt({
98
+ discoveryAnswers,
99
+ initContext,
100
+ docsLanguage,
101
+ architectureRecommendation,
102
+ }) {
103
+ const designIntentSeed = buildDesignIntentSeed({
104
+ discoveryAnswers,
105
+ initContext,
106
+ architectureRecommendation,
107
+ });
108
+
109
+ return [
110
+ '# Bootstrap Prompt: Dynamic Design Contract Synthesis',
111
+ '',
112
+ `Protocol version: ${PROJECT_DOC_SYNTHESIS_PROMPT_VERSION}`,
113
+ '',
114
+ 'You are the Lead UI/UX Art Director for this project.',
115
+ 'Create a dynamic design contract, not a fixed stylistic template.',
116
+ '',
117
+ '## Mission',
118
+ `Author docs/DESIGN.md in ${docsLanguage.toUpperCase()} language with strong art direction and engineering-ready guidance.`,
119
+ 'Keep docs/design-intent.json synchronized as the machine-readable source of design intent.',
120
+ '',
121
+ '## Deliverables',
122
+ '1. docs/DESIGN.md',
123
+ '2. docs/design-intent.json',
124
+ '',
125
+ '## Required DESIGN.md Sections',
126
+ '1. Design Vision and Product Personality',
127
+ '2. Audience and Use-Context Signals',
128
+ '3. Visual Direction and Distinctive Moves',
129
+ '4. Color Science and Semantic Roles',
130
+ '5. Typographic Engineering and Hierarchy',
131
+ '6. Spacing, Layout Rhythm, and Density Strategy',
132
+ '7. Token Architecture and Alias Strategy',
133
+ '8. Responsive Strategy and Cross-Viewport Adaptation Matrix',
134
+ '9. Motion and Interaction Principles',
135
+ '10. Component Language and Morphology (cards, forms, nav, states)',
136
+ '11. Accessibility Non-Negotiables',
137
+ '12. Anti-Patterns to Avoid',
138
+ '13. Implementation Notes for Future UI Tasks',
139
+ '',
140
+ '## Required design-intent.json Fields',
141
+ '1. mode',
142
+ '2. status',
143
+ '3. project',
144
+ '4. designPhilosophy',
145
+ '5. brandAdjectives',
146
+ '6. antiAdjectives',
147
+ '7. visualDirection',
148
+ '8. mathSystems',
149
+ '9. tokenSystem',
150
+ '10. colorTruth',
151
+ '11. crossViewportAdaptation',
152
+ '12. motionSystem',
153
+ '13. componentMorphology',
154
+ '14. experiencePrinciples',
155
+ '15. forbiddenPatterns',
156
+ '16. validationHints',
157
+ '17. requiredDesignSections',
158
+ '18. implementation',
159
+ '',
160
+ '## Hard Rules',
161
+ '1. No copy-paste from external style guides.',
162
+ '2. Every major decision must include psychological/product rationale.',
163
+ '3. Keep implementation feasible for the selected stack and blueprint.',
164
+ '4. Keep tone decisive like an art director, not generic AI boilerplate.',
165
+ '5. Do not anchor the final design language to a famous brand reference. Translate inspiration into original project-specific principles.',
166
+ '6. Reject interchangeable hero layouts, generic SaaS gradients, and trend-chasing decoration unless the project context explicitly justifies them.',
167
+ '7. Encode color intent in perceptual terms first. Hex values may exist only as implementation derivatives.',
168
+ '8. Token guidance must define primitive, semantic, and component layers plus aliasing rules. Component tokens should consume semantic aliases instead of raw values.',
169
+ '9. Responsive guidance must include layout mutation rules for mobile, tablet, and desktop. Shrinking the desktop layout is not enough.',
170
+ '10. Motion can be bold, cinematic, or highly expressive when it improves memorability, hierarchy, feedback, or product confidence. Optimize it instead of flattening it. Only reject motion when it harms clarity, accessibility, or runtime performance.',
171
+ '11. Define component morphology across interaction states and viewports so cards, forms, nav, and feedback surfaces adapt coherently instead of only resizing.',
172
+ '11. Keep UI-only requests context-isolated. Load frontend design rules first and do not eagerly load backend-only rules unless the task explicitly crosses those boundaries.',
173
+ '12. Make at least one memorable visual bet so the resulting system is recognizable and not template-neutral.',
174
+ '',
175
+ '## Project Inputs',
176
+ `- Project name: ${discoveryAnswers.projectName}`,
177
+ `- Product context: ${discoveryAnswers.projectDescription}`,
178
+ `- Project topology: ${discoveryAnswers.architectureStyle || ARCHITECTURE_STYLE_CHOICES[0]}`,
179
+ `- Domain: ${discoveryAnswers.primaryDomain}`,
180
+ `- Stack: ${toTitleCase(initContext.stackFileName)}`,
181
+ `- Blueprint: ${toTitleCase(initContext.blueprintFileName)}`,
182
+ '',
183
+ '## Seed Machine Contract',
184
+ 'Refine this seed instead of discarding it. Keep the final JSON aligned with the markdown design system.',
185
+ '```json',
186
+ designIntentSeed.trim(),
187
+ '```',
188
+ '',
189
+ '## Required Execution',
190
+ '1. Create or update docs/DESIGN.md with complete content.',
191
+ '2. Create or update docs/design-intent.json with machine-readable design intent.',
192
+ '3. Ensure both files stay project-specific, dynamic, and practical for implementation and review.',
193
+ '4. After the contract exists, use it as a first-class source for future UI tasks.',
194
+ '',
195
+ ].join('\n');
196
+ }
@@ -0,0 +1,154 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { ensureDirectory, pathExists } from '../utils.mjs';
5
+ import {
6
+ PROJECT_DOC_FILE_NAMES,
7
+ PROJECT_DOC_SYNTHESIS_PROMPT_VERSION,
8
+ PROJECT_DOC_TEMPLATE_VERSION,
9
+ UI_DESIGN_CONTRACT_FILE_NAMES,
10
+ } from './constants.mjs';
11
+ import {
12
+ buildSynthesisContext,
13
+ normalizeDocsLanguage,
14
+ resolveProjectDocTargets,
15
+ } from './discovery.mjs';
16
+ import {
17
+ buildDesignIntentSeed,
18
+ shouldBootstrapDesignDocument,
19
+ } from './design-contract.mjs';
20
+ import {
21
+ buildDesignBootstrapPrompt,
22
+ buildProjectContextBootstrapPrompt,
23
+ } from './prompt-builders.mjs';
24
+
25
+ export async function generateProjectDocumentation(
26
+ targetDirectoryPath,
27
+ discoveryAnswers,
28
+ initContext,
29
+ options = {}
30
+ ) {
31
+ const normalizedDocsLanguage = normalizeDocsLanguage(options.docsLanguage || 'en');
32
+ if (!normalizedDocsLanguage) {
33
+ throw new Error(`Unsupported docs language: ${options.docsLanguage}. Supported values: en, id`);
34
+ }
35
+
36
+ const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
37
+ const promptsDirectoryPath = path.join(targetDirectoryPath, '.agent-context', 'prompts');
38
+ await ensureDirectory(docsDirectoryPath);
39
+ await ensureDirectory(promptsDirectoryPath);
40
+
41
+ const synthesisContext = buildSynthesisContext(discoveryAnswers, initContext);
42
+ const { requiredDocFileNames } = resolveProjectDocTargets(discoveryAnswers);
43
+ const expectedDocFileNames = [...requiredDocFileNames];
44
+ const generatedPromptFileNames = [];
45
+ const materializedFileNames = [];
46
+
47
+ const projectContextPromptFileName = 'bootstrap-project-context.md';
48
+ const architectureRecommendation = initContext.architectureRecommendation || null;
49
+ const projectContextPromptContent = buildProjectContextBootstrapPrompt({
50
+ discoveryAnswers,
51
+ initContext: synthesisContext,
52
+ expectedDocFileNames,
53
+ docsLanguage: normalizedDocsLanguage,
54
+ architectureRecommendation,
55
+ });
56
+ await fs.writeFile(
57
+ path.join(promptsDirectoryPath, projectContextPromptFileName),
58
+ projectContextPromptContent,
59
+ 'utf8'
60
+ );
61
+ generatedPromptFileNames.push(projectContextPromptFileName);
62
+
63
+ if (shouldBootstrapDesignDocument(discoveryAnswers, initContext)) {
64
+ const designPromptFileName = 'bootstrap-design.md';
65
+ const designPromptContent = buildDesignBootstrapPrompt({
66
+ discoveryAnswers,
67
+ initContext: synthesisContext,
68
+ docsLanguage: normalizedDocsLanguage,
69
+ architectureRecommendation,
70
+ });
71
+ await fs.writeFile(path.join(promptsDirectoryPath, designPromptFileName), designPromptContent, 'utf8');
72
+ generatedPromptFileNames.push(designPromptFileName);
73
+
74
+ const designIntentSeedFileName = 'design-intent.json';
75
+ const designIntentSeedContent = buildDesignIntentSeed({
76
+ discoveryAnswers,
77
+ initContext: synthesisContext,
78
+ architectureRecommendation,
79
+ });
80
+ await fs.writeFile(path.join(docsDirectoryPath, designIntentSeedFileName), designIntentSeedContent, 'utf8');
81
+ materializedFileNames.push(designIntentSeedFileName);
82
+
83
+ for (const designContractFileName of UI_DESIGN_CONTRACT_FILE_NAMES) {
84
+ if (!expectedDocFileNames.includes(designContractFileName)) {
85
+ expectedDocFileNames.push(designContractFileName);
86
+ }
87
+ }
88
+ }
89
+
90
+ return {
91
+ docsDirectoryPath,
92
+ generatedFileNames: expectedDocFileNames,
93
+ generatedPromptFileNames,
94
+ materializedFileNames,
95
+ bootstrapMode: 'ai-synthesis',
96
+ synthesisPromptVersion: PROJECT_DOC_SYNTHESIS_PROMPT_VERSION,
97
+ templateVersion: PROJECT_DOC_TEMPLATE_VERSION,
98
+ docsLanguage: normalizedDocsLanguage,
99
+ discoveryAnswers,
100
+ };
101
+ }
102
+
103
+ export async function isDirectoryEffectivelyEmpty(targetDirectoryPath) {
104
+ try {
105
+ const directoryEntries = await fs.readdir(targetDirectoryPath);
106
+ const meaningfulEntries = directoryEntries.filter(
107
+ (entryName) => entryName !== '.git' && entryName !== '.gitignore'
108
+ );
109
+ return meaningfulEntries.length === 0;
110
+ } catch {
111
+ return true;
112
+ }
113
+ }
114
+
115
+ export async function hasExistingProjectDocs(targetDirectoryPath) {
116
+ const projectBriefPath = path.join(targetDirectoryPath, 'docs', 'project-brief.md');
117
+ return pathExists(projectBriefPath);
118
+ }
119
+
120
+ function extractTemplateVersion(documentContent) {
121
+ const templateVersionMatch = documentContent.match(/^(?:Template version|Versi template):\s*(.+)$/im);
122
+ return templateVersionMatch ? templateVersionMatch[1].trim() : null;
123
+ }
124
+
125
+ export async function detectProjectDocTemplateStaleness(targetDirectoryPath) {
126
+ const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
127
+ const checkedFileNames = [];
128
+ const staleFiles = [];
129
+
130
+ for (const projectDocFileName of PROJECT_DOC_FILE_NAMES) {
131
+ const projectDocFilePath = path.join(docsDirectoryPath, projectDocFileName);
132
+ if (!(await pathExists(projectDocFilePath))) {
133
+ continue;
134
+ }
135
+
136
+ checkedFileNames.push(projectDocFileName);
137
+ const projectDocContent = await fs.readFile(projectDocFilePath, 'utf8');
138
+ const detectedTemplateVersion = extractTemplateVersion(projectDocContent);
139
+
140
+ if (!detectedTemplateVersion || detectedTemplateVersion !== PROJECT_DOC_TEMPLATE_VERSION) {
141
+ staleFiles.push({
142
+ fileName: projectDocFileName,
143
+ detectedTemplateVersion,
144
+ });
145
+ }
146
+ }
147
+
148
+ return {
149
+ hasProjectDocs: checkedFileNames.length > 0,
150
+ expectedTemplateVersion: PROJECT_DOC_TEMPLATE_VERSION,
151
+ checkedFileNames,
152
+ staleFiles,
153
+ };
154
+ }