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/fixtures/pipeline-golden/mini-repo/expected-graph.json b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json index 67bb7e8888..48e06aea3b 100644 --- a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json +++ b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json @@ -24,5 +24,5 @@ "MEMBER_OF": 12, "STEP_IN_PROCESS": 12 }, - "edgeDigest": "1e80aba78cb1784276d387e9debd006ae4e82e60ce33c59e338aac4886d98f61" + "edgeDigest": "02858462a2acca13f09b7ae2a81d56fe858e67064552ea966cd9ca242371e029" } diff --git a/gitnexus/test/integration/cfg/__snapshots__/pipeline-pdg.test.ts.snap b/gitnexus/test/integration/cfg/__snapshots__/pipeline-pdg.test.ts.snap index 06e4b033eb..f1da88949e 100644 --- a/gitnexus/test/integration/cfg/__snapshots__/pipeline-pdg.test.ts.snap +++ b/gitnexus/test/integration/cfg/__snapshots__/pipeline-pdg.test.ts.snap @@ -18,7 +18,7 @@ exports[`U7 — C-family worker-mode --pdg pipeline > C#: --pdg off is byte-iden "Namespace": 1, "Process": 1, }, - "edgeDigest": "2271af66531e4fb54afb2d6e0a024abc536e2109f445e0ecff9868c01661ebfa", + "edgeDigest": "0497d26dbe060d36426bf10cde5db490a6d82c013e0835296723a4c00ef7442a", "relationships": 38, "symbols": 24, } @@ -66,7 +66,7 @@ exports[`U7 — C-family worker-mode --pdg pipeline > Go: --pdg off is byte-iden "File": 1, "Function": 28, }, - "edgeDigest": "731ee1aa406fe7a96c5747dc2e5059f9079fd883dfca70a1933d49ce7f161a99", + "edgeDigest": "33164ebef537538cce43ab1a518d8a5d4944d6d9ad059973b974d3b0fea7caaa", "relationships": 64, "symbols": 37, } @@ -86,7 +86,7 @@ exports[`U7 — C-family worker-mode --pdg pipeline > Java: --pdg off is byte-id "File": 1, "Method": 21, }, - "edgeDigest": "b5e4c1459c59f385949964c3e4e479e67c98a2eaa7e102f4acacb108a9ccec29", + "edgeDigest": "488a3f80683f3e2fd0160847f22537cdd1d48f1e8f34c929562854459abfe03a", "relationships": 46, "symbols": 27, } 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: [