diff --git a/gitnexus-shared/src/graph/types.ts b/gitnexus-shared/src/graph/types.ts index ff660e814..49762d145 100644 --- a/gitnexus-shared/src/graph/types.ts +++ b/gitnexus-shared/src/graph/types.ts @@ -97,7 +97,8 @@ export type RelationshipType = | 'CONTAINS' | 'CALLS' | 'INHERITS' - | 'OVERRIDES' + | 'METHOD_OVERRIDES' + | 'METHOD_IMPLEMENTS' | 'IMPORTS' | 'USES' | 'DEFINES' diff --git a/gitnexus-shared/src/lbug/schema-constants.ts b/gitnexus-shared/src/lbug/schema-constants.ts index e30948492..424a8a949 100644 --- a/gitnexus-shared/src/lbug/schema-constants.ts +++ b/gitnexus-shared/src/lbug/schema-constants.ts @@ -55,7 +55,8 @@ export const REL_TYPES = [ 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', - 'OVERRIDES', + 'METHOD_OVERRIDES', + 'METHOD_IMPLEMENTS', 'MEMBER_OF', 'STEP_IN_PROCESS', 'HANDLES_ROUTE', diff --git a/gitnexus/src/core/ingestion/mro-processor.ts b/gitnexus/src/core/ingestion/mro-processor.ts index 6d96bb2d9..16fa0f666 100644 --- a/gitnexus/src/core/ingestion/mro-processor.ts +++ b/gitnexus/src/core/ingestion/mro-processor.ts @@ -3,7 +3,7 @@ * * Walks the inheritance DAG (EXTENDS/IMPLEMENTS edges), collects methods from * each ancestor via HAS_METHOD edges, detects method-name collisions across - * parents, and applies language-specific resolution rules to emit OVERRIDES edges. + * parents, and applies language-specific resolution rules to emit METHOD_OVERRIDES edges. * * Language-specific rules: * - C++: leftmost base class in declaration order wins @@ -13,10 +13,10 @@ * - Rust: no auto-resolution — requires qualified syntax, resolvedTo = null * - Default: single inheritance — first definition wins * - * OVERRIDES edge direction: Class → Method (not Method → Method). + * METHOD_OVERRIDES edge direction: Class → Method (not Method → Method). * The source is the child class that inherits conflicting methods, * the target is the winning ancestor method node. - * Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'OVERRIDES'}]->(m:Method) + * Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(m:Method) */ import { KnowledgeGraph } from '../graph/types.js'; @@ -47,6 +47,7 @@ export interface MROResult { entries: MROEntry[]; overrideEdges: number; ambiguityCount: number; + methodImplementsEdges: number; } // --------------------------------------------------------------------------- @@ -401,13 +402,13 @@ export function computeMRO(graph: KnowledgeGraph): MROResult { ambiguityCount++; } - // Emit OVERRIDES edge if resolution found + // Emit METHOD_OVERRIDES edge if resolution found if (resolution.resolvedTo !== null) { graph.addRelationship({ - id: generateId('OVERRIDES', `${classId}->${resolution.resolvedTo}`), + id: generateId('METHOD_OVERRIDES', `${classId}->${resolution.resolvedTo}`), sourceId: classId, targetId: resolution.resolvedTo, - type: 'OVERRIDES', + type: 'METHOD_OVERRIDES', confidence: resolution.confidence, reason: resolution.reason, }); @@ -424,7 +425,112 @@ export function computeMRO(graph: KnowledgeGraph): MROResult { }); } - return { entries, overrideEdges, ambiguityCount }; + const methodImplementsEdges = emitMethodImplementsEdges( + graph, + parentMap, + methodMap, + parentEdgeType, + ); + + return { entries, overrideEdges, ambiguityCount, methodImplementsEdges }; +} + +// --------------------------------------------------------------------------- +// METHOD_IMPLEMENTS edge emission +// --------------------------------------------------------------------------- + +/** + * Check if two parameter type arrays match. + * Lenient: if either side has no type info, match by name only. + */ +function parameterTypesMatch(a: string[], b: string[]): boolean { + if (a.length === 0 || b.length === 0) return true; + if (a.length !== b.length) return false; + return a.every((t, i) => t === b[i]); +} + +/** + * For each concrete class that implements/extends an interface or trait, + * find methods in the class that implement methods defined in the interface + * and emit METHOD_IMPLEMENTS edges: ConcreteMethod → InterfaceMethod. + */ +function emitMethodImplementsEdges( + graph: KnowledgeGraph, + parentMap: Map, + methodMap: Map, + parentEdgeType: Map>, +): number { + let edgeCount = 0; + + for (const [classId, parentIds] of parentMap) { + const classNode = graph.getNode(classId); + if (!classNode) continue; + + // Get this class's own methods + const ownMethodIds = methodMap.get(classId) ?? []; + if (ownMethodIds.length === 0) continue; + + // Build a lookup: methodName → Array<{methodId, parameterTypes}> for own methods + const ownMethodsByName = new Map< + string, + Array<{ methodId: string; parameterTypes: string[] }> + >(); + for (const methodId of ownMethodIds) { + const methodNode = graph.getNode(methodId); + if (!methodNode || methodNode.label === 'Property') continue; + const name = methodNode.properties.name as string; + const parameterTypes = (methodNode.properties.parameterTypes as string[] | undefined) ?? []; + let bucket = ownMethodsByName.get(name); + if (!bucket) { + bucket = []; + ownMethodsByName.set(name, bucket); + } + bucket.push({ methodId, parameterTypes }); + } + + // For each parent, check if it's an interface/trait or connected via IMPLEMENTS + for (const parentId of parentIds) { + const parentNode = graph.getNode(parentId); + if (!parentNode) continue; + + const isInterfaceLike = parentNode.label === 'Interface' || parentNode.label === 'Trait'; + const edgeType = parentEdgeType.get(classId)?.get(parentId); + if (!isInterfaceLike && edgeType !== 'IMPLEMENTS') continue; + + // Get parent's methods + const parentMethodIds = methodMap.get(parentId) ?? []; + + for (const parentMethodId of parentMethodIds) { + const parentMethodNode = graph.getNode(parentMethodId); + if (!parentMethodNode || parentMethodNode.label === 'Property') continue; + + const parentName = parentMethodNode.properties.name as string; + const parentParamTypes = + (parentMethodNode.properties.parameterTypes as string[] | undefined) ?? []; + + // Find matching method in own class by name + parameterTypes + const candidates = ownMethodsByName.get(parentName); + if (!candidates) continue; + + for (const candidate of candidates) { + if (parameterTypesMatch(candidate.parameterTypes, parentParamTypes)) { + graph.addRelationship({ + id: generateId('METHOD_IMPLEMENTS', `${candidate.methodId}->${parentMethodId}`), + sourceId: candidate.methodId, + targetId: parentMethodId, + type: 'METHOD_IMPLEMENTS', + confidence: 1.0, + reason: '', + }); + edgeCount++; + break; // first match wins for this parent method + } + } + } + } + } + + return edgeCount; } /** diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index 5f7d21fe1..d49bdbf70 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -1103,7 +1103,7 @@ async function runChunkedParseAndResolve( * Post-parse graph analysis: MRO, community detection, process extraction. * * @reads graph (all nodes and relationships from parse + resolve phases) - * @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, OVERRIDES edges) + * @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, METHOD_OVERRIDES edges) */ async function runGraphAnalysisPhases( graph: ReturnType, @@ -1126,7 +1126,7 @@ async function runGraphAnalysisPhases( const mroResult = computeMRO(graph); if (isDev && mroResult.entries.length > 0) { console.log( - `🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`, + `🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities, ${mroResult.overrideEdges} METHOD_OVERRIDES, ${mroResult.methodImplementsEdges} METHOD_IMPLEMENTS`, ); } diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index fe55a9f30..00b5d4282 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -96,7 +96,7 @@ export const VALID_RELATION_TYPES = new Set([ 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', - 'OVERRIDES', + 'METHOD_OVERRIDES', 'ACCESSES', 'HANDLES_ROUTE', 'FETCHES', @@ -117,7 +117,7 @@ export const VALID_RELATION_TYPES = new Set([ * CALLS / IMPORTS – direct, strongly-typed references → 0.9 * EXTENDS – class hierarchy, statically verifiable → 0.85 * IMPLEMENTS – interface contract, statically verifiable → 0.85 - * OVERRIDES – method override, statically verifiable → 0.85 + * METHOD_OVERRIDES – method override, statically verifiable → 0.85 * HAS_METHOD – structural containment → 0.95 * HAS_PROPERTY – structural containment → 0.95 * ACCESSES – field read/write, may be indirect → 0.8 @@ -129,7 +129,7 @@ export const IMPACT_RELATION_CONFIDENCE: Readonly> = { IMPORTS: 0.9, EXTENDS: 0.85, IMPLEMENTS: 0.85, - OVERRIDES: 0.85, + METHOD_OVERRIDES: 0.85, HAS_METHOD: 0.95, HAS_PROPERTY: 0.95, ACCESSES: 0.8, @@ -174,6 +174,7 @@ export class LocalBackend { private repos: Map = new Map(); private contextCache: Map = new Map(); private initializedRepos: Set = new Set(); + private migratedRepos: Set = new Set(); private reinitPromises: Map> = new Map(); private lastStalenessCheck: Map = new Map(); private groupToolSvc: GroupService | null = null; @@ -406,6 +407,20 @@ export class LocalBackend { try { await initLbug(repoId, handle.lbugPath); this.initializedRepos.add(repoId); + // Migrate legacy OVERRIDES → METHOD_OVERRIDES (once per session per repo) + // TODO: remove this migration after a few releases once most users have migrated indexes + if (!this.migratedRepos.has(repoId)) { + this.migratedRepos.add(repoId); + try { + await executeParameterized( + repoId, + `MATCH ()-[r:CodeRelation {type: 'OVERRIDES'}]->() SET r.type = 'METHOD_OVERRIDES'`, + {}, + ); + } catch { + /* Old index may not have OVERRIDES edges — ignore */ + } + } } catch (err: any) { // If lock error, mark as not initialized so next call retries this.initializedRepos.delete(repoId); @@ -1200,7 +1215,7 @@ export class LocalBackend { repo.id, ` MATCH (caller)-[r:CodeRelation]->(n {id: $symId}) - WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES'] + WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'ACCESSES'] RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind LIMIT 30 `, @@ -1290,7 +1305,7 @@ export class LocalBackend { repo.id, ` MATCH (n {id: $symId})-[r:CodeRelation]->(target) - WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES'] + WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'ACCESSES'] RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind LIMIT 30 `, diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts index 3fc2afdee..dce81bab4 100644 --- a/gitnexus/src/mcp/resources.ts +++ b/gitnexus/src/mcp/resources.ts @@ -353,7 +353,8 @@ relationships: - HAS_METHOD: Class/Struct/Interface owns a Method - HAS_PROPERTY: Class/Struct/Interface owns a Property (field) - ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write') - - OVERRIDES: Method overrides another Method (MRO) + - METHOD_OVERRIDES: Method overrides another Method (MRO) + - METHOD_IMPLEMENTS: ConcreteMethod implements InterfaceMethod (matched by name + parameterTypes) - MEMBER_OF: Symbol belongs to community - STEP_IN_PROCESS: Symbol is step N in process diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index c74b19464..fcb68a172 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -99,7 +99,7 @@ SCHEMA: - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc. - All edges via single CodeRelation table with 'type' property -- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF +- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32) EXAMPLES: @@ -122,7 +122,7 @@ EXAMPLES: MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath • Find method overrides (MRO resolution): - MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason + MATCH (winner:Method)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason • Detect diamond inheritance: MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name @@ -265,7 +265,7 @@ Depth groups: TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes. -EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES +EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, ACCESSES Confidence: 1.0 = certain, <0.8 = fuzzy match`, inputSchema: { type: 'object', @@ -284,7 +284,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`, type: 'array', items: { type: 'string' }, description: - 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)', + 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)', }, includeTests: { type: 'boolean', description: 'Include test files (default: false)' }, minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' }, diff --git a/gitnexus/test/fixtures/local-backend-seed.ts b/gitnexus/test/fixtures/local-backend-seed.ts index 6958fe5b5..9a4d4e0c1 100644 --- a/gitnexus/test/fixtures/local-backend-seed.ts +++ b/gitnexus/test/fixtures/local-backend-seed.ts @@ -34,7 +34,7 @@ export const LOCAL_BACKEND_SEED_DATA = [ CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`, // OVERRIDES: AuthService.authenticate -> BaseService.authenticate `MATCH (a:Method), (b:Method) WHERE a.id = 'method:AuthService.authenticate' AND b.id = 'method:BaseService.authenticate' - CREATE (a)-[:CodeRelation {type: 'OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`, + CREATE (a)-[:CodeRelation {type: 'METHOD_OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`, // HAS_METHOD: BaseService -> authenticate `MATCH (c:Class), (m:Method) WHERE c.id = 'class:BaseService' AND m.id = 'method:BaseService.authenticate' CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`, diff --git a/gitnexus/test/integration/local-backend-calltool.test.ts b/gitnexus/test/integration/local-backend-calltool.test.ts index f012a96db..55b479131 100644 --- a/gitnexus/test/integration/local-backend-calltool.test.ts +++ b/gitnexus/test/integration/local-backend-calltool.test.ts @@ -144,7 +144,7 @@ withTestLbugDB( const result = await backend.callTool('impact', { target: 'authenticate', direction: 'downstream', - relationTypes: ['OVERRIDES'], + relationTypes: ['METHOD_OVERRIDES'], }); expect(result).not.toHaveProperty('error'); // AuthService.authenticate overrides BaseService.authenticate diff --git a/gitnexus/test/integration/local-backend.test.ts b/gitnexus/test/integration/local-backend.test.ts index 992820419..be35a3c98 100644 --- a/gitnexus/test/integration/local-backend.test.ts +++ b/gitnexus/test/integration/local-backend.test.ts @@ -94,7 +94,7 @@ withTestLbugDB( 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', - 'OVERRIDES', + 'METHOD_OVERRIDES', 'ACCESSES', ]; const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE']; diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 7bda177c5..e9a340564 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -61,7 +61,7 @@ describe('C++ diamond inheritance', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index d88b97c11..a363ab502 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -86,7 +86,7 @@ describe('C# heritage resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/integration/resolvers/go.test.ts b/gitnexus/test/integration/resolvers/go.test.ts index a52ad1775..0ec9b2f6c 100644 --- a/gitnexus/test/integration/resolvers/go.test.ts +++ b/gitnexus/test/integration/resolvers/go.test.ts @@ -80,7 +80,7 @@ describe('Go package import & call resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index 2d8f59303..ff26fef19 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -71,7 +71,7 @@ describe('Java heritage resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); @@ -1651,3 +1651,53 @@ describe('Java method enrichment', () => { expect(classifyCall).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Java interface dispatch (METHOD_IMPLEMENTS) +// Action interface: execute(), priority() +// LogEvent implements Action, SendEmail implements Action +// --------------------------------------------------------------------------- + +describe('Java interface dispatch (METHOD_IMPLEMENTS)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-interface-dispatch'), () => {}); + }, 60000); + + it('emits METHOD_IMPLEMENTS edges from LogEvent.execute → Action.execute', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edge = mi.find( + (e) => + e.source === 'execute' && + e.target === 'execute' && + e.sourceFilePath.includes('LogEvent') && + e.targetFilePath.includes('Action'), + ); + expect(edge).toBeDefined(); + }); + + it('emits METHOD_IMPLEMENTS edges from SendEmail.execute → Action.execute', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edge = mi.find( + (e) => + e.source === 'execute' && + e.target === 'execute' && + e.sourceFilePath.includes('SendEmail') && + e.targetFilePath.includes('Action'), + ); + expect(edge).toBeDefined(); + }); + + it('emits METHOD_IMPLEMENTS for priority() in both implementors', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const priorityEdges = mi.filter( + (e) => + e.source === 'priority' && e.target === 'priority' && e.targetFilePath.includes('Action'), + ); + expect(priorityEdges.length).toBe(2); + const sourceFiles = priorityEdges.map((e) => e.sourceFilePath).sort(); + expect(sourceFiles.some((f) => f.includes('LogEvent'))).toBe(true); + expect(sourceFiles.some((f) => f.includes('SendEmail'))).toBe(true); + }); +}); diff --git a/gitnexus/test/integration/resolvers/kotlin.test.ts b/gitnexus/test/integration/resolvers/kotlin.test.ts index 90f5b8aa3..5a57731a7 100644 --- a/gitnexus/test/integration/resolvers/kotlin.test.ts +++ b/gitnexus/test/integration/resolvers/kotlin.test.ts @@ -90,7 +90,7 @@ describe('Kotlin heritage resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/integration/resolvers/php.test.ts b/gitnexus/test/integration/resolvers/php.test.ts index c7dfdc016..a72da9439 100644 --- a/gitnexus/test/integration/resolvers/php.test.ts +++ b/gitnexus/test/integration/resolvers/php.test.ts @@ -128,7 +128,7 @@ describe('PHP heritage & import resolution', () => { // --- Property OVERRIDES exclusion --- it('does not emit OVERRIDES for property name collisions ($status in both traits)', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); // OVERRIDES should only target Method nodes, never Property nodes for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); @@ -140,7 +140,7 @@ describe('PHP heritage & import resolution', () => { // --- MRO: OVERRIDES edge --- it('emits OVERRIDES edge for User overriding log (inherited from BaseModel)', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); expect(overrides.length).toBe(1); const logOverride = overrides.find((e) => e.source === 'User' && e.target === 'log'); expect(logOverride).toBeDefined(); @@ -1769,4 +1769,14 @@ describe('PHP abstract dispatch', () => { expect(params).toContain('int'); } }); + + it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository interface methods', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edges = mi.filter( + (e) => e.sourceFilePath.includes('SqlRepository') && e.targetFilePath.includes('Repository'), + ); + expect(edges.length).toBe(2); + const names = edges.map((e) => e.source).sort(); + expect(names).toEqual(['find', 'save']); + }); }); diff --git a/gitnexus/test/integration/resolvers/python.test.ts b/gitnexus/test/integration/resolvers/python.test.ts index 0fa62f4c0..f1108dec6 100644 --- a/gitnexus/test/integration/resolvers/python.test.ts +++ b/gitnexus/test/integration/resolvers/python.test.ts @@ -64,7 +64,7 @@ describe('Python relative import & heritage resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); @@ -2100,4 +2100,14 @@ describe('Python abstract dispatch', () => { expect(params).toContain('int'); } }); + + it('does not emit METHOD_IMPLEMENTS for abstract-class inheritance (only interface/trait parents)', () => { + // Python ABC is modelled as a Class with EXTENDS (not Interface with IMPLEMENTS), + // so the MRO processor does not emit METHOD_IMPLEMENTS edges here. + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edges = mi.filter( + (e) => e.sourceFilePath.includes('impl.py') && e.targetFilePath.includes('base.py'), + ); + expect(edges.length).toBe(0); + }); }); diff --git a/gitnexus/test/integration/resolvers/ruby.test.ts b/gitnexus/test/integration/resolvers/ruby.test.ts index bdb76f4ac..84bb137eb 100644 --- a/gitnexus/test/integration/resolvers/ruby.test.ts +++ b/gitnexus/test/integration/resolvers/ruby.test.ts @@ -194,7 +194,7 @@ describe('Ruby require_relative, heritage & property resolution', () => { // --- No OVERRIDES edges target Property nodes --- it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index ad205eee2..496f15e9b 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -66,7 +66,7 @@ describe('Rust trait implementation resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); @@ -1847,4 +1847,13 @@ describe('Rust abstract dispatch (Repository trait)', () => { expect(saveCall).toBeDefined(); expect(countCall).toBeDefined(); }); + + it('emits METHOD_IMPLEMENTS edges from SqlRepo impl methods → Repository trait methods', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + // find and save are required trait methods; count has a default impl so no METHOD_IMPLEMENTS + const libEdges = mi.filter((e) => e.sourceFilePath.includes('lib.rs')); + expect(libEdges.length).toBe(2); + const names = libEdges.map((e) => e.source).sort(); + expect(names).toEqual(['find', 'save']); + }); }); diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index 8534927b8..62fb75615 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -799,4 +799,12 @@ describe.skipIf(!swiftAvailable)('Swift abstract dispatch', () => { expect(sqlFind).toBeDefined(); expect(sqlFind!.properties.returnType).toBe('String'); }); + + it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edges = mi.filter((e) => e.sourceFilePath.includes('Repository.swift')); + expect(edges.length).toBe(2); + const names = edges.map((e) => e.source).sort(); + expect(names).toEqual(['find', 'save']); + }); }); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 00742c2cc..8d663d1cf 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -75,7 +75,7 @@ describe('TypeScript heritage resolution', () => { }); it('no OVERRIDES edges target Property nodes', () => { - const overrides = getRelationships(result, 'OVERRIDES'); + const overrides = getRelationships(result, 'METHOD_OVERRIDES'); for (const edge of overrides) { const target = result.graph.getNode(edge.rel.targetId); expect(target).toBeDefined(); diff --git a/gitnexus/test/unit/impact-confidence.test.ts b/gitnexus/test/unit/impact-confidence.test.ts index c7c3f8b53..b87356f63 100644 --- a/gitnexus/test/unit/impact-confidence.test.ts +++ b/gitnexus/test/unit/impact-confidence.test.ts @@ -35,7 +35,7 @@ describe('IMPACT_RELATION_CONFIDENCE', () => { }); it('OVERRIDES has confidence 0.85 (statically verifiable override)', () => { - expect(IMPACT_RELATION_CONFIDENCE['OVERRIDES']).toBe(0.85); + expect(IMPACT_RELATION_CONFIDENCE['METHOD_OVERRIDES']).toBe(0.85); }); it('HAS_METHOD has confidence 0.95 (structural containment)', () => { @@ -76,7 +76,7 @@ describe('confidenceForRelType', () => { expect(confidenceForRelType('IMPORTS')).toBe(0.9); expect(confidenceForRelType('EXTENDS')).toBe(0.85); expect(confidenceForRelType('IMPLEMENTS')).toBe(0.85); - expect(confidenceForRelType('OVERRIDES')).toBe(0.85); + expect(confidenceForRelType('METHOD_OVERRIDES')).toBe(0.85); expect(confidenceForRelType('HAS_METHOD')).toBe(0.95); expect(confidenceForRelType('HAS_PROPERTY')).toBe(0.95); expect(confidenceForRelType('ACCESSES')).toBe(0.8); diff --git a/gitnexus/test/unit/mro-processor.test.ts b/gitnexus/test/unit/mro-processor.test.ts index d071e41e2..6126fb74a 100644 --- a/gitnexus/test/unit/mro-processor.test.ts +++ b/gitnexus/test/unit/mro-processor.test.ts @@ -28,13 +28,18 @@ function addMethod( className: string, methodName: string, classLabel: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class', + parameterTypes?: string[], ) { const classId = generateId(classLabel, className); const methodId = generateId('Method', `${className}.${methodName}`); graph.addNode({ id: methodId, label: 'Method', - properties: { name: methodName, filePath: `src/${className}.ts` }, + properties: { + name: methodName, + filePath: `src/${className}.ts`, + ...(parameterTypes ? { parameterTypes } : {}), + }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${classId}->${methodId}`), @@ -128,7 +133,7 @@ describe('computeMRO', () => { // OVERRIDES edge emitted expect(result.overrideEdges).toBeGreaterThanOrEqual(1); - const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES'); + const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides.some((r) => r.sourceId === dId && r.targetId === bFoo)).toBe(true); }); @@ -296,7 +301,7 @@ describe('computeMRO', () => { // No OVERRIDES edge emitted for Rust ambiguity const overrides = graph.relationships.filter( - (r) => r.type === 'OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'), + (r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'), ); expect(overrides).toHaveLength(0); }); @@ -347,7 +352,7 @@ describe('computeMRO', () => { const result = computeMRO(graph); // No OVERRIDES edge should be emitted for properties - const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES'); + const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(0); expect(result.overrideEdges).toBe(0); }); @@ -399,7 +404,7 @@ describe('computeMRO', () => { const result = computeMRO(graph); // Only 1 OVERRIDES edge (for the method, not the property) - const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES'); + const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(1); expect(overrides[0].targetId).toBe(methodA); // leftmost base wins for C++ expect(result.overrideEdges).toBe(1); @@ -511,4 +516,222 @@ describe('computeMRO', () => { expect(result).toBeDefined(); }); }); + + // ---- METHOD_IMPLEMENTS edges ----------------------------------------------- + describe('METHOD_IMPLEMENTS edges', () => { + it('emits METHOD_IMPLEMENTS for class implementing interface method', () => { + // IAnimal { speak() } <-- Dog { speak() } + const graph = createKnowledgeGraph(); + addClass(graph, 'IAnimal', 'java', 'Interface'); + addClass(graph, 'Dog', 'java'); + addImplements(graph, 'Dog', 'IAnimal'); + const ifaceMethod = addMethod(graph, 'IAnimal', 'speak', 'Interface'); + const classMethod = addMethod(graph, 'Dog', 'speak'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(1); + + // Verify the edge exists: ConcreteMethod → InterfaceMethod + const edges: any[] = []; + graph.forEachRelationship((rel) => { + if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); + }); + expect(edges).toHaveLength(1); + expect(edges[0].sourceId).toBe(classMethod); + expect(edges[0].targetId).toBe(ifaceMethod); + expect(edges[0].confidence).toBe(1.0); + }); + + it('emits METHOD_IMPLEMENTS for Rust struct implementing trait', () => { + // Drawable { draw() } <-- Circle { draw() } + const graph = createKnowledgeGraph(); + addClass(graph, 'Drawable', 'rust', 'Trait'); + addClass(graph, 'Circle', 'rust', 'Struct'); + addImplements(graph, 'Circle', 'Drawable', 'Struct', 'Trait'); + const traitMethod = addMethod(graph, 'Drawable', 'draw', 'Trait'); + const structMethod = addMethod(graph, 'Circle', 'draw', 'Struct'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(1); + + const edges: any[] = []; + graph.forEachRelationship((rel) => { + if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); + }); + expect(edges[0].sourceId).toBe(structMethod); + expect(edges[0].targetId).toBe(traitMethod); + }); + + it('matches overloaded interface methods by parameterTypes', () => { + // IRepo { find(String), find(String, int) } <-- SqlRepo { find(String), find(String, int) } + const graph = createKnowledgeGraph(); + addClass(graph, 'IRepo', 'java', 'Interface'); + addClass(graph, 'SqlRepo', 'java'); + addImplements(graph, 'SqlRepo', 'IRepo'); + + // Use manual IDs to avoid overloaded-name collision (same name, different types) + const ifaceFind1 = generateId('Method', 'IRepo.find.1'); + graph.addNode({ + id: ifaceFind1, + label: 'Method', + properties: { name: 'find', filePath: 'src/IRepo.ts', parameterTypes: ['String'] }, + }); + graph.addRelationship({ + id: generateId('HAS_METHOD', `${generateId('Interface', 'IRepo')}->${ifaceFind1}`), + sourceId: generateId('Interface', 'IRepo'), + targetId: ifaceFind1, + type: 'HAS_METHOD', + confidence: 1.0, + reason: '', + }); + + const ifaceFind2 = generateId('Method', 'IRepo.find.2'); + graph.addNode({ + id: ifaceFind2, + label: 'Method', + properties: { name: 'find', filePath: 'src/IRepo.ts', parameterTypes: ['String', 'int'] }, + }); + graph.addRelationship({ + id: generateId('HAS_METHOD', `${generateId('Interface', 'IRepo')}->${ifaceFind2}`), + sourceId: generateId('Interface', 'IRepo'), + targetId: ifaceFind2, + type: 'HAS_METHOD', + confidence: 1.0, + reason: '', + }); + + const sqlFind1Id = generateId('Method', 'SqlRepo.find.1'); + graph.addNode({ + id: sqlFind1Id, + label: 'Method', + properties: { name: 'find', filePath: 'src/SqlRepo.ts', parameterTypes: ['String'] }, + }); + graph.addRelationship({ + id: generateId('HAS_METHOD', `${generateId('Class', 'SqlRepo')}->${sqlFind1Id}`), + sourceId: generateId('Class', 'SqlRepo'), + targetId: sqlFind1Id, + type: 'HAS_METHOD', + confidence: 1.0, + reason: '', + }); + + const sqlFind2Id = generateId('Method', 'SqlRepo.find.2'); + graph.addNode({ + id: sqlFind2Id, + label: 'Method', + properties: { name: 'find', filePath: 'src/SqlRepo.ts', parameterTypes: ['String', 'int'] }, + }); + graph.addRelationship({ + id: generateId('HAS_METHOD', `${generateId('Class', 'SqlRepo')}->${sqlFind2Id}`), + sourceId: generateId('Class', 'SqlRepo'), + targetId: sqlFind2Id, + type: 'HAS_METHOD', + confidence: 1.0, + reason: '', + }); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(2); + + const edges: any[] = []; + graph.forEachRelationship((rel) => { + if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); + }); + expect(edges).toHaveLength(2); + // find(String) → find(String) and find(String, int) → find(String, int) + const edge1 = edges.find((e) => e.targetId === ifaceFind1); + const edge2 = edges.find((e) => e.targetId === ifaceFind2); + expect(edge1).toBeDefined(); + expect(edge1!.sourceId).toBe(sqlFind1Id); + expect(edge2).toBeDefined(); + expect(edge2!.sourceId).toBe(sqlFind2Id); + }); + + it('includes default interface methods (not just abstract)', () => { + // Java 8 default method: IFoo { bar() } <-- Baz { bar() } + const graph = createKnowledgeGraph(); + addClass(graph, 'IFoo', 'java', 'Interface'); + addClass(graph, 'Baz', 'java'); + addImplements(graph, 'Baz', 'IFoo'); + // Default method (has body, not abstract) — should still get METHOD_IMPLEMENTS + addMethod(graph, 'IFoo', 'bar', 'Interface'); + addMethod(graph, 'Baz', 'bar'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(1); + }); + + it('does not emit METHOD_IMPLEMENTS for class extending another class', () => { + // Animal { speak() } <-- Dog { speak() } — EXTENDS, not IMPLEMENTS + const graph = createKnowledgeGraph(); + addClass(graph, 'Animal', 'java'); + addClass(graph, 'Dog', 'java'); + addExtends(graph, 'Dog', 'Animal'); + addMethod(graph, 'Animal', 'speak'); + addMethod(graph, 'Dog', 'speak'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(0); + }); + + it('does not emit METHOD_IMPLEMENTS when class has no matching method', () => { + // IAnimal { speak() } <-- Dog { bark() } — no name match + const graph = createKnowledgeGraph(); + addClass(graph, 'IAnimal', 'java', 'Interface'); + addClass(graph, 'Dog', 'java'); + addImplements(graph, 'Dog', 'IAnimal'); + addMethod(graph, 'IAnimal', 'speak', 'Interface'); + addMethod(graph, 'Dog', 'bark'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(0); + }); + + it('skips Property nodes on interface', () => { + const graph = createKnowledgeGraph(); + addClass(graph, 'IFoo', 'csharp', 'Interface'); + addClass(graph, 'Bar', 'csharp'); + addImplements(graph, 'Bar', 'IFoo'); + + // Add a Property to the interface (not a Method) + const propId = generateId('Property', 'IFoo.name'); + graph.addNode({ + id: propId, + label: 'Property', + properties: { name: 'name', filePath: 'src/IFoo.ts' }, + }); + graph.addRelationship({ + id: generateId('HAS_METHOD', `${generateId('Interface', 'IFoo')}->${propId}`), + sourceId: generateId('Interface', 'IFoo'), + targetId: propId, + type: 'HAS_METHOD', + confidence: 1.0, + reason: '', + }); + addMethod(graph, 'Bar', 'name'); + + const result = computeMRO(graph); + expect(result.methodImplementsEdges).toBe(0); + }); + + it('is queryable via MATCH pattern', () => { + const graph = createKnowledgeGraph(); + addClass(graph, 'IRepo', 'typescript', 'Interface'); + addClass(graph, 'SqlRepo', 'typescript'); + addImplements(graph, 'SqlRepo', 'IRepo'); + addMethod(graph, 'IRepo', 'fetch', 'Interface'); + const concreteId = addMethod(graph, 'SqlRepo', 'fetch'); + + computeMRO(graph); + + // Simulate MATCH (m)-[:METHOD_IMPLEMENTS]->(i) RETURN m + const implementingMethods: string[] = []; + graph.forEachRelationship((rel) => { + if (rel.type === 'METHOD_IMPLEMENTS') { + implementingMethods.push(rel.sourceId); + } + }); + expect(implementingMethods).toContain(concreteId); + }); + }); }); diff --git a/gitnexus/test/unit/security.test.ts b/gitnexus/test/unit/security.test.ts index 7d16880ae..d17441ff3 100644 --- a/gitnexus/test/unit/security.test.ts +++ b/gitnexus/test/unit/security.test.ts @@ -114,7 +114,7 @@ describe('VALID_RELATION_TYPES', () => { 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', - 'OVERRIDES', + 'METHOD_OVERRIDES', 'ACCESSES', 'HANDLES_ROUTE', 'FETCHES',