@ryuenn3123/agentic-senior-core 3.0.14 → 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.
Files changed (37) hide show
  1. package/.agent-context/prompts/bootstrap-design.md +30 -16
  2. package/.agent-context/prompts/init-project.md +4 -0
  3. package/.agent-context/rules/architecture.md +13 -0
  4. package/.agent-context/rules/docker-runtime.md +12 -0
  5. package/.agent-context/rules/efficiency-vs-hype.md +17 -6
  6. package/.agent-context/rules/frontend-architecture.md +5 -0
  7. package/.agent-context/state/memory-continuity-benchmark.json +1 -1
  8. package/.agent-context/state/onboarding-report.json +0 -1
  9. package/.cursorrules +66 -29
  10. package/.gemini/instructions.md +1 -1
  11. package/.github/copilot-instructions.md +1 -1
  12. package/.instructions.md +4 -3
  13. package/.windsurfrules +66 -29
  14. package/AGENTS.md +1 -1
  15. package/lib/cli/architect.mjs +71 -784
  16. package/lib/cli/commands/init.mjs +32 -98
  17. package/lib/cli/commands/optimize.mjs +0 -4
  18. package/lib/cli/commands/upgrade.mjs +2 -5
  19. package/lib/cli/compiler.mjs +3 -11
  20. package/lib/cli/constants.mjs +3 -73
  21. package/lib/cli/detector/design-evidence.mjs +427 -0
  22. package/lib/cli/detector.mjs +13 -116
  23. package/lib/cli/init-options.mjs +0 -118
  24. package/lib/cli/project-scaffolder/constants.mjs +67 -0
  25. package/lib/cli/project-scaffolder/design-contract.mjs +554 -0
  26. package/lib/cli/project-scaffolder/discovery.mjs +315 -0
  27. package/lib/cli/project-scaffolder/prompt-builders.mjs +196 -0
  28. package/lib/cli/project-scaffolder/storage.mjs +154 -0
  29. package/lib/cli/project-scaffolder.mjs +32 -1160
  30. package/lib/cli/utils.mjs +2 -11
  31. package/package.json +1 -1
  32. package/scripts/frontend-usability-audit.mjs +53 -0
  33. package/scripts/validate/config.mjs +401 -0
  34. package/scripts/validate/coverage-checks.mjs +429 -0
  35. package/scripts/validate.mjs +44 -754
  36. package/lib/cli/init-architecture-flow.mjs +0 -233
  37. package/lib/cli/profile-packs.mjs +0 -108
@@ -1,1161 +1,33 @@
1
1
  /**
2
- * Project Scaffolder Dynamic project documentation generator.
3
- * Generates project-specific docs during init when the target folder is empty.
4
- * Depends on: constants.mjs, utils.mjs
5
- */
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
-
9
- import { ensureDirectory, askChoice, askYesNo, toTitleCase, pathExists } from './utils.mjs';
10
-
11
- const SUPPORTED_DOC_LANGUAGES = new Set(['en', 'id']);
12
- const PROJECT_DOC_FILE_NAMES = [
13
- 'project-brief.md',
14
- 'architecture-decision-record.md',
15
- 'database-schema.md',
16
- 'api-contract.md',
17
- 'flow-overview.md',
18
- ];
19
- const UI_DESIGN_CONTRACT_FILE_NAMES = ['DESIGN.md', 'design-intent.json'];
20
-
21
- // Legacy project docs may still carry this version header; keep for upgrade staleness checks.
22
- export const PROJECT_DOC_TEMPLATE_VERSION = '1.2.0';
23
- export const PROJECT_DOC_SYNTHESIS_PROMPT_VERSION = '2.0.0';
24
-
25
- const DOMAIN_CHOICES = [
26
- 'API service',
27
- 'Web application',
28
- 'Mobile app',
29
- 'CLI tool',
30
- 'Library / SDK',
31
- 'Other',
32
- ];
33
-
34
- const DATABASE_CHOICES = [
35
- 'None (stateless service)',
36
- 'SQL (PostgreSQL, MySQL, SQLite)',
37
- 'NoSQL (MongoDB, Redis, DynamoDB)',
38
- 'Both (SQL primary + cache layer)',
39
- 'Other',
40
- ];
41
-
42
- const AUTH_CHOICES = [
43
- 'None (public service)',
44
- 'JWT (stateless token auth)',
45
- 'OAuth 2.0 (third-party login)',
46
- 'Session-based (server-side sessions)',
47
- 'API Key (simple key auth)',
48
- 'Other',
49
- ];
50
-
51
- const DOCKER_STRATEGY_CHOICES = [
52
- 'No Docker (run services directly)',
53
- 'Docker for development only',
54
- 'Docker for production only',
55
- 'Docker for both development and production',
56
- ];
57
-
58
- function parseBooleanLikeValue(rawValue) {
59
- const normalizedValue = String(rawValue || '').trim().toLowerCase();
60
- if (['true', 'yes', 'y', '1'].includes(normalizedValue)) {
61
- return true;
62
- }
63
-
64
- if (['false', 'no', 'n', '0'].includes(normalizedValue)) {
65
- return false;
66
- }
67
-
68
- return null;
69
- }
70
-
71
- function resolveDockerStrategy({ dockerStrategy, useDocker, useDockerDevelopment, useDockerProduction }) {
72
- if (typeof dockerStrategy === 'string' && dockerStrategy.trim().length > 0) {
73
- const normalizedDockerStrategy = dockerStrategy.trim().toLowerCase();
74
- const directMatch = DOCKER_STRATEGY_CHOICES.find(
75
- (dockerStrategyChoice) => dockerStrategyChoice.toLowerCase() === normalizedDockerStrategy
76
- );
77
-
78
- if (directMatch) {
79
- return directMatch;
80
- }
81
- }
82
-
83
- const normalizedUseDocker = typeof useDocker === 'boolean' ? useDocker : parseBooleanLikeValue(useDocker);
84
- const normalizedUseDockerDevelopment = typeof useDockerDevelopment === 'boolean'
85
- ? useDockerDevelopment
86
- : parseBooleanLikeValue(useDockerDevelopment);
87
- const normalizedUseDockerProduction = typeof useDockerProduction === 'boolean'
88
- ? useDockerProduction
89
- : parseBooleanLikeValue(useDockerProduction);
90
-
91
- if (normalizedUseDocker === false) {
92
- return DOCKER_STRATEGY_CHOICES[0];
93
- }
94
-
95
- if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction === true) {
96
- return DOCKER_STRATEGY_CHOICES[3];
97
- }
98
-
99
- if (normalizedUseDockerDevelopment === true && normalizedUseDockerProduction !== true) {
100
- return DOCKER_STRATEGY_CHOICES[1];
101
- }
102
-
103
- if (normalizedUseDockerProduction === true && normalizedUseDockerDevelopment !== true) {
104
- return DOCKER_STRATEGY_CHOICES[2];
105
- }
106
-
107
- if (normalizedUseDocker === true) {
108
- return DOCKER_STRATEGY_CHOICES[3];
109
- }
110
-
111
- return DOCKER_STRATEGY_CHOICES[0];
112
- }
113
-
114
- async function askFeatureList(userInterface) {
115
- console.log('\nList your key features (one per line, press Enter to finish):');
116
-
117
- const features = [];
118
- while (features.length < 10) {
119
- const featureLine = (await userInterface.question(` Feature ${features.length + 1}: `)).trim();
120
- if (!featureLine) {
121
- break;
122
- }
123
-
124
- features.push(featureLine);
125
- }
126
-
127
- return features;
128
- }
129
-
130
- export function normalizeDocsLanguage(rawDocsLanguage = 'en') {
131
- const normalizedDocsLanguage = String(rawDocsLanguage || 'en').trim().toLowerCase();
132
- return SUPPORTED_DOC_LANGUAGES.has(normalizedDocsLanguage) ? normalizedDocsLanguage : null;
133
- }
134
-
135
- /**
136
- * Run the project discovery interview.
137
- * Returns a structured object with all user responses.
138
- */
139
- export async function runProjectDiscovery(userInterface, options = {}) {
140
- console.log('\n--- Project Setup ---');
141
- console.log('I will ask one focused set of questions to bootstrap project context and documentation.');
142
- console.log('This helps AI agents understand your project before writing code.\n');
143
- console.log('You can answer in your own language.');
144
- console.log('CLI prompts stay in English, but non-English answers are fully supported.\n');
145
-
146
- const defaultProjectName = (options.defaultProjectName || '').trim();
147
- const defaultProjectDescription = String(options.defaultProjectDescription || '').trim();
148
- const defaultIncludeCiGuardrails = typeof options.defaultIncludeCiGuardrails === 'boolean'
149
- ? options.defaultIncludeCiGuardrails
150
- : true;
151
- const shouldAskForCiGuardrails = options.askForCiGuardrails !== false;
152
- let projectName = '';
153
-
154
- const projectNamePrompt = defaultProjectName
155
- ? `Project name (press Enter to use folder name: ${defaultProjectName}): `
156
- : 'Project name: ';
157
-
158
- projectName = (await userInterface.question(projectNamePrompt)).trim();
159
-
160
- if (!projectName && defaultProjectName) {
161
- projectName = defaultProjectName;
162
- }
163
-
164
- if (!projectName) {
165
- throw new Error('Project name is required for documentation scaffolding.');
166
- }
167
-
168
- const projectDescriptionPrompt = defaultProjectDescription
169
- ? `One-line description (press Enter to use: ${defaultProjectDescription}): `
170
- : 'One-line description: ';
171
-
172
- let projectDescription = (await userInterface.question(projectDescriptionPrompt)).trim();
173
-
174
- if (!projectDescription) {
175
- projectDescription = defaultProjectDescription || `A ${projectName} project.`;
176
- }
177
-
178
- const includeCiGuardrails = shouldAskForCiGuardrails
179
- ? await askYesNo(
180
- 'Enable CI/CD quality checks (guardrails) and the LLM Judge policy?',
181
- userInterface,
182
- defaultIncludeCiGuardrails
183
- )
184
- : defaultIncludeCiGuardrails;
185
-
186
- const domainSelection = await askChoice(
187
- 'Primary domain:',
188
- DOMAIN_CHOICES,
189
- userInterface
190
- );
191
-
192
- let primaryDomain = domainSelection;
193
- if (domainSelection === 'Other') {
194
- primaryDomain = (await userInterface.question('Describe your domain: ')).trim() || 'Custom domain';
195
- }
196
-
197
- const databaseSelection = await askChoice(
198
- 'Database needs:',
199
- DATABASE_CHOICES,
200
- userInterface
201
- );
202
-
203
- let databaseChoice = databaseSelection;
204
- if (databaseSelection === 'Other') {
205
- databaseChoice = (await userInterface.question('Describe your database setup: ')).trim() || 'Custom database';
206
- }
207
-
208
- const authSelection = await askChoice(
209
- 'Auth strategy:',
210
- AUTH_CHOICES,
211
- userInterface
212
- );
213
-
214
- let authStrategy = authSelection;
215
- if (authSelection === 'Other') {
216
- authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
217
- }
218
-
219
- const dockerStrategy = await askChoice(
220
- 'Containerization strategy:',
221
- DOCKER_STRATEGY_CHOICES,
222
- userInterface
223
- );
224
-
225
- const features = await askFeatureList(userInterface);
226
-
227
- return {
228
- projectName,
229
- projectDescription,
230
- includeCiGuardrails,
231
- primaryDomain,
232
- databaseChoice,
233
- authStrategy,
234
- dockerStrategy,
235
- features,
236
- additionalContext: 'No additional context provided.',
237
- };
238
- }
239
-
240
- /**
241
- * Determine required docs based on project discovery answers.
242
- */
243
- export function resolveProjectDocTargets(discoveryAnswers) {
244
- const hasDatabase = !discoveryAnswers.databaseChoice.toLowerCase().startsWith('none');
245
- const isApiOrWebDomain = ['API service', 'Web application'].includes(discoveryAnswers.primaryDomain)
246
- || discoveryAnswers.primaryDomain.toLowerCase().includes('api')
247
- || discoveryAnswers.primaryDomain.toLowerCase().includes('web');
248
-
249
- const requiredDocFileNames = [
250
- 'project-brief.md',
251
- 'architecture-decision-record.md',
252
- 'flow-overview.md',
253
- ];
254
-
255
- if (hasDatabase) {
256
- requiredDocFileNames.push('database-schema.md');
257
- }
258
-
259
- if (isApiOrWebDomain) {
260
- requiredDocFileNames.push('api-contract.md');
261
- }
262
-
263
- return { requiredDocFileNames };
264
- }
265
-
266
- /**
267
- * Build synthesis context from discovery answers and init selections.
268
- */
269
- export function buildSynthesisContext(_discoveryAnswers, initContext) {
270
- const additionalStackFileNames = Array.isArray(initContext.additionalStackFileNames)
271
- ? initContext.additionalStackFileNames
272
- : [];
273
- const additionalBlueprintFileNames = Array.isArray(initContext.additionalBlueprintFileNames)
274
- ? initContext.additionalBlueprintFileNames
275
- : [];
276
-
277
- return {
278
- stackFileName: initContext.stackFileName,
279
- additionalStackFileNames,
280
- blueprintFileName: initContext.blueprintFileName,
281
- additionalBlueprintFileNames,
282
- runtimeEnvironmentKey: initContext.runtimeEnvironmentKey || 'linux',
283
- runtimeEnvironmentLabel: initContext.runtimeEnvironmentLabel || 'Linux',
284
- };
285
- }
286
-
287
- function shouldBootstrapDesignDocument(discoveryAnswers, initContext) {
288
- const normalizedDomain = String(discoveryAnswers.primaryDomain || '').trim().toLowerCase();
289
- const normalizedBlueprint = String(initContext.blueprintFileName || '').trim().toLowerCase();
290
-
291
- const isUiDomain = normalizedDomain.includes('web')
292
- || normalizedDomain.includes('mobile')
293
- || normalizedDomain.includes('frontend')
294
- || normalizedDomain.includes('ui');
295
-
296
- const isBackendOnlyDomain = normalizedDomain.includes('api service')
297
- || normalizedDomain.includes('cli tool')
298
- || normalizedDomain.includes('library');
299
-
300
- const blueprintLooksUi = normalizedBlueprint.includes('frontend')
301
- || normalizedBlueprint.includes('landing')
302
- || normalizedBlueprint.includes('ui');
303
-
304
- if (isUiDomain) {
305
- return true;
306
- }
307
-
308
- if (!isBackendOnlyDomain && blueprintLooksUi) {
309
- return true;
310
- }
311
-
312
- return false;
313
- }
314
-
315
- const DESIGN_REQUIRED_SECTIONS = [
316
- 'Design Intent and Product Personality',
317
- 'Audience and Use-Context Signals',
318
- 'Visual Direction and Distinctive Moves',
319
- 'Color Science and Semantic Roles',
320
- 'Typographic Engineering and Hierarchy',
321
- 'Spacing, Layout Rhythm, and Density Strategy',
322
- 'Responsive Strategy and Cross-Viewport Adaptation Matrix',
323
- 'Interaction, Motion, and Feedback Rules',
324
- 'Component Language, Morphology, and Shared Patterns',
325
- 'Accessibility Non-Negotiables',
326
- 'Anti-Patterns to Avoid',
327
- 'Implementation Notes for Future UI Tasks',
328
- ];
329
-
330
- function inferDesignKeywords(discoveryAnswers) {
331
- const normalizedDescription = String(discoveryAnswers.projectDescription || '').toLowerCase();
332
- const normalizedDomain = String(discoveryAnswers.primaryDomain || '').toLowerCase();
333
- const normalizedFeatures = Array.isArray(discoveryAnswers.features)
334
- ? discoveryAnswers.features.map((featureValue) => String(featureValue).toLowerCase()).join(' ')
335
- : '';
336
- const aggregateText = `${normalizedDescription} ${normalizedDomain} ${normalizedFeatures}`;
337
-
338
- if (aggregateText.includes('commerce') || aggregateText.includes('catalog') || aggregateText.includes('checkout')) {
339
- return {
340
- designPhilosophy: 'Conversion clarity with premium restraint.',
341
- brandAdjectives: ['clear', 'desirable', 'confident'],
342
- antiAdjectives: ['cluttered', 'hesitant', 'coupon-noisy'],
343
- typographyScaleRatio: '1.200',
344
- baseGridUnit: 8,
345
- densityMode: 'conversion-focused',
346
- colorIntent: 'Use a restrained neutral foundation with one controlled accent reserved for buying cues and trust moments.',
347
- distinctiveMoves: [
348
- 'Use product hierarchy and buying cues without turning the interface into a discount template.',
349
- 'Keep decision-critical information prominent while secondary merchandising stays quiet.',
350
- 'Let imagery and spacing create premium perception before decorative effects do.',
351
- ],
352
- motionPurpose: 'Use restrained motion to reinforce buying confidence, product continuity, and feedback. Motion should feel premium, not promotional.',
353
- motionChoreography: 'Favor short hover/focus transitions, sheet choreography, and product-media continuity. Avoid looping hero motion, autoplay spectacle, and parallax-heavy scenes.',
354
- motionDurations: {
355
- desktop: 180,
356
- mobile: 240,
357
- },
358
- componentMorphology: {
359
- mobile: 'Product cards should compress supporting metadata, pin purchase actions closer to the thumb zone, and move comparison into progressive disclosure or bottom sheets.',
360
- tablet: 'Cards and merch modules should preserve comparison affordances while reducing tertiary chrome and keeping visual hierarchy stable.',
361
- desktop: 'Cards can expand media, comparison, and reassurance copy while keeping buying cues dominant and visually disciplined.',
362
- },
363
- mutationRules: {
364
- mobile: 'Convert browsing into vertically stacked product cards, move cart and filter actions into sticky or bottom-sheet patterns, and keep thumb-reach actions persistent.',
365
- tablet: 'Preserve browsing flow with a two-column rhythm, collapse tertiary filters, and keep comparison moments visible without forcing desktop density.',
366
- desktop: 'Expose multi-column merchandising, comparison views, and richer product context while keeping the purchase path visually dominant.',
367
- },
368
- };
369
- }
370
-
371
- if (aggregateText.includes('dashboard') || aggregateText.includes('operations') || aggregateText.includes('report')) {
372
- return {
373
- designPhilosophy: 'Operational calm under high information density.',
374
- brandAdjectives: ['calm', 'precise', 'trustworthy'],
375
- antiAdjectives: ['chaotic', 'gimmicky', 'visually exhausting'],
376
- typographyScaleRatio: '1.125',
377
- baseGridUnit: 4,
378
- densityMode: 'high-density-scanning',
379
- colorIntent: 'Use neutrals for structure and reserve accent saturation for status shifts, risky actions, and alerts.',
380
- distinctiveMoves: [
381
- 'Prioritize scanning clarity and status recognition over decorative density.',
382
- 'Use visual weight to separate signal from operational noise.',
383
- 'Reserve strong accents for alerts, decisions, and state transitions only.',
384
- ],
385
- motionPurpose: 'Use motion as operational feedback and state continuity, never as ambient decoration that competes with dense information.',
386
- motionChoreography: 'Prefer fast transitions for filters, drawers, status reveals, and row expansion. Avoid floaty choreography and ornamental motion that slows scanning.',
387
- motionDurations: {
388
- desktop: 160,
389
- mobile: 220,
390
- },
391
- componentMorphology: {
392
- mobile: 'Data rows should become prioritized cards or grouped summaries, with filters and secondary tools moving into sheets or drawers.',
393
- tablet: 'Operational panels should retain split-view logic where possible, while tertiary panels collapse behind explicit toggles.',
394
- desktop: 'Dense tables, side panels, and comparison surfaces can remain visible simultaneously, with state treatments optimized for rapid scanning.',
395
- },
396
- mutationRules: {
397
- mobile: 'Collapse dense tables into prioritized cards or row groups, move filters into drawers or sheets, and pin the most critical actions to the bottom reach zone.',
398
- tablet: 'Keep two-column or split-pane workflows, collapse tertiary panels, and maintain fast scan paths for operators using touch or keyboard.',
399
- desktop: 'Expose the highest-density views with visible navigation, comparison surfaces, and simultaneous context panels for power users.',
400
- },
401
- };
402
- }
403
-
404
- if (aggregateText.includes('developer') || aggregateText.includes('api') || aggregateText.includes('platform')) {
405
- return {
406
- designPhilosophy: 'Technical precision with explicit structure and honest feedback.',
407
- brandAdjectives: ['precise', 'technical', 'transparent'],
408
- antiAdjectives: ['vague', 'marketing-heavy', 'template-polished'],
409
- typographyScaleRatio: '1.125',
410
- baseGridUnit: 4,
411
- densityMode: 'technical-utility',
412
- colorIntent: 'Anchor the interface in disciplined neutrals and use accent color only where state, feedback, or code-adjacent interaction needs emphasis.',
413
- distinctiveMoves: [
414
- 'Make structure and feedback feel exact without becoming sterile.',
415
- 'Use code-adjacent rhythm and hierarchy to build trust with technical users.',
416
- 'Keep complexity legible through spacing, grouping, and explicit interaction states.',
417
- ],
418
- motionPurpose: 'Use motion to clarify causality, reveal system state, and preserve context during multi-pane technical workflows.',
419
- motionChoreography: 'Prefer subtle panel transitions, command feedback, and disclosure motion. Avoid ornamental sweeps that make technical work feel imprecise.',
420
- motionDurations: {
421
- desktop: 170,
422
- mobile: 230,
423
- },
424
- componentMorphology: {
425
- mobile: 'Technical panes should flatten into sequential sections, with commands and diagnostics colocated near the content they affect.',
426
- tablet: 'Split views should survive where useful, with explicit panel toggles and condensed chrome for code-adjacent tasks.',
427
- desktop: 'Navigation, documentation, diagnostics, and active work surfaces can remain concurrently visible when it improves expert comprehension.',
428
- },
429
- mutationRules: {
430
- mobile: 'Switch multi-pane technical layouts into stacked sections, turn secondary navigation into segmented or sheet-based controls, and keep commands near the content they affect.',
431
- tablet: 'Retain split-view comprehension where possible, compress chrome, and keep documentation or diagnostics adjacent to the active task.',
432
- desktop: 'Expose full navigation, dense comparison surfaces, and multi-pane workflows for expert scanning and debugging.',
433
- },
434
- };
435
- }
436
-
437
- if (aggregateText.includes('content') || aggregateText.includes('community') || aggregateText.includes('publish')) {
438
- return {
439
- designPhilosophy: 'Editorial flow with warm but controlled expression.',
440
- brandAdjectives: ['editorial', 'warm', 'expressive'],
441
- antiAdjectives: ['flat', 'anonymous', 'feed-generic'],
442
- typographyScaleRatio: '1.200',
443
- baseGridUnit: 8,
444
- densityMode: 'reading-first',
445
- colorIntent: 'Let typography and surface contrast lead while chroma supports hierarchy and key participation actions.',
446
- distinctiveMoves: [
447
- 'Build a strong reading rhythm so content feels curated rather than dumped into cards.',
448
- 'Use contrast and spacing to guide attention between creation, moderation, and discovery.',
449
- 'Give key interaction moments personality without sacrificing clarity.',
450
- ],
451
- motionPurpose: 'Use motion to support reading rhythm, reveal structure, and reward contribution moments without overwhelming long-form consumption.',
452
- motionChoreography: 'Favor reveal choreography for section transitions, lightweight feedback on participation, and measured media behavior. Avoid constant background motion.',
453
- motionDurations: {
454
- desktop: 190,
455
- mobile: 250,
456
- },
457
- componentMorphology: {
458
- mobile: 'Reading surfaces should dominate while secondary discovery and community tools collapse behind sheets, tabs, or segmented controls.',
459
- tablet: 'Editorial modules can balance reading and discovery, provided the primary narrative flow remains obvious.',
460
- desktop: 'Long-form content, secondary navigation, and related discovery modules can coexist without fragmenting the reading rhythm.',
461
- },
462
- mutationRules: {
463
- mobile: 'Prioritize reading and contribution flows in a single-column narrative stack, tuck secondary discovery tools behind sheets, and keep primary creation actions within reach.',
464
- tablet: 'Balance narrative reading with supporting discovery modules, using two-column compositions only where hierarchy stays obvious.',
465
- desktop: 'Use wider editorial compositions, visible secondary navigation, and modular discovery surfaces without breaking reading rhythm.',
466
- },
467
- };
468
- }
469
-
470
- return {
471
- designPhilosophy: 'Project-specific clarity with one authored tension.',
472
- brandAdjectives: ['clear', 'human', 'distinct'],
473
- antiAdjectives: ['generic', 'template-like', 'trend-chasing'],
474
- typographyScaleRatio: '1.200',
475
- baseGridUnit: 8,
476
- densityMode: 'balanced-authored',
477
- colorIntent: 'Use a restrained perceptual palette with one deliberate accent budget instead of interchangeable template colors.',
478
- distinctiveMoves: [
479
- 'Create a visual direction with one memorable tension instead of stacking fashionable effects.',
480
- 'Use rhythm, hierarchy, and motion intentionally so the interface feels authored.',
481
- 'Keep the system flexible enough to evolve with product scope without losing identity.',
482
- ],
483
- motionPurpose: 'Allow motion when it improves continuity, feedback, or perceived craft. Avoid banning motion outright, but reject decorative movement with no product value.',
484
- motionChoreography: 'Use short, purposeful transitions and keep heavier choreography rare, opt-in, and explainable in product terms.',
485
- motionDurations: {
486
- desktop: 180,
487
- mobile: 240,
488
- },
489
- componentMorphology: {
490
- mobile: 'Primary components should simplify structure, prioritize direct tasks, and collapse supporting detail into explicit disclosure.',
491
- tablet: 'Components should preserve hierarchy and task continuity while reducing density and compressing tertiary chrome.',
492
- desktop: 'Components can expose richer states, denser supporting information, and broader navigation affordances without losing clarity.',
493
- },
494
- mutationRules: {
495
- mobile: 'Stack primary tasks vertically, convert secondary navigation into thumb-friendly overlays or sheets, and simplify dense comparison layouts into progressive disclosure.',
496
- tablet: 'Preserve hierarchy with fewer columns, condensed chrome, and adaptive navigation that maintains task continuity.',
497
- desktop: 'Expose the full layout system, highest information density, and broadest navigation affordances without sacrificing clarity.',
498
- },
499
- };
500
- }
501
-
502
- function buildDesignIntentContractObject({
503
- projectName,
504
- projectDescription,
505
- primaryDomain,
506
- features = [],
507
- initContext,
508
- architectureRecommendation = null,
509
- status = 'seed-needs-design-synthesis',
510
- supplementalFields = {},
511
- }) {
512
- const inferredKeywords = inferDesignKeywords({
513
- projectDescription,
514
- primaryDomain,
515
- features,
516
- });
517
- const normalizedPrimaryDomain = String(primaryDomain || '').trim().toLowerCase();
518
- const resolvedSpacingPattern = inferredKeywords.densityMode === 'dense'
519
- ? 'compact-grid'
520
- : normalizedPrimaryDomain.includes('mobile')
521
- ? 'mobile-first-single-axis'
522
- : inferredKeywords.densityMode === 'focused'
523
- ? 'high-contrast-rhythm'
524
- : 'balanced-grid';
525
-
526
- return {
527
- mode: 'dynamic',
528
- status,
529
- project: {
530
- name: projectName,
531
- context: projectDescription,
532
- domain: primaryDomain,
533
- stack: toTitleCase(initContext.stackFileName),
534
- blueprint: toTitleCase(initContext.blueprintFileName),
535
- },
536
- designPhilosophy: inferredKeywords.designPhilosophy,
537
- brandAdjectives: inferredKeywords.brandAdjectives,
538
- antiAdjectives: inferredKeywords.antiAdjectives,
539
- visualDirection: {
540
- trendStance: 'trend-aware-not-trend-chasing',
541
- distinctiveMoves: inferredKeywords.distinctiveMoves,
542
- copiedReferenceAllowed: false,
543
- },
544
- mathSystems: {
545
- typographyScaleRatio: inferredKeywords.typographyScaleRatio,
546
- baseGridUnit: inferredKeywords.baseGridUnit,
547
- spacingPattern: resolvedSpacingPattern,
548
- densityMode: inferredKeywords.densityMode,
549
- },
550
- colorTruth: {
551
- format: 'OKLCH',
552
- allowHexDerivatives: true,
553
- requirePerceptualLightnessCurve: true,
554
- paletteRoles: ['base', 'surface', 'accent'],
555
- intent: inferredKeywords.colorIntent,
556
- },
557
- crossViewportAdaptation: {
558
- adaptByRecomposition: true,
559
- touchTargetMinPx: 44,
560
- mutationRules: inferredKeywords.mutationRules,
561
- },
562
- motionSystem: {
563
- allowMeaningfulMotion: true,
564
- purpose: inferredKeywords.motionPurpose,
565
- choreography: inferredKeywords.motionChoreography,
566
- desktopDurationMs: inferredKeywords.motionDurations.desktop,
567
- mobileDurationMs: inferredKeywords.motionDurations.mobile,
568
- respectReducedMotion: true,
569
- preferTransformAndOpacity: true,
570
- avoidDecorativeMotionForItsOwnSake: true,
571
- },
572
- componentMorphology: {
573
- requireStateBehaviorMatrix: true,
574
- preserveIdentityAcrossViewports: true,
575
- stateKeys: ['default', 'hover', 'focus', 'active', 'disabled', 'loading', 'error'],
576
- viewportBehavior: inferredKeywords.componentMorphology,
577
- },
578
- experiencePrinciples: [
579
- 'Design must feel project-specific, not interchangeable with generic SaaS templates.',
580
- 'Major interface decisions must be explainable in product and user terms.',
581
- 'Accessibility, responsiveness, and implementation realism are non-negotiable.',
582
- 'Cross-viewport behavior must reorganize tasks and navigation, not just scale the desktop layout down.',
583
- 'Motion may add character and continuity when it improves the product experience, but it must stay purposeful, performant, and optional for reduced-motion users.',
584
- ],
585
- forbiddenPatterns: [
586
- 'generic-saas-hero',
587
- 'copycat-brand-system',
588
- 'unjustified-default-gradients',
589
- 'placeholder-design-language',
590
- 'scale-only-responsive-layout',
591
- ],
592
- validationHints: {
593
- rejectArbitraryHexOnlyPalette: true,
594
- requireViewportMutationRules: true,
595
- requirePerceptualColorRationale: true,
596
- allowHexDerivatives: true,
597
- requireMotionRationale: true,
598
- requireStateMorphology: true,
599
- },
600
- requiredDesignSections: DESIGN_REQUIRED_SECTIONS,
601
- implementation: {
602
- requiredDeliverables: ['docs/DESIGN.md', 'docs/design-intent.json'],
603
- requireDesignRationale: true,
604
- requireDistinctVisualDirection: true,
605
- requireMachineReadableContract: true,
606
- requireViewportMutationRules: true,
607
- requirePurposefulMotionGuidelines: true,
608
- bootstrapPrompt: '.agent-context/prompts/bootstrap-design.md',
609
- autoLoadedRuleFiles: [
610
- '.agent-context/prompts/bootstrap-design.md',
611
- '.agent-context/rules/frontend-architecture.md',
612
- ],
613
- disallowedAutoLoadedRuleFiles: [
614
- '.agent-context/rules/database-design.md',
615
- '.agent-context/rules/docker-runtime.md',
616
- '.agent-context/rules/microservices.md',
617
- ],
618
- },
619
- ...supplementalFields,
620
- };
621
- }
622
-
623
- export function validateDesignIntentContract(designIntentContract) {
624
- const validationErrors = [];
625
-
626
- if (!designIntentContract || typeof designIntentContract !== 'object') {
627
- return ['Design intent contract must be an object.'];
628
- }
629
-
630
- if (designIntentContract.mode !== 'dynamic') {
631
- validationErrors.push('designIntent.mode must equal "dynamic".');
632
- }
633
-
634
- if (!designIntentContract.project || typeof designIntentContract.project !== 'object') {
635
- validationErrors.push('designIntent.project must exist.');
636
- }
637
-
638
- if (!designIntentContract.designPhilosophy || typeof designIntentContract.designPhilosophy !== 'string') {
639
- validationErrors.push('designIntent.designPhilosophy must be a non-empty string.');
640
- }
641
-
642
- if (!designIntentContract.mathSystems || typeof designIntentContract.mathSystems !== 'object') {
643
- validationErrors.push('designIntent.mathSystems must exist.');
644
- } else {
645
- if (!/^\d+(\.\d+)?$/.test(String(designIntentContract.mathSystems.typographyScaleRatio || '').trim())) {
646
- validationErrors.push('designIntent.mathSystems.typographyScaleRatio must be numeric text.');
647
- }
648
- if (!Number.isInteger(designIntentContract.mathSystems.baseGridUnit) || designIntentContract.mathSystems.baseGridUnit <= 0) {
649
- validationErrors.push('designIntent.mathSystems.baseGridUnit must be a positive integer.');
650
- }
651
- }
652
-
653
- if (!designIntentContract.colorTruth || typeof designIntentContract.colorTruth !== 'object') {
654
- validationErrors.push('designIntent.colorTruth must exist.');
655
- } else {
656
- if (designIntentContract.colorTruth.format !== 'OKLCH') {
657
- validationErrors.push('designIntent.colorTruth.format must equal "OKLCH".');
658
- }
659
- if (designIntentContract.colorTruth.allowHexDerivatives !== true) {
660
- validationErrors.push('designIntent.colorTruth.allowHexDerivatives must equal true.');
661
- }
662
- }
663
-
664
- if (!designIntentContract.crossViewportAdaptation || typeof designIntentContract.crossViewportAdaptation !== 'object') {
665
- validationErrors.push('designIntent.crossViewportAdaptation must exist.');
666
- } else {
667
- const mutationRules = designIntentContract.crossViewportAdaptation.mutationRules;
668
- if (!mutationRules || typeof mutationRules !== 'object') {
669
- validationErrors.push('designIntent.crossViewportAdaptation.mutationRules must exist.');
670
- } else {
671
- for (const viewportKey of ['mobile', 'tablet', 'desktop']) {
672
- if (!String(mutationRules[viewportKey] || '').trim()) {
673
- validationErrors.push(`designIntent.crossViewportAdaptation.mutationRules.${viewportKey} must be a non-empty string.`);
674
- }
675
- }
676
- }
677
- }
678
-
679
- if (!designIntentContract.motionSystem || typeof designIntentContract.motionSystem !== 'object') {
680
- validationErrors.push('designIntent.motionSystem must exist.');
681
- } else {
682
- if (designIntentContract.motionSystem.allowMeaningfulMotion !== true) {
683
- validationErrors.push('designIntent.motionSystem.allowMeaningfulMotion must equal true.');
684
- }
685
- if (!String(designIntentContract.motionSystem.purpose || '').trim()) {
686
- validationErrors.push('designIntent.motionSystem.purpose must be a non-empty string.');
687
- }
688
- if (designIntentContract.motionSystem.respectReducedMotion !== true) {
689
- validationErrors.push('designIntent.motionSystem.respectReducedMotion must equal true.');
690
- }
691
- }
692
-
693
- if (!designIntentContract.componentMorphology || typeof designIntentContract.componentMorphology !== 'object') {
694
- validationErrors.push('designIntent.componentMorphology must exist.');
695
- } else {
696
- if (designIntentContract.componentMorphology.requireStateBehaviorMatrix !== true) {
697
- validationErrors.push('designIntent.componentMorphology.requireStateBehaviorMatrix must equal true.');
698
- }
699
- if (!Array.isArray(designIntentContract.componentMorphology.stateKeys) || designIntentContract.componentMorphology.stateKeys.length < 4) {
700
- validationErrors.push('designIntent.componentMorphology.stateKeys must contain multiple interaction states.');
701
- }
702
- const viewportBehavior = designIntentContract.componentMorphology.viewportBehavior;
703
- if (!viewportBehavior || typeof viewportBehavior !== 'object') {
704
- validationErrors.push('designIntent.componentMorphology.viewportBehavior must exist.');
705
- } else {
706
- for (const viewportKey of ['mobile', 'tablet', 'desktop']) {
707
- if (!String(viewportBehavior[viewportKey] || '').trim()) {
708
- validationErrors.push(`designIntent.componentMorphology.viewportBehavior.${viewportKey} must be a non-empty string.`);
709
- }
710
- }
711
- }
712
- }
713
-
714
- if (!Array.isArray(designIntentContract.requiredDesignSections) || designIntentContract.requiredDesignSections.length !== DESIGN_REQUIRED_SECTIONS.length) {
715
- validationErrors.push('designIntent.requiredDesignSections must match the required design contract sections.');
716
- } else {
717
- for (const requiredSectionName of DESIGN_REQUIRED_SECTIONS) {
718
- if (!designIntentContract.requiredDesignSections.includes(requiredSectionName)) {
719
- validationErrors.push(`designIntent.requiredDesignSections is missing "${requiredSectionName}".`);
720
- }
721
- }
722
- }
723
-
724
- return validationErrors;
725
- }
726
-
727
- export function buildDesignIntentSeedFromSignals({
728
- projectName,
729
- projectDescription,
730
- primaryDomain,
731
- features = [],
732
- initContext,
733
- architectureRecommendation = null,
734
- status = 'seed-needs-design-synthesis',
735
- supplementalFields = {},
736
- }) {
737
- const designIntentContract = buildDesignIntentContractObject({
738
- projectName,
739
- projectDescription,
740
- primaryDomain,
741
- features,
742
- initContext,
743
- architectureRecommendation,
744
- status,
745
- supplementalFields,
746
- });
747
- const validationErrors = validateDesignIntentContract(designIntentContract);
748
-
749
- if (validationErrors.length > 0) {
750
- throw new Error(`Invalid design intent contract seed: ${validationErrors.join(' ')}`);
751
- }
752
-
753
- return `${JSON.stringify(designIntentContract, null, 2)}\n`;
754
- }
755
-
756
- function buildDesignIntentSeed({
757
- discoveryAnswers,
758
- initContext,
759
- architectureRecommendation,
760
- }) {
761
- return buildDesignIntentSeedFromSignals({
762
- projectName: discoveryAnswers.projectName,
763
- projectDescription: discoveryAnswers.projectDescription,
764
- primaryDomain: discoveryAnswers.primaryDomain,
765
- features: discoveryAnswers.features,
766
- initContext,
767
- architectureRecommendation,
768
- status: 'seed-needs-design-synthesis',
769
- });
770
- }
771
-
772
- function buildProjectContextBootstrapPrompt({
773
- discoveryAnswers,
774
- initContext,
775
- expectedDocFileNames,
776
- docsLanguage,
777
- architectureRecommendation,
778
- }) {
779
- const featuresList = Array.isArray(discoveryAnswers.features) && discoveryAnswers.features.length > 0
780
- ? discoveryAnswers.features.map((feature, featureIndex) => `${featureIndex + 1}. ${feature}`).join('\n')
781
- : 'Derive the first concrete feature set from the project name, description, and domain. Do not invent arbitrary modules just to fill space.';
782
-
783
- const expectedDocsList = expectedDocFileNames
784
- .map((fileName, fileIndex) => `${fileIndex + 1}. docs/${fileName}`)
785
- .join('\n');
786
-
787
- const architectureSnapshot = architectureRecommendation
788
- ? JSON.stringify({
789
- stack: architectureRecommendation.recommendedStackFileName,
790
- blueprint: architectureRecommendation.recommendedBlueprintFileName,
791
- confidenceLabel: architectureRecommendation.confidenceLabel,
792
- confidenceScore: architectureRecommendation.confidenceScore,
793
- research: architectureRecommendation.research,
794
- evidenceCitations: architectureRecommendation.evidenceCitations,
795
- designGuidance: architectureRecommendation.designGuidance,
796
- }, null, 2)
797
- : 'null';
798
-
799
- return [
800
- '# Bootstrap Prompt: Dynamic Project Context Synthesis',
801
- '',
802
- `Protocol version: ${PROJECT_DOC_SYNTHESIS_PROMPT_VERSION}`,
803
- '',
804
- 'You are a Lead Solution Architect and Principal Engineer.',
805
- 'Write project context docs from scratch (no template rendering, no placeholder boilerplate).',
806
- '',
807
- '## Mission',
808
- `Create or update these files in ${docsLanguage.toUpperCase()} language:`,
809
- expectedDocsList,
810
- '',
811
- '## Hard Rules',
812
- '1. No copy-paste from external prose.',
813
- '2. Every major section must explain rationale and tradeoffs.',
814
- '3. Keep stack, database, and auth aligned with the project constraints below unless user explicitly requests migration.',
815
- '4. Output must be implementation-ready for engineers, not generic textbook explanation.',
816
- '5. For any research-backed claim, include citation metadata (source + fetchedAt timestamp) from the Architect Engine Snapshot.',
817
- '6. Write for native English speakers at an 8th-grade reading level. Use clear, direct, plain language.',
818
- '7. Avoid emoji, AI cliches, buzzwords, academic phrasing, padding, and generic filler.',
819
- '8. Separate confirmed facts from assumptions explicitly. When context is incomplete, add an `Assumptions to Validate` section and a `Next Validation Action` line.',
820
- '9. If user inputs conflict with repo evidence, call out the conflict and choose the safer interpretation instead of silently forcing a generic answer.',
821
- '10. Do not invent modules or architecture layers only to make the docs look complete.',
822
- '',
823
- '## Project Inputs',
824
- `- Project name: ${discoveryAnswers.projectName}`,
825
- `- Project description: ${discoveryAnswers.projectDescription}`,
826
- `- Primary domain: ${discoveryAnswers.primaryDomain}`,
827
- `- Database strategy: ${discoveryAnswers.databaseChoice}`,
828
- `- Auth strategy: ${discoveryAnswers.authStrategy}`,
829
- `- Docker strategy: ${discoveryAnswers.dockerStrategy}`,
830
- `- Runtime environment: ${initContext.runtimeEnvironmentLabel || initContext.runtimeEnvironmentKey || 'Linux'}`,
831
- `- Selected stack: ${toTitleCase(initContext.stackFileName)}`,
832
- `- Selected blueprint: ${toTitleCase(initContext.blueprintFileName)}`,
833
- `- Additional stacks: ${Array.isArray(initContext.additionalStackFileNames) && initContext.additionalStackFileNames.length > 0 ? initContext.additionalStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ') : 'none'}`,
834
- `- Additional blueprints: ${Array.isArray(initContext.additionalBlueprintFileNames) && initContext.additionalBlueprintFileNames.length > 0 ? initContext.additionalBlueprintFileNames.map((blueprintFileName) => toTitleCase(blueprintFileName)).join(', ') : 'none'}`,
835
- '',
836
- '## Key Features',
837
- featuresList,
838
- '',
839
- '## Additional Context',
840
- discoveryAnswers.additionalContext || 'No additional context provided.',
841
- '',
842
- '## Architect Engine Snapshot (for grounding)',
843
- '```json',
844
- architectureSnapshot,
845
- '```',
846
- '',
847
- '## Required Execution',
848
- '1. Create all required docs files listed above with complete Markdown content.',
849
- '2. Make the docs adaptive to the real repo and prompt context. These are living references, not frozen templates.',
850
- '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.',
851
- '4. Keep content original, specific to this project, and actionable for implementation.',
852
- '5. After writing docs, continue coding tasks using these docs as living project context.',
853
- '',
854
- ].join('\n');
855
- }
856
-
857
- function buildDesignBootstrapPrompt({
858
- discoveryAnswers,
859
- initContext,
860
- docsLanguage,
861
- architectureRecommendation,
862
- }) {
863
- const designIntentSeed = buildDesignIntentSeed({
864
- discoveryAnswers,
865
- initContext,
866
- architectureRecommendation,
867
- });
868
-
869
- return [
870
- '# Bootstrap Prompt: Dynamic Design Contract Synthesis',
871
- '',
872
- `Protocol version: ${PROJECT_DOC_SYNTHESIS_PROMPT_VERSION}`,
873
- '',
874
- 'You are the Lead UI/UX Art Director for this project.',
875
- 'Create a dynamic design contract, not a fixed stylistic template.',
876
- '',
877
- '## Mission',
878
- `Author docs/DESIGN.md in ${docsLanguage.toUpperCase()} language with strong art direction and engineering-ready guidance.`,
879
- 'Keep docs/design-intent.json synchronized as the machine-readable source of design intent.',
880
- '',
881
- '## Deliverables',
882
- '1. docs/DESIGN.md',
883
- '2. docs/design-intent.json',
884
- '',
885
- '## Required DESIGN.md Sections',
886
- '1. Design Vision and Product Personality',
887
- '2. Audience and Use-Context Signals',
888
- '3. Visual Direction and Distinctive Moves',
889
- '4. Color Science and Semantic Roles',
890
- '5. Typographic Engineering and Hierarchy',
891
- '6. Spacing, Layout Rhythm, and Density Strategy',
892
- '7. Responsive Strategy and Cross-Viewport Adaptation Matrix',
893
- '8. Motion and Interaction Principles',
894
- '9. Component Language and Morphology (cards, forms, nav, states)',
895
- '10. Accessibility Non-Negotiables',
896
- '11. Anti-Patterns to Avoid',
897
- '12. Implementation Notes for Future UI Tasks',
898
- '',
899
- '## Required design-intent.json Fields',
900
- '1. mode',
901
- '2. status',
902
- '3. project',
903
- '4. designPhilosophy',
904
- '5. brandAdjectives',
905
- '6. antiAdjectives',
906
- '7. visualDirection',
907
- '8. mathSystems',
908
- '9. colorTruth',
909
- '10. crossViewportAdaptation',
910
- '11. motionSystem',
911
- '12. componentMorphology',
912
- '13. experiencePrinciples',
913
- '14. forbiddenPatterns',
914
- '15. validationHints',
915
- '16. requiredDesignSections',
916
- '17. implementation',
917
- '',
918
- '## Hard Rules',
919
- '1. No copy-paste from external style guides.',
920
- '2. Every major decision must include psychological/product rationale.',
921
- '3. Keep implementation feasible for the selected stack and blueprint.',
922
- '4. Keep tone decisive like an art director, not generic AI boilerplate.',
923
- '5. Do not anchor the final design language to a famous brand reference. Translate inspiration into original project-specific principles.',
924
- '6. Reject interchangeable hero layouts, generic SaaS gradients, and trend-chasing decoration unless the project context explicitly justifies them.',
925
- '7. Encode color intent in perceptual terms first. Hex values may exist only as implementation derivatives.',
926
- '8. Responsive guidance must include layout mutation rules for mobile, tablet, and desktop. Shrinking the desktop layout is not enough.',
927
- '9. Motion is allowed when it reinforces feedback, continuity, or hierarchy. Do not remove motion entirely, but reject decorative motion that harms clarity or performance.',
928
- '10. Define component morphology across interaction states and viewports so cards, forms, nav, and feedback surfaces adapt coherently instead of only resizing.',
929
- '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.',
930
- '',
931
- '## Project Inputs',
932
- `- Project name: ${discoveryAnswers.projectName}`,
933
- `- Product context: ${discoveryAnswers.projectDescription}`,
934
- `- Domain: ${discoveryAnswers.primaryDomain}`,
935
- `- Stack: ${toTitleCase(initContext.stackFileName)}`,
936
- `- Blueprint: ${toTitleCase(initContext.blueprintFileName)}`,
937
- '',
938
- '## Seed Machine Contract',
939
- 'Refine this seed instead of discarding it. Keep the final JSON aligned with the markdown design system.',
940
- '```json',
941
- designIntentSeed.trim(),
942
- '```',
943
- '',
944
- '## Required Execution',
945
- '1. Create or update docs/DESIGN.md with complete content.',
946
- '2. Create or update docs/design-intent.json with machine-readable design intent.',
947
- '3. Ensure both files stay project-specific, dynamic, and practical for implementation and review.',
948
- '4. After the contract exists, use it as a first-class source for future UI tasks.',
949
- '',
950
- ].join('\n');
951
- }
952
-
953
- /**
954
- * Generate AI-first bootstrap prompts for dynamic project documentation synthesis.
955
- */
956
- export async function generateProjectDocumentation(
957
- targetDirectoryPath,
958
- discoveryAnswers,
959
- initContext,
960
- options = {}
961
- ) {
962
- const normalizedDocsLanguage = normalizeDocsLanguage(options.docsLanguage || 'en');
963
- if (!normalizedDocsLanguage) {
964
- throw new Error(`Unsupported docs language: ${options.docsLanguage}. Supported values: en, id`);
965
- }
966
-
967
- const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
968
- const promptsDirectoryPath = path.join(targetDirectoryPath, '.agent-context', 'prompts');
969
- await ensureDirectory(docsDirectoryPath);
970
- await ensureDirectory(promptsDirectoryPath);
971
-
972
- const synthesisContext = buildSynthesisContext(discoveryAnswers, initContext);
973
- const { requiredDocFileNames } = resolveProjectDocTargets(discoveryAnswers);
974
- const expectedDocFileNames = [...requiredDocFileNames];
975
- const generatedPromptFileNames = [];
976
- const materializedFileNames = [];
977
-
978
- const projectContextPromptFileName = 'bootstrap-project-context.md';
979
- const architectureRecommendation = initContext.architectureRecommendation || null;
980
- const projectContextPromptContent = buildProjectContextBootstrapPrompt({
981
- discoveryAnswers,
982
- initContext: synthesisContext,
983
- expectedDocFileNames,
984
- docsLanguage: normalizedDocsLanguage,
985
- architectureRecommendation,
986
- });
987
- await fs.writeFile(
988
- path.join(promptsDirectoryPath, projectContextPromptFileName),
989
- projectContextPromptContent,
990
- 'utf8'
991
- );
992
- generatedPromptFileNames.push(projectContextPromptFileName);
993
-
994
- if (shouldBootstrapDesignDocument(discoveryAnswers, initContext)) {
995
- const designPromptFileName = 'bootstrap-design.md';
996
- const designPromptContent = buildDesignBootstrapPrompt({
997
- discoveryAnswers,
998
- initContext: synthesisContext,
999
- docsLanguage: normalizedDocsLanguage,
1000
- architectureRecommendation,
1001
- });
1002
- await fs.writeFile(path.join(promptsDirectoryPath, designPromptFileName), designPromptContent, 'utf8');
1003
- generatedPromptFileNames.push(designPromptFileName);
1004
-
1005
- const designIntentSeedFileName = 'design-intent.json';
1006
- const designIntentSeedContent = buildDesignIntentSeed({
1007
- discoveryAnswers,
1008
- initContext: synthesisContext,
1009
- architectureRecommendation,
1010
- });
1011
- await fs.writeFile(path.join(docsDirectoryPath, designIntentSeedFileName), designIntentSeedContent, 'utf8');
1012
- materializedFileNames.push(designIntentSeedFileName);
1013
-
1014
- for (const designContractFileName of UI_DESIGN_CONTRACT_FILE_NAMES) {
1015
- if (!expectedDocFileNames.includes(designContractFileName)) {
1016
- expectedDocFileNames.push(designContractFileName);
1017
- }
1018
- }
1019
- }
1020
-
1021
- return {
1022
- docsDirectoryPath,
1023
- generatedFileNames: expectedDocFileNames,
1024
- generatedPromptFileNames,
1025
- materializedFileNames,
1026
- bootstrapMode: 'ai-synthesis',
1027
- synthesisPromptVersion: PROJECT_DOC_SYNTHESIS_PROMPT_VERSION,
1028
- templateVersion: PROJECT_DOC_TEMPLATE_VERSION,
1029
- docsLanguage: normalizedDocsLanguage,
1030
- discoveryAnswers,
1031
- };
1032
- }
1033
-
1034
- /**
1035
- * Check if the target directory qualifies as "empty" for scaffolding purposes.
1036
- * A directory with only .git is still considered empty.
1037
- */
1038
- export async function isDirectoryEffectivelyEmpty(targetDirectoryPath) {
1039
- try {
1040
- const directoryEntries = await fs.readdir(targetDirectoryPath);
1041
- const meaningfulEntries = directoryEntries.filter(
1042
- (entryName) => entryName !== '.git' && entryName !== '.gitignore'
1043
- );
1044
- return meaningfulEntries.length === 0;
1045
- } catch {
1046
- return true;
1047
- }
1048
- }
1049
-
1050
- /**
1051
- * Check if project docs already exist in the target directory.
1052
- */
1053
- export async function hasExistingProjectDocs(targetDirectoryPath) {
1054
- const projectBriefPath = path.join(targetDirectoryPath, 'docs', 'project-brief.md');
1055
- return pathExists(projectBriefPath);
1056
- }
1057
-
1058
- function extractTemplateVersion(documentContent) {
1059
- const templateVersionMatch = documentContent.match(/^(?:Template version|Versi template):\s*(.+)$/im);
1060
- return templateVersionMatch ? templateVersionMatch[1].trim() : null;
1061
- }
1062
-
1063
- export async function detectProjectDocTemplateStaleness(targetDirectoryPath) {
1064
- const docsDirectoryPath = path.join(targetDirectoryPath, 'docs');
1065
- const checkedFileNames = [];
1066
- const staleFiles = [];
1067
-
1068
- for (const projectDocFileName of PROJECT_DOC_FILE_NAMES) {
1069
- const projectDocFilePath = path.join(docsDirectoryPath, projectDocFileName);
1070
- if (!(await pathExists(projectDocFilePath))) {
1071
- continue;
1072
- }
1073
-
1074
- checkedFileNames.push(projectDocFileName);
1075
- const projectDocContent = await fs.readFile(projectDocFilePath, 'utf8');
1076
- const detectedTemplateVersion = extractTemplateVersion(projectDocContent);
1077
-
1078
- if (!detectedTemplateVersion || detectedTemplateVersion !== PROJECT_DOC_TEMPLATE_VERSION) {
1079
- staleFiles.push({
1080
- fileName: projectDocFileName,
1081
- detectedTemplateVersion,
1082
- });
1083
- }
1084
- }
1085
-
1086
- return {
1087
- hasProjectDocs: checkedFileNames.length > 0,
1088
- expectedTemplateVersion: PROJECT_DOC_TEMPLATE_VERSION,
1089
- checkedFileNames,
1090
- staleFiles,
1091
- };
1092
- }
1093
-
1094
- /**
1095
- * Load project config from a YAML-like file for non-interactive mode.
1096
- * Uses a simple key: value format (one per line) for zero-dependency parsing.
1097
- */
1098
- export async function loadProjectConfig(configFilePath) {
1099
- const configContent = await fs.readFile(configFilePath, 'utf8');
1100
- const configLines = configContent.split(/\r?\n/);
1101
- const configEntries = {};
1102
- let currentKey = null;
1103
- let currentArrayValues = null;
1104
-
1105
- for (const configLine of configLines) {
1106
- const trimmedLine = configLine.trim();
1107
-
1108
- if (!trimmedLine || trimmedLine.startsWith('#')) {
1109
- continue;
1110
- }
1111
-
1112
- if (trimmedLine.startsWith('- ') && currentKey && currentArrayValues !== null) {
1113
- currentArrayValues.push(trimmedLine.slice(2).trim());
1114
- continue;
1115
- }
1116
-
1117
- if (currentKey && currentArrayValues !== null) {
1118
- configEntries[currentKey] = currentArrayValues;
1119
- currentKey = null;
1120
- currentArrayValues = null;
1121
- }
1122
-
1123
- const colonIndex = trimmedLine.indexOf(':');
1124
- if (colonIndex === -1) {
1125
- continue;
1126
- }
1127
-
1128
- const entryKey = trimmedLine.slice(0, colonIndex).trim();
1129
- const entryValue = trimmedLine.slice(colonIndex + 1).trim();
1130
-
1131
- if (!entryValue) {
1132
- currentKey = entryKey;
1133
- currentArrayValues = [];
1134
- continue;
1135
- }
1136
-
1137
- configEntries[entryKey] = entryValue;
1138
- }
1139
-
1140
- if (currentKey && currentArrayValues !== null) {
1141
- configEntries[currentKey] = currentArrayValues;
1142
- }
1143
-
1144
- return {
1145
- projectName: configEntries.projectName || configEntries.name || '',
1146
- projectDescription: configEntries.projectDescription || configEntries.description || '',
1147
- includeCiGuardrails: parseBooleanLikeValue(configEntries.includeCiGuardrails) ?? parseBooleanLikeValue(configEntries.ci) ?? true,
1148
- primaryDomain: configEntries.primaryDomain || configEntries.domain || 'API service',
1149
- databaseChoice: configEntries.databaseChoice || configEntries.database || 'None (stateless service)',
1150
- authStrategy: configEntries.authStrategy || configEntries.auth || 'None (public service)',
1151
- dockerStrategy: resolveDockerStrategy({
1152
- dockerStrategy: configEntries.dockerStrategy || configEntries.containerStrategy,
1153
- useDocker: configEntries.useDocker,
1154
- useDockerDevelopment: configEntries.useDockerDevelopment || configEntries.dockerDevelopment,
1155
- useDockerProduction: configEntries.useDockerProduction || configEntries.dockerProduction,
1156
- }),
1157
- features: Array.isArray(configEntries.features) ? configEntries.features : [],
1158
- additionalContext: configEntries.additionalContext || configEntries.context || 'No additional context provided.',
1159
- docsLang: configEntries.docsLang || configEntries.docsLanguage || 'en',
1160
- };
1161
- }
2
+ * Project scaffolder public surface.
3
+ *
4
+ * The implementation is grouped by function under `lib/cli/project-scaffolder/`
5
+ * so discovery, design-contract logic, prompt building, and persistence do not
6
+ * collapse into one oversized module.
7
+ */
8
+
9
+ export {
10
+ PROJECT_DOC_TEMPLATE_VERSION,
11
+ PROJECT_DOC_SYNTHESIS_PROMPT_VERSION,
12
+ } from './project-scaffolder/constants.mjs';
13
+
14
+ export {
15
+ normalizeDocsLanguage,
16
+ runProjectDiscovery,
17
+ resolveProjectDocTargets,
18
+ buildSynthesisContext,
19
+ loadProjectConfig,
20
+ } from './project-scaffolder/discovery.mjs';
21
+
22
+ export {
23
+ shouldBootstrapDesignDocument,
24
+ validateDesignIntentContract,
25
+ buildDesignIntentSeedFromSignals,
26
+ } from './project-scaffolder/design-contract.mjs';
27
+
28
+ export {
29
+ generateProjectDocumentation,
30
+ isDirectoryEffectivelyEmpty,
31
+ hasExistingProjectDocs,
32
+ detectProjectDocTemplateStaleness,
33
+ } from './project-scaffolder/storage.mjs';