@optave/codegraph 3.11.2 → 3.13.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 (236) hide show
  1. package/README.md +73 -37
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +2 -1
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/batch.d.ts.map +1 -1
  6. package/dist/cli/commands/batch.js +1 -0
  7. package/dist/cli/commands/batch.js.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/build.js +6 -1
  10. package/dist/cli/commands/build.js.map +1 -1
  11. package/dist/cli/commands/config.d.ts +3 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +272 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/triage.js +1 -1
  16. package/dist/cli/commands/triage.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +10 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/shared/options.d.ts +2 -1
  21. package/dist/cli/shared/options.d.ts.map +1 -1
  22. package/dist/cli/shared/options.js +11 -1
  23. package/dist/cli/shared/options.js.map +1 -1
  24. package/dist/cli/types.d.ts +2 -0
  25. package/dist/cli/types.d.ts.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/migrations.js +8 -1
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/domain/analysis/module-map.d.ts +2 -0
  30. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  31. package/dist/domain/analysis/module-map.js +24 -2
  32. package/dist/domain/analysis/module-map.js.map +1 -1
  33. package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
  34. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  35. package/dist/domain/graph/builder/call-resolver.js +251 -34
  36. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  37. package/dist/domain/graph/builder/cha.d.ts +69 -0
  38. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  39. package/dist/domain/graph/builder/cha.js +158 -0
  40. package/dist/domain/graph/builder/cha.js.map +1 -0
  41. package/dist/domain/graph/builder/context.d.ts +3 -0
  42. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/context.js +2 -0
  44. package/dist/domain/graph/builder/context.js.map +1 -1
  45. package/dist/domain/graph/builder/helpers.d.ts +25 -1
  46. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/helpers.js +178 -5
  48. package/dist/domain/graph/builder/helpers.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +74 -2
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/pipeline.js +37 -2
  54. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-edges.js +704 -34
  57. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  59. package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
  60. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  63. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  68. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  71. package/dist/domain/graph/journal.js +1 -1
  72. package/dist/domain/graph/journal.js.map +1 -1
  73. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  74. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  75. package/dist/domain/graph/resolver/points-to.js +213 -0
  76. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  77. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  78. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  79. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  80. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  81. package/dist/domain/parser.d.ts +12 -4
  82. package/dist/domain/parser.d.ts.map +1 -1
  83. package/dist/domain/parser.js +83 -20
  84. package/dist/domain/parser.js.map +1 -1
  85. package/dist/domain/wasm-worker-entry.js +35 -2
  86. package/dist/domain/wasm-worker-entry.js.map +1 -1
  87. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  88. package/dist/domain/wasm-worker-pool.js +34 -0
  89. package/dist/domain/wasm-worker-pool.js.map +1 -1
  90. package/dist/domain/wasm-worker-protocol.d.ts +15 -1
  91. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  92. package/dist/extractors/c.js +3 -3
  93. package/dist/extractors/c.js.map +1 -1
  94. package/dist/extractors/clojure.js +1 -1
  95. package/dist/extractors/clojure.js.map +1 -1
  96. package/dist/extractors/cpp.d.ts.map +1 -1
  97. package/dist/extractors/cpp.js +45 -4
  98. package/dist/extractors/cpp.js.map +1 -1
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +37 -8
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/cuda.d.ts.map +1 -1
  103. package/dist/extractors/cuda.js +45 -4
  104. package/dist/extractors/cuda.js.map +1 -1
  105. package/dist/extractors/elixir.js +6 -6
  106. package/dist/extractors/elixir.js.map +1 -1
  107. package/dist/extractors/fsharp.js +1 -1
  108. package/dist/extractors/fsharp.js.map +1 -1
  109. package/dist/extractors/go.js +5 -5
  110. package/dist/extractors/go.js.map +1 -1
  111. package/dist/extractors/haskell.js +1 -1
  112. package/dist/extractors/haskell.js.map +1 -1
  113. package/dist/extractors/helpers.d.ts +11 -0
  114. package/dist/extractors/helpers.d.ts.map +1 -1
  115. package/dist/extractors/helpers.js +40 -0
  116. package/dist/extractors/helpers.js.map +1 -1
  117. package/dist/extractors/java.d.ts.map +1 -1
  118. package/dist/extractors/java.js +10 -9
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.d.ts +2 -0
  121. package/dist/extractors/javascript.d.ts.map +1 -1
  122. package/dist/extractors/javascript.js +1812 -71
  123. package/dist/extractors/javascript.js.map +1 -1
  124. package/dist/extractors/kotlin.js +5 -5
  125. package/dist/extractors/kotlin.js.map +1 -1
  126. package/dist/extractors/lua.js +1 -1
  127. package/dist/extractors/lua.js.map +1 -1
  128. package/dist/extractors/objc.js +3 -3
  129. package/dist/extractors/objc.js.map +1 -1
  130. package/dist/extractors/ocaml.js +1 -1
  131. package/dist/extractors/ocaml.js.map +1 -1
  132. package/dist/extractors/php.js +2 -2
  133. package/dist/extractors/php.js.map +1 -1
  134. package/dist/extractors/python.js +7 -7
  135. package/dist/extractors/python.js.map +1 -1
  136. package/dist/extractors/ruby.js +2 -2
  137. package/dist/extractors/ruby.js.map +1 -1
  138. package/dist/extractors/scala.js +1 -1
  139. package/dist/extractors/scala.js.map +1 -1
  140. package/dist/extractors/solidity.js +1 -1
  141. package/dist/extractors/solidity.js.map +1 -1
  142. package/dist/extractors/swift.js +4 -4
  143. package/dist/extractors/swift.js.map +1 -1
  144. package/dist/extractors/zig.js +4 -4
  145. package/dist/extractors/zig.js.map +1 -1
  146. package/dist/features/structure-query.d.ts +1 -1
  147. package/dist/features/structure-query.d.ts.map +1 -1
  148. package/dist/features/structure-query.js +6 -6
  149. package/dist/features/structure-query.js.map +1 -1
  150. package/dist/index.d.ts +1 -1
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +1 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/infrastructure/config.d.ts +85 -2
  155. package/dist/infrastructure/config.d.ts.map +1 -1
  156. package/dist/infrastructure/config.js +408 -19
  157. package/dist/infrastructure/config.js.map +1 -1
  158. package/dist/infrastructure/native.d.ts +11 -0
  159. package/dist/infrastructure/native.d.ts.map +1 -1
  160. package/dist/infrastructure/native.js +78 -5
  161. package/dist/infrastructure/native.js.map +1 -1
  162. package/dist/infrastructure/registry.d.ts +27 -0
  163. package/dist/infrastructure/registry.d.ts.map +1 -1
  164. package/dist/infrastructure/registry.js +59 -1
  165. package/dist/infrastructure/registry.js.map +1 -1
  166. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  167. package/dist/presentation/queries-cli/overview.js +5 -0
  168. package/dist/presentation/queries-cli/overview.js.map +1 -1
  169. package/dist/presentation/structure.d.ts +1 -1
  170. package/dist/presentation/structure.d.ts.map +1 -1
  171. package/dist/presentation/structure.js +2 -2
  172. package/dist/presentation/structure.js.map +1 -1
  173. package/dist/types.d.ts +221 -0
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-gleam.wasm +0 -0
  176. package/package.json +7 -8
  177. package/src/cli/commands/audit.ts +2 -1
  178. package/src/cli/commands/batch.ts +1 -0
  179. package/src/cli/commands/build.ts +6 -1
  180. package/src/cli/commands/config.ts +353 -0
  181. package/src/cli/commands/triage.ts +1 -1
  182. package/src/cli/index.ts +10 -0
  183. package/src/cli/shared/options.ts +11 -1
  184. package/src/cli/types.ts +2 -0
  185. package/src/db/migrations.ts +8 -1
  186. package/src/domain/analysis/module-map.ts +29 -1
  187. package/src/domain/graph/builder/call-resolver.ts +263 -35
  188. package/src/domain/graph/builder/cha.ts +192 -0
  189. package/src/domain/graph/builder/context.ts +3 -0
  190. package/src/domain/graph/builder/helpers.ts +195 -5
  191. package/src/domain/graph/builder/incremental.ts +80 -1
  192. package/src/domain/graph/builder/pipeline.ts +49 -2
  193. package/src/domain/graph/builder/stages/build-edges.ts +867 -32
  194. package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
  195. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  196. package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
  197. package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
  198. package/src/domain/graph/journal.ts +1 -1
  199. package/src/domain/graph/resolver/points-to.ts +254 -0
  200. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  201. package/src/domain/parser.ts +86 -17
  202. package/src/domain/wasm-worker-entry.ts +35 -2
  203. package/src/domain/wasm-worker-pool.ts +22 -0
  204. package/src/domain/wasm-worker-protocol.ts +15 -0
  205. package/src/extractors/c.ts +3 -3
  206. package/src/extractors/clojure.ts +1 -1
  207. package/src/extractors/cpp.ts +47 -4
  208. package/src/extractors/csharp.ts +33 -9
  209. package/src/extractors/cuda.ts +47 -4
  210. package/src/extractors/elixir.ts +6 -6
  211. package/src/extractors/fsharp.ts +1 -1
  212. package/src/extractors/go.ts +5 -5
  213. package/src/extractors/haskell.ts +1 -1
  214. package/src/extractors/helpers.ts +43 -0
  215. package/src/extractors/java.ts +10 -9
  216. package/src/extractors/javascript.ts +1929 -72
  217. package/src/extractors/kotlin.ts +5 -5
  218. package/src/extractors/lua.ts +1 -1
  219. package/src/extractors/objc.ts +3 -3
  220. package/src/extractors/ocaml.ts +1 -1
  221. package/src/extractors/php.ts +2 -2
  222. package/src/extractors/python.ts +7 -7
  223. package/src/extractors/ruby.ts +2 -2
  224. package/src/extractors/scala.ts +1 -1
  225. package/src/extractors/solidity.ts +1 -1
  226. package/src/extractors/swift.ts +4 -4
  227. package/src/extractors/zig.ts +4 -4
  228. package/src/features/structure-query.ts +7 -7
  229. package/src/index.ts +5 -1
  230. package/src/infrastructure/config.ts +494 -20
  231. package/src/infrastructure/native.ts +87 -5
  232. package/src/infrastructure/registry.ts +82 -1
  233. package/src/presentation/queries-cli/overview.ts +15 -1
  234. package/src/presentation/structure.ts +3 -3
  235. package/src/types.ts +235 -0
  236. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -7,12 +7,17 @@
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
9
  import { getNodeId } from '../../../../db/index.js';
10
+ import { setTypeMapEntry } from '../../../../extractors/helpers.js';
11
+ import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
10
12
  import { debug } from '../../../../infrastructure/logger.js';
11
13
  import { loadNative } from '../../../../infrastructure/native.js';
12
14
  import { computeConfidence } from '../../resolve.js';
13
- import { findCaller, resolveCallTargets, resolveReceiverEdge, } from '../call-resolver.js';
14
- import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
15
- import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
15
+ import { buildPointsToMap, resolveViaPointsTo } from '../../resolver/points-to.js';
16
+ import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
17
+ import { findCaller, isModuleScopedLanguage, resolveCallTargets, resolveReceiverEdge, } from '../call-resolver.js';
18
+ import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
19
+ import { BUILTIN_RECEIVERS, batchInsertEdges, CHA_DISPATCH_PENALTY, CHA_TYPED_DISPATCH_CONFIDENCE, runChaPostPass, } from '../helpers.js';
20
+ import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
16
21
  // ── Node lookup setup ───────────────────────────────────────────────────
17
22
  function makeGetNodeIdStmt(db) {
18
23
  return {
@@ -59,13 +64,13 @@ function emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows
59
64
  const cleanName = name.replace(/^\*\s+as\s+/, '');
60
65
  let targetFile = resolvedPath;
61
66
  if (isBarrelFile(ctx, resolvedPath)) {
62
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
67
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
63
68
  if (actual)
64
69
  targetFile = actual;
65
70
  }
66
71
  const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
67
72
  if (candidates && candidates.length > 0) {
68
- allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0]);
73
+ allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0, null]);
69
74
  }
70
75
  }
71
76
  }
@@ -79,7 +84,7 @@ function emitEdgesForImport(ctx, imp, fileNodeId, relPath, getNodeIdStmt, allEdg
79
84
  if (!targetRow)
80
85
  return;
81
86
  const edgeKind = importEdgeKind(imp);
82
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
87
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0, null]);
83
88
  if (imp.typeOnly) {
84
89
  emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
85
90
  }
@@ -107,7 +112,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
107
112
  const resolvedSources = new Set();
108
113
  for (const name of imp.names) {
109
114
  const cleanName = name.replace(/^\*\s+as\s+/, '');
110
- const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
115
+ const actualSource = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
111
116
  if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
112
117
  resolvedSources.add(actualSource);
113
118
  const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
@@ -117,7 +122,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
117
122
  : edgeKind === 'dynamic-imports'
118
123
  ? 'dynamic-imports'
119
124
  : 'imports';
120
- edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
125
+ edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0, null]);
121
126
  }
122
127
  }
123
128
  }
@@ -232,7 +237,66 @@ function buildImportEdgesNative(ctx, getNodeIdStmt, allEdgeRows, native) {
232
237
  const symbolNodes = collectSymbolNodes(ctx);
233
238
  const nativeEdges = native.buildImportEdges(files, resolvedImports, fileReexports, registry.ids, barrelFiles, ctx.rootDir, symbolNodes);
234
239
  for (const e of nativeEdges) {
235
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
240
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic, null]);
241
+ }
242
+ }
243
+ // ── Phase 8.2: Cross-file return-type propagation ───────────────────────
244
+ /**
245
+ * Augment each file's typeMap with return types from imported functions.
246
+ *
247
+ * The per-file extractor already resolves same-file call assignments (intra-file
248
+ * propagation). This function handles the cross-file case: when a file imports a
249
+ * function from another file and assigns its return value to a variable, we look up
250
+ * the callee's return type in the source file's returnTypeMap and inject it.
251
+ *
252
+ * Called once before call-edge building so both the native and JS paths benefit.
253
+ */
254
+ function propagateReturnTypesAcrossFiles(fileSymbols, ctx, rootDir) {
255
+ // Index: filePath → per-file return-type map
256
+ const returnTypeIndex = new Map();
257
+ for (const [relPath, symbols] of fileSymbols) {
258
+ if (symbols.returnTypeMap?.size)
259
+ returnTypeIndex.set(relPath, symbols.returnTypeMap);
260
+ }
261
+ if (returnTypeIndex.size === 0)
262
+ return;
263
+ // Flat global map for qualified method lookups (TypeName.methodName → entry).
264
+ // Conflicts resolved by keeping the highest-confidence entry.
265
+ const globalReturnTypeMap = new Map();
266
+ for (const rtm of returnTypeIndex.values()) {
267
+ for (const [name, entry] of rtm) {
268
+ const existing = globalReturnTypeMap.get(name);
269
+ if (!existing || entry.confidence > existing.confidence)
270
+ globalReturnTypeMap.set(name, entry);
271
+ }
272
+ }
273
+ for (const [relPath, symbols] of fileSymbols) {
274
+ if (!symbols.callAssignments?.length)
275
+ continue;
276
+ // Phase 8.4 side-effect: buildImportedNamesMap now traces through barrel
277
+ // files (traceBarrel), so `importedFrom` resolves to the leaf definition
278
+ // file rather than the barrel. This means returnTypeIndex.get(importedFrom)
279
+ // now finds entries it previously missed, improving cross-file return-type
280
+ // propagation through re-export chains (Phase 8.2 improvement).
281
+ const importedNamesMap = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
282
+ for (const ca of symbols.callAssignments) {
283
+ if (symbols.typeMap.has(ca.varName))
284
+ continue; // already resolved locally
285
+ let returnEntry;
286
+ if (ca.receiverTypeName) {
287
+ returnEntry = globalReturnTypeMap.get(`${ca.receiverTypeName}.${ca.calleeName}`);
288
+ }
289
+ else {
290
+ const importedFrom = importedNamesMap.get(ca.calleeName);
291
+ if (importedFrom)
292
+ returnEntry = returnTypeIndex.get(importedFrom)?.get(ca.calleeName);
293
+ }
294
+ if (returnEntry) {
295
+ const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;
296
+ if (propagatedConf > 0)
297
+ setTypeMapEntry(symbols.typeMap, ca.varName, returnEntry.type, propagatedConf);
298
+ }
299
+ }
236
300
  }
237
301
  }
238
302
  // ── Call edges (native engine) ──────────────────────────────────────────
@@ -271,23 +335,195 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
271
335
  nativeFiles.push({
272
336
  file: relPath,
273
337
  fileNodeId: fileNodeRow.id,
274
- definitions: symbols.definitions.map((d) => ({
275
- name: d.name,
276
- kind: d.kind,
277
- line: d.line,
278
- endLine: d.endLine ?? null,
279
- })),
338
+ definitions: symbols.definitions.map((d) => {
339
+ const params = d.children?.filter((c) => c.kind === 'parameter').map((c) => c.name);
340
+ return {
341
+ name: d.name,
342
+ kind: d.kind,
343
+ line: d.line,
344
+ endLine: d.endLine ?? null,
345
+ params: params?.length ? params : undefined,
346
+ };
347
+ }),
280
348
  calls: symbols.calls,
281
349
  importedNames,
282
350
  classes: symbols.classes,
283
351
  typeMap,
352
+ fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
353
+ paramBindings: symbols.paramBindings?.length ? symbols.paramBindings : undefined,
354
+ thisCallBindings: symbols.thisCallBindings?.length ? symbols.thisCallBindings : undefined,
355
+ arrayElemBindings: symbols.arrayElemBindings?.length ? symbols.arrayElemBindings : undefined,
356
+ spreadArgBindings: symbols.spreadArgBindings?.length ? symbols.spreadArgBindings : undefined,
357
+ forOfBindings: symbols.forOfBindings?.length ? symbols.forOfBindings : undefined,
358
+ arrayCallbackBindings: symbols.arrayCallbackBindings?.length
359
+ ? symbols.arrayCallbackBindings
360
+ : undefined,
361
+ objectRestParamBindings: symbols.objectRestParamBindings?.length
362
+ ? symbols.objectRestParamBindings
363
+ : undefined,
364
+ objectPropBindings: symbols.objectPropBindings?.length
365
+ ? symbols.objectPropBindings
366
+ : undefined,
284
367
  });
285
368
  }
286
369
  const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [
287
370
  ...BUILTIN_RECEIVERS,
288
371
  ]);
289
372
  for (const e of nativeEdges) {
290
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
373
+ allEdgeRows.push([
374
+ e.sourceId,
375
+ e.targetId,
376
+ e.kind,
377
+ e.confidence,
378
+ e.dynamic,
379
+ e.kind === 'calls' ? 'ts-native' : null,
380
+ ]);
381
+ }
382
+ }
383
+ /**
384
+ * Object.defineProperty accessor post-pass for the native call-edge path.
385
+ *
386
+ * When a function is registered as a getter/setter via
387
+ * `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()`
388
+ * inside `getter` need to resolve against `obj` (because `this === obj` when
389
+ * the accessor is invoked). The native Rust engine has no knowledge of
390
+ * `definePropertyReceivers`, so this JS post-pass adds the missing edges.
391
+ */
392
+ function buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
393
+ const filesWithReceivers = [...ctx.fileSymbols].filter(([, symbols]) => symbols.definePropertyReceivers && symbols.definePropertyReceivers.size > 0);
394
+ if (filesWithReceivers.length === 0)
395
+ return;
396
+ const seenByPair = new Set();
397
+ for (const [srcId, tgtId] of allEdgeRows) {
398
+ seenByPair.add(`${srcId}|${tgtId}`);
399
+ }
400
+ const { barrelOnlyFiles, rootDir } = ctx;
401
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
402
+ for (const [relPath, symbols] of filesWithReceivers) {
403
+ if (barrelOnlyFiles.has(relPath))
404
+ continue;
405
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
406
+ if (!fileNodeRow)
407
+ continue;
408
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
409
+ const typeMap = symbols.typeMap || new Map();
410
+ const definePropertyReceivers = symbols.definePropertyReceivers;
411
+ for (const call of symbols.calls) {
412
+ if (call.receiver !== 'this')
413
+ continue;
414
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
415
+ if (!caller.callerName)
416
+ continue;
417
+ const receiverVarName = definePropertyReceivers.get(caller.callerName);
418
+ if (!receiverVarName)
419
+ continue;
420
+ // Only add edges the native engine missed (no direct target already).
421
+ const { targets: directTargets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
422
+ if (directTargets.length > 0)
423
+ continue;
424
+ // Resolve via receiver type
425
+ let targets = [];
426
+ const typeEntry = typeMap.get(receiverVarName);
427
+ const typeName = typeEntry
428
+ ? typeof typeEntry === 'string'
429
+ ? typeEntry
430
+ : typeEntry.type
431
+ : null;
432
+ if (typeName) {
433
+ const qualifiedName = `${typeName}.${call.name}`;
434
+ targets = lookup.byNameAndFile(qualifiedName, relPath);
435
+ }
436
+ // Same-file fallback for plain object-literal methods
437
+ if (targets.length === 0) {
438
+ targets = lookup.byNameAndFile(call.name, relPath);
439
+ }
440
+ for (const t of targets) {
441
+ const edgeKey = `${caller.id}|${t.id}`;
442
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
443
+ const conf = computeConfidence(relPath, t.file, null);
444
+ if (conf > 0) {
445
+ seenByPair.add(edgeKey);
446
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'ts-native']);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ /**
454
+ * Phase 8.5: CHA + RTA post-pass for the native call-edge path.
455
+ *
456
+ * The native Rust engine has no knowledge of the CHA context, so `this.method()`
457
+ * calls and interface method dispatches are not expanded to their concrete
458
+ * implementations. This JS post-pass runs after the native edges and adds only
459
+ * the CHA-resolved edges that the native engine missed.
460
+ *
461
+ * Seeds seenByPair from the current allEdgeRows snapshot to avoid duplicating
462
+ * edges the native engine already produced.
463
+ */
464
+ function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
465
+ // Fast-exit when the CHA context is empty (no class hierarchy in the project)
466
+ if (chaCtx.implementors.size === 0 && chaCtx.parents.size === 0)
467
+ return;
468
+ // Seed only from 'calls' edges — import/extends/implements edges share (src,tgt) pairs
469
+ // with real call edges at the file-node level and would cause false dedup if included.
470
+ const seenByPair = new Set();
471
+ for (const row of allEdgeRows) {
472
+ if (row[2] === 'calls')
473
+ seenByPair.add(`${row[0]}|${row[1]}`);
474
+ }
475
+ const { fileSymbols, barrelOnlyFiles } = ctx;
476
+ const lookup = makeContextLookup(ctx, getNodeIdStmt);
477
+ for (const [relPath, symbols] of fileSymbols) {
478
+ if (barrelOnlyFiles.has(relPath))
479
+ continue;
480
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
481
+ if (!fileNodeRow)
482
+ continue;
483
+ const typeMap = symbols.typeMap || new Map();
484
+ for (const call of symbols.calls) {
485
+ if (!call.receiver)
486
+ continue;
487
+ if (BUILTIN_RECEIVERS.has(call.receiver))
488
+ continue;
489
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
490
+ let chaTargets = [];
491
+ let isTypedReceiverDispatch = false;
492
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
493
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup, relPath);
494
+ }
495
+ else {
496
+ const typeEntry = typeMap.get(call.receiver);
497
+ const typeName = typeEntry
498
+ ? typeof typeEntry === 'string'
499
+ ? typeEntry
500
+ : typeEntry.type
501
+ : null;
502
+ if (typeName) {
503
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
504
+ isTypedReceiverDispatch = true;
505
+ }
506
+ }
507
+ for (const t of chaTargets) {
508
+ const edgeKey = `${caller.id}|${t.id}`;
509
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
510
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
511
+ // — file proximity is not meaningful for virtual dispatch confidence.
512
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
513
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
514
+ const conf = isTypedReceiverDispatch
515
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
516
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
517
+ if (conf > 0) {
518
+ seenByPair.add(edgeKey);
519
+ // Tag super-dispatch edges distinctly so runChaPostPass can exclude them
520
+ // from further CHA expansion (super calls are not virtual dispatch).
521
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
522
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, technique]);
523
+ }
524
+ }
525
+ }
526
+ }
291
527
  }
292
528
  }
293
529
  function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
@@ -301,7 +537,7 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
301
537
  const cleanName = name.replace(/^\*\s+as\s+/, '');
302
538
  let targetFile = resolvedPath;
303
539
  if (isBarrelFile(ctx, resolvedPath)) {
304
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
540
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
305
541
  if (actual)
306
542
  targetFile = actual;
307
543
  }
@@ -319,7 +555,7 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
319
555
  return importedNames;
320
556
  }
321
557
  // ── Call edges (JS fallback) ────────────────────────────────────────────
322
- function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
558
+ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
323
559
  const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
324
560
  const lookup = makeContextLookup(ctx, getNodeIdStmt);
325
561
  for (const [relPath, symbols] of fileSymbols) {
@@ -329,9 +565,36 @@ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
329
565
  if (!fileNodeRow)
330
566
  continue;
331
567
  const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
332
- const typeMap = symbols.typeMap || new Map();
568
+ const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []);
569
+ // Phase 8.3f: seed typeMap[callee::restName] = { type: argName } for each
570
+ // object-destructuring rest parameter binding × call-site argument binding.
571
+ // Keys are scoped so two functions with the same rest-param name in the same
572
+ // file don't collide (#1358). When only one callee uses a given rest name,
573
+ // also seed the unscoped key as a null-callerName fallback.
574
+ if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
575
+ const restNameCallees = new Map();
576
+ for (const orpb of symbols.objectRestParamBindings) {
577
+ if (!restNameCallees.has(orpb.restName))
578
+ restNameCallees.set(orpb.restName, new Set());
579
+ restNameCallees.get(orpb.restName).add(orpb.callee);
580
+ }
581
+ for (const orpb of symbols.objectRestParamBindings) {
582
+ for (const pb of symbols.paramBindings) {
583
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
584
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
585
+ if (!typeMap.has(scopedKey)) {
586
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
587
+ if (restNameCallees.get(orpb.restName).size === 1 && !typeMap.has(orpb.restName)) {
588
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
333
595
  const seenCallEdges = new Set();
334
- buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap);
596
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
597
+ buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx);
335
598
  buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
336
599
  }
337
600
  }
@@ -341,12 +604,23 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
341
604
  // (higher priority). Static imports represent direct bindings while dynamic
342
605
  // imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
343
606
  // When both contribute the same name, the static binding is authoritative.
607
+ //
608
+ // Phase 8.4: trace through barrel files so that symbol names map to their
609
+ // actual definition file, not the re-exporting barrel. Mirrors the tracing
610
+ // already done in buildImportedNamesForNative (the native path).
611
+ const traceBarrel = (resolvedPath, cleanName) => {
612
+ if (!isBarrelFile(ctx, resolvedPath))
613
+ return resolvedPath;
614
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
615
+ return actual ?? resolvedPath;
616
+ };
344
617
  for (const imp of symbols.imports) {
345
618
  if (!imp.dynamicImport)
346
619
  continue;
347
620
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
348
621
  for (const name of imp.names) {
349
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
622
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
623
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
350
624
  }
351
625
  }
352
626
  for (const imp of symbols.imports) {
@@ -355,7 +629,7 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
355
629
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
356
630
  for (const name of imp.names) {
357
631
  const cleanName = name.replace(/^\*\s+as\s+/, '');
358
- importedNames.set(cleanName, resolvedPath);
632
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
359
633
  }
360
634
  }
361
635
  return importedNames;
@@ -365,23 +639,298 @@ function makeContextLookup(ctx, getNodeIdStmt) {
365
639
  byNameAndFile: (name, file) => ctx.nodesByNameAndFile.get(`${name}|${file}`) ?? [],
366
640
  byName: (name) => ctx.nodesByName.get(name) ?? [],
367
641
  isBarrel: (file) => isBarrelFile(ctx, file),
368
- resolveBarrel: (barrelFile, symbolName) => resolveBarrelExport(ctx, barrelFile, symbolName),
642
+ resolveBarrel: (barrelFile, symbolName) => resolveBarrelExportCached(ctx, barrelFile, symbolName),
369
643
  nodeId: (name, kind, file, line) => getNodeIdStmt.get(name, kind, file, line),
370
644
  };
371
645
  }
372
- function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap) {
646
+ /**
647
+ * Build a per-file points-to map for Phase 8.3 alias resolution.
648
+ * Returns null fast when the file has no function-reference bindings.
649
+ *
650
+ * Only callable definitions (function/method) are seeded as concrete targets.
651
+ * Class and interface names are intentionally excluded — aliasing a constructor
652
+ * (`const Svc = MyService`) is an uncommon pattern that would require tracking
653
+ * `new`-expression flows separately from the alias chain. That is left to Phase
654
+ * 8.2 call-assignment propagation, which already handles constructor assignments.
655
+ */
656
+ function buildPointsToMapForFile(symbols, importedNames) {
657
+ const hasThisCallBindings = !!symbols.thisCallBindings?.length;
658
+ if (!symbols.fnRefBindings?.length &&
659
+ !symbols.paramBindings?.length &&
660
+ !symbols.arrayElemBindings?.length &&
661
+ !symbols.spreadArgBindings?.length &&
662
+ !symbols.forOfBindings?.length &&
663
+ !symbols.arrayCallbackBindings?.length &&
664
+ !symbols.objectRestParamBindings?.length &&
665
+ !symbols.objectPropBindings?.length &&
666
+ !hasThisCallBindings)
667
+ return null;
668
+ const defNames = new Set(symbols.definitions
669
+ .filter((d) => d.kind === 'function' || d.kind === 'method')
670
+ .map((d) => d.name));
671
+ const definitionParams = buildDefinitionParamsMap(symbols.definitions);
672
+ // Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
673
+ // The scoped key `fn::this` is looked up when `this()` calls are resolved inside
674
+ // function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
675
+ let allFnRefBindings = symbols.fnRefBindings ?? [];
676
+ if (hasThisCallBindings) {
677
+ const extra = (symbols.thisCallBindings ?? []).map((b) => ({
678
+ lhs: `${b.callee}::this`,
679
+ rhs: b.thisArg,
680
+ }));
681
+ allFnRefBindings = [...allFnRefBindings, ...extra];
682
+ }
683
+ return buildPointsToMap(allFnRefBindings, defNames, importedNames, symbols.paramBindings, definitionParams, symbols.arrayElemBindings, symbols.spreadArgBindings, symbols.forOfBindings, symbols.arrayCallbackBindings, symbols.objectRestParamBindings, symbols.objectPropBindings);
684
+ }
685
+ function buildDefinitionParamsMap(definitions) {
686
+ const map = new Map();
687
+ for (const def of definitions) {
688
+ if ((def.kind === 'function' || def.kind === 'method') && def.children) {
689
+ const params = def.children.filter((c) => c.kind === 'parameter').map((c) => c.name);
690
+ if (params.length > 0) {
691
+ if (map.has(def.name)) {
692
+ // Two definitions share the same name (e.g. overloads, same-named method and
693
+ // function, or conditional redeclaration). Keep the first entry — using the
694
+ // wrong parameter list would map argIndex to the wrong parameter name.
695
+ debug(`buildDefinitionParamsMap: duplicate def name "${def.name}" (kind=${def.kind}, line=${def.line}) — skipping; first entry kept`);
696
+ }
697
+ else {
698
+ map.set(def.name, params);
699
+ }
700
+ }
701
+ }
702
+ }
703
+ return map;
704
+ }
705
+ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx) {
706
+ // Tracks edges that were inserted by the pts fallback (edgeKey → allEdgeRows index).
707
+ // Kept separate from seenCallEdges so that a subsequent direct-call edge for the same
708
+ // caller→target pair can upgrade the confidence in-place rather than being silently
709
+ // dropped by the dedup guard. Once upgraded, the key moves to seenCallEdges and is
710
+ // no longer tracked here.
711
+ const ptsEdgeRows = new Map();
712
+ // Pre-compute the set of names that appear as lhs in fnRefBindings so that
713
+ // case (c) of the pts gate below only fires for names that are genuine
714
+ // bind/alias entries, not for every locally-defined function or import that
715
+ // buildPointsToMap seeds with a self-pointing entry.
716
+ const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
373
717
  for (const call of symbols.calls) {
374
718
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver))
375
719
  continue;
376
720
  const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
377
721
  const isDynamic = call.dynamic ? 1 : 0;
378
- const { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap);
722
+ let { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
723
+ // Same-class `this.method()` fallback: when the call receiver is `this` and
724
+ // resolveCallTargets found nothing, derive the enclosing class name from the
725
+ // caller (e.g. `Logger.info` → class prefix `Logger`) and retry with the
726
+ // qualified method name `Logger._write`. This mirrors what the native Rust
727
+ // engine does implicitly via its class-scoped symbol table.
728
+ // NOTE: restricted to `this` only — `super.method()` targets a parent class,
729
+ // not the enclosing class, so qualifying with the child class name would
730
+ // produce a false edge when the child also defines a same-named method.
731
+ if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
732
+ const lastDot = caller.callerName.lastIndexOf('.');
733
+ if (lastDot > 0) {
734
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
735
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
736
+ const qualifiedName = `${className}.${call.name}`;
737
+ const qualified = lookup
738
+ .byNameAndFile(qualifiedName, relPath)
739
+ .filter((n) => n.kind === 'method');
740
+ if (qualified.length > 0) {
741
+ targets = qualified;
742
+ }
743
+ }
744
+ }
745
+ // Same-class bare-call fallback: when a no-receiver call can't be resolved
746
+ // globally, try the caller's own class as a qualifier. Handles C# static
747
+ // sibling calls: `IsValidEmail()` inside `Validators.ValidateUser` resolves
748
+ // to `Validators.IsValidEmail`. Skipped for JS/TS where bare calls are
749
+ // module-scoped, not class-scoped.
750
+ if (targets.length === 0 &&
751
+ !call.receiver &&
752
+ caller.callerName != null &&
753
+ !isModuleScopedLanguage(relPath)) {
754
+ const lastDot = caller.callerName.lastIndexOf('.');
755
+ if (lastDot > 0) {
756
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
757
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
758
+ const qualifiedName = `${className}.${call.name}`;
759
+ const qualified = lookup
760
+ .byNameAndFile(qualifiedName, relPath)
761
+ .filter((n) => n.kind === 'method');
762
+ if (qualified.length > 0) {
763
+ targets = qualified;
764
+ }
765
+ }
766
+ }
767
+ // Object.defineProperty accessor fallback: when a function is registered as
768
+ // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
769
+ // calls to `this.X()` inside `getter` resolve against `obj` (this === obj
770
+ // when the accessor is invoked). If the same-class fallback above found
771
+ // nothing, try treating `obj` as the receiver and look up `obj.X` in the
772
+ // typeMap, or fall back to a same-file lookup of any definition named X
773
+ // that belongs to the object literal or its type.
774
+ if (targets.length === 0 &&
775
+ call.receiver === 'this' &&
776
+ caller.callerName != null &&
777
+ symbols.definePropertyReceivers) {
778
+ const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
779
+ if (receiverVarName) {
780
+ // Try typeMap lookup for receiver.methodName
781
+ const typeEntry = typeMap.get(receiverVarName);
782
+ const typeName = typeEntry
783
+ ? typeof typeEntry === 'string'
784
+ ? typeEntry
785
+ : typeEntry.type
786
+ : null;
787
+ if (typeName) {
788
+ const qualifiedName = `${typeName}.${call.name}`;
789
+ const qualified = lookup.byNameAndFile(qualifiedName, relPath);
790
+ if (qualified.length > 0) {
791
+ targets = [...qualified];
792
+ }
793
+ }
794
+ // If still no targets, search for any definition named `call.name` in
795
+ // the same file — handles plain object literals where the method isn't
796
+ // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
797
+ // Note: this is intentionally broad — it matches any same-file definition
798
+ // with the called name, not just members of the receiver object. This is
799
+ // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
800
+ if (targets.length === 0) {
801
+ const sameFile = lookup.byNameAndFile(call.name, relPath);
802
+ if (sameFile.length > 0) {
803
+ targets = [...sameFile];
804
+ }
805
+ }
806
+ }
807
+ }
808
+ // Sort targets by confidence descending before emitting edges.
809
+ // For multi-target calls with duplicate (source_id, target_id) pairs the
810
+ // stored confidence depends on which duplicate is processed last — sorting
811
+ // here guarantees the highest-confidence target wins on dedup, matching the
812
+ // native engine's sort_targets_by_confidence call in build_edges.rs.
813
+ if (targets.length > 1) {
814
+ targets = [...targets].sort((a, b) => computeConfidence(relPath, b.file, importedFrom ?? null) -
815
+ computeConfidence(relPath, a.file, importedFrom ?? null));
816
+ }
379
817
  for (const t of targets) {
380
818
  const edgeKey = `${caller.id}|${t.id}`;
381
- if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
382
- seenCallEdges.add(edgeKey);
819
+ if (t.id !== caller.id) {
383
820
  const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
384
- allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
821
+ if (seenCallEdges.has(edgeKey))
822
+ continue;
823
+ const ptsIdx = ptsEdgeRows.get(edgeKey);
824
+ if (ptsIdx !== undefined) {
825
+ // A pts-resolved edge already exists for this caller→target pair with a
826
+ // penalised confidence. Upgrade it to the direct-call confidence in-place,
827
+ // then promote to seenCallEdges so no further processing is needed.
828
+ const ptsRow = allEdgeRows[ptsIdx];
829
+ if (ptsRow) {
830
+ ptsRow[3] = confidence;
831
+ ptsRow[4] = isDynamic; // upgrade is_dynamic: direct call overrides the pts-alias dynamic flag
832
+ ptsRow[5] = 'ts-native'; // promoted from pts to direct-call resolution
833
+ }
834
+ ptsEdgeRows.delete(edgeKey);
835
+ seenCallEdges.add(edgeKey);
836
+ }
837
+ else {
838
+ seenCallEdges.add(edgeKey);
839
+ allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic, 'ts-native']);
840
+ }
841
+ }
842
+ }
843
+ // Phase 8.3 / 8.3c / bind: points-to fallback for unresolved calls.
844
+ // Fires for three cases:
845
+ // (a) dynamic=true: alias calls emitted by extractCallbackReferenceCalls.
846
+ // Looks up `call.name` directly (alias entries are flat-keyed).
847
+ // (b) non-dynamic: parameter variable calls (fn() where fn is a param).
848
+ // Looks up the scoped key `callerName::call.name` to avoid spurious
849
+ // edges from same-named parameters across different functions.
850
+ // (c) non-dynamic: module-level alias bindings — `f = fn.bind(ctx)` or
851
+ // `const f = handler` — where pts('f') was seeded by fnRefBindings.
852
+ // Checked against fnRefBindingLhs (the pre-computed set of lhs names from
853
+ // fnRefBindings) rather than the full ptsMap, so case (c) only fires for
854
+ // genuine bind/alias entries and never for self-seeded local definitions.
855
+ // Confidence is penalised by one hop to reflect the extra indirection.
856
+ //
857
+ // Note: pts edges are added to ptsEdgeRows (not seenCallEdges) so that a later
858
+ // direct call to the same target in the same function body can upgrade confidence
859
+ // rather than being silently dropped by the dedup guard.
860
+ const scopedPtsKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
861
+ // Module-level calls (callerName === null) use the '<module>' sentinel emitted by
862
+ // extractSpreadForOfWalk for top-level for-of loops. Look it up as a fallback so
863
+ // that `for (const f of arr) { f(); }` at module scope resolves correctly.
864
+ const modulePtsKey = caller.callerName === null && ptsMap?.has(`<module>::${call.name}`)
865
+ ? `<module>::${call.name}`
866
+ : null;
867
+ const flatPtsKey = !call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;
868
+ if (targets.length === 0 &&
869
+ !call.receiver &&
870
+ ptsMap &&
871
+ (call.dynamic ||
872
+ (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) ||
873
+ modulePtsKey != null ||
874
+ flatPtsKey != null)) {
875
+ const ptsLookupName = call.dynamic
876
+ ? call.name
877
+ : scopedPtsKey != null && ptsMap.has(scopedPtsKey)
878
+ ? scopedPtsKey
879
+ : modulePtsKey != null
880
+ ? modulePtsKey
881
+ : // flatPtsKey != null is guaranteed by the outer if condition: if neither
882
+ // call.dynamic nor scopedPtsKey nor modulePtsKey matched, flatPtsKey must be non-null.
883
+ flatPtsKey;
884
+ for (const alias of resolveViaPointsTo(ptsLookupName, ptsMap)) {
885
+ // Resolve the concrete alias target. Only `name` is needed here — receiver
886
+ // and line are not relevant for alias resolution (we are looking up the
887
+ // aliased function by name, not dispatching a method call).
888
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
889
+ const sortedAliasTargets = aliasTargets.length > 1
890
+ ? [...aliasTargets].sort((a, b) => computeConfidence(relPath, b.file, aliasFrom ?? null) -
891
+ computeConfidence(relPath, a.file, aliasFrom ?? null))
892
+ : aliasTargets;
893
+ for (const t of sortedAliasTargets) {
894
+ const edgeKey = `${caller.id}|${t.id}`;
895
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
896
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
897
+ if (conf > 0) {
898
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
899
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
900
+ }
901
+ }
902
+ }
903
+ }
904
+ }
905
+ // Phase 8.3f: pts fallback for receiver calls via object-rest param bindings.
906
+ // Fires when `rest.prop()` is encountered and `rest` was seeded as `pts["rest.prop"]`
907
+ // by the object-rest dispatch chain (ObjectRestParamBinding + paramBinding + ObjectPropBinding).
908
+ if (targets.length === 0 &&
909
+ call.receiver &&
910
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
911
+ call.receiver !== 'this' &&
912
+ call.receiver !== 'self' &&
913
+ call.receiver !== 'super' &&
914
+ ptsMap) {
915
+ const receiverKey = `${call.receiver}.${call.name}`;
916
+ if (ptsMap.has(receiverKey)) {
917
+ for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
918
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
919
+ const sortedAliasTargets = aliasTargets.length > 1
920
+ ? [...aliasTargets].sort((a, b) => computeConfidence(relPath, b.file, aliasFrom ?? null) -
921
+ computeConfidence(relPath, a.file, aliasFrom ?? null))
922
+ : aliasTargets;
923
+ for (const t of sortedAliasTargets) {
924
+ const edgeKey = `${caller.id}|${t.id}`;
925
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
926
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
927
+ if (conf > 0) {
928
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
929
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
930
+ }
931
+ }
932
+ }
933
+ }
385
934
  }
386
935
  }
387
936
  if (call.receiver &&
@@ -389,9 +938,48 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
389
938
  call.receiver !== 'this' &&
390
939
  call.receiver !== 'self' &&
391
940
  call.receiver !== 'super') {
392
- const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges);
941
+ const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges, importedNames);
393
942
  if (recv) {
394
- allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0]);
943
+ allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
944
+ }
945
+ }
946
+ // Phase 8.5: CHA + RTA dispatch expansion.
947
+ // For `this`/`self`/`super` calls: resolve through the class hierarchy instead
948
+ // of relying solely on global name matching.
949
+ // For typed receiver calls: expand to all instantiated concrete implementations.
950
+ if (chaCtx && call.receiver) {
951
+ let chaTargets = [];
952
+ let isTypedReceiverDispatch = false;
953
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
954
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup, relPath);
955
+ }
956
+ else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
957
+ const typeEntry = typeMap.get(call.receiver);
958
+ const typeName = typeEntry
959
+ ? typeof typeEntry === 'string'
960
+ ? typeEntry
961
+ : typeEntry.type
962
+ : null;
963
+ if (typeName) {
964
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
965
+ isTypedReceiverDispatch = true;
966
+ }
967
+ }
968
+ for (const t of chaTargets) {
969
+ const edgeKey = `${caller.id}|${t.id}`;
970
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
971
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
972
+ // — file proximity is not meaningful for virtual dispatch confidence.
973
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
974
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
975
+ const conf = isTypedReceiverDispatch
976
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
977
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
978
+ if (conf > 0) {
979
+ seenCallEdges.add(edgeKey);
980
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
981
+ }
982
+ }
395
983
  }
396
984
  }
397
985
  }
@@ -407,7 +995,7 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
407
995
  const targetRows = (ctx.nodesByName.get(cls.extends) || []).filter((n) => EXTENDS_TARGET_KINDS.has(n.kind));
408
996
  if (sourceRow) {
409
997
  for (const t of targetRows) {
410
- allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
998
+ allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0, null]);
411
999
  }
412
1000
  }
413
1001
  }
@@ -416,12 +1004,46 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
416
1004
  const targetRows = (ctx.nodesByName.get(cls.implements) || []).filter((n) => IMPLEMENTS_TARGET_KINDS.has(n.kind));
417
1005
  if (sourceRow) {
418
1006
  for (const t of targetRows) {
419
- allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
1007
+ allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0, null]);
420
1008
  }
421
1009
  }
422
1010
  }
423
1011
  }
424
1012
  }
1013
+ // ── Native bulk-insert technique back-fill ──────────────────────────────
1014
+ /**
1015
+ * After native bulkInsertEdges (which does not write the technique column),
1016
+ * apply technique values from the in-memory row array back to the DB.
1017
+ *
1018
+ * Rows with an explicit technique get a targeted UPDATE by (source_id, target_id).
1019
+ * The catch-all 'ts-native' tag is scoped to only the source_ids present in this
1020
+ * batch — this prevents mis-tagging pre-migration NULL-technique edges from
1021
+ * unchanged files that were never purged and re-inserted.
1022
+ */
1023
+ function applyEdgeTechniquesAfterNativeInsert(db, rows) {
1024
+ const callRows = rows.filter((r) => r[2] === 'calls');
1025
+ if (callRows.length === 0)
1026
+ return;
1027
+ const taggedRows = callRows.filter((r) => r[5] != null);
1028
+ // Collect distinct source IDs for this batch so the catch-all UPDATE is scoped
1029
+ // to edges inserted in the current run, not the entire table.
1030
+ const sourceIds = [...new Set(callRows.map((r) => r[0]))];
1031
+ // Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
1032
+ const CHUNK_SIZE = 500;
1033
+ const tx = db.transaction(() => {
1034
+ if (taggedRows.length > 0) {
1035
+ const stmt = db.prepare("UPDATE edges SET technique = ? WHERE kind = 'calls' AND source_id = ? AND target_id = ? AND technique IS NULL");
1036
+ for (const r of taggedRows)
1037
+ stmt.run(r[5], r[0], r[1]);
1038
+ }
1039
+ for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
1040
+ const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
1041
+ const placeholders = chunk.map(() => '?').join(',');
1042
+ db.prepare(`UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL AND source_id IN (${placeholders})`).run(...chunk);
1043
+ }
1044
+ });
1045
+ tx();
1046
+ }
425
1047
  // ── Reverse-dep edge reconnection (#932, #933) ─────────────────────────
426
1048
  /**
427
1049
  * Reconnect edges that were saved before changed-file purge.
@@ -445,6 +1067,7 @@ function reconnectReverseDepEdges(ctx) {
445
1067
  saved.edgeKind,
446
1068
  saved.confidence,
447
1069
  saved.dynamic,
1070
+ saved.technique,
448
1071
  ]);
449
1072
  }
450
1073
  else {
@@ -465,6 +1088,9 @@ function reconnectReverseDepEdges(ctx) {
465
1088
  if (!ok) {
466
1089
  batchInsertEdges(db, reconnectedRows);
467
1090
  }
1091
+ else {
1092
+ applyEdgeTechniquesAfterNativeInsert(db, reconnectedRows);
1093
+ }
468
1094
  }
469
1095
  else {
470
1096
  batchInsertEdges(db, reconnectedRows);
@@ -480,7 +1106,7 @@ function reconnectReverseDepEdges(ctx) {
480
1106
  * their import targets. Falls back to loading ALL nodes for full builds or
481
1107
  * larger incremental changes.
482
1108
  */
483
- const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
1109
+ const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant','variable')`;
484
1110
  function loadNodes(ctx) {
485
1111
  const { db, fileSymbols, isFullBuild, batchResolved } = ctx;
486
1112
  const nodeKindFilter = NODE_KIND_FILTER_SQL;
@@ -546,7 +1172,29 @@ export async function buildEdges(ctx) {
546
1172
  setupNodeLookups(ctx, allNodesBefore);
547
1173
  addLazyFallback(ctx, scopedLoad);
548
1174
  const t0 = performance.now();
1175
+ // Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
1176
+ // Runs before call-edge construction so the accurate types are available
1177
+ // for method-call resolution. Gated on config so users can opt out.
1178
+ //
1179
+ // Skip for small incremental builds: TypeScript program creation requires
1180
+ // loading the entire tsconfig file list (~700ms startup on the codegraph
1181
+ // corpus), which dominates the 1-file rebuild time. Native engine bypasses
1182
+ // this entirely via the Rust orchestrator; WASM/JS engines need this gate
1183
+ // to match native's effective behaviour on tiny incremental changes.
1184
+ // Mirrors the smallFilesThreshold gates for nativeDb and native call-edges.
1185
+ const isSmallIncremental = !ctx.isFullBuild && ctx.fileSymbols.size <= ctx.config.build.smallFilesThreshold;
1186
+ if (ctx.config.build.typescriptResolver && !isSmallIncremental) {
1187
+ await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
1188
+ }
549
1189
  const native = engineName === 'native' ? loadNative() : null;
1190
+ // Phase 8.2: Augment typeMaps with cross-file return-type propagation before
1191
+ // the transaction opens. This is pure in-memory mutation (no DB I/O) and must
1192
+ // run outside the transaction to avoid leaving ctx.fileSymbols in a partial
1193
+ // state if the transaction rolls back unexpectedly.
1194
+ propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
1195
+ // Phase 8.5: Build CHA context after propagation so typeMap confidence values
1196
+ // (used for RTA seeding) reflect any cross-file propagated types.
1197
+ const chaCtx = buildChaContext(ctx.fileSymbols);
550
1198
  // Phase 1: Compute edges inside a better-sqlite3 transaction.
551
1199
  // Barrel-edge deletion lives here so that the JS path (which also inserts
552
1200
  // edges in this transaction) keeps deletion + insertion atomic.
@@ -590,9 +1238,22 @@ export async function buildEdges(ctx) {
590
1238
  (ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
591
1239
  if (useNativeCallEdges) {
592
1240
  buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native);
1241
+ // The native engine receives all pts bindings (paramBindings,
1242
+ // fnRefBindings, thisCallBindings, objectRestParamBindings, …) through
1243
+ // NativeFileEntry and runs the same points-to solver as the JS path, so
1244
+ // no pts post-passes are needed here. Only capabilities that remain
1245
+ // JS-only run as post-passes below.
1246
+ const sharedLookup = makeContextLookup(ctx, getNodeIdStmt);
1247
+ // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1248
+ // getter/setter functions registered via Object.defineProperty.
1249
+ buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1250
+ // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
1251
+ // The native Rust engine has no knowledge of the CHA context, so this/self
1252
+ // calls and interface dispatch are not expanded to concrete implementations.
1253
+ buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
593
1254
  }
594
1255
  else {
595
- buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
1256
+ buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
596
1257
  }
597
1258
  // When using native edge insert, skip JS insert here — do it after tx commits.
598
1259
  // Otherwise insert edges within this transaction for atomicity.
@@ -618,6 +1279,9 @@ export async function buildEdges(ctx) {
618
1279
  debug('Native bulkInsertEdges failed — falling back to JS batchInsertEdges');
619
1280
  batchInsertEdges(ctx.db, allEdgeRows);
620
1281
  }
1282
+ else {
1283
+ applyEdgeTechniquesAfterNativeInsert(ctx.db, allEdgeRows);
1284
+ }
621
1285
  }
622
1286
  // Phase 3: Reconnect saved reverse-dep edges (#932, #933).
623
1287
  // When the WASM/JS path purged changed files, edges FROM reverse-dep files TO
@@ -627,6 +1291,12 @@ export async function buildEdges(ctx) {
627
1291
  if (ctx.savedReverseDepEdges.length > 0) {
628
1292
  reconnectReverseDepEdges(ctx);
629
1293
  }
1294
+ // Phase 4: CHA post-pass — expand virtual-dispatch edges for class hierarchies
1295
+ // and interface implementations. Runs after all call + hierarchy edges are
1296
+ // committed so the DB is consistent.
1297
+ // Note: the native orchestrator success path runs this independently in
1298
+ // tryNativeOrchestrator; this phase covers the WASM and native-fallback paths.
1299
+ runChaPostPass(db);
630
1300
  ctx.timing.edgesMs = performance.now() - t0;
631
1301
  }
632
1302
  //# sourceMappingURL=build-edges.js.map