UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

626 lines 26.2 kB
import * as fs from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; import { TodoSyncStateSchema, KnowledgeGraphSnapshotSchema, } from '../types/knowledge-graph-schemas.js'; import { loadConfig } from './config.js'; // Using new MemoryHealthScoring instead of deprecated ProjectHealthScoring import { MemoryHealthScoring } from './memory-health-scoring.js'; export class KnowledgeGraphManager { cacheDir; snapshotsFile; syncStateFile; memoryScoring; constructor() { const config = loadConfig(); this.cacheDir = path.join(config.projectPath, '.mcp-adr-cache'); this.snapshotsFile = path.join(this.cacheDir, 'knowledge-graph-snapshots.json'); this.syncStateFile = path.join(this.cacheDir, 'todo-sync-state.json'); this.memoryScoring = new MemoryHealthScoring(); } async ensureCacheDirectory() { try { await fs.access(this.cacheDir); } catch { await fs.mkdir(this.cacheDir, { recursive: true }); } } async loadKnowledgeGraph() { await this.ensureCacheDirectory(); try { const data = await fs.readFile(this.snapshotsFile, 'utf-8'); const parsed = JSON.parse(data); return KnowledgeGraphSnapshotSchema.parse(parsed); } catch { const defaultSyncState = await this.getDefaultSyncState(); const defaultSnapshot = { version: '1.0.0', timestamp: new Date().toISOString(), intents: [], todoSyncState: defaultSyncState, analytics: { totalIntents: 0, completedIntents: 0, activeIntents: 0, averageGoalCompletion: 0, mostUsedTools: [], successfulPatterns: [], }, scoreHistory: [], }; await this.saveKnowledgeGraph(defaultSnapshot); // Also create the separate sync state file await fs.writeFile(this.syncStateFile, JSON.stringify(defaultSyncState, null, 2)); return defaultSnapshot; } } async saveKnowledgeGraph(snapshot) { await this.ensureCacheDirectory(); await fs.writeFile(this.snapshotsFile, JSON.stringify(snapshot, null, 2)); } async createIntent(humanRequest, parsedGoals, priority = 'medium') { const intentId = crypto.randomUUID(); const timestamp = new Date().toISOString(); // Get current score as baseline // Calculate memory-based health score const kgData = await this.loadKnowledgeGraph(); const currentScore = await this.calculateMemoryScore(kgData); const intent = { intentId, humanRequest, parsedGoals, priority, timestamp, toolChain: [], currentStatus: 'planning', todoMdSnapshot: '', scoreTracking: { initialScore: currentScore.overall, currentScore: currentScore.overall, componentScores: { taskCompletion: currentScore.memoryQuality, deploymentReadiness: currentScore.retrievalPerformance, architectureCompliance: currentScore.entityCoherence, securityPosture: currentScore.contextUtilization, codeQuality: currentScore.decisionAlignment, }, lastScoreUpdate: timestamp, }, }; const kgUpdate = await this.loadKnowledgeGraph(); kgUpdate.intents.push(intent); kgUpdate.analytics.totalIntents = kgUpdate.intents.length; kgUpdate.analytics.activeIntents = kgUpdate.intents.filter(i => i.currentStatus !== 'completed').length; // Add score history entry if (!kgUpdate.scoreHistory) kgUpdate.scoreHistory = []; kgUpdate.scoreHistory.push({ timestamp, intentId, overallScore: currentScore.overall, componentScores: { taskCompletion: currentScore.memoryQuality, deploymentReadiness: currentScore.retrievalPerformance, architectureCompliance: currentScore.entityCoherence, securityPosture: currentScore.contextUtilization, codeQuality: currentScore.decisionAlignment, }, triggerEvent: `Intent created: ${humanRequest.substring(0, 100)}...`, confidence: currentScore.confidence, }); await this.saveKnowledgeGraph(kgUpdate); return intentId; } async addToolExecution(intentId, toolName, parameters, result, success, todoTasksCreated = [], todoTasksModified = [], error) { const kg = await this.loadKnowledgeGraph(); const intent = kg.intents.find(i => i.intentId === intentId); if (!intent) { throw new Error(`Intent ${intentId} not found`); } // Capture before score // ProjectHealthScoring removed - get current memory score const beforeScore = await this.memoryScoring.calculateMemoryHealth([], {}, {}); const execution = { toolName, parameters, result, todoTasksCreated, todoTasksModified, executionTime: new Date().toISOString(), success, error, }; intent.toolChain.push(execution); intent.currentStatus = success ? 'executing' : 'failed'; // Update scores after tool execution and capture impact await this.updateScoreTracking(intentId, toolName, beforeScore, execution); this.updateAnalytics(kg); await this.saveKnowledgeGraph(kg); } async addMemoryExecution(toolName, action, entityType, success, details) { try { const kg = await this.loadKnowledgeGraph(); // Create memory execution record const memoryExecution = { toolName, action, entityType, success, details: details || {}, timestamp: new Date().toISOString(), }; // Initialize memory operations array if it doesn't exist if (!kg.memoryOperations) { kg.memoryOperations = []; } // Add memory execution to knowledge graph kg.memoryOperations.push(memoryExecution); // Limit memory operations history to last 1000 entries if (kg.memoryOperations.length > 1000) { kg.memoryOperations = kg.memoryOperations.slice(-1000); } // Update analytics to include memory operation metrics this.updateMemoryAnalytics(kg); await this.saveKnowledgeGraph(kg); } catch (error) { console.error('[WARN] Failed to track memory execution:', error); // Don't throw - memory tracking shouldn't break tool execution } } updateMemoryAnalytics(kg) { if (!kg.memoryOperations || kg.memoryOperations.length === 0) { return; } // Calculate memory operation statistics const totalOps = kg.memoryOperations.length; const successfulOps = kg.memoryOperations.filter((op) => op.success).length; const successRate = totalOps > 0 ? successfulOps / totalOps : 0; // Group by entity type const byEntityType = {}; const byAction = {}; const byTool = {}; for (const op of kg.memoryOperations) { byEntityType[op.entityType] = (byEntityType[op.entityType] || 0) + 1; byAction[op.action] = (byAction[op.action] || 0) + 1; byTool[op.toolName] = (byTool[op.toolName] || 0) + 1; } // Add memory analytics to knowledge graph if (!kg.analytics) { kg.analytics = { totalIntents: 0, completedIntents: 0, activeIntents: 0, averageGoalCompletion: 0, mostUsedTools: [], successfulPatterns: [], }; } kg.analytics.memoryOperations = { totalOperations: totalOps, successRate, byEntityType, byAction, byTool, lastMemoryOperation: kg.memoryOperations[kg.memoryOperations.length - 1]?.timestamp, }; } async updateIntentStatus(intentId, status) { const kg = await this.loadKnowledgeGraph(); const intent = kg.intents.find(i => i.intentId === intentId); if (!intent) { throw new Error(`Intent ${intentId} not found`); } intent.currentStatus = status; this.updateAnalytics(kg); await this.saveKnowledgeGraph(kg); } async updateTodoSnapshot(intentId, todoContent) { const kg = await this.loadKnowledgeGraph(); const intent = kg.intents.find(i => i.intentId === intentId); if (!intent) { throw new Error(`Intent ${intentId} not found`); } intent.todoMdSnapshot = todoContent; await this.saveKnowledgeGraph(kg); } async getSyncState() { try { const data = await fs.readFile(this.syncStateFile, 'utf-8'); const parsed = JSON.parse(data); return TodoSyncStateSchema.parse(parsed); } catch { return this.getDefaultSyncState(); } } async updateSyncState(updates) { const current = await this.getSyncState(); const updated = { ...current, ...updates }; await fs.writeFile(this.syncStateFile, JSON.stringify(updated, null, 2)); const kg = await this.loadKnowledgeGraph(); kg.todoSyncState = updated; await this.saveKnowledgeGraph(kg); } async getIntentById(intentId) { const kg = await this.loadKnowledgeGraph(); return kg.intents.find(i => i.intentId === intentId) || null; } async getActiveIntents() { const kg = await this.loadKnowledgeGraph(); return kg.intents.filter(i => i.currentStatus !== 'completed' && i.currentStatus !== 'failed'); } async getIntentsByStatus(status) { const kg = await this.loadKnowledgeGraph(); return kg.intents.filter(i => i.currentStatus === status); } async getDefaultSyncState() { return { lastSyncTimestamp: new Date().toISOString(), todoMdHash: '', knowledgeGraphHash: '', syncStatus: 'synced', lastModifiedBy: 'tool', version: 1, }; } updateAnalytics(kg) { const intents = kg.intents; kg.analytics.totalIntents = intents.length; kg.analytics.completedIntents = intents.filter(i => i.currentStatus === 'completed').length; kg.analytics.activeIntents = intents.filter(i => i.currentStatus !== 'completed' && i.currentStatus !== 'failed').length; if (kg.analytics.totalIntents > 0) { kg.analytics.averageGoalCompletion = kg.analytics.completedIntents / kg.analytics.totalIntents; } const toolUsage = new Map(); intents.forEach(intent => { intent.toolChain.forEach(execution => { toolUsage.set(execution.toolName, (toolUsage.get(execution.toolName) || 0) + 1); }); }); kg.analytics.mostUsedTools = Array.from(toolUsage.entries()) .map(([toolName, usageCount]) => ({ toolName, usageCount })) .sort((a, b) => b.usageCount - a.usageCount) .slice(0, 10); } async calculateTodoMdHash(todoPath) { try { const content = await fs.readFile(todoPath, 'utf-8'); return crypto.createHash('sha256').update(content).digest('hex'); } catch { return ''; } } async detectTodoChanges(todoPath) { const syncState = await this.getSyncState(); const currentHash = await this.calculateTodoMdHash(todoPath); return { hasChanges: currentHash !== syncState.todoMdHash, currentHash, lastHash: syncState.todoMdHash, }; } /** * Update score tracking for an intent after tool execution */ async updateScoreTracking(intentId, toolName, beforeScore, execution) { const kg = await this.loadKnowledgeGraph(); const intent = kg.intents.find(i => i.intentId === intentId); if (!intent) return; // Get current score after tool execution const afterScore = await this.calculateMemoryScore(kg); // Calculate score impact - map memory scores to legacy component names const scoreImpact = { beforeScore: beforeScore.overall, afterScore: afterScore.overall, componentImpacts: { taskCompletion: afterScore.memoryQuality - beforeScore.memoryQuality, deploymentReadiness: afterScore.retrievalPerformance - beforeScore.retrievalPerformance, architectureCompliance: afterScore.entityCoherence - beforeScore.entityCoherence, securityPosture: afterScore.contextUtilization - beforeScore.contextUtilization, codeQuality: afterScore.decisionAlignment - beforeScore.decisionAlignment, }, scoreConfidence: afterScore.confidence, }; // Update execution with score impact execution.scoreImpact = scoreImpact; // Update intent score tracking - map memory scores to legacy component names if (intent.scoreTracking) { intent.scoreTracking.currentScore = afterScore.overall; intent.scoreTracking.componentScores = { taskCompletion: afterScore.memoryQuality, deploymentReadiness: afterScore.retrievalPerformance, architectureCompliance: afterScore.entityCoherence, securityPosture: afterScore.contextUtilization, codeQuality: afterScore.decisionAlignment, }; intent.scoreTracking.lastScoreUpdate = new Date().toISOString(); // Calculate progress if we have initial score if (intent.scoreTracking.initialScore !== undefined) { const initialScore = intent.scoreTracking.initialScore; const targetScore = intent.scoreTracking.targetScore || 100; const currentScore = afterScore.overall; // Calculate progress as percentage of improvement toward target const totalPossibleImprovement = targetScore - initialScore; const actualImprovement = currentScore - initialScore; intent.scoreTracking.scoreProgress = totalPossibleImprovement > 0 ? Math.min(100, (actualImprovement / totalPossibleImprovement) * 100) : 0; } } // Add to score history if (!kg.scoreHistory) kg.scoreHistory = []; kg.scoreHistory.push({ timestamp: new Date().toISOString(), intentId, overallScore: afterScore.overall, componentScores: { taskCompletion: afterScore.memoryQuality, deploymentReadiness: afterScore.retrievalPerformance, architectureCompliance: afterScore.entityCoherence, securityPosture: afterScore.contextUtilization, codeQuality: afterScore.decisionAlignment, }, triggerEvent: `Tool executed: ${toolName}`, confidence: afterScore.confidence, }); // Keep only last 100 score history entries if (kg.scoreHistory.length > 100) { kg.scoreHistory = kg.scoreHistory.slice(-100); } } /** * Get score trends for an intent */ async getIntentScoreTrends(intentId) { const kg = await this.loadKnowledgeGraph(); const intent = kg.intents.find(i => i.intentId === intentId); if (!intent?.scoreTracking) { throw new Error(`Intent ${intentId} not found or has no score tracking`); } const scoreHistory = kg.scoreHistory?.filter(h => h.intentId === intentId) || []; return { initialScore: intent.scoreTracking.initialScore || 0, currentScore: intent.scoreTracking.currentScore || 0, progress: intent.scoreTracking.scoreProgress || 0, componentTrends: intent.scoreTracking.componentScores || {}, scoreHistory: scoreHistory.map(h => ({ timestamp: h.timestamp, score: h.overallScore, triggerEvent: h.triggerEvent, })), }; } /** * Get overall project score trends */ async getProjectScoreTrends() { const kg = await this.loadKnowledgeGraph(); // Calculate memory-based health score const kgData = await this.loadKnowledgeGraph(); const currentScore = await this.calculateMemoryScore(kgData); const scoreHistory = kg.scoreHistory || []; const intentImpacts = kg.intents .filter(i => i.scoreTracking?.initialScore !== undefined && i.scoreTracking?.currentScore !== undefined) .map(i => ({ intentId: i.intentId, humanRequest: i.humanRequest, scoreImprovement: i.scoreTracking.currentScore - i.scoreTracking.initialScore, })) .sort((a, b) => b.scoreImprovement - a.scoreImprovement) .slice(0, 5); const averageImprovement = intentImpacts.length > 0 ? intentImpacts.reduce((sum, i) => sum + i.scoreImprovement, 0) / intentImpacts.length : 0; return { currentScore: currentScore.overall, scoreHistory: scoreHistory.map(h => ({ timestamp: h.timestamp, score: h.overallScore, triggerEvent: h.triggerEvent, ...(h.intentId && { intentId: h.intentId }), })), averageImprovement, topImpactingIntents: intentImpacts, }; } /** * Record project structure analysis to knowledge graph */ async recordProjectStructure(structureSnapshot) { const intentId = `project-structure-${Date.now()}`; const intent = { intentId: intentId, humanRequest: `Analyze project ecosystem with ${structureSnapshot.analysisDepth} depth`, parsedGoals: [ `Analyze project structure at ${structureSnapshot.projectPath}`, `Record directory structure and technology patterns`, `Track architectural decisions and dependencies`, ], priority: 'medium', timestamp: structureSnapshot.timestamp, toolChain: [ { toolName: 'analyze_project_ecosystem', parameters: { projectPath: structureSnapshot.projectPath, analysisDepth: structureSnapshot.analysisDepth, recursiveDepth: structureSnapshot.recursiveDepth, technologyFocus: structureSnapshot.technologyFocus, analysisScope: structureSnapshot.analysisScope, includeEnvironment: structureSnapshot.includeEnvironment, }, result: { structureData: structureSnapshot.structureData, timestamp: structureSnapshot.timestamp, }, todoTasksCreated: [], todoTasksModified: [], executionTime: structureSnapshot.timestamp, success: true, }, ], currentStatus: 'completed', todoMdSnapshot: '', // No specific TODO.md impact for structure analysis tags: ['project-structure', 'ecosystem-analysis', 'architecture'], }; await this.createIntent(intent.humanRequest, intent.parsedGoals, intent.priority); } /** * Calculate memory-based health score from knowledge graph */ async calculateMemoryScore(kg) { // Extract memories from intents and their tool executions const memories = this.extractMemoriesFromKG(kg); // Calculate retrieval metrics from tool execution patterns const retrievalMetrics = this.calculateRetrievalMetrics(kg); // Build entity graph from intents and relationships const entityGraph = this.buildEntityGraph(kg); return this.memoryScoring.calculateMemoryHealth(memories, retrievalMetrics, entityGraph); } /** * Extract memory representations from knowledge graph */ extractMemoriesFromKG(kg) { const memories = []; kg.intents.forEach(intent => { // Each intent is a memory memories.push({ id: intent.intentId, content: intent.humanRequest, context: { goals: intent.parsedGoals, priority: intent.priority, status: intent.currentStatus, }, timestamp: intent.timestamp, metadata: { relevanceScore: this.calculateIntentRelevance(intent), toolExecutions: intent.toolChain.length, }, relatedDecisions: intent.adrsCreated || [], }); // Each tool execution is also a memory intent.toolChain.forEach(tool => { memories.push({ id: `${intent.intentId}-${tool.toolName}`, content: `${tool.toolName} execution`, context: { parameters: tool.parameters, result: tool.result, success: tool.success, }, timestamp: tool.executionTime, metadata: { relevanceScore: tool.success ? 0.8 : 0.3, parentIntent: intent.intentId, }, }); }); }); return memories; } /** * Calculate retrieval metrics from tool execution patterns */ calculateRetrievalMetrics(kg) { let totalRetrievals = 0; let successfulRetrievals = 0; let totalTime = 0; kg.intents.forEach(intent => { intent.toolChain.forEach(tool => { totalRetrievals++; if (tool.success) successfulRetrievals++; // Estimate retrieval time (would be actual in production) totalTime += tool.executionTime ? 50 : 100; }); }); return { totalRetrievals, successfulRetrievals, averageRetrievalTime: totalRetrievals > 0 ? totalTime / totalRetrievals : 0, precisionScore: 0.8, // Placeholder - would calculate from actual retrievals recallScore: 0.7, // Placeholder - would calculate from actual retrievals }; } /** * Build entity graph from knowledge graph */ buildEntityGraph(kg) { const entities = []; const relationships = []; // Intents as entities kg.intents.forEach(intent => { entities.push({ id: intent.intentId, type: 'intent', name: intent.humanRequest.substring(0, 50), }); // Create relationships between intents and their tools intent.toolChain.forEach(tool => { entities.push({ id: `tool-${tool.toolName}`, type: 'tool', name: tool.toolName, }); relationships.push({ sourceId: intent.intentId, targetId: `tool-${tool.toolName}`, type: 'uses', strength: tool.success ? 0.9 : 0.3, }); }); }); // Add ADR entities and relationships kg.intents.forEach(intent => { (intent.adrsCreated || []).forEach((adrId) => { entities.push({ id: `adr-${adrId}`, type: 'decision', name: `ADR ${adrId}`, }); relationships.push({ sourceId: intent.intentId, targetId: `adr-${adrId}`, type: 'created', strength: 0.95, }); }); }); return { entities, relationships, decisions: entities.filter(e => e.type === 'decision'), }; } /** * Calculate relevance score for an intent */ calculateIntentRelevance(intent) { const now = new Date(); const age = now.getTime() - new Date(intent.timestamp).getTime(); const ageInDays = age / (24 * 60 * 60 * 1000); // Base relevance on status and age let relevance = 0.5; if (intent.currentStatus === 'executing') relevance = 0.9; else if (intent.currentStatus === 'planning') relevance = 0.8; else if (intent.currentStatus === 'completed') relevance = 0.6; // Decay relevance over time relevance *= Math.max(0.3, 1 - ageInDays / 30); // Boost relevance if it has successful tool executions const successRate = intent.toolChain.filter(t => t.success).length / (intent.toolChain.length || 1); relevance = (relevance + successRate) / 2; return relevance; } } //# sourceMappingURL=knowledge-graph-manager.js.map