mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
277 lines • 11.1 kB
JavaScript
/**
* Memory-Centric Health Scoring System
*
* Evaluates system health based on memory quality, retrieval performance,
* and entity relationship coherence rather than traditional task completion metrics.
*
* This replaces the deprecated ProjectHealthScoring with a memory-aware approach
* that aligns with the memory-centric architecture.
*/
export class MemoryHealthScoring {
weights;
constructor(weights) {
this.weights = {
memoryQuality: 0.25,
retrievalPerformance: 0.25,
entityCoherence: 0.2,
contextUtilization: 0.15,
decisionAlignment: 0.15,
...weights,
};
}
/**
* Calculate overall memory health score
*/
async calculateMemoryHealth(memories, retrievalMetrics, entityGraph) {
const now = new Date().toISOString();
const breakdown = {
memoryQuality: this.assessMemoryQuality(memories, now),
retrievalPerformance: this.assessRetrievalPerformance(retrievalMetrics, now),
entityCoherence: this.assessEntityCoherence(entityGraph, now),
contextUtilization: this.assessContextUtilization(memories, now),
decisionAlignment: this.assessDecisionAlignment(memories, entityGraph, now),
};
const scores = {
memoryQuality: this.calculateMemoryQualityScore(breakdown.memoryQuality),
retrievalPerformance: this.calculateRetrievalScore(breakdown.retrievalPerformance),
entityCoherence: this.calculateCoherenceScore(breakdown.entityCoherence),
contextUtilization: this.calculateContextScore(breakdown.contextUtilization),
decisionAlignment: this.calculateAlignmentScore(breakdown.decisionAlignment),
};
const overall = Math.round(scores.memoryQuality * this.weights.memoryQuality +
scores.retrievalPerformance * this.weights.retrievalPerformance +
scores.entityCoherence * this.weights.entityCoherence +
scores.contextUtilization * this.weights.contextUtilization +
scores.decisionAlignment * this.weights.decisionAlignment);
return {
overall,
...scores,
confidence: this.calculateConfidence(breakdown),
lastUpdated: now,
breakdown,
};
}
/**
* Assess memory quality based on relevance and freshness
*/
assessMemoryQuality(memories, timestamp) {
const now = new Date();
const staleThreshold = 7 * 24 * 60 * 60 * 1000; // 7 days
let relevantCount = 0;
let staleCount = 0;
let totalRelevance = 0;
memories.forEach(memory => {
// Check relevance (simplified - would use embeddings in production)
const relevance = memory.metadata?.relevanceScore || 0.5;
totalRelevance += relevance;
if (relevance > 0.7)
relevantCount++;
// Check staleness
if (memory.timestamp) {
const age = now.getTime() - new Date(memory.timestamp).getTime();
if (age > staleThreshold)
staleCount++;
}
});
return {
totalMemories: memories.length,
relevantMemories: relevantCount,
staleMemories: staleCount,
averageRelevanceScore: memories.length > 0 ? totalRelevance / memories.length : 0,
lastUpdated: timestamp,
};
}
/**
* Assess retrieval performance metrics
*/
assessRetrievalPerformance(metrics, timestamp) {
return {
totalRetrievals: metrics?.totalRetrievals || 0,
successfulRetrievals: metrics?.successfulRetrievals || 0,
averageRetrievalTime: metrics?.averageRetrievalTime || 0,
precisionScore: metrics?.precisionScore || 0.5,
recallScore: metrics?.recallScore || 0.5,
lastUpdated: timestamp,
};
}
/**
* Assess entity relationship coherence
*/
assessEntityCoherence(entityGraph, timestamp) {
const entities = entityGraph?.entities || [];
const relationships = entityGraph?.relationships || [];
// Count connected vs orphaned entities
const connectedEntityIds = new Set();
relationships.forEach((rel) => {
connectedEntityIds.add(rel.sourceId);
connectedEntityIds.add(rel.targetId);
});
const orphanedCount = entities.filter((e) => !connectedEntityIds.has(e.id)).length;
// Calculate average relationship strength
const avgStrength = relationships.length > 0
? relationships.reduce((sum, rel) => sum + (rel.strength || 0.5), 0) /
relationships.length
: 0;
return {
totalEntities: entities.length,
connectedEntities: connectedEntityIds.size,
orphanedEntities: orphanedCount,
relationshipStrength: avgStrength,
lastUpdated: timestamp,
};
}
/**
* Assess context utilization
*/
assessContextUtilization(memories, timestamp) {
let totalContextSize = 0;
let totalRelevance = 0;
let totalCompleteness = 0;
memories.forEach(memory => {
const context = memory.context || {};
totalContextSize += Object.keys(context).length;
totalRelevance += context.relevanceScore || 0.5;
totalCompleteness += context.completeness || 0.5;
});
const count = memories.length || 1;
return {
averageContextSize: totalContextSize / count,
contextRelevance: totalRelevance / count,
contextCompleteness: totalCompleteness / count,
lastUpdated: timestamp,
};
}
/**
* Assess alignment with architectural decisions
*/
assessDecisionAlignment(memories, entityGraph, timestamp) {
const decisions = entityGraph?.decisions || [];
let alignedCount = 0;
let conflictingCount = 0;
decisions.forEach((decision) => {
// Check if memories support this decision
const supportingMemories = memories.filter(m => m.relatedDecisions?.includes(decision.id));
if (supportingMemories.length > 0) {
alignedCount++;
}
else if (decision.conflicts?.length > 0) {
conflictingCount++;
}
});
return {
totalDecisions: decisions.length,
alignedDecisions: alignedCount,
conflictingDecisions: conflictingCount,
alignmentScore: decisions.length > 0 ? alignedCount / decisions.length : 0,
lastUpdated: timestamp,
};
}
/**
* Calculate memory quality score
*/
calculateMemoryQualityScore(quality) {
if (quality.totalMemories === 0)
return 50; // Neutral score for no memories
let score = 100;
// Penalize stale memories
const staleRatio = quality.staleMemories / quality.totalMemories;
score -= staleRatio * 30;
// Reward relevant memories
const relevantRatio = quality.relevantMemories / quality.totalMemories;
score = (score + relevantRatio * 100) / 2;
// Factor in average relevance
score = (score + quality.averageRelevanceScore * 100) / 2;
return Math.round(Math.max(0, Math.min(100, score)));
}
/**
* Calculate retrieval performance score
*/
calculateRetrievalScore(perf) {
if (perf.totalRetrievals === 0)
return 50; // Neutral score for no retrievals
const successRate = perf.successfulRetrievals / perf.totalRetrievals;
const f1Score = (2 * (perf.precisionScore * perf.recallScore)) /
(perf.precisionScore + perf.recallScore || 1);
// Penalize slow retrieval (over 100ms is considered slow)
const speedScore = Math.max(0, 1 - perf.averageRetrievalTime / 100);
return Math.round((successRate * 40 + f1Score * 40 + speedScore * 20) * 100);
}
/**
* Calculate entity coherence score
*/
calculateCoherenceScore(coherence) {
if (coherence.totalEntities === 0)
return 100; // Perfect score for no entities
const connectedRatio = coherence.connectedEntities / coherence.totalEntities;
const orphanPenalty = (coherence.orphanedEntities / coherence.totalEntities) * 30;
let score = connectedRatio * 100 - orphanPenalty;
score = (score + coherence.relationshipStrength * 100) / 2;
return Math.round(Math.max(0, Math.min(100, score)));
}
/**
* Calculate context utilization score
*/
calculateContextScore(context) {
// Ideal context size is between 5-15 items
const sizeScore = context.averageContextSize < 5
? context.averageContextSize / 5
: context.averageContextSize > 15
? Math.max(0, 1 - (context.averageContextSize - 15) / 15)
: 1;
return Math.round((sizeScore * 30 + context.contextRelevance * 35 + context.contextCompleteness * 35) * 100);
}
/**
* Calculate decision alignment score
*/
calculateAlignmentScore(alignment) {
if (alignment.totalDecisions === 0)
return 100; // Perfect score for no decisions
let score = alignment.alignmentScore * 100;
// Heavily penalize conflicting decisions
const conflictPenalty = (alignment.conflictingDecisions / alignment.totalDecisions) * 50;
score -= conflictPenalty;
return Math.round(Math.max(0, Math.min(100, score)));
}
/**
* Calculate confidence in the overall score
*/
calculateConfidence(breakdown) {
const now = new Date();
const maxAgeMs = 60 * 60 * 1000; // 1 hour for memory systems
let confidence = 100;
// Check data freshness
Object.values(breakdown).forEach(component => {
if (component.lastUpdated) {
const age = now.getTime() - new Date(component.lastUpdated).getTime();
if (age > maxAgeMs) {
confidence -= 20;
}
}
});
// Reduce confidence if we have very little data
if (breakdown.memoryQuality.totalMemories < 10)
confidence -= 20;
if (breakdown.retrievalPerformance.totalRetrievals < 5)
confidence -= 20;
if (breakdown.entityCoherence.totalEntities < 5)
confidence -= 20;
return Math.max(0, confidence);
}
/**
* Get emoji representation of score
*/
getScoreEmoji(score) {
if (score >= 90)
return '🟢';
if (score >= 75)
return '🟡';
if (score >= 60)
return '🟠';
return '🔴';
}
}
/**
* Singleton instance for easy access
*/
export const memoryHealthScoring = new MemoryHealthScoring();
//# sourceMappingURL=memory-health-scoring.js.map