diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 5e42edece0..c8c62921aa 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 b37c2dfc79..6c77e79585 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 9c8d17ad21..8a7946d327 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 f8518a3bd8..26d9f1a54b 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 34f2a61068..69d14b191e 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" }