mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
fix(scope): stabilize graph lookup collisions
This commit is contained in:
parent
c6445096eb
commit
031a160090
2 changed files with 87 additions and 4 deletions
|
|
@ -18,7 +18,7 @@
|
|||
* format that downstream consumers (queries, edges, MCP) expect.
|
||||
*/
|
||||
|
||||
import type { NodeLabel, ParameterTypeClass } from 'gitnexus-shared';
|
||||
import type { GraphNode, NodeLabel, ParameterTypeClass } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import { isOverloadableCallable } from '../../utils/callable-labels.js';
|
||||
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
||||
|
|
@ -67,9 +67,34 @@ export function simpleKey(filePath: string, name: string): string {
|
|||
return `${filePath}::${name}`;
|
||||
}
|
||||
|
||||
function compareText(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function compareSourceOrder(left: GraphNode, right: GraphNode): number {
|
||||
const fileOrder = compareText(left.properties.filePath, right.properties.filePath);
|
||||
if (fileOrder !== 0) return fileOrder;
|
||||
|
||||
const leftLine = Number.isFinite(left.properties.startLine)
|
||||
? (left.properties.startLine ?? Number.MAX_SAFE_INTEGER)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const rightLine = Number.isFinite(right.properties.startLine)
|
||||
? (right.properties.startLine ?? Number.MAX_SAFE_INTEGER)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
if (leftLine !== rightLine) return leftLine - rightLine;
|
||||
|
||||
return compareText(left.id, right.id);
|
||||
}
|
||||
|
||||
export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
|
||||
const lookup = new Map<string, string>();
|
||||
for (const node of graph.iterNodes()) {
|
||||
const linkableNodes = Array.from(graph.iterNodes()).filter((node) => {
|
||||
const props = node.properties as { filePath?: string; name?: string };
|
||||
return props.filePath !== undefined && props.name !== undefined && isLinkableLabel(node.label);
|
||||
});
|
||||
linkableNodes.sort(compareSourceOrder);
|
||||
|
||||
for (const node of linkableNodes) {
|
||||
const props = node.properties as {
|
||||
filePath?: string;
|
||||
name?: string;
|
||||
|
|
@ -77,7 +102,6 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
|
|||
templateArguments?: readonly string[];
|
||||
};
|
||||
if (props.filePath === undefined || props.name === undefined) continue;
|
||||
if (!isLinkableLabel(node.label)) continue;
|
||||
|
||||
// Primary key: fully-qualified name + label, in a separate
|
||||
// keyspace from simple names. Class nodes carry `qualifiedName`
|
||||
|
|
@ -163,7 +187,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,59 @@
|
|||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
||||
import {
|
||||
buildGraphNodeLookup,
|
||||
qualifiedKey,
|
||||
simpleKey,
|
||||
} from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
|
||||
|
||||
const FILE = 'src/service.ts';
|
||||
|
||||
interface Candidate {
|
||||
id: string;
|
||||
startLine: number;
|
||||
}
|
||||
|
||||
function buildLookup(candidates: readonly Candidate[]) {
|
||||
const graph = createKnowledgeGraph();
|
||||
for (const candidate of candidates) {
|
||||
graph.addNode({
|
||||
id: candidate.id,
|
||||
label: 'Method' as NodeLabel,
|
||||
properties: {
|
||||
name: 'save',
|
||||
qualifiedName: 'Service.save',
|
||||
filePath: FILE,
|
||||
startLine: candidate.startLine,
|
||||
},
|
||||
});
|
||||
}
|
||||
return buildGraphNodeLookup(graph);
|
||||
}
|
||||
|
||||
describe('buildGraphNodeLookup determinism', () => {
|
||||
it('selects the earliest source definition regardless of graph insertion 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue