diff --git a/gitnexus/src/core/ingestion/model/resolve.ts b/gitnexus/src/core/ingestion/model/resolve.ts index 62653a762..55da60c62 100644 --- a/gitnexus/src/core/ingestion/model/resolve.ts +++ b/gitnexus/src/core/ingestion/model/resolve.ts @@ -157,19 +157,27 @@ export function c3Linearize( // Add the direct parents list as the final sequence const sequences = [...parentLinearizations, [...directParents]]; + const heads = new Uint32Array(sequences.length); // head pointer per sequence const result: string[] = []; + // Tail-count map: how many sequences contain this id at index > head. + // O(1) membership check replaces O(n) indexOf scans. + const tailCount = new Map(); + for (const seq of sequences) { + for (let i = 1; i < seq.length; i++) { + tailCount.set(seq[i], (tailCount.get(seq[i]) ?? 0) + 1); + } + } + + let remaining = sequences.reduce((n, s) => n + s.length, 0); let inconsistent = false; - while (sequences.some((s) => s.length > 0)) { - // Find a good head: one that doesn't appear in the tail of any other sequence + + while (remaining > 0) { let head: string | null = null; - for (const seq of sequences) { - if (seq.length === 0) continue; - const candidate = seq[0]; - const inTail = sequences.some( - (other) => other.length > 1 && other.indexOf(candidate, 1) !== -1, - ); - if (!inTail) { + for (let si = 0; si < sequences.length; si++) { + if (heads[si] >= sequences[si].length) continue; + const candidate = sequences[si][heads[si]]; + if ((tailCount.get(candidate) ?? 0) === 0) { head = candidate; break; } @@ -182,10 +190,19 @@ export function c3Linearize( result.push(head); - // Remove the chosen head from all sequences - for (const seq of sequences) { - if (seq.length > 0 && seq[0] === head) { - seq.shift(); + // Advance head pointers past the chosen head; update tail counts + for (let si = 0; si < sequences.length; si++) { + if (heads[si] >= sequences[si].length) continue; + if (sequences[si][heads[si]] === head) { + heads[si]++; + remaining--; + // promoted was in this sequence's active tail; now it's the new head — remove from tailCount + if (heads[si] < sequences[si].length) { + const promoted = sequences[si][heads[si]]; + const prev = tailCount.get(promoted)!; + if (prev <= 1) tailCount.delete(promoted); + else tailCount.set(promoted, prev - 1); + } } } } diff --git a/gitnexus/test/unit/mro-processor.test.ts b/gitnexus/test/unit/mro-processor.test.ts index d313fb2e5..c2adf867c 100644 --- a/gitnexus/test/unit/mro-processor.test.ts +++ b/gitnexus/test/unit/mro-processor.test.ts @@ -540,10 +540,35 @@ describe('computeMRO', () => { 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', () => { - // Chain of 2000 classes: C0 ← C1 ← C2 ← ... ← C1999 - // The iterative c3Linearize handles this without blowing the stack. - // (The recursive version overflows at ~1K–5K levels depending on platform.) const graph = createKnowledgeGraph(); const DEPTH = 2000; for (let i = 0; i < DEPTH; i++) { @@ -552,12 +577,10 @@ describe('computeMRO', () => { for (let i = 1; i < DEPTH; i++) { addExtends(graph, `C${i}`, `C${i - 1}`); } - // Add a method on the root so MRO produces an entry addMethod(graph, 'C0', 'baseMethod'); const result = computeMRO(graph); expect(result).toBeDefined(); - // The deepest class should have all ancestors in its MRO const deepest = result.entries.find((e) => e.className === `C${DEPTH - 1}`); if (deepest) { expect(deepest.mro.length).toBe(DEPTH - 1);