Context System Architecture

How Bootspring generates and manages project context.

Overview#

The context system generates CLAUDE.md files that help AI assistants understand your project. It analyzes your codebase and produces structured documentation.

Context Generation Pipeline#

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Discovery │───▶│ Analysis │───▶│ Aggregation │───▶│ Rendering │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ Find files Run analyzers Combine data Apply template

Discovery Phase#

File Scanner#

1// core/context/discovery/scanner.ts 2 3export class FileScanner { 4 private includePatterns: string[]; 5 private excludePatterns: string[]; 6 7 async scan(projectPath: string): Promise<FileInfo[]> { 8 const files: FileInfo[] = []; 9 10 // Use fast-glob for efficient scanning 11 const matches = await glob(this.includePatterns, { 12 cwd: projectPath, 13 ignore: this.excludePatterns, 14 stats: true, 15 }); 16 17 for (const match of matches) { 18 files.push({ 19 path: match.path, 20 size: match.stats.size, 21 modified: match.stats.mtime, 22 type: this.classifyFile(match.path), 23 }); 24 } 25 26 return files; 27 } 28 29 private classifyFile(path: string): FileType { 30 const ext = extname(path); 31 const name = basename(path); 32 33 // Config files 34 if (CONFIG_FILES.includes(name)) return 'config'; 35 36 // By extension 37 if (CODE_EXTENSIONS.includes(ext)) return 'code'; 38 if (DOC_EXTENSIONS.includes(ext)) return 'documentation'; 39 if (TEST_PATTERNS.some(p => path.includes(p))) return 'test'; 40 41 return 'other'; 42 } 43}

Default Patterns#

1const defaultInclude = [ 2 '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', 3 '**/*.py', '**/*.go', '**/*.rs', 4 'package.json', 'tsconfig.json', 5 'prisma/schema.prisma', 6 '*.md', 'docs/**/*.md', 7]; 8 9const defaultExclude = [ 10 '**/node_modules/**', 11 '**/.git/**', 12 '**/dist/**', 13 '**/build/**', 14 '**/*.min.js', 15 '**/coverage/**', 16];

Analysis Phase#

Analyzer Interface#

1interface Analyzer { 2 name: string; 3 priority: number; 4 analyze(project: Project): Promise<AnalysisResult>; 5} 6 7interface AnalysisResult { 8 type: string; 9 data: Record<string, unknown>; 10 confidence: number; 11}

Built-in Analyzers#

Package Analyzer#

1// core/context/analyzers/package.ts 2 3export class PackageAnalyzer implements Analyzer { 4 name = 'package'; 5 priority = 100; 6 7 async analyze(project: Project): Promise<AnalysisResult> { 8 const packagePath = join(project.path, 'package.json'); 9 10 if (!await exists(packagePath)) { 11 return { type: 'package', data: {}, confidence: 0 }; 12 } 13 14 const pkg = await readJson(packagePath); 15 16 return { 17 type: 'package', 18 data: { 19 name: pkg.name, 20 version: pkg.version, 21 description: pkg.description, 22 dependencies: Object.keys(pkg.dependencies || {}), 23 devDependencies: Object.keys(pkg.devDependencies || {}), 24 scripts: Object.keys(pkg.scripts || {}), 25 }, 26 confidence: 1.0, 27 }; 28 } 29}

Framework Analyzer#

1// core/context/analyzers/framework.ts 2 3export class FrameworkAnalyzer implements Analyzer { 4 name = 'framework'; 5 priority = 90; 6 7 private detectors: FrameworkDetector[] = [ 8 new NextJsDetector(), 9 new ReactDetector(), 10 new VueDetector(), 11 new ExpressDetector(), 12 new FastApiDetector(), 13 ]; 14 15 async analyze(project: Project): Promise<AnalysisResult> { 16 for (const detector of this.detectors) { 17 const result = await detector.detect(project); 18 if (result.detected) { 19 return { 20 type: 'framework', 21 data: { 22 name: result.framework, 23 version: result.version, 24 features: result.features, 25 }, 26 confidence: result.confidence, 27 }; 28 } 29 } 30 31 return { type: 'framework', data: {}, confidence: 0 }; 32 } 33}

Structure Analyzer#

1// core/context/analyzers/structure.ts 2 3export class StructureAnalyzer implements Analyzer { 4 name = 'structure'; 5 priority = 80; 6 7 async analyze(project: Project): Promise<AnalysisResult> { 8 const structure: DirectoryInfo[] = []; 9 10 // Analyze top-level directories 11 const entries = await readdir(project.path, { withFileTypes: true }); 12 13 for (const entry of entries) { 14 if (entry.isDirectory() && !this.isIgnored(entry.name)) { 15 const info = await this.analyzeDirectory( 16 join(project.path, entry.name) 17 ); 18 structure.push(info); 19 } 20 } 21 22 return { 23 type: 'structure', 24 data: { 25 directories: structure, 26 pattern: this.detectPattern(structure), 27 }, 28 confidence: 0.9, 29 }; 30 } 31 32 private detectPattern(dirs: DirectoryInfo[]): string { 33 const names = dirs.map(d => d.name); 34 35 if (names.includes('app') && names.includes('components')) { 36 return 'nextjs-app-router'; 37 } 38 if (names.includes('pages') && names.includes('components')) { 39 return 'nextjs-pages-router'; 40 } 41 if (names.includes('src') && names.includes('tests')) { 42 return 'standard-src'; 43 } 44 45 return 'unknown'; 46 } 47}

Database Analyzer#

1// core/context/analyzers/database.ts 2 3export class DatabaseAnalyzer implements Analyzer { 4 name = 'database'; 5 priority = 70; 6 7 async analyze(project: Project): Promise<AnalysisResult> { 8 // Check for Prisma 9 const prismaPath = join(project.path, 'prisma/schema.prisma'); 10 if (await exists(prismaPath)) { 11 return this.analyzePrisma(prismaPath); 12 } 13 14 // Check for Drizzle 15 const drizzlePath = await this.findDrizzleConfig(project.path); 16 if (drizzlePath) { 17 return this.analyzeDrizzle(drizzlePath); 18 } 19 20 return { type: 'database', data: {}, confidence: 0 }; 21 } 22 23 private async analyzePrisma(schemaPath: string): Promise<AnalysisResult> { 24 const schema = await readFile(schemaPath, 'utf-8'); 25 const parsed = parsePrismaSchema(schema); 26 27 return { 28 type: 'database', 29 data: { 30 orm: 'prisma', 31 provider: parsed.datasource?.provider, 32 models: parsed.models.map(m => ({ 33 name: m.name, 34 fields: m.fields.length, 35 relations: m.fields.filter(f => f.isRelation).length, 36 })), 37 }, 38 confidence: 1.0, 39 }; 40 } 41}

Aggregation Phase#

Result Aggregator#

1// core/context/aggregator.ts 2 3export class ResultAggregator { 4 aggregate(results: AnalysisResult[]): AggregatedContext { 5 // Sort by confidence 6 const sorted = results.sort((a, b) => b.confidence - a.confidence); 7 8 // Build aggregated context 9 const context: AggregatedContext = { 10 project: this.extractProjectInfo(sorted), 11 stack: this.extractStack(sorted), 12 structure: this.extractStructure(sorted), 13 database: this.extractDatabase(sorted), 14 api: this.extractApi(sorted), 15 components: this.extractComponents(sorted), 16 tests: this.extractTests(sorted), 17 }; 18 19 return context; 20 } 21 22 private extractStack(results: AnalysisResult[]): StackInfo { 23 const framework = results.find(r => r.type === 'framework'); 24 const pkg = results.find(r => r.type === 'package'); 25 26 return { 27 language: this.detectLanguage(results), 28 framework: framework?.data.name, 29 runtime: this.detectRuntime(results), 30 dependencies: pkg?.data.dependencies || [], 31 }; 32 } 33}

Rendering Phase#

Template System#

1// core/context/templates/renderer.ts 2 3export class TemplateRenderer { 4 private templates: Map<string, Template> = new Map(); 5 6 async render(context: AggregatedContext): Promise<string> { 7 const template = this.selectTemplate(context); 8 return template.render(context); 9 } 10 11 private selectTemplate(context: AggregatedContext): Template { 12 // Select based on project type 13 if (context.stack.framework === 'nextjs') { 14 return this.templates.get('nextjs')!; 15 } 16 if (context.stack.framework === 'react') { 17 return this.templates.get('react')!; 18 } 19 return this.templates.get('default')!; 20 } 21}

Template Structure#

1# {{project.name}} 2 3{{project.description}} 4 5## Tech Stack 6 7| Category | Technology | 8|----------|------------| 9{{#each stack}} 10| {{category}} | {{technology}} | 11{{/each}} 12 13## Project Structure 14

{{structure.tree}}

## Key Files {{#each keyFiles}} ### {{path}} {{description}} {{/each}} ## Database Schema {{#if database.models}} {{#each database.models}} ### {{name}} {{#each fields}} - {{name}}: {{type}} {{/each}} {{/each}} {{/if}} ## API Routes {{#each api.routes}} - `{{method}}` {{path}} - {{description}} {{/each}} ## Development ```bash {{commands.dev}}
## Incremental Updates ### Change Detection ```typescript // core/context/incremental.ts export class IncrementalUpdater { private hashStore: HashStore; async shouldRegenerate(project: Project): Promise<boolean> { const currentHashes = await this.computeHashes(project); const storedHashes = await this.hashStore.get(project.path); if (!storedHashes) return true; // Check for significant changes const changed = this.findChanges(storedHashes, currentHashes); return changed.some(c => this.isSignificant(c)); } private isSignificant(change: FileChange): boolean { // Config changes are always significant if (CONFIG_FILES.includes(change.path)) return true; // Schema changes are significant if (change.path.includes('schema')) return true; // Many file changes are significant return change.type === 'added' || change.type === 'deleted'; } }

Partial Regeneration#

1async function regenerateSection( 2 project: Project, 3 section: ContextSection 4): Promise<void> { 5 // Run only relevant analyzers 6 const analyzers = getAnalyzersForSection(section); 7 const results = await runAnalyzers(analyzers, project); 8 9 // Update only that section of context 10 const context = await loadExistingContext(project); 11 context[section] = aggregate(results); 12 13 // Re-render 14 await saveContext(project, render(context)); 15}

Caching#

Analysis Cache#

1// core/context/cache.ts 2 3export class AnalysisCache { 4 private cache: Map<string, CachedAnalysis> = new Map(); 5 private ttl = 60000; // 1 minute 6 7 async get(key: string): Promise<AnalysisResult | null> { 8 const cached = this.cache.get(key); 9 10 if (!cached) return null; 11 if (Date.now() > cached.expires) { 12 this.cache.delete(key); 13 return null; 14 } 15 16 return cached.result; 17 } 18 19 set(key: string, result: AnalysisResult): void { 20 this.cache.set(key, { 21 result, 22 expires: Date.now() + this.ttl, 23 }); 24 } 25}

Configuration#

Context Config#

1interface ContextConfig { 2 // Output path 3 path: string; // default: 'CLAUDE.md' 4 5 // File patterns 6 include: string[]; 7 exclude: string[]; 8 9 // Sections to include 10 sections: { 11 stack: boolean; 12 structure: boolean; 13 database: boolean; 14 api: boolean; 15 components: boolean; 16 tests: boolean; 17 }; 18 19 // Custom additions 20 customSections: CustomSection[]; 21 22 // Auto-regeneration 23 autoRegenerate: boolean; 24 watchPatterns: string[]; 25}

Performance#

Parallel Analysis#

1async function runAnalyzers( 2 analyzers: Analyzer[], 3 project: Project 4): Promise<AnalysisResult[]> { 5 // Group by dependency 6 const groups = groupByDependency(analyzers); 7 8 const results: AnalysisResult[] = []; 9 10 for (const group of groups) { 11 // Run group in parallel 12 const groupResults = await Promise.all( 13 group.map(a => a.analyze(project)) 14 ); 15 results.push(...groupResults); 16 } 17 18 return results; 19}

Large Project Handling#

For large codebases:

1const largeProjectConfig = { 2 // Sample instead of scanning all 3 sampling: { 4 enabled: true, 5 maxFiles: 1000, 6 strategy: 'representative', 7 }, 8 9 // Limit analysis depth 10 maxDepth: 5, 11 12 // Timeout per analyzer 13 analyzerTimeout: 10000, 14};