@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.
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +20 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- 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
|
+
}
|