fix(ingestion): skip File->Member DEFINES edges for class members (#1949)

* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
Hugo Gu 2026-06-01 14:43:55 +08:00 • committed by GitHub
parent 60355a0370
commit a5a890439f
5 changed files with 71 additions and 48 deletions

View file

@ -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}`),

View file

@ -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({

View file

@ -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) {

View file

@ -57,6 +57,9 @@ export async function closeWikiDb(): Promise<void> {
/**
* 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<FileWithExports[]> {
const rows = await executeQuery(
@ -65,7 +68,12 @@ export async function getFilesWithExports(): Promise<FileWithExports[]> {
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
`,
);

View file

@ -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"
}