mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
398 lines • 17.2 kB
JavaScript
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';
import { ProjectHealthScoring } from './project-health-scoring.js';
export class KnowledgeGraphManager {
cacheDir;
snapshotsFile;
syncStateFile;
healthScoring;
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.healthScoring = new ProjectHealthScoring(config.projectPath);
}
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
const currentScore = await this.healthScoring.getProjectHealthScore();
const intent = {
intentId,
humanRequest,
parsedGoals,
priority,
timestamp,
toolChain: [],
currentStatus: 'planning',
todoMdSnapshot: '',
scoreTracking: {
initialScore: currentScore.overall,
currentScore: currentScore.overall,
componentScores: {
taskCompletion: currentScore.taskCompletion,
deploymentReadiness: currentScore.deploymentReadiness,
architectureCompliance: currentScore.architectureCompliance,
securityPosture: currentScore.securityPosture,
codeQuality: currentScore.codeQuality
},
lastScoreUpdate: timestamp
}
};
const kg = await this.loadKnowledgeGraph();
kg.intents.push(intent);
kg.analytics.totalIntents = kg.intents.length;
kg.analytics.activeIntents = kg.intents.filter(i => i.currentStatus !== 'completed').length;
// Add score history entry
if (!kg.scoreHistory)
kg.scoreHistory = [];
kg.scoreHistory.push({
timestamp,
intentId,
overallScore: currentScore.overall,
componentScores: {
taskCompletion: currentScore.taskCompletion,
deploymentReadiness: currentScore.deploymentReadiness,
architectureCompliance: currentScore.architectureCompliance,
securityPosture: currentScore.securityPosture,
codeQuality: currentScore.codeQuality
},
triggerEvent: `Intent created: ${humanRequest.substring(0, 100)}...`,
confidence: currentScore.confidence
});
await this.saveKnowledgeGraph(kg);
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
const beforeScore = await this.healthScoring.getProjectHealthScore();
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 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.healthScoring.getProjectHealthScore();
// Calculate score impact
const scoreImpact = {
beforeScore: beforeScore.overall,
afterScore: afterScore.overall,
componentImpacts: {
taskCompletion: afterScore.taskCompletion - beforeScore.taskCompletion,
deploymentReadiness: afterScore.deploymentReadiness - beforeScore.deploymentReadiness,
architectureCompliance: afterScore.architectureCompliance - beforeScore.architectureCompliance,
securityPosture: afterScore.securityPosture - beforeScore.securityPosture,
codeQuality: afterScore.codeQuality - beforeScore.codeQuality
},
scoreConfidence: afterScore.confidence
};
// Update execution with score impact
execution.scoreImpact = scoreImpact;
// Update intent score tracking
if (intent.scoreTracking) {
intent.scoreTracking.currentScore = afterScore.overall;
intent.scoreTracking.componentScores = {
taskCompletion: afterScore.taskCompletion,
deploymentReadiness: afterScore.deploymentReadiness,
architectureCompliance: afterScore.architectureCompliance,
securityPosture: afterScore.securityPosture,
codeQuality: afterScore.codeQuality
};
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.taskCompletion,
deploymentReadiness: afterScore.deploymentReadiness,
architectureCompliance: afterScore.architectureCompliance,
securityPosture: afterScore.securityPosture,
codeQuality: afterScore.codeQuality
},
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();
const currentScore = await this.healthScoring.getProjectHealthScore();
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);
}
}
//# sourceMappingURL=knowledge-graph-manager.js.map