@girardmedia/bootspring 2.0.21 → 2.0.22

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 (147) hide show
  1. package/cli/preseed/index.js +16 -0
  2. package/cli/preseed/interactive.js +143 -0
  3. package/cli/preseed/templates.js +227 -0
  4. package/cli/seed/builders/ai-context-builder.js +85 -0
  5. package/cli/seed/builders/index.js +13 -0
  6. package/cli/seed/builders/seed-builder.js +272 -0
  7. package/cli/seed/extractors/content-extractors.js +383 -0
  8. package/cli/seed/extractors/index.js +47 -0
  9. package/cli/seed/extractors/metadata-extractors.js +167 -0
  10. package/cli/seed/extractors/section-extractor.js +54 -0
  11. package/cli/seed/extractors/stack-extractors.js +228 -0
  12. package/cli/seed/index.js +18 -0
  13. package/cli/seed/utils/folder-structure.js +84 -0
  14. package/cli/seed/utils/index.js +11 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.js +3220 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/context-McpJQa_2.d.ts +5710 -0
  19. package/dist/core/index.d.ts +635 -0
  20. package/dist/core/index.js +2593 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/index-QqbeEiDm.d.ts +857 -0
  23. package/dist/index-UiYCgwiH.d.ts +174 -0
  24. package/dist/index.d.ts +453 -0
  25. package/dist/index.js +44228 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/index.d.ts +1 -0
  28. package/dist/mcp/index.js +41173 -0
  29. package/dist/mcp/index.js.map +1 -0
  30. package/generators/index.ts +82 -0
  31. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  32. package/intelligence/orchestrator/config/index.js +20 -0
  33. package/intelligence/orchestrator/config/phases.js +111 -0
  34. package/intelligence/orchestrator/config/remediation.js +150 -0
  35. package/intelligence/orchestrator/config/workflows.js +168 -0
  36. package/intelligence/orchestrator/core/index.js +16 -0
  37. package/intelligence/orchestrator/core/state-manager.js +88 -0
  38. package/intelligence/orchestrator/core/telemetry.js +24 -0
  39. package/intelligence/orchestrator/index.js +17 -0
  40. package/mcp/contracts/mcp-contract.v1.json +1 -1
  41. package/package.json +16 -3
  42. package/src/cli/agent.ts +703 -0
  43. package/src/cli/analyze.ts +640 -0
  44. package/src/cli/audit.ts +707 -0
  45. package/src/cli/auth.ts +930 -0
  46. package/src/cli/billing.ts +364 -0
  47. package/src/cli/build.ts +1089 -0
  48. package/src/cli/business.ts +508 -0
  49. package/src/cli/checkpoint-utils.ts +236 -0
  50. package/src/cli/checkpoint.ts +757 -0
  51. package/src/cli/cloud-sync.ts +534 -0
  52. package/src/cli/content.ts +273 -0
  53. package/src/cli/context.ts +667 -0
  54. package/src/cli/dashboard.ts +133 -0
  55. package/src/cli/deploy.ts +704 -0
  56. package/src/cli/doctor.ts +480 -0
  57. package/src/cli/fundraise.ts +494 -0
  58. package/src/cli/generate.ts +346 -0
  59. package/src/cli/github-cmd.ts +566 -0
  60. package/src/cli/health.ts +599 -0
  61. package/src/cli/index.ts +113 -0
  62. package/src/cli/init.ts +838 -0
  63. package/src/cli/legal.ts +495 -0
  64. package/src/cli/log.ts +316 -0
  65. package/src/cli/loop.ts +1660 -0
  66. package/src/cli/manager.ts +878 -0
  67. package/src/cli/mcp.ts +275 -0
  68. package/src/cli/memory.ts +346 -0
  69. package/src/cli/metrics.ts +590 -0
  70. package/src/cli/monitor.ts +960 -0
  71. package/src/cli/mvp.ts +662 -0
  72. package/src/cli/onboard.ts +663 -0
  73. package/src/cli/orchestrator.ts +622 -0
  74. package/src/cli/plugin.ts +483 -0
  75. package/src/cli/prd.ts +671 -0
  76. package/src/cli/preseed-start.ts +1633 -0
  77. package/src/cli/preseed.ts +2434 -0
  78. package/src/cli/project.ts +526 -0
  79. package/src/cli/quality.ts +885 -0
  80. package/src/cli/security.ts +1079 -0
  81. package/src/cli/seed.ts +1224 -0
  82. package/src/cli/skill.ts +537 -0
  83. package/src/cli/suggest.ts +1225 -0
  84. package/src/cli/switch.ts +518 -0
  85. package/src/cli/task.ts +780 -0
  86. package/src/cli/telemetry.ts +172 -0
  87. package/src/cli/todo.ts +627 -0
  88. package/src/cli/types.ts +15 -0
  89. package/src/cli/update.ts +334 -0
  90. package/src/cli/visualize.ts +609 -0
  91. package/src/cli/watch.ts +895 -0
  92. package/src/cli/workspace.ts +709 -0
  93. package/src/core/action-recorder.ts +673 -0
  94. package/src/core/analyze-workflow.ts +1453 -0
  95. package/src/core/api-client.ts +1120 -0
  96. package/src/core/audit-workflow.ts +1681 -0
  97. package/src/core/auth.ts +471 -0
  98. package/src/core/build-orchestrator.ts +509 -0
  99. package/src/core/build-state.ts +621 -0
  100. package/src/core/checkpoint-engine.ts +482 -0
  101. package/src/core/config.ts +1285 -0
  102. package/src/core/context-loader.ts +694 -0
  103. package/src/core/context.ts +410 -0
  104. package/src/core/deploy-workflow.ts +1085 -0
  105. package/src/core/entitlements.ts +322 -0
  106. package/src/core/github-sync.ts +720 -0
  107. package/src/core/index.ts +981 -0
  108. package/src/core/ingest.ts +1186 -0
  109. package/src/core/metrics-engine.ts +886 -0
  110. package/src/core/mvp.ts +847 -0
  111. package/src/core/onboard-workflow.ts +1293 -0
  112. package/src/core/policies.ts +81 -0
  113. package/src/core/preseed-workflow.ts +1163 -0
  114. package/src/core/preseed.ts +1826 -0
  115. package/src/core/project-context.ts +380 -0
  116. package/src/core/project-state.ts +699 -0
  117. package/src/core/r2-sync.ts +691 -0
  118. package/src/core/scaffold.ts +1715 -0
  119. package/src/core/session.ts +286 -0
  120. package/src/core/task-extractor.ts +799 -0
  121. package/src/core/telemetry.ts +371 -0
  122. package/src/core/tier-enforcement.ts +737 -0
  123. package/src/core/utils.ts +437 -0
  124. package/src/index.ts +29 -0
  125. package/src/intelligence/agent-collab.ts +2376 -0
  126. package/src/intelligence/auto-suggest.ts +713 -0
  127. package/src/intelligence/content-gen.ts +1351 -0
  128. package/src/intelligence/cross-project.ts +1692 -0
  129. package/src/intelligence/git-memory.ts +529 -0
  130. package/src/intelligence/index.ts +318 -0
  131. package/src/intelligence/orchestrator.ts +534 -0
  132. package/src/intelligence/prd.ts +466 -0
  133. package/src/intelligence/recommendations.ts +982 -0
  134. package/src/intelligence/workflow-composer.ts +1472 -0
  135. package/src/mcp/capabilities.ts +233 -0
  136. package/src/mcp/index.ts +37 -0
  137. package/src/mcp/registry.ts +1268 -0
  138. package/src/mcp/response-formatter.ts +797 -0
  139. package/src/mcp/server.ts +240 -0
  140. package/src/types/agent.ts +69 -0
  141. package/src/types/config.ts +86 -0
  142. package/src/types/context.ts +77 -0
  143. package/src/types/index.ts +53 -0
  144. package/src/types/mcp.ts +91 -0
  145. package/src/types/skills.ts +47 -0
  146. package/src/types/workflow.ts +155 -0
  147. package/generators/index.js +0 -18
@@ -0,0 +1,1186 @@
1
+ /**
2
+ * Bootspring File Ingestion System
3
+ *
4
+ * Processes user-provided files from .bootspring/inputs/ and extracts
5
+ * structured information for document generation.
6
+ *
7
+ * @package bootspring
8
+ * @module core/ingest
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ export interface ParsedYaml {
19
+ [key: string]: string;
20
+ }
21
+
22
+ export interface MvpStats {
23
+ totalFiles: number;
24
+ totalLines: number;
25
+ }
26
+
27
+ export interface MvpIngestion {
28
+ files: string[];
29
+ stats: MvpStats;
30
+ patterns: string[];
31
+ components: string[];
32
+ services: string[];
33
+ hooks: string[];
34
+ utilities: string[];
35
+ }
36
+
37
+ export interface MarkdownSection {
38
+ level: number;
39
+ title: string;
40
+ }
41
+
42
+ export interface BusinessDocument {
43
+ file: string;
44
+ type: string;
45
+ sections: MarkdownSection[];
46
+ summary: string;
47
+ wordCount: number;
48
+ }
49
+
50
+ export interface UserStory {
51
+ role: string;
52
+ want: string;
53
+ benefit: string;
54
+ }
55
+
56
+ export interface Requirements {
57
+ mustHave: string[];
58
+ shouldHave: string[];
59
+ niceToHave: string[];
60
+ }
61
+
62
+ export interface PrdDocument {
63
+ file: string;
64
+ userStories: UserStory[];
65
+ requirements: Requirements;
66
+ features: string[];
67
+ openQuestions: string[];
68
+ sections: MarkdownSection[];
69
+ }
70
+
71
+ export interface DesignFile {
72
+ path: string;
73
+ type: string;
74
+ name: string;
75
+ }
76
+
77
+ export interface DesignsIngestion {
78
+ files: DesignFile[];
79
+ count: number;
80
+ imageCount: number;
81
+ docCount: number;
82
+ }
83
+
84
+ export interface ApiEndpoint {
85
+ path: string;
86
+ }
87
+
88
+ export interface ApiSpec {
89
+ file: string;
90
+ type: string;
91
+ endpoints: ApiEndpoint[];
92
+ }
93
+
94
+ export interface JsonStructure {
95
+ type: string;
96
+ length?: number | undefined;
97
+ itemType?: JsonStructure | string | undefined;
98
+ fields?: Record<string, string> | undefined;
99
+ }
100
+
101
+ export interface SqlDataFile {
102
+ file: string;
103
+ type: 'sql';
104
+ tables: string[];
105
+ }
106
+
107
+ export interface JsonDataFile {
108
+ file: string;
109
+ type: 'json';
110
+ structure: JsonStructure;
111
+ }
112
+
113
+ export interface CsvDataFile {
114
+ file: string;
115
+ type: 'csv';
116
+ headers: string[];
117
+ }
118
+
119
+ export type DataFile = SqlDataFile | JsonDataFile | CsvDataFile;
120
+
121
+ export interface IngestionResult {
122
+ mvp: MvpIngestion;
123
+ business: BusinessDocument[];
124
+ prd: PrdDocument[];
125
+ designs: DesignsIngestion;
126
+ api: ApiSpec[];
127
+ data: DataFile[];
128
+ timestamp: string;
129
+ }
130
+
131
+ export interface SeedConfig {
132
+ project?: {
133
+ name?: string | undefined;
134
+ } | undefined;
135
+ }
136
+
137
+ // ============================================================================
138
+ // Helper Functions
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Simple YAML parser for basic key-value pairs
143
+ */
144
+ export function parseYamlSimple(content: string): ParsedYaml {
145
+ const result: ParsedYaml = {};
146
+ const lines = content.split('\n');
147
+
148
+ for (const line of lines) {
149
+ const trimmed = line.trim();
150
+ if (!trimmed || trimmed.startsWith('#')) continue;
151
+
152
+ const match = line.match(/^(\s*)(\w+):\s*(.*)/);
153
+ if (match) {
154
+ const key = match[2];
155
+ const value = match[3]?.trim();
156
+
157
+ if (key && value) {
158
+ result[key] = value.replace(/^["']|["']$/g, '');
159
+ }
160
+ }
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Detect business document type from filename and content
168
+ */
169
+ function detectBusinessDocType(filename: string, content: string): string {
170
+ const lower = filename.toLowerCase();
171
+ const contentLower = content.toLowerCase();
172
+
173
+ if (lower.includes('business-plan') || lower.includes('businessplan')) return 'business-plan';
174
+ if (lower.includes('pitch') || lower.includes('deck')) return 'pitch-deck';
175
+ if (lower.includes('competitive') || lower.includes('competitor')) return 'competitive-analysis';
176
+ if (lower.includes('market') && lower.includes('research')) return 'market-research';
177
+ if (lower.includes('financial') || lower.includes('model')) return 'financial-model';
178
+ if (contentLower.includes('executive summary')) return 'business-plan';
179
+ if (contentLower.includes('tam') && contentLower.includes('sam')) return 'market-research';
180
+ if (contentLower.includes('competitor') || contentLower.includes('competitive advantage')) return 'competitive-analysis';
181
+
182
+ return 'general';
183
+ }
184
+
185
+ /**
186
+ * Parse markdown sections
187
+ */
188
+ function parseMarkdownSections(content: string): MarkdownSection[] {
189
+ const sections: MarkdownSection[] = [];
190
+ const lines = content.split('\n');
191
+
192
+ for (const line of lines) {
193
+ const match = line.match(/^(#{1,3})\s+(.+)/);
194
+ if (match && match[1] && match[2]) {
195
+ sections.push({
196
+ level: match[1].length,
197
+ title: match[2].trim()
198
+ });
199
+ }
200
+ }
201
+
202
+ return sections;
203
+ }
204
+
205
+ /**
206
+ * Extract summary from document
207
+ */
208
+ function extractSummary(content: string): string {
209
+ // Skip frontmatter
210
+ let text = content;
211
+ if (text.startsWith('---')) {
212
+ const endFrontmatter = text.indexOf('---', 3);
213
+ if (endFrontmatter !== -1) {
214
+ text = text.substring(endFrontmatter + 3);
215
+ }
216
+ }
217
+
218
+ // Find first paragraph
219
+ const lines = text.split('\n').filter(l => l.trim() && !l.startsWith('#'));
220
+ const firstPara = lines.slice(0, 3).join(' ').trim();
221
+
222
+ return firstPara.substring(0, 200) + (firstPara.length > 200 ? '...' : '');
223
+ }
224
+
225
+ /**
226
+ * Extract user stories from PRD
227
+ */
228
+ function extractUserStories(content: string): UserStory[] {
229
+ const stories: UserStory[] = [];
230
+
231
+ // Match "As a [user], I want [action] so that [benefit]" pattern
232
+ const storyPattern = /As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(.+?)\s+so\s+that\s+(.+?)(?:\.|$)/gi;
233
+ let match;
234
+
235
+ while ((match = storyPattern.exec(content)) !== null) {
236
+ if (match[1] && match[2] && match[3]) {
237
+ stories.push({
238
+ role: match[1].trim(),
239
+ want: match[2].trim(),
240
+ benefit: match[3].trim()
241
+ });
242
+ }
243
+ }
244
+
245
+ // Also look for bullet points starting with "As a"
246
+ const lines = content.split('\n');
247
+ for (const line of lines) {
248
+ if (line.match(/^[-*]\s+As\s+(?:a|an)/i) && !stories.some(s => line.includes(s.want))) {
249
+ const bulletMatch = line.match(/As\s+(?:a|an)\s+(.+?),\s+I\s+want\s+(.+?)(?:\s+so\s+that\s+(.+?))?(?:\.|$)/i);
250
+ if (bulletMatch && bulletMatch[1] && bulletMatch[2]) {
251
+ stories.push({
252
+ role: bulletMatch[1].trim(),
253
+ want: bulletMatch[2].trim(),
254
+ benefit: bulletMatch[3]?.trim() || ''
255
+ });
256
+ }
257
+ }
258
+ }
259
+
260
+ return stories;
261
+ }
262
+
263
+ /**
264
+ * Extract requirements from PRD
265
+ */
266
+ function extractRequirements(content: string): Requirements {
267
+ const requirements: Requirements = {
268
+ mustHave: [],
269
+ shouldHave: [],
270
+ niceToHave: []
271
+ };
272
+
273
+ const lines = content.split('\n');
274
+ let currentPriority: keyof Requirements | null = null;
275
+
276
+ for (const line of lines) {
277
+ const lowerLine = line.toLowerCase();
278
+
279
+ // Detect priority sections
280
+ if (lowerLine.includes('must have') || lowerLine.includes('p0') || lowerLine.includes('critical')) {
281
+ currentPriority = 'mustHave';
282
+ continue;
283
+ }
284
+ if (lowerLine.includes('should have') || lowerLine.includes('p1') || lowerLine.includes('high')) {
285
+ currentPriority = 'shouldHave';
286
+ continue;
287
+ }
288
+ if (lowerLine.includes('nice to have') || lowerLine.includes('p2') || lowerLine.includes('low')) {
289
+ currentPriority = 'niceToHave';
290
+ continue;
291
+ }
292
+
293
+ // Extract bullet points
294
+ if (currentPriority && line.match(/^[-*]\s+/)) {
295
+ const req = line.replace(/^[-*]\s+/, '').trim();
296
+ if (req && !req.match(/^(must|should|nice|p[012])/i)) {
297
+ requirements[currentPriority].push(req);
298
+ }
299
+ }
300
+ }
301
+
302
+ return requirements;
303
+ }
304
+
305
+ /**
306
+ * Extract features from PRD
307
+ */
308
+ function extractFeatures(content: string): string[] {
309
+ const features: string[] = [];
310
+ const lines = content.split('\n');
311
+ let inFeatureSection = false;
312
+
313
+ for (const line of lines) {
314
+ const lowerLine = line.toLowerCase();
315
+
316
+ // Detect feature sections
317
+ if (lowerLine.includes('## feature') || lowerLine.includes('### feature') || lowerLine.includes('## core feature')) {
318
+ inFeatureSection = true;
319
+ continue;
320
+ }
321
+
322
+ // New section ends feature section
323
+ if (inFeatureSection && line.match(/^#{1,3}\s/) && !lowerLine.includes('feature')) {
324
+ inFeatureSection = false;
325
+ }
326
+
327
+ // Extract bullet points
328
+ if (inFeatureSection && line.match(/^[-*]\s+/)) {
329
+ const feature = line.replace(/^[-*]\s+/, '').trim();
330
+ if (feature) {
331
+ features.push(feature);
332
+ }
333
+ }
334
+ }
335
+
336
+ return features;
337
+ }
338
+
339
+ /**
340
+ * Extract open questions from PRD
341
+ */
342
+ function extractOpenQuestions(content: string): string[] {
343
+ const questions: string[] = [];
344
+ const lines = content.split('\n');
345
+ let inQuestionSection = false;
346
+
347
+ for (const line of lines) {
348
+ const lowerLine = line.toLowerCase();
349
+
350
+ // Detect question sections
351
+ if (lowerLine.includes('open question') || lowerLine.includes('tbd') || lowerLine.includes('to be determined')) {
352
+ inQuestionSection = true;
353
+ continue;
354
+ }
355
+
356
+ // New section ends question section
357
+ if (inQuestionSection && line.match(/^#{1,3}\s/)) {
358
+ inQuestionSection = false;
359
+ }
360
+
361
+ // Extract questions (bullet points or lines ending with ?)
362
+ if (line.match(/\?[\s]*$/) || (inQuestionSection && line.match(/^[-*]\s+/))) {
363
+ const question = line.replace(/^[-*]\s+/, '').trim();
364
+ if (question && question.length > 5) {
365
+ questions.push(question);
366
+ }
367
+ }
368
+ }
369
+
370
+ return questions;
371
+ }
372
+
373
+ /**
374
+ * Extract endpoints from OpenAPI spec
375
+ */
376
+ function extractOpenAPIEndpoints(content: string): ApiEndpoint[] {
377
+ const endpoints: ApiEndpoint[] = [];
378
+
379
+ // Simple regex-based extraction (not full YAML parsing)
380
+ const pathPattern = /["']?(\/[^"'\s:]+)["']?\s*:/g;
381
+ const paths = new Set<string>();
382
+
383
+ let match;
384
+ while ((match = pathPattern.exec(content)) !== null) {
385
+ const pathStr = match[1];
386
+ if (pathStr && !pathStr.includes('$') && pathStr.length < 100) {
387
+ paths.add(pathStr);
388
+ }
389
+ }
390
+
391
+ for (const pathStr of paths) {
392
+ endpoints.push({ path: pathStr });
393
+ }
394
+
395
+ return endpoints;
396
+ }
397
+
398
+ /**
399
+ * Extract table definitions from SQL
400
+ */
401
+ function extractSQLTables(content: string): string[] {
402
+ const tables: string[] = [];
403
+ const tablePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?\s*\(/gi;
404
+
405
+ let match;
406
+ while ((match = tablePattern.exec(content)) !== null) {
407
+ if (match[1]) {
408
+ tables.push(match[1]);
409
+ }
410
+ }
411
+
412
+ return tables;
413
+ }
414
+
415
+ /**
416
+ * Infer structure from JSON
417
+ */
418
+ function inferJSONStructure(data: unknown): JsonStructure {
419
+ if (Array.isArray(data)) {
420
+ return {
421
+ type: 'array',
422
+ length: data.length,
423
+ itemType: data.length > 0 ? inferJSONStructure(data[0]) : 'unknown'
424
+ };
425
+ }
426
+
427
+ if (data === null) return { type: 'null' };
428
+ if (typeof data !== 'object') return { type: typeof data };
429
+
430
+ const fields: Record<string, string> = {};
431
+ for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
432
+ fields[key] = typeof value === 'object' && value !== null
433
+ ? (Array.isArray(value) ? 'array' : 'object')
434
+ : typeof value;
435
+ }
436
+
437
+ return { type: 'object', fields };
438
+ }
439
+
440
+ // ============================================================================
441
+ // Ingestion Functions
442
+ // ============================================================================
443
+
444
+ /**
445
+ * Ingest all files from .bootspring/inputs
446
+ */
447
+ export async function ingestAll(projectRoot: string): Promise<IngestionResult> {
448
+ const results: IngestionResult = {
449
+ mvp: await ingestMVP(projectRoot),
450
+ business: await ingestBusiness(projectRoot),
451
+ prd: await ingestPRD(projectRoot),
452
+ designs: await ingestDesigns(projectRoot),
453
+ api: await ingestAPI(projectRoot),
454
+ data: await ingestData(projectRoot),
455
+ timestamp: new Date().toISOString()
456
+ };
457
+
458
+ return results;
459
+ }
460
+
461
+ /**
462
+ * Ingest MVP source code
463
+ */
464
+ export async function ingestMVP(projectRoot: string): Promise<MvpIngestion> {
465
+ const mvpPath = path.join(projectRoot, '.bootspring', 'inputs', 'mvp', 'source');
466
+
467
+ if (!fs.existsSync(mvpPath)) {
468
+ return { files: [], stats: { totalFiles: 0, totalLines: 0 }, patterns: [], components: [], services: [], hooks: [], utilities: [] };
469
+ }
470
+
471
+ const files: string[] = [];
472
+ const stats: MvpStats = { totalFiles: 0, totalLines: 0 };
473
+ const patterns: string[] = [];
474
+ const components: string[] = [];
475
+ const services: string[] = [];
476
+ const hooks: string[] = [];
477
+ const utilities: string[] = [];
478
+
479
+ // Walk the directory
480
+ function walkDir(dir: string, relativePath: string = ''): void {
481
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
482
+
483
+ for (const entry of entries) {
484
+ const fullPath = path.join(dir, entry.name);
485
+ const relPath = path.join(relativePath, entry.name);
486
+
487
+ if (entry.isDirectory()) {
488
+ if (!['node_modules', '.git', '.next', 'dist', 'build'].includes(entry.name)) {
489
+ walkDir(fullPath, relPath);
490
+ }
491
+ } else if (entry.isFile()) {
492
+ const ext = path.extname(entry.name);
493
+ if (['.ts', '.tsx', '.js', '.jsx', '.css', '.scss'].includes(ext)) {
494
+ files.push(relPath);
495
+ stats.totalFiles++;
496
+
497
+ try {
498
+ const content = fs.readFileSync(fullPath, 'utf-8');
499
+ const lines = content.split('\n').length;
500
+ stats.totalLines += lines;
501
+
502
+ // Detect patterns
503
+ if (content.includes("'use client'") || content.includes('"use client"')) {
504
+ if (!patterns.includes('client-components')) patterns.push('client-components');
505
+ }
506
+ if (content.includes("'use server'") || content.includes('"use server"')) {
507
+ if (!patterns.includes('server-actions')) patterns.push('server-actions');
508
+ }
509
+ if (content.includes('useState') || content.includes('useEffect')) {
510
+ if (!patterns.includes('react-hooks')) patterns.push('react-hooks');
511
+ }
512
+ if (content.includes('prisma') || content.includes('PrismaClient')) {
513
+ if (!patterns.includes('prisma-orm')) patterns.push('prisma-orm');
514
+ }
515
+ if (content.includes('zod') || content.includes('.parse(')) {
516
+ if (!patterns.includes('zod-validation')) patterns.push('zod-validation');
517
+ }
518
+ if (content.includes('clerk') || content.includes('Clerk')) {
519
+ if (!patterns.includes('clerk-auth')) patterns.push('clerk-auth');
520
+ }
521
+ if (content.includes('stripe') || content.includes('Stripe')) {
522
+ if (!patterns.includes('stripe-payments')) patterns.push('stripe-payments');
523
+ }
524
+
525
+ // Categorize files
526
+ const lowerPath = relPath.toLowerCase();
527
+ if (lowerPath.includes('component') || entry.name.match(/^[A-Z].*\.(tsx|jsx)$/)) {
528
+ components.push(relPath);
529
+ }
530
+ if (lowerPath.includes('service') || lowerPath.includes('api') || lowerPath.includes('lib')) {
531
+ services.push(relPath);
532
+ }
533
+ if (lowerPath.includes('hook') || entry.name.startsWith('use')) {
534
+ hooks.push(relPath);
535
+ }
536
+ if (lowerPath.includes('util') || lowerPath.includes('helper')) {
537
+ utilities.push(relPath);
538
+ }
539
+ } catch {
540
+ // Skip files that can't be read
541
+ }
542
+ }
543
+ }
544
+ }
545
+ }
546
+
547
+ walkDir(mvpPath);
548
+
549
+ return {
550
+ files,
551
+ stats,
552
+ patterns,
553
+ components,
554
+ services,
555
+ hooks,
556
+ utilities
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Ingest business documents
562
+ */
563
+ export async function ingestBusiness(projectRoot: string): Promise<BusinessDocument[]> {
564
+ const businessPath = path.join(projectRoot, '.bootspring', 'inputs', 'business');
565
+
566
+ if (!fs.existsSync(businessPath)) {
567
+ return [];
568
+ }
569
+
570
+ const documents: BusinessDocument[] = [];
571
+
572
+ function walkDir(dir: string): void {
573
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
574
+
575
+ for (const entry of entries) {
576
+ const fullPath = path.join(dir, entry.name);
577
+
578
+ if (entry.isDirectory()) {
579
+ walkDir(fullPath);
580
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
581
+ try {
582
+ const content = fs.readFileSync(fullPath, 'utf-8');
583
+ const relativePath = path.relative(businessPath, fullPath);
584
+
585
+ documents.push({
586
+ file: relativePath,
587
+ type: detectBusinessDocType(entry.name, content),
588
+ sections: parseMarkdownSections(content),
589
+ summary: extractSummary(content),
590
+ wordCount: content.split(/\s+/).length
591
+ });
592
+ } catch {
593
+ // Skip files that can't be read
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ walkDir(businessPath);
600
+ return documents;
601
+ }
602
+
603
+ /**
604
+ * Ingest PRD documents
605
+ */
606
+ export async function ingestPRD(projectRoot: string): Promise<PrdDocument[]> {
607
+ const prdPath = path.join(projectRoot, '.bootspring', 'inputs', 'prd');
608
+
609
+ if (!fs.existsSync(prdPath)) {
610
+ return [];
611
+ }
612
+
613
+ const prds: PrdDocument[] = [];
614
+
615
+ function walkDir(dir: string): void {
616
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
617
+
618
+ for (const entry of entries) {
619
+ const fullPath = path.join(dir, entry.name);
620
+
621
+ if (entry.isDirectory()) {
622
+ walkDir(fullPath);
623
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
624
+ try {
625
+ const content = fs.readFileSync(fullPath, 'utf-8');
626
+ const relativePath = path.relative(prdPath, fullPath);
627
+
628
+ prds.push({
629
+ file: relativePath,
630
+ userStories: extractUserStories(content),
631
+ requirements: extractRequirements(content),
632
+ features: extractFeatures(content),
633
+ openQuestions: extractOpenQuestions(content),
634
+ sections: parseMarkdownSections(content)
635
+ });
636
+ } catch {
637
+ // Skip files that can't be read
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ walkDir(prdPath);
644
+ return prds;
645
+ }
646
+
647
+ /**
648
+ * Ingest design files
649
+ */
650
+ export async function ingestDesigns(projectRoot: string): Promise<DesignsIngestion> {
651
+ const designsPath = path.join(projectRoot, '.bootspring', 'inputs', 'designs');
652
+
653
+ if (!fs.existsSync(designsPath)) {
654
+ return { files: [], count: 0, imageCount: 0, docCount: 0 };
655
+ }
656
+
657
+ const files: DesignFile[] = [];
658
+
659
+ function walkDir(dir: string): void {
660
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
661
+
662
+ for (const entry of entries) {
663
+ const fullPath = path.join(dir, entry.name);
664
+
665
+ if (entry.isDirectory()) {
666
+ walkDir(fullPath);
667
+ } else if (entry.isFile()) {
668
+ const ext = path.extname(entry.name).toLowerCase();
669
+ const relativePath = path.relative(designsPath, fullPath);
670
+
671
+ files.push({
672
+ path: relativePath,
673
+ type: ext,
674
+ name: entry.name
675
+ });
676
+ }
677
+ }
678
+ }
679
+
680
+ walkDir(designsPath);
681
+
682
+ return {
683
+ files,
684
+ count: files.length,
685
+ imageCount: files.filter(f => ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp'].includes(f.type)).length,
686
+ docCount: files.filter(f => ['.md', '.pdf', '.doc', '.docx'].includes(f.type)).length
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Ingest API specifications
692
+ */
693
+ export async function ingestAPI(projectRoot: string): Promise<ApiSpec[]> {
694
+ const apiPath = path.join(projectRoot, '.bootspring', 'inputs', 'api');
695
+
696
+ if (!fs.existsSync(apiPath)) {
697
+ return [];
698
+ }
699
+
700
+ const specs: ApiSpec[] = [];
701
+
702
+ function walkDir(dir: string): void {
703
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
704
+
705
+ for (const entry of entries) {
706
+ const fullPath = path.join(dir, entry.name);
707
+
708
+ if (entry.isDirectory()) {
709
+ walkDir(fullPath);
710
+ } else if (entry.isFile()) {
711
+ const ext = path.extname(entry.name).toLowerCase();
712
+
713
+ if (['.yaml', '.yml', '.json'].includes(ext)) {
714
+ try {
715
+ const content = fs.readFileSync(fullPath, 'utf-8');
716
+ const relativePath = path.relative(apiPath, fullPath);
717
+
718
+ // Check if it's an OpenAPI spec
719
+ if (content.includes('openapi') || content.includes('swagger')) {
720
+ specs.push({
721
+ file: relativePath,
722
+ type: 'openapi',
723
+ endpoints: extractOpenAPIEndpoints(content)
724
+ });
725
+ } else {
726
+ specs.push({
727
+ file: relativePath,
728
+ type: 'unknown',
729
+ endpoints: []
730
+ });
731
+ }
732
+ } catch {
733
+ // Skip files that can't be read
734
+ }
735
+ }
736
+ }
737
+ }
738
+ }
739
+
740
+ walkDir(apiPath);
741
+ return specs;
742
+ }
743
+
744
+ /**
745
+ * Ingest data files
746
+ */
747
+ export async function ingestData(projectRoot: string): Promise<DataFile[]> {
748
+ const dataPath = path.join(projectRoot, '.bootspring', 'inputs', 'data');
749
+
750
+ if (!fs.existsSync(dataPath)) {
751
+ return [];
752
+ }
753
+
754
+ const dataFiles: DataFile[] = [];
755
+
756
+ function walkDir(dir: string): void {
757
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
758
+
759
+ for (const entry of entries) {
760
+ const fullPath = path.join(dir, entry.name);
761
+
762
+ if (entry.isDirectory()) {
763
+ walkDir(fullPath);
764
+ } else if (entry.isFile()) {
765
+ const ext = path.extname(entry.name).toLowerCase();
766
+ const relativePath = path.relative(dataPath, fullPath);
767
+
768
+ try {
769
+ const content = fs.readFileSync(fullPath, 'utf-8');
770
+
771
+ if (ext === '.sql') {
772
+ dataFiles.push({
773
+ file: relativePath,
774
+ type: 'sql',
775
+ tables: extractSQLTables(content)
776
+ });
777
+ } else if (ext === '.json') {
778
+ const parsed = JSON.parse(content) as unknown;
779
+ dataFiles.push({
780
+ file: relativePath,
781
+ type: 'json',
782
+ structure: inferJSONStructure(parsed)
783
+ });
784
+ } else if (ext === '.csv') {
785
+ const headers = content.split('\n')[0]?.split(',').map(h => h.trim()) ?? [];
786
+ dataFiles.push({
787
+ file: relativePath,
788
+ type: 'csv',
789
+ headers
790
+ });
791
+ }
792
+ } catch {
793
+ // Skip files that can't be parsed
794
+ }
795
+ }
796
+ }
797
+ }
798
+
799
+ walkDir(dataPath);
800
+ return dataFiles;
801
+ }
802
+
803
+ // ============================================================================
804
+ // Document Generation
805
+ // ============================================================================
806
+
807
+ /**
808
+ * Generate context summary document
809
+ */
810
+ function generateContextSummary(ingested: IngestionResult, seedConfig: SeedConfig): string {
811
+ const projectName = seedConfig.project?.name || 'Project';
812
+
813
+ return `# ${projectName} - Context Summary
814
+
815
+ > Auto-generated by Bootspring on ${new Date().toISOString().split('T')[0]}
816
+
817
+ ## Input Files Summary
818
+
819
+ | Category | Count | Status |
820
+ |----------|-------|--------|
821
+ | MVP Code | ${ingested.mvp?.files?.length || 0} files | ${(ingested.mvp?.files?.length ?? 0) > 0 ? 'Analyzed' : 'Not provided'} |
822
+ | Business Docs | ${ingested.business?.length || 0} files | ${(ingested.business?.length ?? 0) > 0 ? 'Analyzed' : 'Not provided'} |
823
+ | PRD Docs | ${ingested.prd?.length || 0} files | ${(ingested.prd?.length ?? 0) > 0 ? 'Analyzed' : 'Not provided'} |
824
+ | Design Files | ${ingested.designs?.count || 0} files | ${(ingested.designs?.count ?? 0) > 0 ? 'Indexed' : 'Not provided'} |
825
+ | API Specs | ${ingested.api?.length || 0} files | ${(ingested.api?.length ?? 0) > 0 ? 'Analyzed' : 'Not provided'} |
826
+ | Data Files | ${ingested.data?.length || 0} files | ${(ingested.data?.length ?? 0) > 0 ? 'Analyzed' : 'Not provided'} |
827
+
828
+ ## Detected Patterns
829
+
830
+ ${(ingested.mvp?.patterns?.length ?? 0) > 0 ? ingested.mvp.patterns.map(p => `- ${p}`).join('\n') : '- No patterns detected'}
831
+
832
+ ## Key Statistics
833
+
834
+ ${ingested.mvp?.stats ? `
835
+ - **Total MVP Files**: ${ingested.mvp.stats.totalFiles}
836
+ - **Total Lines of Code**: ${ingested.mvp.stats.totalLines.toLocaleString()}
837
+ - **Components**: ${ingested.mvp.components?.length || 0}
838
+ - **Services**: ${ingested.mvp.services?.length || 0}
839
+ - **Hooks**: ${ingested.mvp.hooks?.length || 0}
840
+ ` : '- No MVP code provided'}
841
+
842
+ ${(ingested.prd?.length ?? 0) > 0 ? `
843
+ ## User Stories Found
844
+
845
+ ${ingested.prd.flatMap(p => p.userStories || []).slice(0, 10).map(s =>
846
+ `- As a **${s.role}**, I want **${s.want}**${s.benefit ? ` so that ${s.benefit}` : ''}`
847
+ ).join('\n') || '- No user stories found'}
848
+ ` : ''}
849
+
850
+ ---
851
+
852
+ *Generated by Bootspring*
853
+ `;
854
+ }
855
+
856
+ /**
857
+ * Generate MVP analysis document
858
+ */
859
+ function generateMVPAnalysis(mvp: MvpIngestion): string {
860
+ return `# MVP Code Analysis
861
+
862
+ > Auto-generated by Bootspring
863
+
864
+ ## Overview
865
+
866
+ | Metric | Value |
867
+ |--------|-------|
868
+ | Total Files | ${mvp.stats.totalFiles} |
869
+ | Total Lines | ${mvp.stats.totalLines.toLocaleString()} |
870
+ | Components | ${mvp.components.length} |
871
+ | Services | ${mvp.services.length} |
872
+ | Hooks | ${mvp.hooks.length} |
873
+ | Utilities | ${mvp.utilities.length} |
874
+
875
+ ## Detected Patterns
876
+
877
+ ${mvp.patterns.map(p => `- ${p}`).join('\n') || '- None detected'}
878
+
879
+ ## Components
880
+
881
+ ${mvp.components.slice(0, 20).map(c => `- \`${c}\``).join('\n') || '- None found'}
882
+ ${mvp.components.length > 20 ? `\n*... and ${mvp.components.length - 20} more*` : ''}
883
+
884
+ ## Services
885
+
886
+ ${mvp.services.slice(0, 10).map(s => `- \`${s}\``).join('\n') || '- None found'}
887
+ ${mvp.services.length > 10 ? `\n*... and ${mvp.services.length - 10} more*` : ''}
888
+
889
+ ## Hooks
890
+
891
+ ${mvp.hooks.slice(0, 10).map(h => `- \`${h}\``).join('\n') || '- None found'}
892
+
893
+ ## Recommendations
894
+
895
+ Based on the detected patterns:
896
+
897
+ ${mvp.patterns.includes('client-components') ? '- Consider reviewing client components for server-side rendering opportunities' : ''}
898
+ ${mvp.patterns.includes('prisma-orm') ? '- Prisma ORM detected - ensure schema is optimized for production' : ''}
899
+ ${mvp.patterns.includes('clerk-auth') ? '- Clerk authentication detected - verify middleware configuration' : ''}
900
+ ${mvp.patterns.includes('stripe-payments') ? '- Stripe integration detected - ensure webhook handlers are secure' : ''}
901
+
902
+ ---
903
+
904
+ *Generated by Bootspring*
905
+ `;
906
+ }
907
+
908
+ /**
909
+ * Generate consolidated PRD document
910
+ */
911
+ function generateConsolidatedPRD(prds: PrdDocument[], _seedConfig: SeedConfig): string {
912
+ const allStories = prds.flatMap(p => p.userStories || []);
913
+ const allRequirements = {
914
+ mustHave: prds.flatMap(p => p.requirements?.mustHave || []),
915
+ shouldHave: prds.flatMap(p => p.requirements?.shouldHave || []),
916
+ niceToHave: prds.flatMap(p => p.requirements?.niceToHave || [])
917
+ };
918
+ const allFeatures = prds.flatMap(p => p.features || []);
919
+ const allQuestions = prds.flatMap(p => p.openQuestions || []);
920
+
921
+ return `# Consolidated Product Requirements
922
+
923
+ > Auto-generated from ${prds.length} PRD file(s)
924
+
925
+ ## User Stories
926
+
927
+ ${allStories.map((s, i) => `${i + 1}. As a **${s.role}**, I want **${s.want}**${s.benefit ? ` so that ${s.benefit}` : ''}`).join('\n') || '- No user stories extracted'}
928
+
929
+ ## Requirements
930
+
931
+ ### Must Have (P0)
932
+
933
+ ${allRequirements.mustHave.map(r => `- [ ] ${r}`).join('\n') || '- None specified'}
934
+
935
+ ### Should Have (P1)
936
+
937
+ ${allRequirements.shouldHave.map(r => `- [ ] ${r}`).join('\n') || '- None specified'}
938
+
939
+ ### Nice to Have (P2)
940
+
941
+ ${allRequirements.niceToHave.map(r => `- [ ] ${r}`).join('\n') || '- None specified'}
942
+
943
+ ## Features
944
+
945
+ ${allFeatures.map(f => `- ${f}`).join('\n') || '- No features extracted'}
946
+
947
+ ## Open Questions
948
+
949
+ ${allQuestions.map(q => `- ${q}`).join('\n') || '- No open questions found'}
950
+
951
+ ## Source Files
952
+
953
+ ${prds.map(p => `- \`${p.file}\``).join('\n')}
954
+
955
+ ---
956
+
957
+ *Generated by Bootspring*
958
+ `;
959
+ }
960
+
961
+ /**
962
+ * Generate business summary document
963
+ */
964
+ function generateBusinessSummary(businessDocs: BusinessDocument[], _seedConfig: SeedConfig): string {
965
+ return `# Business Context Summary
966
+
967
+ > Auto-generated from ${businessDocs.length} business document(s)
968
+
969
+ ## Documents Analyzed
970
+
971
+ | Document | Type | Word Count |
972
+ |----------|------|------------|
973
+ ${businessDocs.map(d => `| ${d.file} | ${d.type} | ${d.wordCount} |`).join('\n')}
974
+
975
+ ## Key Sections Found
976
+
977
+ ${businessDocs.map(d => `
978
+ ### ${d.file}
979
+
980
+ ${d.sections.filter(s => s.level <= 2).map(s => `${' '.repeat(s.level - 1)}- ${s.title}`).join('\n')}
981
+ `).join('\n')}
982
+
983
+ ## Summaries
984
+
985
+ ${businessDocs.map(d => `
986
+ ### ${d.file}
987
+
988
+ ${d.summary}
989
+ `).join('\n')}
990
+
991
+ ---
992
+
993
+ *Generated by Bootspring*
994
+ `;
995
+ }
996
+
997
+ /**
998
+ * Generate API summary document
999
+ */
1000
+ function generateAPISummary(apiSpecs: ApiSpec[]): string {
1001
+ return `# API Specification Summary
1002
+
1003
+ > Auto-generated from ${apiSpecs.length} API spec file(s)
1004
+
1005
+ ## Specifications
1006
+
1007
+ ${apiSpecs.map(spec => `
1008
+ ### ${spec.file}
1009
+
1010
+ **Type**: ${spec.type}
1011
+
1012
+ **Endpoints** (${spec.endpoints.length}):
1013
+
1014
+ ${spec.endpoints.slice(0, 20).map(e => `- \`${e.path}\``).join('\n') || '- None extracted'}
1015
+ ${spec.endpoints.length > 20 ? `\n*... and ${spec.endpoints.length - 20} more*` : ''}
1016
+ `).join('\n')}
1017
+
1018
+ ---
1019
+
1020
+ *Generated by Bootspring*
1021
+ `;
1022
+ }
1023
+
1024
+ /**
1025
+ * Generate data model document
1026
+ */
1027
+ function generateDataModel(dataFiles: DataFile[]): string {
1028
+ const sqlFiles = dataFiles.filter((f): f is SqlDataFile => f.type === 'sql');
1029
+ const jsonFiles = dataFiles.filter((f): f is JsonDataFile => f.type === 'json');
1030
+ const csvFiles = dataFiles.filter((f): f is CsvDataFile => f.type === 'csv');
1031
+
1032
+ return `# Data Model Summary
1033
+
1034
+ > Auto-generated from ${dataFiles.length} data file(s)
1035
+
1036
+ ## Database Tables
1037
+
1038
+ ${sqlFiles.length > 0 ? sqlFiles.map(f => `
1039
+ ### ${f.file}
1040
+
1041
+ Tables: ${f.tables.join(', ') || 'None found'}
1042
+ `).join('\n') : '- No SQL files provided'}
1043
+
1044
+ ## JSON Structures
1045
+
1046
+ ${jsonFiles.length > 0 ? jsonFiles.map(f => `
1047
+ ### ${f.file}
1048
+
1049
+ Type: ${f.structure.type}
1050
+ ${f.structure.fields ? `Fields: ${Object.keys(f.structure.fields).join(', ')}` : ''}
1051
+ `).join('\n') : '- No JSON files provided'}
1052
+
1053
+ ## CSV Files
1054
+
1055
+ ${csvFiles.length > 0 ? csvFiles.map(f => `
1056
+ ### ${f.file}
1057
+
1058
+ Headers: ${f.headers.join(', ')}
1059
+ `).join('\n') : '- No CSV files provided'}
1060
+
1061
+ ---
1062
+
1063
+ *Generated by Bootspring*
1064
+ `;
1065
+ }
1066
+
1067
+ /**
1068
+ * Generate documents from ingested data
1069
+ */
1070
+ export async function generateDocuments(
1071
+ ingested: IngestionResult,
1072
+ seedConfig: SeedConfig,
1073
+ projectRoot: string
1074
+ ): Promise<Record<string, string>> {
1075
+ const outputs: Record<string, string> = {};
1076
+ const generatedDir = path.join(projectRoot, '.bootspring', 'generated');
1077
+
1078
+ // Generate context summary
1079
+ outputs['context-summary.md'] = generateContextSummary(ingested, seedConfig);
1080
+ fs.writeFileSync(
1081
+ path.join(generatedDir, 'context-summary.md'),
1082
+ outputs['context-summary.md']
1083
+ );
1084
+
1085
+ // Generate MVP analysis if MVP exists
1086
+ if (ingested.mvp && ingested.mvp.files && ingested.mvp.files.length > 0) {
1087
+ outputs['mvp-analysis.md'] = generateMVPAnalysis(ingested.mvp);
1088
+ fs.mkdirSync(path.join(generatedDir, 'architecture'), { recursive: true });
1089
+ fs.writeFileSync(
1090
+ path.join(generatedDir, 'architecture', 'mvp-analysis.md'),
1091
+ outputs['mvp-analysis.md']
1092
+ );
1093
+ }
1094
+
1095
+ // Generate PRD consolidation if PRDs exist
1096
+ if (ingested.prd && ingested.prd.length > 0) {
1097
+ outputs['consolidated-prd.md'] = generateConsolidatedPRD(ingested.prd, seedConfig);
1098
+ fs.mkdirSync(path.join(generatedDir, 'prd'), { recursive: true });
1099
+ fs.writeFileSync(
1100
+ path.join(generatedDir, 'prd', 'consolidated-prd.md'),
1101
+ outputs['consolidated-prd.md']
1102
+ );
1103
+ }
1104
+
1105
+ // Generate business summary if business docs exist
1106
+ if (ingested.business && ingested.business.length > 0) {
1107
+ outputs['business-summary.md'] = generateBusinessSummary(ingested.business, seedConfig);
1108
+ fs.mkdirSync(path.join(generatedDir, 'business'), { recursive: true });
1109
+ fs.writeFileSync(
1110
+ path.join(generatedDir, 'business', 'business-summary.md'),
1111
+ outputs['business-summary.md']
1112
+ );
1113
+ }
1114
+
1115
+ // Generate API summary if API specs exist
1116
+ if (ingested.api && ingested.api.length > 0) {
1117
+ outputs['api-summary.md'] = generateAPISummary(ingested.api);
1118
+ fs.mkdirSync(path.join(generatedDir, 'architecture'), { recursive: true });
1119
+ fs.writeFileSync(
1120
+ path.join(generatedDir, 'architecture', 'api-summary.md'),
1121
+ outputs['api-summary.md']
1122
+ );
1123
+ }
1124
+
1125
+ // Generate data model if data files exist
1126
+ if (ingested.data && ingested.data.length > 0) {
1127
+ outputs['data-model.md'] = generateDataModel(ingested.data);
1128
+ fs.mkdirSync(path.join(generatedDir, 'architecture'), { recursive: true });
1129
+ fs.writeFileSync(
1130
+ path.join(generatedDir, 'architecture', 'data-model.md'),
1131
+ outputs['data-model.md']
1132
+ );
1133
+ }
1134
+
1135
+ return outputs;
1136
+ }
1137
+
1138
+ /**
1139
+ * Update context index
1140
+ */
1141
+ export async function updateContextIndex(projectRoot: string, ingested: IngestionResult): Promise<void> {
1142
+ const contextDir = path.join(projectRoot, '.bootspring', 'context');
1143
+ const indexPath = path.join(contextDir, 'context-index.json');
1144
+
1145
+ if (!fs.existsSync(contextDir)) {
1146
+ fs.mkdirSync(contextDir, { recursive: true });
1147
+ }
1148
+
1149
+ const index = {
1150
+ lastUpdated: new Date().toISOString(),
1151
+ inputsAnalyzed: {
1152
+ mvp: {
1153
+ fileCount: ingested.mvp?.files?.length || 0,
1154
+ patterns: ingested.mvp?.patterns || []
1155
+ },
1156
+ business: {
1157
+ fileCount: ingested.business?.length || 0,
1158
+ types: ingested.business?.map(d => d.type) || []
1159
+ },
1160
+ prd: {
1161
+ fileCount: ingested.prd?.length || 0,
1162
+ userStoryCount: ingested.prd?.reduce((sum, p) => sum + (p.userStories?.length || 0), 0) || 0
1163
+ },
1164
+ designs: {
1165
+ fileCount: ingested.designs?.count || 0
1166
+ },
1167
+ api: {
1168
+ fileCount: ingested.api?.length || 0,
1169
+ endpointCount: ingested.api?.reduce((sum, s) => sum + (s.endpoints?.length || 0), 0) || 0
1170
+ },
1171
+ data: {
1172
+ fileCount: ingested.data?.length || 0
1173
+ }
1174
+ },
1175
+ generatedFiles: [
1176
+ 'context-summary.md',
1177
+ ...((ingested.mvp?.files?.length ?? 0) > 0 ? ['architecture/mvp-analysis.md'] : []),
1178
+ ...((ingested.prd?.length ?? 0) > 0 ? ['prd/consolidated-prd.md'] : []),
1179
+ ...((ingested.business?.length ?? 0) > 0 ? ['business/business-summary.md'] : []),
1180
+ ...((ingested.api?.length ?? 0) > 0 ? ['architecture/api-summary.md'] : []),
1181
+ ...((ingested.data?.length ?? 0) > 0 ? ['architecture/data-model.md'] : [])
1182
+ ]
1183
+ };
1184
+
1185
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
1186
+ }