Skip to main content
Glama

In Memoria

change-analyzer.ts12.4 kB
import { EventEmitter } from 'eventemitter3'; import { FileChange } from './file-watcher.js'; import { SemanticEngine } from '../engines/semantic-engine.js'; import { PatternEngine } from '../engines/pattern-engine.js'; import { SQLiteDatabase } from '../storage/sqlite-db.js'; export interface ChangeAnalysis { change: FileChange; impact: { scope: 'file' | 'module' | 'project'; confidence: number; affectedConcepts: string[]; suggestedActions: string[]; }; patterns: { detected: string[]; violations: string[]; recommendations: string[]; }; intelligence: { conceptsUpdated: number; patternsLearned: number; insights: string[]; }; timestamp: Date; } export interface AnalyzerOptions { enableRealTimeAnalysis: boolean; enablePatternLearning: boolean; batchSize: number; analysisDelay: number; } export class ChangeAnalyzer extends EventEmitter { private analysisQueue: FileChange[] = []; private analyzing = false; private batchTimer: NodeJS.Timeout | null = null; constructor( private semanticEngine: SemanticEngine, private patternEngine: PatternEngine, private database: SQLiteDatabase, private options: AnalyzerOptions = { enableRealTimeAnalysis: true, enablePatternLearning: true, batchSize: 5, analysisDelay: 1000 } ) { super(); } async analyzeChange(change: FileChange): Promise<ChangeAnalysis> { if (!this.options.enableRealTimeAnalysis) { return this.createMinimalAnalysis(change); } // Add to queue for batch processing this.analysisQueue.push(change); // Schedule batch processing this.scheduleBatchAnalysis(); // For immediate response, perform lightweight analysis return await this.performLightweightAnalysis(change); } async analyzeBatch(changes: FileChange[]): Promise<ChangeAnalysis[]> { const analyses: ChangeAnalysis[] = []; for (const change of changes) { const analysis = await this.performFullAnalysis(change); analyses.push(analysis); } // Cross-file impact analysis if (changes.length > 1) { await this.performCrossFileAnalysis(analyses); } return analyses; } private scheduleBatchAnalysis(): void { if (this.batchTimer) { clearTimeout(this.batchTimer); } this.batchTimer = setTimeout(async () => { if (this.analysisQueue.length > 0 && !this.analyzing) { await this.processBatch(); } }, this.options.analysisDelay); } private async processBatch(): Promise<void> { if (this.analyzing || this.analysisQueue.length === 0) { return; } this.analyzing = true; try { const batch = this.analysisQueue.splice(0, this.options.batchSize); const analyses = await this.analyzeBatch(batch); for (const analysis of analyses) { this.emit('analysis:complete', analysis); // Learn from the changes if enabled if (this.options.enablePatternLearning) { await this.learnFromChange(analysis); } } this.emit('batch:complete', { count: analyses.length, insights: analyses.flatMap(a => a.intelligence.insights) }); } catch (error) { this.emit('analysis:error', { error, batch: this.analysisQueue.length }); } finally { this.analyzing = false; // Process remaining queue if any if (this.analysisQueue.length > 0) { this.scheduleBatchAnalysis(); } } } private async performLightweightAnalysis(change: FileChange): Promise<ChangeAnalysis> { const analysis: ChangeAnalysis = { change, impact: { scope: 'file', confidence: 0.5, affectedConcepts: [], suggestedActions: [] }, patterns: { detected: [], violations: [], recommendations: [] }, intelligence: { conceptsUpdated: 0, patternsLearned: 0, insights: [] }, timestamp: new Date() }; // Quick impact assessment based on file type and size if (change.language) { analysis.impact.scope = this.estimateScope(change); analysis.impact.suggestedActions = this.getSuggestedActions(change); } return analysis; } private async performFullAnalysis(change: FileChange): Promise<ChangeAnalysis> { const analysis = await this.performLightweightAnalysis(change); try { // Semantic analysis if (change.content && change.type !== 'unlink') { const concepts = await this.semanticEngine.analyzeFileContent( change.path, change.content ); analysis.impact.affectedConcepts = concepts.map(c => c.name); analysis.intelligence.conceptsUpdated = concepts.length; } // Pattern analysis const patterns = await this.patternEngine.analyzeFileChange(change); analysis.patterns.detected = patterns.detected; analysis.patterns.violations = patterns.violations; analysis.patterns.recommendations = patterns.recommendations; analysis.intelligence.patternsLearned = patterns.learned?.length || 0; // Impact analysis analysis.impact = await this.calculateImpact(change, analysis); // Generate insights analysis.intelligence.insights = await this.generateInsights(analysis); } catch (error) { analysis.intelligence.insights.push(`Analysis error: ${error}`); } return analysis; } private async performCrossFileAnalysis(analyses: ChangeAnalysis[]): Promise<void> { // Analyze relationships between changed files const concepts = analyses.flatMap(a => a.impact.affectedConcepts); const uniqueConcepts = [...new Set(concepts)]; // Check for architectural impacts if (uniqueConcepts.length > 3) { const architecturalImpact = await this.assessArchitecturalImpact(analyses); for (const analysis of analyses) { analysis.impact.scope = 'project'; analysis.impact.confidence = Math.max( analysis.impact.confidence, architecturalImpact.confidence ); analysis.intelligence.insights.push( ...architecturalImpact.insights ); } } } private estimateScope(change: FileChange): 'file' | 'module' | 'project' { // Configuration files often have project-wide impact const projectFiles = ['package.json', 'tsconfig.json', 'Cargo.toml', 'go.mod']; if (projectFiles.some(file => change.path.endsWith(file))) { return 'project'; } // Test files usually have module scope if (change.path.includes('test') || change.path.includes('spec')) { return 'module'; } // Index files often have module scope if (change.path.endsWith('index.ts') || change.path.endsWith('mod.rs')) { return 'module'; } return 'file'; } private getSuggestedActions(change: FileChange): string[] { const actions: string[] = []; switch (change.type) { case 'add': actions.push('Update documentation'); actions.push('Add tests if applicable'); break; case 'change': actions.push('Review related tests'); actions.push('Check for breaking changes'); break; case 'unlink': actions.push('Remove related tests'); actions.push('Update imports/dependencies'); break; } if (change.language === 'typescript' || change.language === 'javascript') { actions.push('Run type checking'); } return actions; } private async calculateImpact( change: FileChange, analysis: ChangeAnalysis ): Promise<ChangeAnalysis['impact']> { let confidence = 0.5; let scope: 'file' | 'module' | 'project' = 'file'; // Increase confidence based on concept count if (analysis.impact.affectedConcepts.length > 0) { confidence += Math.min(0.3, analysis.impact.affectedConcepts.length * 0.1); } // Adjust scope based on patterns if (analysis.patterns.violations.length > 0) { scope = 'module'; confidence += 0.2; } // Check for dependencies on this file const dependents = await this.findDependentFiles(change.path); if (dependents.length > 5) { scope = 'project'; confidence += 0.3; } else if (dependents.length > 1) { scope = 'module'; confidence += 0.1; } return { scope, confidence: Math.min(1.0, confidence), affectedConcepts: analysis.impact.affectedConcepts, suggestedActions: analysis.impact.suggestedActions }; } private async generateInsights(analysis: ChangeAnalysis): Promise<string[]> { const insights: string[] = []; // Pattern-based insights if (analysis.patterns.detected.length > 0) { insights.push(`Detected ${analysis.patterns.detected.length} patterns in change`); } if (analysis.patterns.violations.length > 0) { insights.push(`Found ${analysis.patterns.violations.length} pattern violations`); } // Impact insights if (analysis.impact.scope === 'project') { insights.push('Change has project-wide impact - consider comprehensive testing'); } // Learning insights if (analysis.intelligence.conceptsUpdated > 0) { insights.push(`Updated understanding of ${analysis.intelligence.conceptsUpdated} concepts`); } return insights; } private async assessArchitecturalImpact(analyses: ChangeAnalysis[]): Promise<{ confidence: number; insights: string[]; }> { const allConcepts = analyses.flatMap(a => a.impact.affectedConcepts); const uniqueConcepts = new Set(allConcepts); return { confidence: Math.min(1.0, uniqueConcepts.size * 0.1), insights: [ `Architectural change detected affecting ${uniqueConcepts.size} concepts`, 'Consider updating system documentation', 'Review integration tests' ] }; } private async findDependentFiles(filePath: string): Promise<string[]> { try { // Query the database for files that import/depend on the given file const concepts = this.database.getSemanticConcepts(filePath); const dependentFiles: string[] = []; // Find files that reference concepts from this file for (const concept of concepts) { // Get all concepts to find related ones const allConcepts = this.database.getSemanticConcepts(); const relatedConcepts = allConcepts.filter(c => c.conceptName === concept.conceptName && c.filePath !== filePath ); dependentFiles.push(...relatedConcepts.map(c => c.filePath)); } // Remove duplicates and return return [...new Set(dependentFiles)]; } catch (error: unknown) { console.warn('Could not find dependent files:', error instanceof Error ? error.message : String(error)); return []; } } private async learnFromChange(analysis: ChangeAnalysis): Promise<void> { // Learn new patterns from the change if (analysis.patterns.detected.length > 0) { await this.patternEngine.learnFromAnalysis(analysis); } // Update semantic understanding if (analysis.intelligence.conceptsUpdated > 0) { await this.semanticEngine.updateFromAnalysis(analysis); } } private createMinimalAnalysis(change: FileChange): ChangeAnalysis { return { change, impact: { scope: 'file', confidence: 0.1, affectedConcepts: [], suggestedActions: [] }, patterns: { detected: [], violations: [], recommendations: [] }, intelligence: { conceptsUpdated: 0, patternsLearned: 0, insights: ['Real-time analysis disabled'] }, timestamp: new Date() }; } // Public control methods enableRealTimeAnalysis(): void { this.options.enableRealTimeAnalysis = true; } disableRealTimeAnalysis(): void { this.options.enableRealTimeAnalysis = false; } getQueueSize(): number { return this.analysisQueue.length; } isAnalyzing(): boolean { return this.analyzing; } clearQueue(): void { this.analysisQueue = []; if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pi22by7/In-Memoria'

If you have feedback or need assistance with the MCP directory API, please join our Discord server