From b9ddfda945d00da73b7d8a0843438ab716d5ff38 Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 14 Jul 2026 03:20:54 +0700 Subject: [PATCH] fix(communities): canonicalize projection order --- .../src/core/ingestion/community-processor.ts | 10 +++++++++- .../test/unit/community-processor.test.ts | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/community-processor.ts b/gitnexus/src/core/ingestion/community-processor.ts index e5c85ef586..ff892ae73a 100644 --- a/gitnexus/src/core/ingestion/community-processor.ts +++ b/gitnexus/src/core/ingestion/community-processor.ts @@ -302,6 +302,7 @@ export const buildCommunityProjection = (knowledgeGraph: KnowledgeGraph): Commun const nodes: CommunityProjectionNode[] = []; const nodeIndexById = new Map(); + const eligibleNodes: GraphNode[] = []; knowledgeGraph.forEachNode((node) => { if (!isCommunitySymbol(node) || !connectedNodes.has(node.id)) return; @@ -309,6 +310,12 @@ export const buildCommunityProjection = (knowledgeGraph: KnowledgeGraph): Commun // get absorbed into their single neighbor's community, but cost iteration time. if (isLarge && (nodeDegree.get(node.id) || 0) < 2) return; + eligibleNodes.push(node); + }); + + eligibleNodes.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)); + + for (const node of eligibleNodes) { nodeIndexById.set(node.id, nodes.length); nodes.push({ id: node.id, @@ -316,7 +323,7 @@ export const buildCommunityProjection = (knowledgeGraph: KnowledgeGraph): Commun filePath: node.properties.filePath, type: node.label, }); - }); + } const seenEdges = new Set(); const edges: Array = []; @@ -338,6 +345,7 @@ export const buildCommunityProjection = (knowledgeGraph: KnowledgeGraph): Commun seenEdges.add(edgeKey); edges.push([a, b]); }); + edges.sort(([leftA, leftB], [rightA, rightB]) => leftA - rightA || leftB - rightB); return { nodes, edges, symbolCount, isLarge }; }; diff --git a/gitnexus/test/unit/community-processor.test.ts b/gitnexus/test/unit/community-processor.test.ts index 7f62861d3e..c0e5ff14c0 100644 --- a/gitnexus/test/unit/community-processor.test.ts +++ b/gitnexus/test/unit/community-processor.test.ts @@ -102,6 +102,26 @@ describe('community-processor', () => { expect(projection.symbolCount).toBe(3); }); + it('produces the same projection regardless of graph insertion order', () => { + const first = createKnowledgeGraph(); + for (const id of ['fn:c', 'fn:a', 'fn:b']) { + first.addNode(makeNode(id, id.slice(3))); + } + first.addRelationship(makeRel('rel:ac', 'fn:a', 'fn:c')); + first.addRelationship(makeRel('rel:ab', 'fn:a', 'fn:b')); + first.addRelationship(makeRel('rel:bc', 'fn:b', 'fn:c')); + + const second = createKnowledgeGraph(); + for (const id of ['fn:b', 'fn:c', 'fn:a']) { + second.addNode(makeNode(id, id.slice(3))); + } + second.addRelationship(makeRel('rel:bc', 'fn:c', 'fn:b')); + second.addRelationship(makeRel('rel:ab', 'fn:b', 'fn:a')); + second.addRelationship(makeRel('rel:ac', 'fn:c', 'fn:a')); + + expect(buildCommunityProjection(second)).toEqual(buildCommunityProjection(first)); + }); + it('exports a deterministic undirected CSR adjacency', () => { const projection = { nodes: [