Skip to main content
Glama

NervusDB MCP Server

Official
by nervusdb
relatedFilesScorer.ts12.1 kB
import type { QueryService, GraphFact } from '../domain/query/queryService.js'; /** * Relationship type weights for scoring * Higher weight = stronger relationship */ const RELATIONSHIP_WEIGHTS: Record<string, number> = { IMPORTS: 1.0, // Direct dependency (strongest) IMPLEMENTS: 0.9, // Inheritance/interface implementation EXTENDS: 0.9, // Class inheritance CALLS: 0.8, // Function calls CONTAINS: 0.5, // File contains entity DEFINES: 0.4, // File defines entity // Shared dependency: calculated separately (0.3 per shared dep) }; /** * Reason for relationship with human-readable explanation */ export interface RelationshipReason { type: string; weight: number; description: string; fact: GraphFact; } /** * Scored file with detailed reasons */ export interface ScoredFile { filePath: string; score: number; normalizedScore: number; // 0-1 range for comparison reasons: RelationshipReason[]; relationshipCounts: Record<string, number>; } /** * Configuration for scoring algorithm */ export interface ScorerConfig { includeSharedDependencies?: boolean; // Include files that import same dependencies maxSharedDepsToCheck?: number; // Limit shared dependency analysis minScoreThreshold?: number; // Filter out low-scoring results } /** * Dependencies for RelatedFilesScorer */ export interface RelatedFilesScorerDependencies { queryService: QueryService; } /** * Input for scoring related files */ export interface ScoreRelatedFilesInput { projectPath: string; filePath: string; limit?: number; config?: ScorerConfig; } /** * Output of scoring operation */ export interface ScoreRelatedFilesResult { targetFile: string; scoredFiles: ScoredFile[]; totalFilesAnalyzed: number; maxScore: number; } /** * RelatedFilesScorer: Intelligent multi-relationship file scoring service * * Analyzes code relationships to find related files with weighted scoring: * - IMPORTS (1.0): Direct dependencies * - IMPLEMENTS/EXTENDS (0.9): Inheritance relationships * - CALLS (0.8): Function call relationships * - CONTAINS/DEFINES (0.5/0.4): Entity membership * - Shared dependencies (0.3): Indirect relationships */ export class RelatedFilesScorer { private readonly queryService: QueryService; constructor(deps: RelatedFilesScorerDependencies) { if (!deps.queryService) { throw new Error('RelatedFilesScorer requires QueryService'); } this.queryService = deps.queryService; } /** * Score and rank files by their relationship strength to target file */ async scoreRelatedFiles(input: ScoreRelatedFilesInput): Promise<ScoreRelatedFilesResult> { const { projectPath, filePath, limit = 50, config = {} } = input; const { includeSharedDependencies = true, maxSharedDepsToCheck = 10, minScoreThreshold = 0.1, } = config; const targetFileNode = this.normalizeFileNode(filePath); // Step 1: Collect direct relationships (outgoing and incoming) const directRelations = await this.collectDirectRelations(projectPath, targetFileNode); // Step 2: Build initial scores from direct relationships const fileScores = new Map< string, { score: number; reasons: RelationshipReason[]; relationshipCounts: Record<string, number>; } >(); this.processDirectRelations(directRelations, targetFileNode, fileScores); // Step 3: Add shared dependency scoring (optional) if (includeSharedDependencies) { await this.addSharedDependencyScores( projectPath, targetFileNode, directRelations, fileScores, maxSharedDepsToCheck, ); } // Step 4: Normalize scores and filter const scoredFiles = this.normalizeAndFilter(fileScores, minScoreThreshold); // Step 5: Sort and limit results scoredFiles.sort((a, b) => b.score - a.score); const limitedResults = scoredFiles.slice(0, limit); const maxScore = limitedResults.length > 0 ? limitedResults[0].score : 0; return { targetFile: filePath, scoredFiles: limitedResults, totalFilesAnalyzed: fileScores.size, maxScore, }; } /** * Collect all direct relationships (both directions) */ private async collectDirectRelations( projectPath: string, fileNode: string, ): Promise<GraphFact[]> { // Query both outgoing and incoming edges const [outgoing, incoming] = await Promise.all([ this.queryService.findFacts(projectPath, { subject: fileNode }, { limit: 200 }), this.queryService.findFacts(projectPath, { object: fileNode }, { limit: 200 }), ]); return [...outgoing, ...incoming]; } /** * Process direct relationships and build initial scores */ private processDirectRelations( relations: GraphFact[], targetFileNode: string, fileScores: Map< string, { score: number; reasons: RelationshipReason[]; relationshipCounts: Record<string, number>; } >, ): void { for (const fact of relations) { const relatedFile = this.extractRelatedFile(fact, targetFileNode); if (!relatedFile) continue; const weight = RELATIONSHIP_WEIGHTS[fact.predicate] ?? 0.2; // Default weight for unknown predicates const reason = this.createReason(fact, weight, targetFileNode); const current = fileScores.get(relatedFile) ?? { score: 0, reasons: [], relationshipCounts: {}, }; current.score += weight; current.reasons.push(reason); current.relationshipCounts[fact.predicate] = (current.relationshipCounts[fact.predicate] ?? 0) + 1; fileScores.set(relatedFile, current); } } /** * Add scores for shared dependencies * Files that import the same dependencies are likely related */ private async addSharedDependencyScores( projectPath: string, targetFileNode: string, directRelations: GraphFact[], fileScores: Map< string, { score: number; reasons: RelationshipReason[]; relationshipCounts: Record<string, number>; } >, maxDepsToCheck: number, ): Promise<void> { // Find dependencies imported by target file const targetImports = directRelations .filter((f) => f.subject === targetFileNode && f.predicate === 'IMPORTS') .map((f) => f.object) .slice(0, maxDepsToCheck); if (targetImports.length === 0) return; // For each dependency, find other files that import it const sharedImportersPromises = targetImports.map((dep) => this.queryService.findFacts( projectPath, { predicate: 'IMPORTS', object: dep, }, { limit: 50 }, ), ); const sharedImportersResults = await Promise.all(sharedImportersPromises); // Count shared dependencies per file const sharedDepCounts = new Map<string, { count: number; sharedDeps: string[] }>(); for (let i = 0; i < targetImports.length; i++) { const dep = targetImports[i]; const importers = sharedImportersResults[i]; for (const fact of importers) { const importer = fact.subject; if (importer === targetFileNode) continue; // Skip self const current = sharedDepCounts.get(importer) ?? { count: 0, sharedDeps: [] }; current.count++; current.sharedDeps.push(dep); sharedDepCounts.set(importer, current); } } // Add shared dependency scores const SHARED_DEP_WEIGHT = 0.3; for (const [file, { count, sharedDeps }] of sharedDepCounts.entries()) { const score = count * SHARED_DEP_WEIGHT; const current = fileScores.get(file) ?? { score: 0, reasons: [], relationshipCounts: {}, }; // Create synthetic fact for shared dependency const reason: RelationshipReason = { type: 'SHARED_DEPENDENCIES', weight: score, description: `Shares ${count} dependenc${count > 1 ? 'ies' : 'y'}: ${this.formatDepList(sharedDeps)}`, fact: { subject: targetFileNode, predicate: 'SHARED_DEPENDENCIES', object: file, properties: { count, sharedDeps }, }, }; current.score += score; current.reasons.push(reason); current.relationshipCounts.SHARED_DEPENDENCIES = count; fileScores.set(file, current); } } /** * Normalize scores and filter out low scores */ private normalizeAndFilter( fileScores: Map< string, { score: number; reasons: RelationshipReason[]; relationshipCounts: Record<string, number>; } >, minThreshold: number, ): ScoredFile[] { const scores = Array.from(fileScores.entries()).map(([, data]) => data.score); const maxScore = Math.max(...scores, 1); // Avoid division by zero const results: ScoredFile[] = []; for (const [filePath, data] of fileScores.entries()) { const normalizedScore = data.score / maxScore; if (normalizedScore >= minThreshold) { results.push({ filePath: this.denormalizeFileNode(filePath), score: data.score, normalizedScore, reasons: data.reasons, relationshipCounts: data.relationshipCounts, }); } } return results; } /** * Create human-readable reason for relationship */ private createReason( fact: GraphFact, weight: number, targetFileNode: string, ): RelationshipReason { const isOutgoing = fact.subject === targetFileNode; const relationType = fact.predicate; let description = ''; switch (relationType) { case 'IMPORTS': description = isOutgoing ? `Imports: ${this.denormalizeFileNode(fact.object)}` : `Imported by: ${this.denormalizeFileNode(fact.subject)}`; break; case 'CALLS': description = isOutgoing ? `Calls function in: ${this.denormalizeFileNode(fact.object)}` : `Function called by: ${this.denormalizeFileNode(fact.subject)}`; break; case 'IMPLEMENTS': case 'EXTENDS': description = isOutgoing ? `${relationType} interface/class in: ${this.denormalizeFileNode(fact.object)}` : `Interface/class ${relationType.toLowerCase()} by: ${this.denormalizeFileNode(fact.subject)}`; break; case 'CONTAINS': description = isOutgoing ? `Contains: ${fact.object}` : `Contained in: ${this.denormalizeFileNode(fact.subject)}`; break; case 'DEFINES': description = isOutgoing ? `Defines: ${fact.object}` : `Defined in: ${this.denormalizeFileNode(fact.subject)}`; break; default: description = `${relationType}: ${isOutgoing ? fact.object : fact.subject}`; } return { type: relationType, weight, description, fact, }; } /** * Extract related file from fact (not the target file) */ private extractRelatedFile(fact: GraphFact, targetFileNode: string): string | null { const candidates = [fact.subject, fact.object].filter( (node) => node.startsWith('file:') && node !== targetFileNode, ); return candidates[0] ?? null; } /** * Normalize file path to node format (file:...) */ private normalizeFileNode(filePath: string): string { return filePath.startsWith('file:') ? filePath : `file:${filePath}`; } /** * Remove file: prefix from node */ private denormalizeFileNode(nodeId: string): string { return nodeId.startsWith('file:') ? nodeId.substring(5) : nodeId; } /** * Format dependency list for display (truncate if too long) */ private formatDepList(deps: string[], maxDisplay = 3): string { const displayDeps = deps.slice(0, maxDisplay).map((d) => this.denormalizeFileNode(d)); if (deps.length > maxDisplay) { return `${displayDeps.join(', ')}, +${deps.length - maxDisplay} more`; } return displayDeps.join(', '); } }

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/nervusdb/nervusdb-mcp'

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