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};