@optave/codegraph 3.1.5 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -12,25 +12,18 @@ import { computeConfidence } from '../../resolve.js';
12
12
  import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
13
13
  import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
14
14
 
15
- /**
16
- * @param {import('../context.js').PipelineContext} ctx
17
- */
18
- export async function buildEdges(ctx) {
19
- const { db, fileSymbols, barrelOnlyFiles, rootDir, engineName } = ctx;
15
+ // ── Node lookup setup ───────────────────────────────────────────────────
20
16
 
21
- const getNodeIdStmt = {
17
+ function makeGetNodeIdStmt(db) {
18
+ return {
22
19
  get: (name, kind, file, line) => {
23
20
  const id = getNodeId(db, name, kind, file, line);
24
21
  return id != null ? { id } : undefined;
25
22
  },
26
23
  };
24
+ }
27
25
 
28
- // Pre-load all nodes into lookup maps
29
- const allNodes = db
30
- .prepare(
31
- `SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait')`,
32
- )
33
- .all();
26
+ function setupNodeLookups(ctx, allNodes) {
34
27
  ctx.nodesByName = new Map();
35
28
  for (const node of allNodes) {
36
29
  if (!ctx.nodesByName.has(node.name)) ctx.nodesByName.set(node.name, []);
@@ -42,253 +35,339 @@ export async function buildEdges(ctx) {
42
35
  if (!ctx.nodesByNameAndFile.has(key)) ctx.nodesByNameAndFile.set(key, []);
43
36
  ctx.nodesByNameAndFile.get(key).push(node);
44
37
  }
38
+ }
45
39
 
46
- const t0 = performance.now();
47
- const buildEdgesTx = db.transaction(() => {
48
- const allEdgeRows = [];
40
+ // ── Import edges ────────────────────────────────────────────────────────
49
41
 
50
- // ── Import edges ────────────────────────────────────────────────
51
- for (const [relPath, symbols] of fileSymbols) {
52
- if (barrelOnlyFiles.has(relPath)) continue;
53
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
54
- if (!fileNodeRow) continue;
55
- const fileNodeId = fileNodeRow.id;
56
-
57
- for (const imp of symbols.imports) {
58
- const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
59
- const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
60
- if (targetRow) {
61
- const edgeKind = imp.reexport
62
- ? 'reexports'
63
- : imp.typeOnly
64
- ? 'imports-type'
65
- : imp.dynamicImport
66
- ? 'dynamic-imports'
67
- : 'imports';
68
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
69
-
70
- if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
71
- const resolvedSources = new Set();
72
- for (const name of imp.names) {
73
- const cleanName = name.replace(/^\*\s+as\s+/, '');
74
- const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
75
- if (
76
- actualSource &&
77
- actualSource !== resolvedPath &&
78
- !resolvedSources.has(actualSource)
79
- ) {
80
- resolvedSources.add(actualSource);
81
- const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
82
- if (actualRow) {
83
- allEdgeRows.push([
84
- fileNodeId,
85
- actualRow.id,
86
- edgeKind === 'imports-type'
87
- ? 'imports-type'
88
- : edgeKind === 'dynamic-imports'
89
- ? 'dynamic-imports'
90
- : 'imports',
91
- 0.9,
92
- 0,
93
- ]);
94
- }
95
- }
96
- }
97
- }
98
- }
42
+ function buildImportEdges(ctx, getNodeIdStmt, allEdgeRows) {
43
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
44
+
45
+ for (const [relPath, symbols] of fileSymbols) {
46
+ if (barrelOnlyFiles.has(relPath)) continue;
47
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
48
+ if (!fileNodeRow) continue;
49
+ const fileNodeId = fileNodeRow.id;
50
+
51
+ for (const imp of symbols.imports) {
52
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
53
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
54
+ if (!targetRow) continue;
55
+
56
+ const edgeKind = imp.reexport
57
+ ? 'reexports'
58
+ : imp.typeOnly
59
+ ? 'imports-type'
60
+ : imp.dynamicImport
61
+ ? 'dynamic-imports'
62
+ : 'imports';
63
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
64
+
65
+ if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
66
+ buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
99
67
  }
100
68
  }
69
+ }
70
+ }
101
71
 
102
- // ── Call/receiver/extends/implements edges ───────────────────────
103
- const native = engineName === 'native' ? loadNative() : null;
104
- if (native?.buildCallEdges) {
105
- const nativeFiles = [];
106
- for (const [relPath, symbols] of fileSymbols) {
107
- if (barrelOnlyFiles.has(relPath)) continue;
108
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
109
- if (!fileNodeRow) continue;
110
-
111
- const importedNames = [];
112
- for (const imp of symbols.imports) {
113
- const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
114
- for (const name of imp.names) {
115
- const cleanName = name.replace(/^\*\s+as\s+/, '');
116
- let targetFile = resolvedPath;
117
- if (isBarrelFile(ctx, resolvedPath)) {
118
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
119
- if (actual) targetFile = actual;
120
- }
121
- importedNames.push({ name: cleanName, file: targetFile });
72
+ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, edgeRows) {
73
+ const resolvedSources = new Set();
74
+ for (const name of imp.names) {
75
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
76
+ const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
77
+ if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
78
+ resolvedSources.add(actualSource);
79
+ const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
80
+ if (actualRow) {
81
+ const kind =
82
+ edgeKind === 'imports-type'
83
+ ? 'imports-type'
84
+ : edgeKind === 'dynamic-imports'
85
+ ? 'dynamic-imports'
86
+ : 'imports';
87
+ edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ // ── Call edges (native engine) ──────────────────────────────────────────
94
+
95
+ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native) {
96
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
97
+ const nativeFiles = [];
98
+
99
+ for (const [relPath, symbols] of fileSymbols) {
100
+ if (barrelOnlyFiles.has(relPath)) continue;
101
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
102
+ if (!fileNodeRow) continue;
103
+
104
+ const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
105
+ nativeFiles.push({
106
+ file: relPath,
107
+ fileNodeId: fileNodeRow.id,
108
+ definitions: symbols.definitions.map((d) => ({
109
+ name: d.name,
110
+ kind: d.kind,
111
+ line: d.line,
112
+ endLine: d.endLine ?? null,
113
+ })),
114
+ calls: symbols.calls,
115
+ importedNames,
116
+ classes: symbols.classes,
117
+ });
118
+ }
119
+
120
+ const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [...BUILTIN_RECEIVERS]);
121
+ for (const e of nativeEdges) {
122
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
123
+ }
124
+ }
125
+
126
+ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
127
+ const importedNames = [];
128
+ for (const imp of symbols.imports) {
129
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
130
+ for (const name of imp.names) {
131
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
132
+ let targetFile = resolvedPath;
133
+ if (isBarrelFile(ctx, resolvedPath)) {
134
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
135
+ if (actual) targetFile = actual;
136
+ }
137
+ importedNames.push({ name: cleanName, file: targetFile });
138
+ }
139
+ }
140
+ return importedNames;
141
+ }
142
+
143
+ // ── Call edges (JS fallback) ────────────────────────────────────────────
144
+
145
+ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
146
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
147
+
148
+ for (const [relPath, symbols] of fileSymbols) {
149
+ if (barrelOnlyFiles.has(relPath)) continue;
150
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
151
+ if (!fileNodeRow) continue;
152
+
153
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
154
+ const seenCallEdges = new Set();
155
+
156
+ buildFileCallEdges(
157
+ ctx,
158
+ relPath,
159
+ symbols,
160
+ fileNodeRow,
161
+ importedNames,
162
+ seenCallEdges,
163
+ getNodeIdStmt,
164
+ allEdgeRows,
165
+ );
166
+ buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
167
+ }
168
+ }
169
+
170
+ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
171
+ const importedNames = new Map();
172
+ for (const imp of symbols.imports) {
173
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
174
+ for (const name of imp.names) {
175
+ importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
176
+ }
177
+ }
178
+ return importedNames;
179
+ }
180
+
181
+ function findCaller(call, definitions, relPath, getNodeIdStmt, fileNodeRow) {
182
+ let caller = null;
183
+ let callerSpan = Infinity;
184
+ for (const def of definitions) {
185
+ if (def.line <= call.line) {
186
+ const end = def.endLine || Infinity;
187
+ if (call.line <= end) {
188
+ const span = end - def.line;
189
+ if (span < callerSpan) {
190
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
191
+ if (row) {
192
+ caller = row;
193
+ callerSpan = span;
122
194
  }
123
195
  }
196
+ } else if (!caller) {
197
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
198
+ if (row) caller = row;
199
+ }
200
+ }
201
+ }
202
+ return caller || fileNodeRow;
203
+ }
124
204
 
125
- nativeFiles.push({
126
- file: relPath,
127
- fileNodeId: fileNodeRow.id,
128
- definitions: symbols.definitions.map((d) => ({
129
- name: d.name,
130
- kind: d.kind,
131
- line: d.line,
132
- endLine: d.endLine ?? null,
133
- })),
134
- calls: symbols.calls,
135
- importedNames,
136
- classes: symbols.classes,
137
- });
205
+ function resolveCallTargets(ctx, call, relPath, importedNames) {
206
+ const importedFrom = importedNames.get(call.name);
207
+ let targets;
208
+
209
+ if (importedFrom) {
210
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
211
+ if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
212
+ const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
213
+ if (actualSource) {
214
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
138
215
  }
216
+ }
217
+ }
139
218
 
140
- const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [...BUILTIN_RECEIVERS]);
141
- for (const e of nativeEdges) {
142
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
219
+ if (!targets || targets.length === 0) {
220
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
221
+ if (targets.length === 0) {
222
+ targets = resolveByMethodOrGlobal(ctx, call, relPath);
223
+ }
224
+ }
225
+
226
+ if (targets.length > 1) {
227
+ targets.sort((a, b) => {
228
+ const confA = computeConfidence(relPath, a.file, importedFrom);
229
+ const confB = computeConfidence(relPath, b.file, importedFrom);
230
+ return confB - confA;
231
+ });
232
+ }
233
+
234
+ return { targets, importedFrom };
235
+ }
236
+
237
+ function resolveByMethodOrGlobal(ctx, call, relPath) {
238
+ const methodCandidates = (ctx.nodesByName.get(call.name) || []).filter(
239
+ (n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
240
+ );
241
+ if (methodCandidates.length > 0) return methodCandidates;
242
+
243
+ if (
244
+ !call.receiver ||
245
+ call.receiver === 'this' ||
246
+ call.receiver === 'self' ||
247
+ call.receiver === 'super'
248
+ ) {
249
+ return (ctx.nodesByName.get(call.name) || []).filter(
250
+ (n) => computeConfidence(relPath, n.file, null) >= 0.5,
251
+ );
252
+ }
253
+ return [];
254
+ }
255
+
256
+ function buildFileCallEdges(
257
+ ctx,
258
+ relPath,
259
+ symbols,
260
+ fileNodeRow,
261
+ importedNames,
262
+ seenCallEdges,
263
+ getNodeIdStmt,
264
+ allEdgeRows,
265
+ ) {
266
+ for (const call of symbols.calls) {
267
+ if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
268
+
269
+ const caller = findCaller(call, symbols.definitions, relPath, getNodeIdStmt, fileNodeRow);
270
+ const isDynamic = call.dynamic ? 1 : 0;
271
+ const { targets, importedFrom } = resolveCallTargets(ctx, call, relPath, importedNames);
272
+
273
+ for (const t of targets) {
274
+ const edgeKey = `${caller.id}|${t.id}`;
275
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
276
+ seenCallEdges.add(edgeKey);
277
+ const confidence = computeConfidence(relPath, t.file, importedFrom);
278
+ allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
143
279
  }
144
- } else {
145
- // JS fallback
146
- for (const [relPath, symbols] of fileSymbols) {
147
- if (barrelOnlyFiles.has(relPath)) continue;
148
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
149
- if (!fileNodeRow) continue;
150
-
151
- const importedNames = new Map();
152
- for (const imp of symbols.imports) {
153
- const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
154
- for (const name of imp.names) {
155
- const cleanName = name.replace(/^\*\s+as\s+/, '');
156
- importedNames.set(cleanName, resolvedPath);
157
- }
158
- }
280
+ }
159
281
 
160
- const seenCallEdges = new Set();
161
- for (const call of symbols.calls) {
162
- if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
163
- let caller = null;
164
- let callerSpan = Infinity;
165
- for (const def of symbols.definitions) {
166
- if (def.line <= call.line) {
167
- const end = def.endLine || Infinity;
168
- if (call.line <= end) {
169
- const span = end - def.line;
170
- if (span < callerSpan) {
171
- const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
172
- if (row) {
173
- caller = row;
174
- callerSpan = span;
175
- }
176
- }
177
- } else if (!caller) {
178
- const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
179
- if (row) caller = row;
180
- }
181
- }
182
- }
183
- if (!caller) caller = fileNodeRow;
184
-
185
- const isDynamic = call.dynamic ? 1 : 0;
186
- let targets;
187
- const importedFrom = importedNames.get(call.name);
188
-
189
- if (importedFrom) {
190
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
191
- if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
192
- const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
193
- if (actualSource) {
194
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
195
- }
196
- }
197
- }
198
- if (!targets || targets.length === 0) {
199
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
200
- if (targets.length === 0) {
201
- const methodCandidates = (ctx.nodesByName.get(call.name) || []).filter(
202
- (n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
203
- );
204
- if (methodCandidates.length > 0) {
205
- targets = methodCandidates;
206
- } else if (
207
- !call.receiver ||
208
- call.receiver === 'this' ||
209
- call.receiver === 'self' ||
210
- call.receiver === 'super'
211
- ) {
212
- targets = (ctx.nodesByName.get(call.name) || []).filter(
213
- (n) => computeConfidence(relPath, n.file, null) >= 0.5,
214
- );
215
- }
216
- }
217
- }
282
+ // Receiver edge
283
+ if (
284
+ call.receiver &&
285
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
286
+ call.receiver !== 'this' &&
287
+ call.receiver !== 'self' &&
288
+ call.receiver !== 'super'
289
+ ) {
290
+ buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows);
291
+ }
292
+ }
293
+ }
218
294
 
219
- if (targets.length > 1) {
220
- targets.sort((a, b) => {
221
- const confA = computeConfidence(relPath, a.file, importedFrom);
222
- const confB = computeConfidence(relPath, b.file, importedFrom);
223
- return confB - confA;
224
- });
225
- }
295
+ function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows) {
296
+ const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
297
+ const samefile = ctx.nodesByNameAndFile.get(`${call.receiver}|${relPath}`) || [];
298
+ const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(call.receiver) || [];
299
+ const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
300
+ if (receiverNodes.length > 0 && caller) {
301
+ const recvTarget = receiverNodes[0];
302
+ const recvKey = `recv|${caller.id}|${recvTarget.id}`;
303
+ if (!seenCallEdges.has(recvKey)) {
304
+ seenCallEdges.add(recvKey);
305
+ allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
306
+ }
307
+ }
308
+ }
226
309
 
227
- for (const t of targets) {
228
- const edgeKey = `${caller.id}|${t.id}`;
229
- if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
230
- seenCallEdges.add(edgeKey);
231
- const confidence = computeConfidence(relPath, t.file, importedFrom);
232
- allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
233
- }
234
- }
310
+ // ── Class hierarchy edges ───────────────────────────────────────────────
235
311
 
236
- // Receiver edge
237
- if (
238
- call.receiver &&
239
- !BUILTIN_RECEIVERS.has(call.receiver) &&
240
- call.receiver !== 'this' &&
241
- call.receiver !== 'self' &&
242
- call.receiver !== 'super'
243
- ) {
244
- const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
245
- const samefile = ctx.nodesByNameAndFile.get(`${call.receiver}|${relPath}`) || [];
246
- const candidates =
247
- samefile.length > 0 ? samefile : ctx.nodesByName.get(call.receiver) || [];
248
- const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
249
- if (receiverNodes.length > 0 && caller) {
250
- const recvTarget = receiverNodes[0];
251
- const recvKey = `recv|${caller.id}|${recvTarget.id}`;
252
- if (!seenCallEdges.has(recvKey)) {
253
- seenCallEdges.add(recvKey);
254
- allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
255
- }
256
- }
257
- }
312
+ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
313
+ for (const cls of symbols.classes) {
314
+ if (cls.extends) {
315
+ const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
316
+ (n) => n.kind === 'class',
317
+ );
318
+ const targetRows = (ctx.nodesByName.get(cls.extends) || []).filter((n) => n.kind === 'class');
319
+ if (sourceRow) {
320
+ for (const t of targetRows) {
321
+ allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
258
322
  }
323
+ }
324
+ }
259
325
 
260
- // Class extends edges
261
- for (const cls of symbols.classes) {
262
- if (cls.extends) {
263
- const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
264
- (n) => n.kind === 'class',
265
- );
266
- const targetCandidates = ctx.nodesByName.get(cls.extends) || [];
267
- const targetRows = targetCandidates.filter((n) => n.kind === 'class');
268
- if (sourceRow) {
269
- for (const t of targetRows) {
270
- allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
271
- }
272
- }
273
- }
274
-
275
- if (cls.implements) {
276
- const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
277
- (n) => n.kind === 'class',
278
- );
279
- const targetCandidates = ctx.nodesByName.get(cls.implements) || [];
280
- const targetRows = targetCandidates.filter(
281
- (n) => n.kind === 'interface' || n.kind === 'class',
282
- );
283
- if (sourceRow) {
284
- for (const t of targetRows) {
285
- allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
286
- }
287
- }
288
- }
326
+ if (cls.implements) {
327
+ const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
328
+ (n) => n.kind === 'class',
329
+ );
330
+ const targetRows = (ctx.nodesByName.get(cls.implements) || []).filter(
331
+ (n) => n.kind === 'interface' || n.kind === 'class',
332
+ );
333
+ if (sourceRow) {
334
+ for (const t of targetRows) {
335
+ allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
289
336
  }
290
337
  }
291
338
  }
339
+ }
340
+ }
341
+
342
+ // ── Main entry point ────────────────────────────────────────────────────
343
+
344
+ /**
345
+ * @param {import('../context.js').PipelineContext} ctx
346
+ */
347
+ export async function buildEdges(ctx) {
348
+ const { db, engineName } = ctx;
349
+
350
+ const getNodeIdStmt = makeGetNodeIdStmt(db);
351
+
352
+ const allNodes = db
353
+ .prepare(
354
+ `SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`,
355
+ )
356
+ .all();
357
+ setupNodeLookups(ctx, allNodes);
358
+
359
+ const t0 = performance.now();
360
+ const buildEdgesTx = db.transaction(() => {
361
+ const allEdgeRows = [];
362
+
363
+ buildImportEdges(ctx, getNodeIdStmt, allEdgeRows);
364
+
365
+ const native = engineName === 'native' ? loadNative() : null;
366
+ if (native?.buildCallEdges) {
367
+ buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native);
368
+ } else {
369
+ buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
370
+ }
292
371
 
293
372
  batchInsertEdges(db, allEdgeRows);
294
373
  });