GitNexus/gitnexus/test/unit/mro-processor.test.ts

1941 lines
74 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { computeMRO } from '../../src/core/ingestion/mro-processor.js';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { KnowledgeGraph } from '../../src/core/graph/types.js';
import { generateId } from '../../src/lib/utils.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function addClass(
graph: KnowledgeGraph,
name: string,
language: string,
label: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class',
) {
const id = generateId(label, name);
graph.addNode({
id,
label,
properties: { name, filePath: `src/${name}.ts`, language },
});
return id;
}
function addMethod(
graph: KnowledgeGraph,
className: string,
methodName: string,
classLabel: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class',
parameterTypes?: string[],
opts?: { isAbstract?: boolean; parameterCount?: number },
) {
// Derive arity for the ID suffix: explicit parameterCount > parameterTypes.length > 0
const arity = opts?.parameterCount ?? parameterTypes?.length ?? 0;
const classId = generateId(classLabel, className);
const methodId = generateId('Method', `${className}.${methodName}#${arity}`);
graph.addNode({
id: methodId,
label: 'Method',
properties: {
name: methodName,
filePath: `src/${className}.ts`,
parameterCount: arity,
...(parameterTypes ? { parameterTypes } : {}),
...(opts?.isAbstract !== undefined ? { isAbstract: opts.isAbstract } : {}),
},
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${classId}->${methodId}`),
sourceId: classId,
targetId: methodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
return methodId;
}
function addExtends(
graph: KnowledgeGraph,
childName: string,
parentName: string,
childLabel: 'Class' | 'Struct' = 'Class',
parentLabel: 'Class' | 'Interface' | 'Trait' = 'Class',
) {
const childId = generateId(childLabel, childName);
const parentId = generateId(parentLabel, parentName);
graph.addRelationship({
id: generateId('EXTENDS', `${childId}->${parentId}`),
sourceId: childId,
targetId: parentId,
type: 'EXTENDS',
confidence: 1.0,
reason: '',
});
}
function addInterfaceExtends(
graph: KnowledgeGraph,
childName: string,
parentName: string,
childLabel: 'Interface' | 'Trait' = 'Interface',
parentLabel: 'Interface' | 'Trait' = 'Interface',
) {
const childId = generateId(childLabel, childName);
const parentId = generateId(parentLabel, parentName);
graph.addRelationship({
id: generateId('EXTENDS', `${childId}->${parentId}`),
sourceId: childId,
targetId: parentId,
type: 'EXTENDS',
confidence: 1.0,
reason: '',
});
}
function addImplements(
graph: KnowledgeGraph,
childName: string,
parentName: string,
childLabel: 'Class' | 'Struct' = 'Class',
parentLabel: 'Interface' | 'Trait' = 'Interface',
) {
const childId = generateId(childLabel, childName);
const parentId = generateId(parentLabel, parentName);
graph.addRelationship({
id: generateId('IMPLEMENTS', `${childId}->${parentId}`),
sourceId: childId,
targetId: parentId,
type: 'IMPLEMENTS',
confidence: 1.0,
reason: '',
});
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('computeMRO', () => {
// ---- C++ diamond --------------------------------------------------------
describe('C++ diamond inheritance', () => {
it('leftmost base wins when both B and C override foo', () => {
// Diamond: A <- B, A <- C, B <- D, C <- D
const graph = createKnowledgeGraph();
const aId = addClass(graph, 'A', 'cpp');
const bId = addClass(graph, 'B', 'cpp');
const cId = addClass(graph, 'C', 'cpp');
const dId = addClass(graph, 'D', 'cpp');
addExtends(graph, 'B', 'A');
addExtends(graph, 'C', 'A');
addExtends(graph, 'D', 'B'); // B is leftmost
addExtends(graph, 'D', 'C');
// A has foo, B overrides foo, C overrides foo
addMethod(graph, 'A', 'foo');
const bFoo = addMethod(graph, 'B', 'foo');
const cFoo = addMethod(graph, 'C', 'foo');
const result = computeMRO(graph);
// D should have an entry with ambiguity on foo
const dEntry = result.entries.find((e) => e.className === 'D');
expect(dEntry).toBeDefined();
expect(dEntry!.language).toBe('cpp');
const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo');
expect(fooAmbiguity).toBeDefined();
expect(fooAmbiguity!.definedIn.length).toBeGreaterThanOrEqual(2);
// Leftmost base (B) wins
expect(fooAmbiguity!.resolvedTo).toBe(bFoo);
expect(fooAmbiguity!.reason).toContain('leftmost base');
expect(fooAmbiguity!.reason).toContain('B');
// OVERRIDES edge emitted
expect(result.overrideEdges).toBeGreaterThanOrEqual(1);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides.some((r) => r.sourceId === dId && r.targetId === bFoo)).toBe(true);
});
it('no ambiguity when foo only in A (diamond no override)', () => {
// Diamond: A <- B, A <- C, B <- D, C <- D, but only A has foo
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'cpp');
addClass(graph, 'B', 'cpp');
addClass(graph, 'C', 'cpp');
addClass(graph, 'D', 'cpp');
addExtends(graph, 'B', 'A');
addExtends(graph, 'C', 'A');
addExtends(graph, 'D', 'B');
addExtends(graph, 'D', 'C');
// Only A has foo
addMethod(graph, 'A', 'foo');
const result = computeMRO(graph);
const dEntry = result.entries.find((e) => e.className === 'D');
expect(dEntry).toBeDefined();
// A::foo appears only once across ancestors — no collision
// (B and C don't have their own foo, the duplicate is A::foo seen through both paths)
const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo');
expect(fooAmbiguity).toBeUndefined();
});
});
// ---- C# class + interface -----------------------------------------------
describe('C# class + interface', () => {
it('class method beats interface default', () => {
const graph = createKnowledgeGraph();
const classId = addClass(graph, 'MyClass', 'csharp');
const baseId = addClass(graph, 'BaseClass', 'csharp');
const ifaceId = addClass(graph, 'IDoSomething', 'csharp', 'Interface');
addExtends(graph, 'MyClass', 'BaseClass');
addImplements(graph, 'MyClass', 'IDoSomething');
const baseDoIt = addMethod(graph, 'BaseClass', 'doIt');
const ifaceDoIt = addMethod(graph, 'IDoSomething', 'doIt', 'Interface');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'MyClass');
expect(entry).toBeDefined();
const doItAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'doIt');
expect(doItAmbiguity).toBeDefined();
// Class method wins
expect(doItAmbiguity!.resolvedTo).toBe(baseDoIt);
expect(doItAmbiguity!.reason).toContain('class method wins');
});
it('multiple interface methods with same name are ambiguous', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'MyClass', 'csharp');
addClass(graph, 'IFoo', 'csharp', 'Interface');
addClass(graph, 'IBar', 'csharp', 'Interface');
addImplements(graph, 'MyClass', 'IFoo');
addImplements(graph, 'MyClass', 'IBar');
addMethod(graph, 'IFoo', 'process', 'Interface');
addMethod(graph, 'IBar', 'process', 'Interface');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'MyClass');
expect(entry).toBeDefined();
const processAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'process');
expect(processAmbiguity).toBeDefined();
expect(processAmbiguity!.resolvedTo).toBeNull();
expect(processAmbiguity!.reason).toContain('ambiguous');
expect(result.ambiguityCount).toBeGreaterThanOrEqual(1);
});
});
// ---- Python C3 ----------------------------------------------------------
describe('Python C3 linearization', () => {
it('C3 order determines winner in diamond with overrides', () => {
// Diamond: A <- B, A <- C, B <- D, C <- D
// class D(B, C) → C3 MRO: B, C, A
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'python');
addClass(graph, 'B', 'python');
addClass(graph, 'C', 'python');
const dId = addClass(graph, 'D', 'python');
addExtends(graph, 'B', 'A');
addExtends(graph, 'C', 'A');
addExtends(graph, 'D', 'B'); // B first → leftmost in C3
addExtends(graph, 'D', 'C');
addMethod(graph, 'A', 'foo');
const bFoo = addMethod(graph, 'B', 'foo');
addMethod(graph, 'C', 'foo');
const result = computeMRO(graph);
const dEntry = result.entries.find((e) => e.className === 'D');
expect(dEntry).toBeDefined();
const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo');
expect(fooAmbiguity).toBeDefined();
// C3 linearization for D(B, C): B comes first
expect(fooAmbiguity!.resolvedTo).toBe(bFoo);
expect(fooAmbiguity!.reason).toContain('C3 MRO');
});
});
// ---- Java class + interface ---------------------------------------------
describe('Java class + interface', () => {
it('class method beats interface default', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Service', 'java');
addClass(graph, 'BaseService', 'java');
addClass(graph, 'Runnable', 'java', 'Interface');
addExtends(graph, 'Service', 'BaseService');
addImplements(graph, 'Service', 'Runnable');
const baseRun = addMethod(graph, 'BaseService', 'run');
addMethod(graph, 'Runnable', 'run', 'Interface');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'Service');
expect(entry).toBeDefined();
const runAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'run');
expect(runAmbiguity).toBeDefined();
expect(runAmbiguity!.resolvedTo).toBe(baseRun);
expect(runAmbiguity!.reason).toContain('class method wins');
});
});
// ---- Rust trait conflicts -----------------------------------------------
describe('Rust trait conflicts', () => {
it('trait conflicts result in null resolution with qualified syntax reason', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'MyStruct', 'rust', 'Struct');
addClass(graph, 'TraitA', 'rust', 'Trait');
addClass(graph, 'TraitB', 'rust', 'Trait');
addImplements(graph, 'MyStruct', 'TraitA', 'Struct', 'Trait');
addImplements(graph, 'MyStruct', 'TraitB', 'Struct', 'Trait');
addMethod(graph, 'TraitA', 'execute', 'Trait');
addMethod(graph, 'TraitB', 'execute', 'Trait');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'MyStruct');
expect(entry).toBeDefined();
const execAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'execute');
expect(execAmbiguity).toBeDefined();
expect(execAmbiguity!.resolvedTo).toBeNull();
expect(execAmbiguity!.reason).toContain('qualified syntax');
expect(result.ambiguityCount).toBeGreaterThanOrEqual(1);
// No OVERRIDES edge emitted for Rust ambiguity
const overrides = graph.relationships.filter(
(r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'),
);
expect(overrides).toHaveLength(0);
});
});
// ---- Property collisions don't trigger OVERRIDES ------------------------
describe('Property nodes excluded from OVERRIDES', () => {
it('property name collision across parents does not emit OVERRIDES edge', () => {
const graph = createKnowledgeGraph();
const parentA = addClass(graph, 'ParentA', 'typescript');
const parentB = addClass(graph, 'ParentB', 'typescript');
const child = addClass(graph, 'Child', 'typescript');
addExtends(graph, 'Child', 'ParentA');
addExtends(graph, 'Child', 'ParentB');
// Add Property nodes (same name 'name') to both parents via HAS_PROPERTY
const propA = generateId('Property', 'ParentA.name');
graph.addNode({
id: propA,
label: 'Property',
properties: { name: 'name', filePath: 'src/ParentA.ts' },
});
graph.addRelationship({
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
sourceId: parentA,
targetId: propA,
type: 'HAS_PROPERTY',
confidence: 1.0,
reason: '',
});
const propB = generateId('Property', 'ParentB.name');
graph.addNode({
id: propB,
label: 'Property',
properties: { name: 'name', filePath: 'src/ParentB.ts' },
});
graph.addRelationship({
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
sourceId: parentB,
targetId: propB,
type: 'HAS_PROPERTY',
confidence: 1.0,
reason: '',
});
const result = computeMRO(graph);
// No OVERRIDES edge should be emitted for properties
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(0);
expect(result.overrideEdges).toBe(0);
});
it('method collision still triggers OVERRIDES even when properties also collide', () => {
const graph = createKnowledgeGraph();
const parentA = addClass(graph, 'PA', 'cpp');
const parentB = addClass(graph, 'PB', 'cpp');
addClass(graph, 'Ch', 'cpp');
addExtends(graph, 'Ch', 'PA');
addExtends(graph, 'Ch', 'PB');
// Method collision (should trigger OVERRIDES)
const methodA = addMethod(graph, 'PA', 'doWork');
addMethod(graph, 'PB', 'doWork');
// Property collision (should NOT trigger OVERRIDES — properties use HAS_PROPERTY, not HAS_METHOD)
const propA = generateId('Property', 'PA.id');
graph.addNode({
id: propA,
label: 'Property',
properties: { name: 'id', filePath: 'src/PA.ts' },
});
graph.addRelationship({
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
sourceId: parentA,
targetId: propA,
type: 'HAS_PROPERTY',
confidence: 1.0,
reason: '',
});
const propB = generateId('Property', 'PB.id');
graph.addNode({
id: propB,
label: 'Property',
properties: { name: 'id', filePath: 'src/PB.ts' },
});
graph.addRelationship({
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
sourceId: parentB,
targetId: propB,
type: 'HAS_PROPERTY',
confidence: 1.0,
reason: '',
});
const result = computeMRO(graph);
// Only 1 OVERRIDES edge (for the method, not the property)
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);
});
});
// ---- No ambiguity: single parent ----------------------------------------
describe('single parent, no ambiguity', () => {
it('single parent with unique methods produces no ambiguities', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Parent', 'typescript');
addClass(graph, 'Child', 'typescript');
addExtends(graph, 'Child', 'Parent');
addMethod(graph, 'Parent', 'foo');
addMethod(graph, 'Parent', 'bar');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'Child');
expect(entry).toBeDefined();
expect(entry!.ambiguities).toHaveLength(0);
});
});
// ---- No parents: standalone class not in entries ------------------------
describe('standalone class', () => {
it('class with no parents is not included in entries', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Standalone', 'typescript');
addMethod(graph, 'Standalone', 'doStuff');
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'Standalone');
expect(entry).toBeUndefined();
expect(result.overrideEdges).toBe(0);
expect(result.ambiguityCount).toBe(0);
});
});
// ---- Own method shadows ancestor ----------------------------------------
describe('own method shadows ancestor', () => {
it('class defining its own method suppresses ambiguity', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Base1', 'cpp');
addClass(graph, 'Base2', 'cpp');
addClass(graph, 'Child', 'cpp');
addExtends(graph, 'Child', 'Base1');
addExtends(graph, 'Child', 'Base2');
addMethod(graph, 'Base1', 'foo');
addMethod(graph, 'Base2', 'foo');
addMethod(graph, 'Child', 'foo'); // own method
const result = computeMRO(graph);
const entry = result.entries.find((e) => e.className === 'Child');
expect(entry).toBeDefined();
// No ambiguity because Child defines its own foo
const fooAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'foo');
expect(fooAmbiguity).toBeUndefined();
});
});
// ---- Single-ancestor override detection ---------------------------------
describe('single-ancestor override detection', () => {
it('emits one Class→Method edge when a subclass overrides a parent method', () => {
const graph = createKnowledgeGraph();
const childId = addClass(graph, 'B', 'java');
addClass(graph, 'A', 'java');
addExtends(graph, 'B', 'A');
const aFoo = addMethod(graph, 'A', 'foo');
addMethod(graph, 'B', 'foo'); // B overrides A.foo
const result = computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(1);
expect(overrides[0].sourceId).toBe(childId); // child CLASS is the source
expect(overrides[0].targetId).toBe(aFoo); // ancestor METHOD node, not the class
expect(overrides[0].confidence).toBe(0.9);
expect(result.overrideEdges).toBe(1);
});
it('emits a distinct edge per overridden method of the same parent', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'B', 'java');
addClass(graph, 'A', 'java');
addExtends(graph, 'B', 'A');
const aFoo = addMethod(graph, 'A', 'foo');
const aBar = addMethod(graph, 'A', 'bar');
addMethod(graph, 'B', 'foo');
addMethod(graph, 'B', 'bar');
const result = computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(2);
expect(overrides.map((r) => r.targetId).sort()).toEqual([aBar, aFoo].sort());
expect(result.overrideEdges).toBe(2); // counter matches edges actually added
});
it('does not treat a same-named overload (different arity) as an override', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'B', 'java');
addClass(graph, 'A', 'java');
addExtends(graph, 'B', 'A');
addMethod(graph, 'A', 'foo', 'Class', undefined, { parameterCount: 0 });
addMethod(graph, 'B', 'foo', 'Class', undefined, { parameterCount: 1 }); // foo(x) — overload
const result = computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(0);
expect(result.overrideEdges).toBe(0);
});
it('targets the nearest ancestor when multiple levels define the method', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'C', 'java');
addClass(graph, 'B', 'java');
addClass(graph, 'A', 'java');
addExtends(graph, 'C', 'B');
addExtends(graph, 'B', 'A');
addMethod(graph, 'A', 'foo');
const bFoo = addMethod(graph, 'B', 'foo'); // nearest ancestor of C with foo
addMethod(graph, 'C', 'foo');
computeMRO(graph);
const cOverride = graph.relationships.find(
(r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Class', 'C'),
);
expect(cOverride).toBeDefined();
expect(cOverride!.targetId).toBe(bFoo); // nearest (B), not A
});
it('does not emit overrides for qualified-syntax languages (Rust)', () => {
const graph = createKnowledgeGraph();
// Identical class-inheritance shape to the Java happy-path; the Rust
// (qualified-syntax) gate must suppress it.
addClass(graph, 'Derived', 'rust');
addClass(graph, 'Base', 'rust');
addExtends(graph, 'Derived', 'Base');
addMethod(graph, 'Base', 'run');
addMethod(graph, 'Derived', 'run');
const result = computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(0);
expect(result.overrideEdges).toBe(0);
});
it('does not emit an override for a same-named interface method (implements, not extends)', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Impl', 'java');
addClass(graph, 'Iface', 'java', 'Interface');
addImplements(graph, 'Impl', 'Iface');
addMethod(graph, 'Iface', 'run', 'Interface');
addMethod(graph, 'Impl', 'run');
computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(0); // interface satisfaction is METHOD_IMPLEMENTS, not an override
});
it('overrideEdges count equals the number of METHOD_OVERRIDES relationships', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'B', 'java');
addClass(graph, 'A', 'java');
addExtends(graph, 'B', 'A');
addMethod(graph, 'A', 'foo');
addMethod(graph, 'A', 'bar');
addMethod(graph, 'B', 'foo');
addMethod(graph, 'B', 'bar');
const result = computeMRO(graph);
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(result.overrideEdges).toBe(overrides.length);
});
});
// ---- Empty graph --------------------------------------------------------
describe('empty graph', () => {
it('returns empty result for graph with no classes', () => {
const graph = createKnowledgeGraph();
const result = computeMRO(graph);
expect(result.entries).toHaveLength(0);
expect(result.overrideEdges).toBe(0);
expect(result.ambiguityCount).toBe(0);
});
});
// ---- Cyclic inheritance (P1 fix) ----------------------------------------
describe('cyclic inheritance', () => {
it('does not stack overflow on cyclic Python hierarchy', () => {
// A extends B, B extends A — cyclic
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'python');
addClass(graph, 'B', 'python');
addExtends(graph, 'A', 'B');
addExtends(graph, 'B', 'A');
addMethod(graph, 'A', 'foo');
addMethod(graph, 'B', 'foo');
// Should NOT throw — c3Linearize returns null, falls back to BFS
const result = computeMRO(graph);
expect(result).toBeDefined();
// Both A and B have parents, so both get entries
expect(result.entries.length).toBeGreaterThanOrEqual(1);
});
it('handles 3-node cycle gracefully', () => {
// A → B → C → A
const graph = createKnowledgeGraph();
addClass(graph, 'X', 'python');
addClass(graph, 'Y', 'python');
addClass(graph, 'Z', 'python');
addExtends(graph, 'X', 'Y');
addExtends(graph, 'Y', 'Z');
addExtends(graph, 'Z', 'X');
const result = computeMRO(graph);
expect(result).toBeDefined();
});
it('returns null for C3 merge-conflict inconsistency (non-cyclic)', () => {
// Classic incompatible ordering: A(X,Y) and B(Y,X) → C(A,B) is unresolvable
const graph = createKnowledgeGraph();
addClass(graph, 'X', 'python');
addClass(graph, 'Y', 'python');
addClass(graph, 'A', 'python');
addClass(graph, 'B', 'python');
addClass(graph, 'C', 'python');
addExtends(graph, 'A', 'X');
addExtends(graph, 'A', 'Y');
addExtends(graph, 'B', 'Y');
addExtends(graph, 'B', 'X');
addExtends(graph, 'C', 'A');
addExtends(graph, 'C', 'B');
addMethod(graph, 'X', 'foo');
const result = computeMRO(graph);
expect(result).toBeDefined();
// C3 fails for C — falls back to BFS ancestors
const entryC = result.entries.find((e) => e.className === 'C');
expect(entryC).toBeDefined();
// BFS fallback still produces an MRO (just not C3-ordered)
expect(entryC!.mro.length).toBeGreaterThanOrEqual(2);
});
});
// ---- Performance (deep chains) -------------------------------------------
describe('performance', () => {
it('handles very deep single-inheritance chain without stack overflow', () => {
const graph = createKnowledgeGraph();
const DEPTH = 2000;
for (let i = 0; i < DEPTH; i++) {
addClass(graph, `C${i}`, 'python');
}
for (let i = 1; i < DEPTH; i++) {
addExtends(graph, `C${i}`, `C${i - 1}`);
}
addMethod(graph, 'C0', 'baseMethod');
const result = computeMRO(graph);
expect(result).toBeDefined();
const deepest = result.entries.find((e) => e.className === `C${DEPTH - 1}`);
if (deepest) {
expect(deepest.mro.length).toBe(DEPTH - 1);
}
}, 15_000);
});
// ---- 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);
// Both sides have parameterCount=0 (arity match) → confidence 1.0
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'],
parameterCount: 1,
},
});
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'],
parameterCount: 2,
},
});
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'],
parameterCount: 1,
},
});
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'],
parameterCount: 2,
},
});
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);
});
describe('METHOD_IMPLEMENTS transitive ancestors', () => {
it('transitive interface chain: C.foo links to both B.foo and A.foo', () => {
// A (Interface) has foo, B (Interface) has foo extends A, C (Class) implements B
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'java', 'Interface');
addClass(graph, 'B', 'java', 'Interface');
addClass(graph, 'C', 'java');
addInterfaceExtends(graph, 'B', 'A');
addImplements(graph, 'C', 'B');
const aFoo = addMethod(graph, 'A', 'foo', 'Interface');
const bFoo = addMethod(graph, 'B', 'foo', 'Interface');
addMethod(graph, 'C', 'foo');
const result = computeMRO(graph);
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
// C.foo should link to both B.foo and A.foo
expect(edges.some((e) => e.targetId === bFoo)).toBe(true);
expect(edges.some((e) => e.targetId === aFoo)).toBe(true);
expect(result.methodImplementsEdges).toBeGreaterThanOrEqual(2);
});
it('inherited contract method only on grandparent: C.bar links to A.bar', () => {
// A (Interface) has bar, B (Interface) extends A but has NO bar, C implements B
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'java', 'Interface');
addClass(graph, 'B', 'java', 'Interface');
addClass(graph, 'C', 'java');
addInterfaceExtends(graph, 'B', 'A');
addImplements(graph, 'C', 'B');
const aBar = addMethod(graph, 'A', 'bar', 'Interface');
// B has no bar method
addMethod(graph, 'C', 'bar');
const result = computeMRO(graph);
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
// C.bar should link to A.bar even though A is not a direct parent
expect(edges.some((e) => e.targetId === aBar)).toBe(true);
expect(result.methodImplementsEdges).toBeGreaterThanOrEqual(1);
});
it('diamond deduplication: E.foo gets exactly one edge to A.foo', () => {
// A (Interface) has foo
// B (Interface) has foo, extends A
// D (Interface) has foo, extends A
// E (Class) implements B and D
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'java', 'Interface');
addClass(graph, 'B', 'java', 'Interface');
addClass(graph, 'D', 'java', 'Interface');
addClass(graph, 'E', 'java');
addInterfaceExtends(graph, 'B', 'A');
addInterfaceExtends(graph, 'D', 'A');
addImplements(graph, 'E', 'B');
addImplements(graph, 'E', 'D');
const aFoo = addMethod(graph, 'A', 'foo', 'Interface');
const bFoo = addMethod(graph, 'B', 'foo', 'Interface');
const dFoo = addMethod(graph, 'D', 'foo', 'Interface');
addMethod(graph, 'E', 'foo');
const result = computeMRO(graph);
const eFoo = generateId('Method', 'E.foo#0');
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
// Filter to only edges FROM E.foo
const eFooEdges = edges.filter((e) => e.sourceId === eFoo);
// E.foo should link to B.foo, D.foo, and exactly ONE A.foo (deduplicated)
expect(eFooEdges.filter((e) => e.targetId === bFoo)).toHaveLength(1);
expect(eFooEdges.filter((e) => e.targetId === dFoo)).toHaveLength(1);
expect(eFooEdges.filter((e) => e.targetId === aFoo)).toHaveLength(1);
// Total from E.foo: 3 edges (B.foo + D.foo + A.foo), not 4
expect(eFooEdges).toHaveLength(3);
});
it('no transitive through class-only chain', () => {
// A (Class) has foo, B (Class) extends A has foo, C (Class) extends B has foo
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'java');
addClass(graph, 'B', 'java');
addClass(graph, 'C', 'java');
addExtends(graph, 'B', 'A');
addExtends(graph, 'C', 'B');
addMethod(graph, 'A', 'foo');
addMethod(graph, 'B', 'foo');
addMethod(graph, 'C', 'foo');
const result = computeMRO(graph);
// All class-extends, no interface involved → 0 METHOD_IMPLEMENTS edges
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);
});
describe('METHOD_IMPLEMENTS inherited + arity matching', () => {
it('inherited implementation: Base.foo satisfies I.foo when C has no own foo', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Base', 'java');
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'C', 'java');
addExtends(graph, 'C', 'Base');
addImplements(graph, 'C', 'I');
const baseFoo = addMethod(graph, 'Base', 'foo');
const iFoo = addMethod(graph, 'I', 'foo', 'Interface');
const result = computeMRO(graph);
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(baseFoo);
expect(edges[0].targetId).toBe(iFoo);
expect(result.methodImplementsEdges).toBe(1);
});
it('class has own method — no inherited lookup needed', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'Base2', 'java');
addClass(graph, 'I2', 'java', 'Interface');
addClass(graph, 'C2', 'java');
addExtends(graph, 'C2', 'Base2');
addImplements(graph, 'C2', 'I2');
const baseFoo = addMethod(graph, 'Base2', 'foo');
const iFoo = addMethod(graph, 'I2', 'foo', 'Interface');
const cFoo = addMethod(graph, 'C2', 'foo');
const result = computeMRO(graph);
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
// Should use C2.foo, not Base2.foo
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cFoo);
expect(edges[0].targetId).toBe(iFoo);
});
it('deep inheritance chain: GrandBase.foo satisfies I.foo', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'GrandBase', 'java');
addClass(graph, 'Base3', 'java');
addClass(graph, 'I3', 'java', 'Interface');
addClass(graph, 'C3', 'java');
addExtends(graph, 'Base3', 'GrandBase');
addExtends(graph, 'C3', 'Base3');
addImplements(graph, 'C3', 'I3');
const grandFoo = addMethod(graph, 'GrandBase', 'foo');
// Base3 has NO foo
const iFoo = addMethod(graph, 'I3', 'foo', 'Interface');
const result = computeMRO(graph);
const edges: any[] = [];
graph.forEachRelationship((rel) => {
if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel);
});
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(grandFoo);
expect(edges[0].targetId).toBe(iFoo);
expect(result.methodImplementsEdges).toBe(1);
});
it('arity mismatch prevents false match', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IArity', 'java', 'Interface');
addClass(graph, 'CArity', 'java');
addImplements(graph, 'CArity', 'IArity');
// Interface method: parameterCount=2, no parameterTypes
const iMethodId = generateId('Method', 'IArity.process#2');
graph.addNode({
id: iMethodId,
label: 'Method',
properties: { name: 'process', filePath: 'src/IArity.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'IArity')}->${iMethodId}`),
sourceId: generateId('Interface', 'IArity'),
targetId: iMethodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
// Class method: parameterCount=3, no parameterTypes
const cMethodId = generateId('Method', 'CArity.process#3');
graph.addNode({
id: cMethodId,
label: 'Method',
properties: { name: 'process', filePath: 'src/CArity.ts', parameterCount: 3 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CArity')}->${cMethodId}`),
sourceId: generateId('Class', 'CArity'),
targetId: cMethodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const result = computeMRO(graph);
expect(result.methodImplementsEdges).toBe(0);
});
it('arity match when types missing', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IArityOk', 'java', 'Interface');
addClass(graph, 'CArityOk', 'java');
addImplements(graph, 'CArityOk', 'IArityOk');
// Interface method: parameterCount=2, no parameterTypes
const iMethodId = generateId('Method', 'IArityOk.process#2');
graph.addNode({
id: iMethodId,
label: 'Method',
properties: { name: 'process', filePath: 'src/IArityOk.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'IArityOk')}->${iMethodId}`),
sourceId: generateId('Interface', 'IArityOk'),
targetId: iMethodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
// Class method: parameterCount=2, no parameterTypes
const cMethodId = generateId('Method', 'CArityOk.process#2');
graph.addNode({
id: cMethodId,
label: 'Method',
properties: { name: 'process', filePath: 'src/CArityOk.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CArityOk')}->${cMethodId}`),
sourceId: generateId('Class', 'CArityOk'),
targetId: cMethodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const result = computeMRO(graph);
expect(result.methodImplementsEdges).toBe(1);
});
it('multiple same-arity candidates = ambiguous, no edge emitted', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IAmbig', 'java', 'Interface');
addClass(graph, 'CAmbig', 'java');
addImplements(graph, 'CAmbig', 'IAmbig');
// Interface method: parameterCount=1, no parameterTypes
const iMethodId = generateId('Method', 'IAmbig.handle#1');
graph.addNode({
id: iMethodId,
label: 'Method',
properties: { name: 'handle', filePath: 'src/IAmbig.ts', parameterCount: 1 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'IAmbig')}->${iMethodId}`),
sourceId: generateId('Interface', 'IAmbig'),
targetId: iMethodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
// Two class methods named handle, both with parameterCount=1
const cMethod1 = generateId('Method', 'CAmbig.handle.1#1');
graph.addNode({
id: cMethod1,
label: 'Method',
properties: { name: 'handle', filePath: 'src/CAmbig.ts', parameterCount: 1 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CAmbig')}->${cMethod1}`),
sourceId: generateId('Class', 'CAmbig'),
targetId: cMethod1,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const cMethod2 = generateId('Method', 'CAmbig.handle.2#1');
graph.addNode({
id: cMethod2,
label: 'Method',
properties: { name: 'handle', filePath: 'src/CAmbig.ts', parameterCount: 1 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CAmbig')}->${cMethod2}`),
sourceId: generateId('Class', 'CAmbig'),
targetId: cMethod2,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const result = computeMRO(graph);
expect(result.methodImplementsEdges).toBe(0);
});
});
});
// ---- findInheritedMethod ambiguity detection ------------------------------
describe('findInheritedMethod ambiguity', () => {
it('returns null when two EXTENDS parents both provide matching method', () => {
// I { foo() }, B { foo() }, M { foo() }, C extends B + M, C implements I
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'cpp', 'Interface');
addClass(graph, 'B', 'cpp');
addClass(graph, 'M', 'cpp');
addClass(graph, 'C', 'cpp');
addImplements(graph, 'C', 'I');
addExtends(graph, 'C', 'B');
addExtends(graph, 'C', 'M');
addMethod(graph, 'I', 'foo', 'Interface');
addMethod(graph, 'B', 'foo');
addMethod(graph, 'M', 'foo');
// C has NO own foo — must walk EXTENDS chain
const result = computeMRO(graph);
// Ambiguous: B.foo and M.foo both match — no METHOD_IMPLEMENTS edge
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const fooEdges = mi.filter((e) => graph.getNode(e.targetId)?.properties.name === 'foo');
expect(fooEdges).toHaveLength(0);
});
it('diamond dedup: same method via two paths is NOT ambiguous', () => {
// I { foo() }, GrandBase { foo() }, B extends GrandBase, M extends GrandBase
// C extends B + M, C implements I
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'cpp', 'Interface');
addClass(graph, 'GrandBase', 'cpp');
addClass(graph, 'B', 'cpp');
addClass(graph, 'M', 'cpp');
addClass(graph, 'C', 'cpp');
addImplements(graph, 'C', 'I');
addExtends(graph, 'C', 'B');
addExtends(graph, 'C', 'M');
addExtends(graph, 'B', 'GrandBase');
addExtends(graph, 'M', 'GrandBase');
addMethod(graph, 'I', 'foo', 'Interface');
const gbFoo = addMethod(graph, 'GrandBase', 'foo');
// B and M have NO own foo — both inherit from GrandBase
const result = computeMRO(graph);
// Not ambiguous: same GrandBase.foo via both paths
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const fooEdge = mi.find((e) => e.sourceId === gbFoo);
expect(fooEdge).toBeDefined();
});
it('C extends B extends A, B and A both have foo → returns B.foo (nearest)', () => {
// I { foo() }, A { foo() }, B extends A { foo() }, C extends B implements I { no foo }
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'A', 'java');
addClass(graph, 'B', 'java');
addClass(graph, 'C', 'java');
addImplements(graph, 'C', 'I');
addExtends(graph, 'C', 'B');
addExtends(graph, 'B', 'A');
addMethod(graph, 'I', 'foo', 'Interface');
addMethod(graph, 'A', 'foo');
const bFoo = addMethod(graph, 'B', 'foo');
// C has NO own foo — nearest is B.foo at depth 1
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const fooEdge = mi.find((e) => e.sourceId === bFoo);
expect(fooEdge).toBeDefined();
// A.foo should NOT be reached
const aFooId = generateId('Method', 'A.foo#0');
const aFooEdge = mi.find((e) => e.sourceId === aFooId);
expect(aFooEdge).toBeUndefined();
});
it('C extends B extends A, only A has foo → returns A.foo (single match at depth 2)', () => {
// I { foo() }, A { foo() }, B extends A { no foo }, C extends B implements I { no foo }
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'A', 'java');
addClass(graph, 'B', 'java');
addClass(graph, 'C', 'java');
addImplements(graph, 'C', 'I');
addExtends(graph, 'C', 'B');
addExtends(graph, 'B', 'A');
addMethod(graph, 'I', 'foo', 'Interface');
const aFoo = addMethod(graph, 'A', 'foo');
// B has NO foo, C has NO foo — only A.foo at depth 2
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const fooEdge = mi.find((e) => e.sourceId === aFoo);
expect(fooEdge).toBeDefined();
});
});
// ---- METHOD_IMPLEMENTS concrete-source guard ----------------------------
describe('METHOD_IMPLEMENTS concrete-source guard', () => {
it('interface B extends interface A, B redeclares foo → 0 METHOD_IMPLEMENTS', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'A', 'java', 'Interface');
addClass(graph, 'B', 'java', 'Interface');
addInterfaceExtends(graph, 'B', 'A');
addMethod(graph, 'A', 'foo', 'Interface');
addMethod(graph, 'B', 'foo', 'Interface');
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(mi).toHaveLength(0);
});
it('abstract class C implements I, C has abstract foo → 0 METHOD_IMPLEMENTS for foo', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'C', 'java', 'Class');
addImplements(graph, 'C', 'I');
addMethod(graph, 'I', 'foo', 'Interface');
// Add abstract method manually with isAbstract flag
const classId = generateId('Class', 'C');
const methodId = generateId('Method', 'C.foo#0');
graph.addNode({
id: methodId,
label: 'Method',
properties: { name: 'foo', filePath: 'src/C.ts', isAbstract: true, parameterCount: 0 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${classId}->${methodId}`),
sourceId: classId,
targetId: methodId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(mi).toHaveLength(0);
});
it('abstract class C implements I, C has concrete bar → 1 METHOD_IMPLEMENTS for bar', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'C', 'java', 'Class');
addImplements(graph, 'C', 'I');
addMethod(graph, 'I', 'bar', 'Interface');
const cBar = addMethod(graph, 'C', 'bar');
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(mi).toHaveLength(1);
expect(mi[0].sourceId).toBe(cBar);
});
it('concrete class implements interface → 1 METHOD_IMPLEMENTS (regression)', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'I', 'java', 'Interface');
addClass(graph, 'C', 'java', 'Class');
addImplements(graph, 'C', 'I');
addMethod(graph, 'I', 'foo', 'Interface');
const cFoo = addMethod(graph, 'C', 'foo');
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(mi).toHaveLength(1);
expect(mi[0].sourceId).toBe(cFoo);
});
});
describe('default interface method resolution', () => {
it('interface default method satisfies grandparent interface contract', () => {
// I1 has abstract bar, I2 extends I1 and provides concrete bar,
// C implements I2 with no own bar → edge from I2.bar → I1.bar
const graph = createKnowledgeGraph();
addClass(graph, 'I1Def', 'java', 'Interface');
addClass(graph, 'I2Def', 'java', 'Interface');
addClass(graph, 'CDef', 'java');
addInterfaceExtends(graph, 'I2Def', 'I1Def');
addImplements(graph, 'CDef', 'I2Def');
const i1Bar = addMethod(graph, 'I1Def', 'bar', 'Interface', undefined, { isAbstract: true });
const i2Bar = addMethod(graph, 'I2Def', 'bar', 'Interface', undefined, {
isAbstract: false,
});
const result = computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(i2Bar);
expect(edges[0].targetId).toBe(i1Bar);
});
it('own method takes priority over interface default', () => {
// I has concrete default bar, C implements I and has own bar
// → edge from C.bar → I.bar (own method wins, no IMPLEMENTS fallback needed)
const graph = createKnowledgeGraph();
addClass(graph, 'IOwn', 'java', 'Interface');
addClass(graph, 'COwn', 'java');
addImplements(graph, 'COwn', 'IOwn');
const iBar = addMethod(graph, 'IOwn', 'bar', 'Interface', undefined, { isAbstract: false });
const cBar = addMethod(graph, 'COwn', 'bar');
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cBar);
expect(edges[0].targetId).toBe(iBar);
});
it('transitive interface default: C implements I2, I2 extends I1, I1 has abstract bar, I2 has default bar → I2.bar satisfies I1.bar', () => {
// I1 (Interface) has abstract bar
// I2 (Interface) extends I1, has concrete default bar
// C (Class) implements I2, has NO bar
// The main emitter processes CImpl's ancestor I1 (transitive via I2).
// I1.bar is abstract → CImpl has no own bar → findInheritedMethod runs.
// EXTENDS BFS: nothing. IMPLEMENTS BFS: walks I2 → finds concrete I2.bar.
// Edge: I2.bar → I1.bar
const graph = createKnowledgeGraph();
addClass(graph, 'I1', 'java', 'Interface');
addClass(graph, 'I2', 'java', 'Interface');
addClass(graph, 'CImpl', 'java');
// I1 has abstract bar
const i1Bar = addMethod(graph, 'I1', 'bar', 'Interface', undefined, { isAbstract: true });
// I2 has concrete default bar
const i2Bar = addMethod(graph, 'I2', 'bar', 'Interface');
// I2 extends I1
const i2Id = generateId('Interface', 'I2');
const i1Id = generateId('Interface', 'I1');
graph.addRelationship({
id: generateId('EXTENDS', `${i2Id}->${i1Id}`),
sourceId: i2Id,
targetId: i1Id,
type: 'EXTENDS',
confidence: 1.0,
reason: '',
});
// CImpl implements I2
addImplements(graph, 'CImpl', 'I2');
const result = computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
// I2.bar (concrete default) satisfies I1.bar (abstract contract)
const barEdge = mi.find((e) => e.targetId === i1Bar && e.sourceId === i2Bar);
expect(barEdge).toBeDefined();
});
it('EXTENDS method takes priority over interface default', () => {
// I has concrete default foo, Base has concrete foo,
// C extends Base and implements I with no own foo
// → edge from Base.foo → I.foo (EXTENDS wins over IMPLEMENTS default)
const graph = createKnowledgeGraph();
addClass(graph, 'IExtPri', 'java', 'Interface');
addClass(graph, 'BaseExtPri', 'java');
addClass(graph, 'CExtPri', 'java');
addExtends(graph, 'CExtPri', 'BaseExtPri');
addImplements(graph, 'CExtPri', 'IExtPri');
const iFoo = addMethod(graph, 'IExtPri', 'foo', 'Interface', undefined, {
isAbstract: false,
});
const baseFoo = addMethod(graph, 'BaseExtPri', 'foo');
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(baseFoo);
expect(edges[0].targetId).toBe(iFoo);
});
it('Dart implements Class — does NOT inherit concrete method bodies', () => {
// Dart: class C implements AbstractBase (labeled Class, not Interface)
// AbstractBase has concrete method foo
// C has NO foo — but Dart implements does NOT inherit bodies
// → 0 METHOD_IMPLEMENTS edges from the IMPLEMENTS fallback
const graph = createKnowledgeGraph();
addClass(graph, 'AbstractBase', 'dart'); // Class label, not Interface
addClass(graph, 'DartImpl', 'dart');
// AbstractBase has concrete foo
addMethod(graph, 'AbstractBase', 'foo');
// DartImpl implements AbstractBase (IMPLEMENTS edge to a Class)
addImplements(graph, 'DartImpl', 'AbstractBase', 'Class', 'Interface');
// But we need AbstractBase to be a Class, not Interface — fix the label
// Actually addImplements creates the edge, but AbstractBase was added as Class.
// The IMPLEMENTS edge target needs to match the actual node ID.
// Let's do this manually:
const dartImplId = generateId('Class', 'DartImpl');
const absBaseId = generateId('Class', 'AbstractBase');
graph.addRelationship({
id: generateId('IMPLEMENTS', `${dartImplId}->${absBaseId}`),
sourceId: dartImplId,
targetId: absBaseId,
type: 'IMPLEMENTS',
confidence: 1.0,
reason: '',
});
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
// No edges — IMPLEMENTS fallback skips Class-labeled parents
expect(mi).toHaveLength(0);
});
it('Interface default still works after Dart label gate', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IDefault', 'java', 'Interface');
addClass(graph, 'Impl', 'java');
addImplements(graph, 'Impl', 'IDefault');
// IDefault has abstract contract method
const iFoo = addMethod(graph, 'IDefault', 'foo', 'Interface', undefined, {
isAbstract: true,
});
// IDefault also has concrete default bar
const iBar = addMethod(graph, 'IDefault', 'bar', 'Interface');
// Impl has foo but not bar
addMethod(graph, 'Impl', 'foo');
computeMRO(graph);
const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
// foo: own method matches → edge from Impl.foo → IDefault.foo
const fooEdge = mi.find((e) => e.targetId === iFoo);
expect(fooEdge).toBeDefined();
// bar: no own method, IMPLEMENTS fallback finds IDefault.bar (Interface label OK)
const barEdge = mi.find((e) => e.sourceId === iBar && e.targetId === iBar);
// Actually bar is the same method — it's the default implementation satisfying itself.
// The emitter processes IDefault.bar as an ancestor method, Impl has no bar,
// findInheritedMethod runs, walks IMPLEMENTS → finds IDefault.bar (non-abstract).
// But excludeMethodId = ancestorMethodId = iBar → skipped to prevent self-edge!
// So no bar edge. This is correct — the default satisfies the contract inherently.
});
});
describe('METHOD_IMPLEMENTS confidence tiering', () => {
it('fully-typed match gets confidence 1.0', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'ITyped', 'java', 'Interface');
addClass(graph, 'CTyped', 'java');
addImplements(graph, 'CTyped', 'ITyped');
const iFoo = addMethod(graph, 'ITyped', 'foo', 'Interface', ['int', 'String']);
const cFoo = addMethod(graph, 'CTyped', 'foo', 'Class', ['int', 'String']);
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cFoo);
expect(edges[0].targetId).toBe(iFoo);
expect(edges[0].confidence).toBe(1.0);
});
it('arity-only match (both have parameterCount, no types) gets confidence 1.0', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IArity', 'java', 'Interface');
addClass(graph, 'CArity', 'java');
addImplements(graph, 'CArity', 'IArity');
// Manually add methods with parameterCount but no parameterTypes
const iBarId = generateId('Method', 'IArity.bar#2');
graph.addNode({
id: iBarId,
label: 'Method',
properties: { name: 'bar', filePath: 'src/IArity.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'IArity')}->${iBarId}`),
sourceId: generateId('Interface', 'IArity'),
targetId: iBarId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const cBarId = generateId('Method', 'CArity.bar#2');
graph.addNode({
id: cBarId,
label: 'Method',
properties: { name: 'bar', filePath: 'src/CArity.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CArity')}->${cBarId}`),
sourceId: generateId('Class', 'CArity'),
targetId: cBarId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cBarId);
expect(edges[0].targetId).toBe(iBarId);
expect(edges[0].confidence).toBe(1.0);
});
it('lenient match (no types, no parameterCount on both sides) gets confidence 0.7', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'ILenient', 'java', 'Interface');
addClass(graph, 'CLenient', 'java');
addImplements(graph, 'CLenient', 'ILenient');
// Manually create methods WITHOUT parameterCount to simulate legacy/missing arity
const iBazId = generateId('Method', 'ILenient.baz');
graph.addNode({
id: iBazId,
label: 'Method',
properties: { name: 'baz', filePath: 'src/ILenient.ts' },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'ILenient')}->${iBazId}`),
sourceId: generateId('Interface', 'ILenient'),
targetId: iBazId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
const cBazId = generateId('Method', 'CLenient.baz');
graph.addNode({
id: cBazId,
label: 'Method',
properties: { name: 'baz', filePath: 'src/CLenient.ts' },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CLenient')}->${cBazId}`),
sourceId: generateId('Class', 'CLenient'),
targetId: cBazId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cBazId);
expect(edges[0].targetId).toBe(iBazId);
expect(edges[0].confidence).toBe(0.7);
});
it('one side has parameterCount, other does not → confidence 0.7', () => {
const graph = createKnowledgeGraph();
addClass(graph, 'IHalf', 'java', 'Interface');
addClass(graph, 'CHalf', 'java');
addImplements(graph, 'CHalf', 'IHalf');
// Interface method has parameterCount but no parameterTypes
const iQuxId = generateId('Method', 'IHalf.qux#2');
graph.addNode({
id: iQuxId,
label: 'Method',
properties: { name: 'qux', filePath: 'src/IHalf.ts', parameterCount: 2 },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Interface', 'IHalf')}->${iQuxId}`),
sourceId: generateId('Interface', 'IHalf'),
targetId: iQuxId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
// Class method has neither parameterTypes nor parameterCount (manually constructed)
const cQuxId = generateId('Method', 'CHalf.qux');
graph.addNode({
id: cQuxId,
label: 'Method',
properties: { name: 'qux', filePath: 'src/CHalf.ts' },
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${generateId('Class', 'CHalf')}->${cQuxId}`),
sourceId: generateId('Class', 'CHalf'),
targetId: cQuxId,
type: 'HAS_METHOD',
confidence: 1.0,
reason: '',
});
computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
expect(edges).toHaveLength(1);
expect(edges[0].sourceId).toBe(cQuxId);
expect(edges[0].targetId).toBe(iQuxId);
expect(edges[0].confidence).toBe(0.7);
});
});
// ---- IMPLEMENTS BFS ambiguity for default methods -------------------------
describe('IMPLEMENTS BFS ambiguity for default methods', () => {
it('does not emit METHOD_IMPLEMENTS when two interfaces provide same default method (ambiguous)', () => {
// IAncestor (Interface) has abstract process()
// IAlpha (Interface) extends IAncestor, has concrete process()
// IBeta (Interface) extends IAncestor, has concrete process()
// CImpl (Class) implements IAlpha, implements IBeta
// CImpl has NO process() method
//
// findInheritedMethod walks IMPLEMENTS BFS and finds process() in BOTH
// IAlpha and IBeta => ambiguous => null => no METHOD_IMPLEMENTS edge.
const graph = createKnowledgeGraph();
addClass(graph, 'IAncestor', 'java', 'Interface');
addClass(graph, 'IAlpha', 'java', 'Interface');
addClass(graph, 'IBeta', 'java', 'Interface');
addClass(graph, 'CImpl', 'java');
addInterfaceExtends(graph, 'IAlpha', 'IAncestor');
addInterfaceExtends(graph, 'IBeta', 'IAncestor');
addImplements(graph, 'CImpl', 'IAlpha');
addImplements(graph, 'CImpl', 'IBeta');
addMethod(graph, 'IAncestor', 'process', 'Interface', undefined, { isAbstract: true });
addMethod(graph, 'IAlpha', 'process', 'Interface');
addMethod(graph, 'IBeta', 'process', 'Interface');
const result = computeMRO(graph);
// IAlpha.process -> IAncestor.process and IBeta.process -> IAncestor.process
// are legitimate edges from sub-interface processing. The ambiguity check
// ensures that NO additional edge is emitted on behalf of CImpl (which has
// no own process()). Since CImpl has no methods, no edge should be sourced
// from a CImpl method. Verify by checking that the only METHOD_IMPLEMENTS
// edges for process() are the two interface-to-interface ones.
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const processEdges = edges.filter((e) => {
const target = graph.getNode(e.targetId);
return target?.properties.name === 'process';
});
// IAlpha.process->IAncestor.process and IBeta.process->IAncestor.process
// are emitted from the sub-interface processing (each concrete method
// implements the ancestor's abstract method). No additional edge should be
// emitted on behalf of CImpl because findInheritedMethod returns null
// (ambiguous: two candidates at the same BFS depth).
expect(processEdges).toHaveLength(2);
const alphaProcess = generateId('Method', 'IAlpha.process#0');
const betaProcess = generateId('Method', 'IBeta.process#0');
const sourceIds = processEdges.map((e) => e.sourceId).sort();
expect(sourceIds).toEqual([alphaProcess, betaProcess].sort());
});
it('emits METHOD_IMPLEMENTS when only one interface provides the default method (unambiguous)', () => {
// IAncestor (Interface) has abstract process()
// IAlpha (Interface) extends IAncestor, has concrete process()
// IBeta (Interface) extends IAncestor, does NOT have process()
// CImpl (Class) implements IAlpha, implements IBeta
// CImpl has NO process() method
//
// findInheritedMethod walks IMPLEMENTS BFS and finds process() only in
// IAlpha => unambiguous => emits 1 METHOD_IMPLEMENTS edge.
const graph = createKnowledgeGraph();
addClass(graph, 'IAncestor', 'java', 'Interface');
addClass(graph, 'IAlpha', 'java', 'Interface');
addClass(graph, 'IBeta', 'java', 'Interface');
addClass(graph, 'CImpl', 'java');
addInterfaceExtends(graph, 'IAlpha', 'IAncestor');
addInterfaceExtends(graph, 'IBeta', 'IAncestor');
addImplements(graph, 'CImpl', 'IAlpha');
addImplements(graph, 'CImpl', 'IBeta');
const ancestorProcess = addMethod(graph, 'IAncestor', 'process', 'Interface', undefined, {
isAbstract: true,
});
const alphaProcess = addMethod(graph, 'IAlpha', 'process', 'Interface');
// IBeta has no process() method
const result = computeMRO(graph);
const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS');
const processEdges = edges.filter((e) => {
const target = graph.getNode(e.targetId);
return target?.properties.name === 'process';
});
// At minimum, IAlpha.process -> IAncestor.process is emitted (from IAlpha's
// own processing). CImpl's findInheritedMethod also finds IAlpha.process as
// the sole unambiguous match, potentially emitting the same edge again.
expect(processEdges.length).toBeGreaterThanOrEqual(1);
// Every process edge should point from IAlpha.process to IAncestor.process
for (const edge of processEdges) {
expect(edge.sourceId).toBe(alphaProcess);
expect(edge.targetId).toBe(ancestorProcess);
}
});
});
// ---- PHM parent-order pinning ------------------------------------------
//
// PHM Unit 2 split buildAdjacency's single forEachRelationship into three
// typed iterations (EXTENDS, IMPLEMENTS, HAS_METHOD). Parent enumeration
// now runs ALL EXTENDS edges before ANY IMPLEMENTS edges. For classes
// with parents added in interleaved order, this re-orders `parentMap`
// and any C3 linearization that consumes it.
//
// Python (single EXTENDS model) and Java/C# (resolveCsharpJava partitions
// by edge type regardless of order) are unaffected in practice. This
// test pins the new behavior so a future "simplification" back to a
// single loop would surface as a deliberate change rather than a silent
// semantic drift.
describe('PHM: interleaved EXTENDS + IMPLEMENTS parent ordering', () => {
it('class methods win regardless of the order EXTENDS/IMPLEMENTS edges were added', () => {
const graph = createKnowledgeGraph();
// C extends Base (class) AND implements Iface (interface). Edges
// added in INTERLEAVED order: IMPLEMENTS first, then EXTENDS.
// Under the old single-loop adjacency, parentMap[C] would be
// [IfaceId, BaseId]. Under the new grouped adjacency,
// parentMap[C] is [BaseId, IfaceId] (EXTENDS bucket first).
//
// For resolveCsharpJava, class-method-wins is invariant to parent
// order — both produce the same winner. This test encodes that
// invariant, guarding the behavioral claim that 'Java/C# are
// unaffected' in the PHM commit message.
addClass(graph, 'Base', 'java');
addClass(graph, 'C', 'java');
addClass(graph, 'Iface', 'java', 'Interface');
addMethod(graph, 'Base', 'greet');
addMethod(graph, 'Iface', 'greet', 'Interface');
addMethod(graph, 'C', 'greet');
// Add IMPLEMENTS BEFORE EXTENDS to exercise interleaving.
addImplements(graph, 'C', 'Iface');
addExtends(graph, 'C', 'Base');
const result = computeMRO(graph);
const cId = generateId('Class', 'C');
const entry = result.entries.find((e) => e.classId === cId);
expect(entry).toBeDefined();
const mro = entry!.mro;
// Grouped iteration yields EXTENDS parents first. This pin fails
// loudly if a future refactor reverts the typed-bucket iteration
// to a single full-graph scan and restores insertion-order
// semantics.
// Grouped EXTENDS-before-IMPLEMENTS iteration produces this exact
// MRO for C: [Base, Iface]. A single-loop reversion would yield
// [Iface, Base] (IMPLEMENTS added first in this test).
expect(mro).toEqual(['Base', 'Iface']);
});
});
});