UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

820 lines 33.1 kB
import * as fs from 'fs/promises'; import path from 'path'; import * as os from 'os'; 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'; /** * Manages knowledge graph snapshots and todo synchronization state * Provides persistent storage and retrieval of architectural knowledge * * @deprecated This class will be replaced by knowledge://graph Resource and update_knowledge tool (ADR-018). * For reading graph data, use knowledge://graph resource (zero token cost). * For modifying graph data, use update_knowledge tool (simple CRUD operations). * Internal use only - external tools should migrate to new patterns. * * Migration Path: * - Reading graph: Use knowledge://graph resource instead of queryKnowledgeGraph() * - Adding entities: Use update_knowledge tool with operation='add_entity' * - Removing entities: Use update_knowledge tool with operation='remove_entity' * - Relationships: Use update_knowledge tool with operation='add_relationship'/'remove_relationship' * * This class will be removed in the next major version (v3.0.0). */ export class KnowledgeGraphManager { cacheDir; snapshotsFile; syncStateFile; memoryScoring; /** * Initialize the knowledge graph manager with cache directory setup */ constructor() { const config = loadConfig(); // Use OS temp directory for cache const projectName = path.basename(config.projectPath); this.cacheDir = path.join(os.tmpdir(), projectName, '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(); } /** * Ensure the cache directory exists, creating it if necessary */ async ensureCacheDirectory() { try { await fs.access(this.cacheDir); } catch { await fs.mkdir(this.cacheDir, { recursive: true }); } } /** * Load the knowledge graph snapshot from cache * @returns Promise resolving to the knowledge graph snapshot */ 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); } /** * Query knowledge graph for relevant information based on question */ async queryKnowledgeGraph(question) { const kg = await this.loadKnowledgeGraph(); const questionLower = question.toLowerCase(); const keywords = this.extractQueryKeywords(question); const results = { found: false, nodes: [], relationships: [], relevantIntents: [], relevantDecisions: [], }; // Search through intents for relevant information for (const intent of kg.intents) { let relevanceScore = 0; // Check human request const requestLower = intent.humanRequest.toLowerCase(); for (const keyword of keywords) { if (requestLower.includes(keyword)) { relevanceScore += 0.3; } } // Check parsed goals for (const goal of intent.parsedGoals) { const goalLower = goal.toLowerCase(); for (const keyword of keywords) { if (goalLower.includes(keyword)) { relevanceScore += 0.2; } } } // Check tool chain for (const tool of intent.toolChain) { if (tool.toolName.toLowerCase().includes(questionLower)) { relevanceScore += 0.2; } // Check tool parameters and results const paramStr = JSON.stringify(tool.parameters).toLowerCase(); const resultStr = JSON.stringify(tool.result).toLowerCase(); for (const keyword of keywords) { if (paramStr.includes(keyword) || resultStr.includes(keyword)) { relevanceScore += 0.1; } } } // Check ADRs created if (intent.adrsCreated) { for (const adr of intent.adrsCreated) { if (typeof adr === 'string' && adr.toLowerCase().includes(questionLower)) { relevanceScore += 0.3; } } } // Add to results if relevant if (relevanceScore > 0.3) { results.relevantIntents.push(intent); // Add as node results.nodes.push({ id: intent.intentId, type: 'intent', name: intent.humanRequest, relevanceScore, timestamp: intent.timestamp, status: intent.currentStatus, }); // Add tool relationships for (const tool of intent.toolChain) { results.relationships.push({ source: intent.intentId, target: tool.toolName, type: 'uses', success: tool.success, }); } } } // Extract ADR-related information for (const intent of results.relevantIntents) { if (intent.adrsCreated) { for (const adr of intent.adrsCreated) { results.relevantDecisions.push({ adrId: adr, intentId: intent.intentId, timestamp: intent.timestamp, }); // Add ADR node results.nodes.push({ id: `adr-${adr}`, type: 'decision', name: `ADR ${adr}`, }); // Add ADR relationship results.relationships.push({ source: intent.intentId, target: `adr-${adr}`, type: 'created', }); } } } results.found = results.nodes.length > 0; return results; } /** * Extract keywords from query for knowledge graph search */ extractQueryKeywords(query) { const stopWords = new Set([ 'the', 'is', 'are', 'was', 'were', 'what', 'when', 'where', 'why', 'how', 'which', 'who', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'with', 'about', 'as', 'by', 'from', 'of', 'can', 'could', 'should', 'would', 'do', 'does', 'did', 'have', 'has', 'had', ]); return query .toLowerCase() .replace(/[^\w\s-]/g, ' ') .split(/\s+/) .filter(w => w.length > 2 && !stopWords.has(w)); } /** * Get relationships from knowledge graph * @deprecated Use queryKnowledgeGraph for actual queries */ getRelationships(_nodeType, _relationType) { // This is a synchronous method that requires async data // Return empty array for now - use queryKnowledgeGraph for actual queries return []; } /** * 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