Skip to main content
Glama
metrics-manager.ts12.4 kB
import { Neo4jClient } from '../graph/neo4j-client.js'; export interface CKMetrics { classId: string; className: string; wmc: number; // Weighted Methods per Class dit: number; // Depth of Inheritance Tree noc: number; // Number of Children cbo: number; // Coupling Between Objects rfc: number; // Response for Class lcom: number; // Lack of Cohesion in Methods (basic version) } export interface PackageMetrics { packageName: string; ca: number; // Afferent Coupling ce: number; // Efferent Coupling instability: number; // I = Ce / (Ce + Ca) abstractness: number; // A = abstract classes / total classes distance: number; // D = |A + I - 1| } export interface ArchitecturalIssue { type: 'circular_dependency' | 'layer_violation' | 'god_class' | 'high_coupling'; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; entities: string[]; metrics?: Record<string, number>; } export class MetricsManager { constructor(private client: Neo4jClient) {} async calculateCKMetrics(classId: string): Promise<CKMetrics> { const className = await this.getClassName(classId); const [wmc, dit, noc, cbo, rfc, lcom] = await Promise.all([ this.calculateWMC(classId), this.calculateDIT(classId), this.calculateNOC(classId), this.calculateCBO(classId), this.calculateRFC(classId), this.calculateLCOM(classId) ]); return { classId, className, wmc, dit, noc, cbo, rfc, lcom }; } async calculatePackageMetrics(packageName: string): Promise<PackageMetrics> { const [ca, ce, abstractness] = await Promise.all([ this.calculateAfferentCoupling(packageName), this.calculateEfferentCoupling(packageName), this.calculateAbstractness(packageName) ]); const instability = (ca + ce) === 0 ? 0 : ce / (ca + ce); const distance = Math.abs(abstractness + instability - 1); return { packageName, ca, ce, instability, abstractness, distance }; } async findArchitecturalIssues(): Promise<ArchitecturalIssue[]> { const issues: ArchitecturalIssue[] = []; // Find circular dependencies const circularDeps = await this.findCircularDependencies(); issues.push(...circularDeps); // Find god classes const godClasses = await this.findGodClasses(); issues.push(...godClasses); // Find highly coupled classes const highCoupling = await this.findHighlyCoupledClasses(); issues.push(...highCoupling); return issues; } async calculateProjectSummary(): Promise<{ totalClasses: number; totalMethods: number; totalPackages: number; averageMetrics: { avgCBO: number; avgRFC: number; avgDIT: number; }; issueCount: number; }> { const [ totalClasses, totalMethods, totalPackages, avgMetrics, issues ] = await Promise.all([ this.getTotalClasses(), this.getTotalMethods(), this.getTotalPackages(), this.getAverageMetrics(), this.findArchitecturalIssues() ]); return { totalClasses, totalMethods, totalPackages, averageMetrics: avgMetrics, issueCount: issues.length }; } // CK Metrics Implementation private async calculateWMC(classId: string): Promise<number> { const query = ` MATCH (class:CodeNode {id: $classId})-[:CONTAINS]->(method:CodeNode {type: 'method'}) RETURN count(method) as wmc `; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('wmc').toNumber() || 0; } private async calculateDIT(classId: string): Promise<number> { const query = ` MATCH path = (class:CodeNode {id: $classId})-[:EXTENDS*]->(ancestor:CodeNode) RETURN max(length(path)) as dit `; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('dit')?.toNumber() || 0; } private async calculateNOC(classId: string): Promise<number> { const query = ` MATCH (class:CodeNode {id: $classId})<-[:EXTENDS]-(child:CodeNode) RETURN count(child) as noc `; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('noc').toNumber() || 0; } private async calculateCBO(classId: string): Promise<number> { const query = ` MATCH (class:CodeNode {id: $classId}) OPTIONAL MATCH (class)-[:CALLS|REFERENCES]-(other:CodeNode {type: 'class'}) WHERE other.id <> $classId RETURN count(DISTINCT other) as cbo `; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('cbo').toNumber() || 0; } private async calculateRFC(classId: string): Promise<number> { const query = ` MATCH (class:CodeNode {id: $classId})-[:CONTAINS]->(method:CodeNode {type: 'method'}) OPTIONAL MATCH (method)-[:CALLS]->(calledMethod:CodeNode {type: 'method'}) RETURN count(DISTINCT method) + count(DISTINCT calledMethod) as rfc `; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('rfc').toNumber() || 0; } private async calculateLCOM(classId: string): Promise<number> { // Simplified LCOM calculation - basic version // More sophisticated version would require field usage analysis const query = ` MATCH (class:CodeNode {id: $classId})-[:CONTAINS]->(method:CodeNode {type: 'method'}) MATCH (class)-[:CONTAINS]->(field:CodeNode {type: 'field'}) RETURN count(DISTINCT method) as methods, count(DISTINCT field) as fields `; const result = await this.client.runQuery(query, { classId }); const record = result.records[0]; const methods = record?.get('methods').toNumber() || 0; const fields = record?.get('fields').toNumber() || 0; // Basic LCOM approximation return methods > 0 && fields > 0 ? Math.max(0, methods - fields) : 0; } // Package Metrics Implementation private async calculateAfferentCoupling(packageName: string): Promise<number> { const query = ` MATCH (external:CodeNode)-[:CALLS|REFERENCES]->(internal:CodeNode) WHERE internal.qualified_name STARTS WITH $packagePrefix AND NOT external.qualified_name STARTS WITH $packagePrefix RETURN count(DISTINCT external) as ca `; const result = await this.client.runQuery(query, { packagePrefix: packageName + '.' }); return result.records[0]?.get('ca').toNumber() || 0; } private async calculateEfferentCoupling(packageName: string): Promise<number> { const query = ` MATCH (internal:CodeNode)-[:CALLS|REFERENCES]->(external:CodeNode) WHERE internal.qualified_name STARTS WITH $packagePrefix AND NOT external.qualified_name STARTS WITH $packagePrefix RETURN count(DISTINCT external) as ce `; const result = await this.client.runQuery(query, { packagePrefix: packageName + '.' }); return result.records[0]?.get('ce').toNumber() || 0; } private async calculateAbstractness(packageName: string): Promise<number> { const query = ` MATCH (class:CodeNode {type: 'class'}) WHERE class.qualified_name STARTS WITH $packagePrefix RETURN count(CASE WHEN 'abstract' IN class.modifiers OR class.type = 'interface' THEN 1 END) as abstractClasses, count(class) as totalClasses `; const result = await this.client.runQuery(query, { packagePrefix: packageName + '.' }); const record = result.records[0]; const abstract = record?.get('abstractClasses').toNumber() || 0; const total = record?.get('totalClasses').toNumber() || 0; return total > 0 ? abstract / total : 0; } // Architectural Analysis private async findCircularDependencies(): Promise<ArchitecturalIssue[]> { const query = ` MATCH (p1:CodeNode {type: 'package'})-[:DEPENDS_ON*2..5]->(p2:CodeNode {type: 'package'}) WHERE p1 = p2 RETURN DISTINCT p1.name as packageName `; const result = await this.client.runQuery(query); return result.records.map(record => ({ type: 'circular_dependency' as const, severity: 'high' as const, description: `Circular dependency detected in package: ${record.get('packageName')}`, entities: [record.get('packageName')] })); } private async findGodClasses(): Promise<ArchitecturalIssue[]> { const query = ` MATCH (class:CodeNode {type: 'class'})-[:CONTAINS]->(method:CodeNode {type: 'method'}) WITH class, count(method) as methodCount WHERE methodCount > 20 OPTIONAL MATCH (class)-[:CALLS|REFERENCES]-(other:CodeNode {type: 'class'}) WITH class, methodCount, count(DISTINCT other) as coupling WHERE coupling > 10 RETURN class.id as classId, class.name as className, methodCount, coupling `; const result = await this.client.runQuery(query); return result.records.map(record => ({ type: 'god_class' as const, severity: 'high' as const, description: `God class detected: ${record.get('className')} (${record.get('methodCount')} methods, ${record.get('coupling')} couplings)`, entities: [record.get('classId')], metrics: { methodCount: record.get('methodCount').toNumber(), coupling: record.get('coupling').toNumber() } })); } private async findHighlyCoupledClasses(): Promise<ArchitecturalIssue[]> { const query = ` MATCH (class:CodeNode {type: 'class'})-[:CALLS|REFERENCES]-(other:CodeNode {type: 'class'}) WITH class, count(DISTINCT other) as coupling WHERE coupling > 15 RETURN class.id as classId, class.name as className, coupling ORDER BY coupling DESC `; const result = await this.client.runQuery(query); return result.records.map(record => ({ type: 'high_coupling' as const, severity: record.get('coupling').toNumber() > 25 ? 'critical' as const : 'high' as const, description: `Highly coupled class: ${record.get('className')} (${record.get('coupling')} couplings)`, entities: [record.get('classId')], metrics: { coupling: record.get('coupling').toNumber() } })); } // Helper methods private async getClassName(classId: string): Promise<string> { const query = 'MATCH (class:CodeNode {id: $classId}) RETURN class.name as name'; const result = await this.client.runQuery(query, { classId }); return result.records[0]?.get('name') || 'Unknown'; } private async getTotalClasses(): Promise<number> { const query = 'MATCH (class:CodeNode {type: "class"}) RETURN count(class) as total'; const result = await this.client.runQuery(query); return result.records[0]?.get('total').toNumber() || 0; } private async getTotalMethods(): Promise<number> { const query = 'MATCH (method:CodeNode {type: "method"}) RETURN count(method) as total'; const result = await this.client.runQuery(query); return result.records[0]?.get('total').toNumber() || 0; } private async getTotalPackages(): Promise<number> { const query = 'MATCH (pkg:CodeNode {type: "package"}) RETURN count(pkg) as total'; const result = await this.client.runQuery(query); return result.records[0]?.get('total').toNumber() || 0; } private async getAverageMetrics(): Promise<{ avgCBO: number; avgRFC: number; avgDIT: number }> { const query = ` MATCH (class:CodeNode {type: 'class'}) OPTIONAL MATCH (class)-[:CALLS|REFERENCES]-(other:CodeNode {type: 'class'}) WITH class, count(DISTINCT other) as cbo OPTIONAL MATCH (class)-[:CONTAINS]->(method:CodeNode {type: 'method'}) OPTIONAL MATCH (method)-[:CALLS]->(calledMethod:CodeNode {type: 'method'}) WITH class, cbo, count(DISTINCT method) + count(DISTINCT calledMethod) as rfc OPTIONAL MATCH path = (class)-[:EXTENDS*]->(ancestor:CodeNode) WITH class, cbo, rfc, max(length(path)) as dit RETURN avg(cbo) as avgCBO, avg(rfc) as avgRFC, avg(dit) as avgDIT `; const result = await this.client.runQuery(query); const record = result.records[0]; return { avgCBO: record?.get('avgCBO') || 0, avgRFC: record?.get('avgRFC') || 0, avgDIT: record?.get('avgDIT') || 0 }; } }

Latest Blog Posts

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/JonnoC/CodeRAG'

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