mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
fix(scope): stabilize graph lookup collisions (#2480)
This commit is contained in:
commit
af583a9be1
3 changed files with 133 additions and 2 deletions
|
|
@ -58,6 +58,39 @@ export interface WorkerExtractedData {
|
|||
parsedFiles: ParsedFile[];
|
||||
}
|
||||
|
||||
type ParsedGraphNode = ParseWorkerResult['nodes'][number];
|
||||
|
||||
function compareText(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function sourceLine(node: ParsedGraphNode): number {
|
||||
const value = node.properties.startLine;
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function compareParsedNodeSourceOrder(left: ParsedGraphNode, right: ParsedGraphNode): number {
|
||||
const leftPath = typeof left.properties.filePath === 'string' ? left.properties.filePath : '';
|
||||
const rightPath = typeof right.properties.filePath === 'string' ? right.properties.filePath : '';
|
||||
const fileOrder = compareText(leftPath, rightPath);
|
||||
if (fileOrder !== 0) return fileOrder;
|
||||
|
||||
const leftLine = sourceLine(left);
|
||||
const rightLine = sourceLine(right);
|
||||
if (leftLine !== rightLine) return leftLine < rightLine ? -1 : 1;
|
||||
|
||||
return compareText(left.id, right.id);
|
||||
}
|
||||
|
||||
function nodesInSourceOrder(nodes: readonly ParsedGraphNode[]): readonly ParsedGraphNode[] {
|
||||
for (let index = 1; index < nodes.length; index++) {
|
||||
if (compareParsedNodeSourceOrder(nodes[index - 1], nodes[index]) > 0) {
|
||||
return [...nodes].sort(compareParsedNodeSourceOrder);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker-based parallel parsing
|
||||
// ============================================================================
|
||||
|
|
@ -95,7 +128,11 @@ export const mergeChunkResults = (
|
|||
const allParsedFiles: ParsedFile[] = [];
|
||||
|
||||
for (const result of chunkResults) {
|
||||
for (const node of result.nodes) {
|
||||
// Worker jobs and input files are already merged in stable start-index/path
|
||||
// order. Canonicalize the final per-result node boundary once so graph
|
||||
// insertion, cache replay, and first-wins graph indexes share source order.
|
||||
// The common already-ordered path stays allocation-free and linear.
|
||||
for (const node of nodesInSourceOrder(result.nodes)) {
|
||||
graph.addNode({
|
||||
id: node.id,
|
||||
label: node.label as NodeLabel,
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback key: simple name. First-wins within a file — used when
|
||||
// Fallback key: simple name. Source-order first-wins within a file — used when
|
||||
// the caller doesn't know the qualifier (unqualified free-call
|
||||
// fallback, cross-file resolution where MethodRegistry already
|
||||
// disambiguated the owner).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
||||
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
|
||||
import { mergeChunkResults } from '../../../src/core/ingestion/parsing-processor.js';
|
||||
import {
|
||||
buildGraphNodeLookup,
|
||||
qualifiedKey,
|
||||
simpleKey,
|
||||
} from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
|
||||
import type { ParseWorkerResult } from '../../../src/core/ingestion/workers/parse-worker.js';
|
||||
|
||||
const FILE = 'src/service.ts';
|
||||
|
||||
interface Candidate {
|
||||
id: string;
|
||||
startLine?: number;
|
||||
}
|
||||
|
||||
function buildLookup(candidates: readonly Candidate[]) {
|
||||
const graph = createKnowledgeGraph();
|
||||
const nodes = candidates.map(
|
||||
(candidate) =>
|
||||
({
|
||||
id: candidate.id,
|
||||
label: 'Method' as NodeLabel,
|
||||
properties: {
|
||||
name: 'save',
|
||||
qualifiedName: 'Service.save',
|
||||
filePath: FILE,
|
||||
...(candidate.startLine !== undefined ? { startLine: candidate.startLine } : {}),
|
||||
},
|
||||
}) satisfies ParseWorkerResult['nodes'][number],
|
||||
);
|
||||
const result: ParseWorkerResult = {
|
||||
nodes,
|
||||
relationships: [],
|
||||
symbols: [],
|
||||
calls: [],
|
||||
assignments: [],
|
||||
routes: [],
|
||||
fetchCalls: [],
|
||||
fetchWrapperDefs: [],
|
||||
decoratorRoutes: [],
|
||||
routerIncludes: [],
|
||||
routerImports: [],
|
||||
toolDefs: [],
|
||||
ormQueries: [],
|
||||
constructorBindings: [],
|
||||
fileScopeBindings: [],
|
||||
parsedFiles: [],
|
||||
skippedLanguages: {},
|
||||
fileCount: 1,
|
||||
};
|
||||
|
||||
mergeChunkResults(graph, createSemanticModel().symbols, [result]);
|
||||
return buildGraphNodeLookup(graph);
|
||||
}
|
||||
|
||||
describe('parse-result graph insertion determinism', () => {
|
||||
it('selects the earliest source definition regardless of worker result order', () => {
|
||||
const early = { id: `Method:${FILE}:Service.save#1`, startLine: 10 };
|
||||
const late = { id: `Method:${FILE}:Service.save#2`, startLine: 20 };
|
||||
|
||||
const lateFirst = buildLookup([late, early]);
|
||||
const earlyFirst = buildLookup([early, late]);
|
||||
|
||||
for (const key of [simpleKey(FILE, 'save'), qualifiedKey(FILE, 'Method', 'Service.save')]) {
|
||||
expect(lateFirst.get(key)).toBe(early.id);
|
||||
expect(earlyFirst.get(key)).toBe(early.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses the stable node id when source positions are identical', () => {
|
||||
const first = { id: `Method:${FILE}:Service.save#1`, startLine: 10 };
|
||||
const second = { id: `Method:${FILE}:Service.save#2`, startLine: 10 };
|
||||
|
||||
const firstLookup = buildLookup([second, first]);
|
||||
const secondLookup = buildLookup([first, second]);
|
||||
|
||||
expect(firstLookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
|
||||
expect(secondLookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
|
||||
});
|
||||
|
||||
it('uses the stable node id when source positions are unavailable', () => {
|
||||
const first = { id: `Method:${FILE}:Service.save#1` };
|
||||
const second = { id: `Method:${FILE}:Service.save#2` };
|
||||
|
||||
const lookup = buildLookup([second, first]);
|
||||
|
||||
expect(lookup.get(simpleKey(FILE, 'save'))).toBe(first.id);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue