From a5a890439fbe2d7cf4a447d2f27fd6dee81ce9dc Mon Sep 17 00:00:00 2001 From: Hugo Gu Date: Mon, 1 Jun 2026 14:43:55 +0800 Subject: [PATCH] fix(ingestion): skip File->Member DEFINES edges for class members (#1949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ingestion): skip File->Member DEFINES edges for class members Previously every symbol — including Methods and Properties that belong to a class — received a direct File->Symbol DEFINES edge. This caused the radial-layout view to show File linking directly to all Properties and Methods, bypassing their enclosing class node and flattening the OOP hierarchy. Fix: gate the DEFINES emission on the symbol having no owner. Class members are already reachable through the File->Class DEFINES edge and the Class->Member HAS_METHOD / HAS_PROPERTY edges, so the redundant direct edge is unnecessary and misleading in the graph. The same guard is applied in all four emission sites: - parsing-processor.ts (sequential fallback path) - parse-worker.ts (main symbol loop + routed-property branch) - call-processor.ts (routed-property branch) Pipeline-graph golden updated: DEFINES 21→20, relationships 74→73. Closes #1944 Co-authored-by: Claude AI-model: claude-sonnet-4-6 * fix(wiki): extend getFilesWithExports to include exported class members The PR that removed File→Member DEFINES edges left getFilesWithExports() under-reporting: its one-hop MATCH only reaches top-level symbols. Add a UNION leg that follows File→DEFINES→Class→HAS_METHOD/HAS_PROPERTY→Member so exported class methods and properties appear in wiki/cluster export summaries again. --------- Co-authored-by: Claude Co-authored-by: Gergő Magyar --- gitnexus/src/core/ingestion/call-processor.ts | 21 +++++---- .../src/core/ingestion/parsing-processor.ts | 35 ++++++++------ .../core/ingestion/workers/parse-worker.ts | 47 +++++++++++-------- gitnexus/src/core/wiki/graph-queries.ts | 10 +++- .../mini-repo/expected-graph.json | 6 +-- 5 files changed, 71 insertions(+), 48 deletions(-) diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 5e42edece..c8c62921a 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -1036,15 +1036,18 @@ export const processCalls = async ( ? { declaredType: routedFieldInfo.type } : {}), }); - const relId = generateId('DEFINES', `${fileId}->${nodeId}`); - graph.addRelationship({ - id: relId, - sourceId: fileId, - targetId: nodeId, - type: 'DEFINES', - confidence: 1.0, - reason: '', - }); + // Only emit File -> Property DEFINES for top-level properties (issue #1944). + if (!propEnclosingClassId) { + const relId = generateId('DEFINES', `${fileId}->${nodeId}`); + graph.addRelationship({ + id: relId, + sourceId: fileId, + targetId: nodeId, + type: 'DEFINES', + confidence: 1.0, + reason: '', + }); + } if (propEnclosingClassId) { graph.addRelationship({ id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`), diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index b37c2dfc7..6c77e7958 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -879,23 +879,28 @@ const processParsingSequential = async ( qualifiedName: qualifiedTypeName, }); - const fileId = generateId('File', file.path); - - const relId = generateId('DEFINES', `${fileId}->${nodeId}`); - - const relationship: GraphRelationship = { - id: relId, - sourceId: fileId, - targetId: nodeId, - type: 'DEFINES', - confidence: 1.0, - reason: '', - }; - - graph.addRelationship(relationship); - // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ── const ownerIdForMemberEdge = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? null; + + // Only emit File -> Symbol DEFINES for top-level symbols. Class members + // are reachable via Class -> Member (HAS_METHOD / HAS_PROPERTY), so a + // direct File -> Member edge would bypass the class in the graph and + // produce a flat radial layout instead of the correct File->Class->Member + // hierarchy (issue #1944). + if (!ownerIdForMemberEdge) { + const fileId = generateId('File', file.path); + const relId = generateId('DEFINES', `${fileId}->${nodeId}`); + const relationship: GraphRelationship = { + id: relId, + sourceId: fileId, + targetId: nodeId, + type: 'DEFINES', + confidence: 1.0, + reason: '', + }; + graph.addRelationship(relationship); + } + if (ownerIdForMemberEdge) { const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD'; graph.addRelationship({ diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 9c8d17ad2..8a7946d32 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -1590,16 +1590,20 @@ const processFileGroup = ( ? { isReadonly: routedFieldInfo.isReadonly } : {}), }); - const fileId = generateId('File', file.path); - const relId = generateId('DEFINES', `${fileId}->${nodeId}`); - result.relationships.push({ - id: relId, - sourceId: fileId, - targetId: nodeId, - type: 'DEFINES', - confidence: 1.0, - reason: '', - }); + // Only emit File -> Property DEFINES for top-level properties + // (issue #1944); class members are reached via HAS_PROPERTY. + if (!propEnclosingClassId) { + const fileId = generateId('File', file.path); + const relId = generateId('DEFINES', `${fileId}->${nodeId}`); + result.relationships.push({ + id: relId, + sourceId: fileId, + targetId: nodeId, + type: 'DEFINES', + confidence: 1.0, + reason: '', + }); + } if (propEnclosingClassId) { result.relationships.push({ id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`), @@ -2094,16 +2098,19 @@ const processFileGroup = ( : {}), }); - const fileId = generateId('File', file.path); - const relId = generateId('DEFINES', `${fileId}->${nodeId}`); - result.relationships.push({ - id: relId, - sourceId: fileId, - targetId: nodeId, - type: 'DEFINES', - confidence: 1.0, - reason: '', - }); + // Only emit File -> Symbol DEFINES for top-level symbols (issue #1944). + if (ownerId === undefined) { + const fileId = generateId('File', file.path); + const relId = generateId('DEFINES', `${fileId}->${nodeId}`); + result.relationships.push({ + id: relId, + sourceId: fileId, + targetId: nodeId, + type: 'DEFINES', + confidence: 1.0, + reason: '', + }); + } // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ── if (ownerId !== undefined) { diff --git a/gitnexus/src/core/wiki/graph-queries.ts b/gitnexus/src/core/wiki/graph-queries.ts index f8518a3bd..26d9f1a54 100644 --- a/gitnexus/src/core/wiki/graph-queries.ts +++ b/gitnexus/src/core/wiki/graph-queries.ts @@ -57,6 +57,9 @@ export async function closeWikiDb(): Promise { /** * Get all source files with their exported symbol names and types. + * Includes top-level exports (File→DEFINES→n) and exported class members + * (File→DEFINES→Class→HAS_METHOD/HAS_PROPERTY→n) since class members no + * longer have a direct File→DEFINES edge. */ export async function getFilesWithExports(): Promise { const rows = await executeQuery( @@ -65,7 +68,12 @@ export async function getFilesWithExports(): Promise { MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(n) WHERE n.isExported = true RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type - ORDER BY f.filePath + UNION + MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(c) + -[mr:CodeRelation]->(n) + WHERE mr.type IN ['HAS_METHOD', 'HAS_PROPERTY'] AND n.isExported = true + RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type + ORDER BY filePath `, ); 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 34f2a6106..69d14b191 100644 --- a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json +++ b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json @@ -3,7 +3,7 @@ "fixture": "mini-repo", "totalFileCount": 7, "symbols": 37, - "relationships": 74, + "relationships": 73, "processes": 4, "byType": { "Class": 1, @@ -19,11 +19,11 @@ "byRelType": { "CALLS": 9, "CONTAINS": 7, - "DEFINES": 21, + "DEFINES": 20, "HAS_METHOD": 1, "IMPORTS": 12, "MEMBER_OF": 12, "STEP_IN_PROCESS": 12 }, - "edgeDigest": "6f414427a20c037df3e336f055c83f987e7d381c9bfa73b4d2be690cb8103302" + "edgeDigest": "196198eb9a900721aec892400d141189aa1e874722917843c7f1206805d817f1" }