@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,33 +7,56 @@
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 type {
15
+ ArrayCallbackBinding,
16
+ ArrayElemBinding,
13
17
  BetterSqlite3Database,
14
18
  Call,
15
19
  ClassRelation,
20
+ Definition,
16
21
  ExtractorOutput,
22
+ FnRefBinding,
23
+ ForOfBinding,
17
24
  Import,
18
25
  NativeAddon,
19
26
  NodeRow,
27
+ ObjectPropBinding,
28
+ ObjectRestParamBinding,
29
+ ParamBinding,
30
+ SpreadArgBinding,
31
+ ThisCallBinding,
20
32
  TypeMapEntry,
21
33
  } from '../../../../types.js';
22
34
  import { computeConfidence } from '../../resolve.js';
35
+ import type { PointsToMap } from '../../resolver/points-to.js';
36
+ import { buildPointsToMap, resolveViaPointsTo } from '../../resolver/points-to.js';
37
+ import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
23
38
  import {
24
39
  type CallNodeLookup,
25
40
  findCaller,
41
+ isModuleScopedLanguage,
26
42
  resolveCallTargets,
27
43
  resolveReceiverEdge,
28
44
  } from '../call-resolver.js';
45
+ import type { ChaContext } from '../cha.js';
46
+ import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
29
47
  import type { PipelineContext } from '../context.js';
30
- import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
31
-
32
- import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
48
+ import {
49
+ BUILTIN_RECEIVERS,
50
+ batchInsertEdges,
51
+ CHA_DISPATCH_PENALTY,
52
+ CHA_TYPED_DISPATCH_CONFIDENCE,
53
+ runChaPostPass,
54
+ } from '../helpers.js';
55
+ import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
33
56
 
34
57
  // ── Local types ──────────────────────────────────────────────────────────
35
58
 
36
- type EdgeRowTuple = [number, number, string, number, number];
59
+ type EdgeRowTuple = [number, number, string, number, number, string | null];
37
60
 
38
61
  interface NodeIdStmt {
39
62
  get(name: string, kind: string, file: string, line: number): { id: number } | undefined;
@@ -52,11 +75,27 @@ interface QueryNodeRow {
52
75
  interface NativeFileEntry {
53
76
  file: string;
54
77
  fileNodeId: number;
55
- definitions: Array<{ name: string; kind: string; line: number; endLine: number | null }>;
78
+ definitions: Array<{
79
+ name: string;
80
+ kind: string;
81
+ line: number;
82
+ endLine: number | null;
83
+ params?: string[];
84
+ }>;
56
85
  calls: Call[];
57
86
  importedNames: Array<{ name: string; file: string }>;
58
87
  classes: ClassRelation[];
59
88
  typeMap: Array<{ name: string; typeName: string; confidence: number }>;
89
+ /** Phase 8.3: function-reference bindings for pts analysis. */
90
+ fnRefBindings?: Array<{ lhs: string; rhs: string; rhsReceiver?: string }>;
91
+ paramBindings?: ParamBinding[];
92
+ thisCallBindings?: ThisCallBinding[];
93
+ arrayElemBindings?: ArrayElemBinding[];
94
+ spreadArgBindings?: SpreadArgBinding[];
95
+ forOfBindings?: ForOfBinding[];
96
+ arrayCallbackBindings?: ArrayCallbackBinding[];
97
+ objectRestParamBindings?: ObjectRestParamBinding[];
98
+ objectPropBindings?: ObjectPropBinding[];
60
99
  }
61
100
 
62
101
  /** Shape returned by native buildCallEdges. */
@@ -119,12 +158,12 @@ function emitTypeOnlySymbolEdges(
119
158
  const cleanName = name.replace(/^\*\s+as\s+/, '');
120
159
  let targetFile = resolvedPath;
121
160
  if (isBarrelFile(ctx, resolvedPath)) {
122
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
161
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
123
162
  if (actual) targetFile = actual;
124
163
  }
125
164
  const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
126
165
  if (candidates && candidates.length > 0) {
127
- allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
166
+ allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0, null]);
128
167
  }
129
168
  }
130
169
  }
@@ -146,7 +185,7 @@ function emitEdgesForImport(
146
185
  if (!targetRow) return;
147
186
 
148
187
  const edgeKind = importEdgeKind(imp);
149
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
188
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0, null]);
150
189
 
151
190
  if (imp.typeOnly) {
152
191
  emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
@@ -190,7 +229,7 @@ function buildBarrelEdges(
190
229
  const resolvedSources = new Set<string>();
191
230
  for (const name of imp.names) {
192
231
  const cleanName = name.replace(/^\*\s+as\s+/, '');
193
- const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
232
+ const actualSource = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
194
233
  if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
195
234
  resolvedSources.add(actualSource);
196
235
  const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
@@ -201,7 +240,7 @@ function buildBarrelEdges(
201
240
  : edgeKind === 'dynamic-imports'
202
241
  ? 'dynamic-imports'
203
242
  : 'imports';
204
- edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
243
+ edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0, null]);
205
244
  }
206
245
  }
207
246
  }
@@ -384,7 +423,70 @@ function buildImportEdgesNative(
384
423
  ) as NativeEdge[];
385
424
 
386
425
  for (const e of nativeEdges) {
387
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
426
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic, null]);
427
+ }
428
+ }
429
+
430
+ // ── Phase 8.2: Cross-file return-type propagation ───────────────────────
431
+
432
+ /**
433
+ * Augment each file's typeMap with return types from imported functions.
434
+ *
435
+ * The per-file extractor already resolves same-file call assignments (intra-file
436
+ * propagation). This function handles the cross-file case: when a file imports a
437
+ * function from another file and assigns its return value to a variable, we look up
438
+ * the callee's return type in the source file's returnTypeMap and inject it.
439
+ *
440
+ * Called once before call-edge building so both the native and JS paths benefit.
441
+ */
442
+ function propagateReturnTypesAcrossFiles(
443
+ fileSymbols: Map<string, ExtractorOutput>,
444
+ ctx: PipelineContext,
445
+ rootDir: string,
446
+ ): void {
447
+ // Index: filePath → per-file return-type map
448
+ const returnTypeIndex = new Map<string, Map<string, TypeMapEntry>>();
449
+ for (const [relPath, symbols] of fileSymbols) {
450
+ if (symbols.returnTypeMap?.size) returnTypeIndex.set(relPath, symbols.returnTypeMap);
451
+ }
452
+ if (returnTypeIndex.size === 0) return;
453
+
454
+ // Flat global map for qualified method lookups (TypeName.methodName → entry).
455
+ // Conflicts resolved by keeping the highest-confidence entry.
456
+ const globalReturnTypeMap = new Map<string, TypeMapEntry>();
457
+ for (const rtm of returnTypeIndex.values()) {
458
+ for (const [name, entry] of rtm) {
459
+ const existing = globalReturnTypeMap.get(name);
460
+ if (!existing || entry.confidence > existing.confidence) globalReturnTypeMap.set(name, entry);
461
+ }
462
+ }
463
+
464
+ for (const [relPath, symbols] of fileSymbols) {
465
+ if (!symbols.callAssignments?.length) continue;
466
+ // Phase 8.4 side-effect: buildImportedNamesMap now traces through barrel
467
+ // files (traceBarrel), so `importedFrom` resolves to the leaf definition
468
+ // file rather than the barrel. This means returnTypeIndex.get(importedFrom)
469
+ // now finds entries it previously missed, improving cross-file return-type
470
+ // propagation through re-export chains (Phase 8.2 improvement).
471
+ const importedNamesMap = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
472
+
473
+ for (const ca of symbols.callAssignments) {
474
+ if (symbols.typeMap.has(ca.varName)) continue; // already resolved locally
475
+
476
+ let returnEntry: TypeMapEntry | undefined;
477
+ if (ca.receiverTypeName) {
478
+ returnEntry = globalReturnTypeMap.get(`${ca.receiverTypeName}.${ca.calleeName}`);
479
+ } else {
480
+ const importedFrom = importedNamesMap.get(ca.calleeName);
481
+ if (importedFrom) returnEntry = returnTypeIndex.get(importedFrom)?.get(ca.calleeName);
482
+ }
483
+
484
+ if (returnEntry) {
485
+ const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;
486
+ if (propagatedConf > 0)
487
+ setTypeMapEntry(symbols.typeMap, ca.varName, returnEntry.type, propagatedConf);
488
+ }
489
+ }
388
490
  }
389
491
  }
390
492
 
@@ -432,16 +534,35 @@ function buildCallEdgesNative(
432
534
  nativeFiles.push({
433
535
  file: relPath,
434
536
  fileNodeId: fileNodeRow.id,
435
- definitions: symbols.definitions.map((d) => ({
436
- name: d.name,
437
- kind: d.kind,
438
- line: d.line,
439
- endLine: d.endLine ?? null,
440
- })),
537
+ definitions: symbols.definitions.map((d) => {
538
+ const params = d.children?.filter((c) => c.kind === 'parameter').map((c) => c.name);
539
+ return {
540
+ name: d.name,
541
+ kind: d.kind,
542
+ line: d.line,
543
+ endLine: d.endLine ?? null,
544
+ params: params?.length ? params : undefined,
545
+ };
546
+ }),
441
547
  calls: symbols.calls,
442
548
  importedNames,
443
549
  classes: symbols.classes,
444
550
  typeMap,
551
+ fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
552
+ paramBindings: symbols.paramBindings?.length ? symbols.paramBindings : undefined,
553
+ thisCallBindings: symbols.thisCallBindings?.length ? symbols.thisCallBindings : undefined,
554
+ arrayElemBindings: symbols.arrayElemBindings?.length ? symbols.arrayElemBindings : undefined,
555
+ spreadArgBindings: symbols.spreadArgBindings?.length ? symbols.spreadArgBindings : undefined,
556
+ forOfBindings: symbols.forOfBindings?.length ? symbols.forOfBindings : undefined,
557
+ arrayCallbackBindings: symbols.arrayCallbackBindings?.length
558
+ ? symbols.arrayCallbackBindings
559
+ : undefined,
560
+ objectRestParamBindings: symbols.objectRestParamBindings?.length
561
+ ? symbols.objectRestParamBindings
562
+ : undefined,
563
+ objectPropBindings: symbols.objectPropBindings?.length
564
+ ? symbols.objectPropBindings
565
+ : undefined,
445
566
  });
446
567
  }
447
568
 
@@ -449,7 +570,192 @@ function buildCallEdgesNative(
449
570
  ...BUILTIN_RECEIVERS,
450
571
  ]) as NativeEdge[];
451
572
  for (const e of nativeEdges) {
452
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
573
+ allEdgeRows.push([
574
+ e.sourceId,
575
+ e.targetId,
576
+ e.kind,
577
+ e.confidence,
578
+ e.dynamic,
579
+ e.kind === 'calls' ? 'ts-native' : null,
580
+ ]);
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Object.defineProperty accessor post-pass for the native call-edge path.
586
+ *
587
+ * When a function is registered as a getter/setter via
588
+ * `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()`
589
+ * inside `getter` need to resolve against `obj` (because `this === obj` when
590
+ * the accessor is invoked). The native Rust engine has no knowledge of
591
+ * `definePropertyReceivers`, so this JS post-pass adds the missing edges.
592
+ */
593
+ function buildDefinePropertyPostPass(
594
+ ctx: PipelineContext,
595
+ getNodeIdStmt: NodeIdStmt,
596
+ allEdgeRows: EdgeRowTuple[],
597
+ sharedLookup?: CallNodeLookup,
598
+ ): void {
599
+ const filesWithReceivers = [...ctx.fileSymbols].filter(
600
+ ([, symbols]) => symbols.definePropertyReceivers && symbols.definePropertyReceivers.size > 0,
601
+ );
602
+ if (filesWithReceivers.length === 0) return;
603
+
604
+ const seenByPair = new Set<string>();
605
+ for (const [srcId, tgtId] of allEdgeRows) {
606
+ seenByPair.add(`${srcId}|${tgtId}`);
607
+ }
608
+
609
+ const { barrelOnlyFiles, rootDir } = ctx;
610
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
611
+
612
+ for (const [relPath, symbols] of filesWithReceivers) {
613
+ if (barrelOnlyFiles.has(relPath)) continue;
614
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
615
+ if (!fileNodeRow) continue;
616
+
617
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
618
+ const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
619
+ const definePropertyReceivers = symbols.definePropertyReceivers!;
620
+
621
+ for (const call of symbols.calls) {
622
+ if (call.receiver !== 'this') continue;
623
+
624
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
625
+ if (!caller.callerName) continue;
626
+
627
+ const receiverVarName = definePropertyReceivers.get(caller.callerName);
628
+ if (!receiverVarName) continue;
629
+
630
+ // Only add edges the native engine missed (no direct target already).
631
+ const { targets: directTargets } = resolveCallTargets(
632
+ lookup,
633
+ call,
634
+ relPath,
635
+ importedNames,
636
+ typeMap as Map<string, unknown>,
637
+ caller.callerName,
638
+ );
639
+ if (directTargets.length > 0) continue;
640
+
641
+ // Resolve via receiver type
642
+ let targets: ReadonlyArray<{ id: number; file: string }> = [];
643
+ const typeEntry = typeMap.get(receiverVarName);
644
+ const typeName = typeEntry
645
+ ? typeof typeEntry === 'string'
646
+ ? typeEntry
647
+ : (typeEntry as { type?: string }).type
648
+ : null;
649
+ if (typeName) {
650
+ const qualifiedName = `${typeName}.${call.name}`;
651
+ targets = lookup.byNameAndFile(qualifiedName, relPath);
652
+ }
653
+ // Same-file fallback for plain object-literal methods
654
+ if (targets.length === 0) {
655
+ targets = lookup.byNameAndFile(call.name, relPath);
656
+ }
657
+
658
+ for (const t of targets) {
659
+ const edgeKey = `${caller.id}|${t.id}`;
660
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
661
+ const conf = computeConfidence(relPath, t.file, null);
662
+ if (conf > 0) {
663
+ seenByPair.add(edgeKey);
664
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'ts-native']);
665
+ }
666
+ }
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Phase 8.5: CHA + RTA post-pass for the native call-edge path.
674
+ *
675
+ * The native Rust engine has no knowledge of the CHA context, so `this.method()`
676
+ * calls and interface method dispatches are not expanded to their concrete
677
+ * implementations. This JS post-pass runs after the native edges and adds only
678
+ * the CHA-resolved edges that the native engine missed.
679
+ *
680
+ * Seeds seenByPair from the current allEdgeRows snapshot to avoid duplicating
681
+ * edges the native engine already produced.
682
+ */
683
+ function buildChaPostPass(
684
+ ctx: PipelineContext,
685
+ getNodeIdStmt: NodeIdStmt,
686
+ allEdgeRows: EdgeRowTuple[],
687
+ chaCtx: ChaContext,
688
+ ): void {
689
+ // Fast-exit when the CHA context is empty (no class hierarchy in the project)
690
+ if (chaCtx.implementors.size === 0 && chaCtx.parents.size === 0) return;
691
+
692
+ // Seed only from 'calls' edges — import/extends/implements edges share (src,tgt) pairs
693
+ // with real call edges at the file-node level and would cause false dedup if included.
694
+ const seenByPair = new Set<string>();
695
+ for (const row of allEdgeRows) {
696
+ if (row[2] === 'calls') seenByPair.add(`${row[0]}|${row[1]}`);
697
+ }
698
+
699
+ const { fileSymbols, barrelOnlyFiles } = ctx;
700
+ const lookup = makeContextLookup(ctx, getNodeIdStmt);
701
+
702
+ for (const [relPath, symbols] of fileSymbols) {
703
+ if (barrelOnlyFiles.has(relPath)) continue;
704
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
705
+ if (!fileNodeRow) continue;
706
+
707
+ const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
708
+
709
+ for (const call of symbols.calls) {
710
+ if (!call.receiver) continue;
711
+ if (BUILTIN_RECEIVERS.has(call.receiver)) continue;
712
+
713
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
714
+ let chaTargets: ReadonlyArray<{ id: number; file: string }> = [];
715
+ let isTypedReceiverDispatch = false;
716
+
717
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
718
+ chaTargets = resolveThisDispatch(
719
+ call.name,
720
+ caller.callerName,
721
+ call.receiver,
722
+ chaCtx,
723
+ lookup,
724
+ relPath,
725
+ );
726
+ } else {
727
+ const typeEntry = typeMap.get(call.receiver);
728
+ const typeName = typeEntry
729
+ ? typeof typeEntry === 'string'
730
+ ? typeEntry
731
+ : (typeEntry as { type?: string }).type
732
+ : null;
733
+ if (typeName) {
734
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
735
+ isTypedReceiverDispatch = true;
736
+ }
737
+ }
738
+
739
+ for (const t of chaTargets) {
740
+ const edgeKey = `${caller.id}|${t.id}`;
741
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
742
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
743
+ // — file proximity is not meaningful for virtual dispatch confidence.
744
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
745
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
746
+ const conf = isTypedReceiverDispatch
747
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
748
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
749
+ if (conf > 0) {
750
+ seenByPair.add(edgeKey);
751
+ // Tag super-dispatch edges distinctly so runChaPostPass can exclude them
752
+ // from further CHA expansion (super calls are not virtual dispatch).
753
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
754
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, technique]);
755
+ }
756
+ }
757
+ }
758
+ }
453
759
  }
454
760
  }
455
761
 
@@ -469,7 +775,7 @@ function buildImportedNamesForNative(
469
775
  const cleanName = name.replace(/^\*\s+as\s+/, '');
470
776
  let targetFile = resolvedPath;
471
777
  if (isBarrelFile(ctx, resolvedPath)) {
472
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
778
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
473
779
  if (actual) targetFile = actual;
474
780
  }
475
781
  importedNames.push({ name: cleanName, file: targetFile });
@@ -490,6 +796,7 @@ function buildCallEdgesJS(
490
796
  ctx: PipelineContext,
491
797
  getNodeIdStmt: NodeIdStmt,
492
798
  allEdgeRows: EdgeRowTuple[],
799
+ chaCtx?: ChaContext,
493
800
  ): void {
494
801
  const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
495
802
  const lookup = makeContextLookup(ctx, getNodeIdStmt);
@@ -500,8 +807,38 @@ function buildCallEdgesJS(
500
807
  if (!fileNodeRow) continue;
501
808
 
502
809
  const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
503
- const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
810
+ const typeMap: Map<string, TypeMapEntry | string> = new Map(
811
+ symbols.typeMap instanceof Map ? symbols.typeMap : [],
812
+ );
813
+
814
+ // Phase 8.3f: seed typeMap[callee::restName] = { type: argName } for each
815
+ // object-destructuring rest parameter binding × call-site argument binding.
816
+ // Keys are scoped so two functions with the same rest-param name in the same
817
+ // file don't collide (#1358). When only one callee uses a given rest name,
818
+ // also seed the unscoped key as a null-callerName fallback.
819
+ if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
820
+ const restNameCallees = new Map<string, Set<string>>();
821
+ for (const orpb of symbols.objectRestParamBindings) {
822
+ if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
823
+ restNameCallees.get(orpb.restName)!.add(orpb.callee);
824
+ }
825
+ for (const orpb of symbols.objectRestParamBindings) {
826
+ for (const pb of symbols.paramBindings) {
827
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
828
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
829
+ if (!typeMap.has(scopedKey)) {
830
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
831
+ if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
832
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
833
+ }
834
+ }
835
+ }
836
+ }
837
+ }
838
+ }
839
+
504
840
  const seenCallEdges = new Set<string>();
841
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
505
842
 
506
843
  buildFileCallEdges(
507
844
  relPath,
@@ -512,6 +849,8 @@ function buildCallEdgesJS(
512
849
  lookup,
513
850
  allEdgeRows,
514
851
  typeMap,
852
+ ptsMap,
853
+ chaCtx,
515
854
  );
516
855
  buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
517
856
  }
@@ -528,11 +867,21 @@ function buildImportedNamesMap(
528
867
  // (higher priority). Static imports represent direct bindings while dynamic
529
868
  // imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
530
869
  // When both contribute the same name, the static binding is authoritative.
870
+ //
871
+ // Phase 8.4: trace through barrel files so that symbol names map to their
872
+ // actual definition file, not the re-exporting barrel. Mirrors the tracing
873
+ // already done in buildImportedNamesForNative (the native path).
874
+ const traceBarrel = (resolvedPath: string, cleanName: string): string => {
875
+ if (!isBarrelFile(ctx, resolvedPath)) return resolvedPath;
876
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
877
+ return actual ?? resolvedPath;
878
+ };
531
879
  for (const imp of symbols.imports) {
532
880
  if (!imp.dynamicImport) continue;
533
881
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
534
882
  for (const name of imp.names) {
535
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
883
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
884
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
536
885
  }
537
886
  }
538
887
  for (const imp of symbols.imports) {
@@ -540,7 +889,7 @@ function buildImportedNamesMap(
540
889
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
541
890
  for (const name of imp.names) {
542
891
  const cleanName = name.replace(/^\*\s+as\s+/, '');
543
- importedNames.set(cleanName, resolvedPath);
892
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
544
893
  }
545
894
  }
546
895
  return importedNames;
@@ -551,11 +900,97 @@ function makeContextLookup(ctx: PipelineContext, getNodeIdStmt: NodeIdStmt): Cal
551
900
  byNameAndFile: (name, file) => ctx.nodesByNameAndFile.get(`${name}|${file}`) ?? [],
552
901
  byName: (name) => ctx.nodesByName.get(name) ?? [],
553
902
  isBarrel: (file) => isBarrelFile(ctx, file),
554
- resolveBarrel: (barrelFile, symbolName) => resolveBarrelExport(ctx, barrelFile, symbolName),
903
+ resolveBarrel: (barrelFile, symbolName) =>
904
+ resolveBarrelExportCached(ctx, barrelFile, symbolName),
555
905
  nodeId: (name, kind, file, line) => getNodeIdStmt.get(name, kind, file, line),
556
906
  };
557
907
  }
558
908
 
909
+ /**
910
+ * Build a per-file points-to map for Phase 8.3 alias resolution.
911
+ * Returns null fast when the file has no function-reference bindings.
912
+ *
913
+ * Only callable definitions (function/method) are seeded as concrete targets.
914
+ * Class and interface names are intentionally excluded — aliasing a constructor
915
+ * (`const Svc = MyService`) is an uncommon pattern that would require tracking
916
+ * `new`-expression flows separately from the alias chain. That is left to Phase
917
+ * 8.2 call-assignment propagation, which already handles constructor assignments.
918
+ */
919
+ function buildPointsToMapForFile(
920
+ symbols: ExtractorOutput,
921
+ importedNames: Map<string, string>,
922
+ ): PointsToMap | null {
923
+ const hasThisCallBindings = !!symbols.thisCallBindings?.length;
924
+ if (
925
+ !symbols.fnRefBindings?.length &&
926
+ !symbols.paramBindings?.length &&
927
+ !symbols.arrayElemBindings?.length &&
928
+ !symbols.spreadArgBindings?.length &&
929
+ !symbols.forOfBindings?.length &&
930
+ !symbols.arrayCallbackBindings?.length &&
931
+ !symbols.objectRestParamBindings?.length &&
932
+ !symbols.objectPropBindings?.length &&
933
+ !hasThisCallBindings
934
+ )
935
+ return null;
936
+ const defNames = new Set(
937
+ symbols.definitions
938
+ .filter((d) => d.kind === 'function' || d.kind === 'method')
939
+ .map((d) => d.name),
940
+ );
941
+ const definitionParams = buildDefinitionParamsMap(symbols.definitions);
942
+
943
+ // Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
944
+ // The scoped key `fn::this` is looked up when `this()` calls are resolved inside
945
+ // function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
946
+ let allFnRefBindings: readonly FnRefBinding[] = symbols.fnRefBindings ?? [];
947
+ if (hasThisCallBindings) {
948
+ const extra: FnRefBinding[] = (symbols.thisCallBindings ?? []).map((b) => ({
949
+ lhs: `${b.callee}::this`,
950
+ rhs: b.thisArg,
951
+ }));
952
+ allFnRefBindings = [...allFnRefBindings, ...extra];
953
+ }
954
+
955
+ return buildPointsToMap(
956
+ allFnRefBindings,
957
+ defNames,
958
+ importedNames,
959
+ symbols.paramBindings,
960
+ definitionParams,
961
+ symbols.arrayElemBindings,
962
+ symbols.spreadArgBindings,
963
+ symbols.forOfBindings,
964
+ symbols.arrayCallbackBindings,
965
+ symbols.objectRestParamBindings,
966
+ symbols.objectPropBindings,
967
+ );
968
+ }
969
+
970
+ function buildDefinitionParamsMap(
971
+ definitions: readonly Definition[],
972
+ ): Map<string, readonly string[]> {
973
+ const map = new Map<string, readonly string[]>();
974
+ for (const def of definitions) {
975
+ if ((def.kind === 'function' || def.kind === 'method') && def.children) {
976
+ const params = def.children.filter((c) => c.kind === 'parameter').map((c) => c.name);
977
+ if (params.length > 0) {
978
+ if (map.has(def.name)) {
979
+ // Two definitions share the same name (e.g. overloads, same-named method and
980
+ // function, or conditional redeclaration). Keep the first entry — using the
981
+ // wrong parameter list would map argIndex to the wrong parameter name.
982
+ debug(
983
+ `buildDefinitionParamsMap: duplicate def name "${def.name}" (kind=${def.kind}, line=${def.line}) — skipping; first entry kept`,
984
+ );
985
+ } else {
986
+ map.set(def.name, params);
987
+ }
988
+ }
989
+ }
990
+ }
991
+ return map;
992
+ }
993
+
559
994
  function buildFileCallEdges(
560
995
  relPath: string,
561
996
  symbols: ExtractorOutput,
@@ -565,26 +1000,285 @@ function buildFileCallEdges(
565
1000
  lookup: CallNodeLookup,
566
1001
  allEdgeRows: EdgeRowTuple[],
567
1002
  typeMap: Map<string, TypeMapEntry | string>,
1003
+ ptsMap?: PointsToMap | null,
1004
+ chaCtx?: ChaContext,
568
1005
  ): void {
1006
+ // Tracks edges that were inserted by the pts fallback (edgeKey → allEdgeRows index).
1007
+ // Kept separate from seenCallEdges so that a subsequent direct-call edge for the same
1008
+ // caller→target pair can upgrade the confidence in-place rather than being silently
1009
+ // dropped by the dedup guard. Once upgraded, the key moves to seenCallEdges and is
1010
+ // no longer tracked here.
1011
+ const ptsEdgeRows = new Map<string, number>();
1012
+
1013
+ // Pre-compute the set of names that appear as lhs in fnRefBindings so that
1014
+ // case (c) of the pts gate below only fires for names that are genuine
1015
+ // bind/alias entries, not for every locally-defined function or import that
1016
+ // buildPointsToMap seeds with a self-pointing entry.
1017
+ const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
569
1018
  for (const call of symbols.calls) {
570
1019
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
571
1020
 
572
1021
  const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
573
1022
  const isDynamic: number = call.dynamic ? 1 : 0;
574
- const { targets, importedFrom } = resolveCallTargets(
1023
+ let { targets, importedFrom } = resolveCallTargets(
575
1024
  lookup,
576
1025
  call,
577
1026
  relPath,
578
1027
  importedNames,
579
1028
  typeMap as Map<string, unknown>,
1029
+ caller.callerName,
580
1030
  );
581
1031
 
1032
+ // Same-class `this.method()` fallback: when the call receiver is `this` and
1033
+ // resolveCallTargets found nothing, derive the enclosing class name from the
1034
+ // caller (e.g. `Logger.info` → class prefix `Logger`) and retry with the
1035
+ // qualified method name `Logger._write`. This mirrors what the native Rust
1036
+ // engine does implicitly via its class-scoped symbol table.
1037
+ // NOTE: restricted to `this` only — `super.method()` targets a parent class,
1038
+ // not the enclosing class, so qualifying with the child class name would
1039
+ // produce a false edge when the child also defines a same-named method.
1040
+ if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
1041
+ const lastDot = caller.callerName.lastIndexOf('.');
1042
+ if (lastDot > 0) {
1043
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
1044
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
1045
+ const qualifiedName = `${className}.${call.name}`;
1046
+ const qualified = lookup
1047
+ .byNameAndFile(qualifiedName, relPath)
1048
+ .filter((n) => n.kind === 'method');
1049
+ if (qualified.length > 0) {
1050
+ targets = qualified;
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ // Same-class bare-call fallback: when a no-receiver call can't be resolved
1056
+ // globally, try the caller's own class as a qualifier. Handles C# static
1057
+ // sibling calls: `IsValidEmail()` inside `Validators.ValidateUser` resolves
1058
+ // to `Validators.IsValidEmail`. Skipped for JS/TS where bare calls are
1059
+ // module-scoped, not class-scoped.
1060
+ if (
1061
+ targets.length === 0 &&
1062
+ !call.receiver &&
1063
+ caller.callerName != null &&
1064
+ !isModuleScopedLanguage(relPath)
1065
+ ) {
1066
+ const lastDot = caller.callerName.lastIndexOf('.');
1067
+ if (lastDot > 0) {
1068
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
1069
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
1070
+ const qualifiedName = `${className}.${call.name}`;
1071
+ const qualified = lookup
1072
+ .byNameAndFile(qualifiedName, relPath)
1073
+ .filter((n) => n.kind === 'method');
1074
+ if (qualified.length > 0) {
1075
+ targets = qualified;
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ // Object.defineProperty accessor fallback: when a function is registered as
1081
+ // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
1082
+ // calls to `this.X()` inside `getter` resolve against `obj` (this === obj
1083
+ // when the accessor is invoked). If the same-class fallback above found
1084
+ // nothing, try treating `obj` as the receiver and look up `obj.X` in the
1085
+ // typeMap, or fall back to a same-file lookup of any definition named X
1086
+ // that belongs to the object literal or its type.
1087
+ if (
1088
+ targets.length === 0 &&
1089
+ call.receiver === 'this' &&
1090
+ caller.callerName != null &&
1091
+ symbols.definePropertyReceivers
1092
+ ) {
1093
+ const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
1094
+ if (receiverVarName) {
1095
+ // Try typeMap lookup for receiver.methodName
1096
+ const typeEntry = typeMap.get(receiverVarName);
1097
+ const typeName = typeEntry
1098
+ ? typeof typeEntry === 'string'
1099
+ ? typeEntry
1100
+ : (typeEntry as { type?: string }).type
1101
+ : null;
1102
+ if (typeName) {
1103
+ const qualifiedName = `${typeName}.${call.name}`;
1104
+ const qualified = lookup.byNameAndFile(qualifiedName, relPath);
1105
+ if (qualified.length > 0) {
1106
+ targets = [...qualified];
1107
+ }
1108
+ }
1109
+ // If still no targets, search for any definition named `call.name` in
1110
+ // the same file — handles plain object literals where the method isn't
1111
+ // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
1112
+ // Note: this is intentionally broad — it matches any same-file definition
1113
+ // with the called name, not just members of the receiver object. This is
1114
+ // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
1115
+ if (targets.length === 0) {
1116
+ const sameFile = lookup.byNameAndFile(call.name, relPath);
1117
+ if (sameFile.length > 0) {
1118
+ targets = [...sameFile];
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ // Sort targets by confidence descending before emitting edges.
1125
+ // For multi-target calls with duplicate (source_id, target_id) pairs the
1126
+ // stored confidence depends on which duplicate is processed last — sorting
1127
+ // here guarantees the highest-confidence target wins on dedup, matching the
1128
+ // native engine's sort_targets_by_confidence call in build_edges.rs.
1129
+ if (targets.length > 1) {
1130
+ targets = [...targets].sort(
1131
+ (a, b) =>
1132
+ computeConfidence(relPath, b.file, importedFrom ?? null) -
1133
+ computeConfidence(relPath, a.file, importedFrom ?? null),
1134
+ );
1135
+ }
1136
+
582
1137
  for (const t of targets) {
583
1138
  const edgeKey = `${caller.id}|${t.id}`;
584
- if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
585
- seenCallEdges.add(edgeKey);
1139
+ if (t.id !== caller.id) {
586
1140
  const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
587
- allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
1141
+ if (seenCallEdges.has(edgeKey)) continue;
1142
+ const ptsIdx = ptsEdgeRows.get(edgeKey);
1143
+ if (ptsIdx !== undefined) {
1144
+ // A pts-resolved edge already exists for this caller→target pair with a
1145
+ // penalised confidence. Upgrade it to the direct-call confidence in-place,
1146
+ // then promote to seenCallEdges so no further processing is needed.
1147
+ const ptsRow = allEdgeRows[ptsIdx];
1148
+ if (ptsRow) {
1149
+ ptsRow[3] = confidence;
1150
+ ptsRow[4] = isDynamic; // upgrade is_dynamic: direct call overrides the pts-alias dynamic flag
1151
+ ptsRow[5] = 'ts-native'; // promoted from pts to direct-call resolution
1152
+ }
1153
+ ptsEdgeRows.delete(edgeKey);
1154
+ seenCallEdges.add(edgeKey);
1155
+ } else {
1156
+ seenCallEdges.add(edgeKey);
1157
+ allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic, 'ts-native']);
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ // Phase 8.3 / 8.3c / bind: points-to fallback for unresolved calls.
1163
+ // Fires for three cases:
1164
+ // (a) dynamic=true: alias calls emitted by extractCallbackReferenceCalls.
1165
+ // Looks up `call.name` directly (alias entries are flat-keyed).
1166
+ // (b) non-dynamic: parameter variable calls (fn() where fn is a param).
1167
+ // Looks up the scoped key `callerName::call.name` to avoid spurious
1168
+ // edges from same-named parameters across different functions.
1169
+ // (c) non-dynamic: module-level alias bindings — `f = fn.bind(ctx)` or
1170
+ // `const f = handler` — where pts('f') was seeded by fnRefBindings.
1171
+ // Checked against fnRefBindingLhs (the pre-computed set of lhs names from
1172
+ // fnRefBindings) rather than the full ptsMap, so case (c) only fires for
1173
+ // genuine bind/alias entries and never for self-seeded local definitions.
1174
+ // Confidence is penalised by one hop to reflect the extra indirection.
1175
+ //
1176
+ // Note: pts edges are added to ptsEdgeRows (not seenCallEdges) so that a later
1177
+ // direct call to the same target in the same function body can upgrade confidence
1178
+ // rather than being silently dropped by the dedup guard.
1179
+ const scopedPtsKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
1180
+ // Module-level calls (callerName === null) use the '<module>' sentinel emitted by
1181
+ // extractSpreadForOfWalk for top-level for-of loops. Look it up as a fallback so
1182
+ // that `for (const f of arr) { f(); }` at module scope resolves correctly.
1183
+ const modulePtsKey =
1184
+ caller.callerName === null && ptsMap?.has(`<module>::${call.name}`)
1185
+ ? `<module>::${call.name}`
1186
+ : null;
1187
+ const flatPtsKey =
1188
+ !call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;
1189
+ if (
1190
+ targets.length === 0 &&
1191
+ !call.receiver &&
1192
+ ptsMap &&
1193
+ (call.dynamic ||
1194
+ (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) ||
1195
+ modulePtsKey != null ||
1196
+ flatPtsKey != null)
1197
+ ) {
1198
+ const ptsLookupName = call.dynamic
1199
+ ? call.name
1200
+ : scopedPtsKey != null && ptsMap.has(scopedPtsKey)
1201
+ ? scopedPtsKey
1202
+ : modulePtsKey != null
1203
+ ? modulePtsKey
1204
+ : // flatPtsKey != null is guaranteed by the outer if condition: if neither
1205
+ // call.dynamic nor scopedPtsKey nor modulePtsKey matched, flatPtsKey must be non-null.
1206
+ flatPtsKey!;
1207
+ for (const alias of resolveViaPointsTo(ptsLookupName, ptsMap)) {
1208
+ // Resolve the concrete alias target. Only `name` is needed here — receiver
1209
+ // and line are not relevant for alias resolution (we are looking up the
1210
+ // aliased function by name, not dispatching a method call).
1211
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
1212
+ lookup,
1213
+ { name: alias },
1214
+ relPath,
1215
+ importedNames,
1216
+ typeMap as Map<string, unknown>,
1217
+ );
1218
+ const sortedAliasTargets =
1219
+ aliasTargets.length > 1
1220
+ ? [...aliasTargets].sort(
1221
+ (a, b) =>
1222
+ computeConfidence(relPath, b.file, aliasFrom ?? null) -
1223
+ computeConfidence(relPath, a.file, aliasFrom ?? null),
1224
+ )
1225
+ : aliasTargets;
1226
+ for (const t of sortedAliasTargets) {
1227
+ const edgeKey = `${caller.id}|${t.id}`;
1228
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1229
+ const conf =
1230
+ computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
1231
+ if (conf > 0) {
1232
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
1233
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // Phase 8.3f: pts fallback for receiver calls via object-rest param bindings.
1241
+ // Fires when `rest.prop()` is encountered and `rest` was seeded as `pts["rest.prop"]`
1242
+ // by the object-rest dispatch chain (ObjectRestParamBinding + paramBinding + ObjectPropBinding).
1243
+ if (
1244
+ targets.length === 0 &&
1245
+ call.receiver &&
1246
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
1247
+ call.receiver !== 'this' &&
1248
+ call.receiver !== 'self' &&
1249
+ call.receiver !== 'super' &&
1250
+ ptsMap
1251
+ ) {
1252
+ const receiverKey = `${call.receiver}.${call.name}`;
1253
+ if (ptsMap.has(receiverKey)) {
1254
+ for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
1255
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
1256
+ lookup,
1257
+ { name: alias },
1258
+ relPath,
1259
+ importedNames,
1260
+ typeMap as Map<string, unknown>,
1261
+ );
1262
+ const sortedAliasTargets =
1263
+ aliasTargets.length > 1
1264
+ ? [...aliasTargets].sort(
1265
+ (a, b) =>
1266
+ computeConfidence(relPath, b.file, aliasFrom ?? null) -
1267
+ computeConfidence(relPath, a.file, aliasFrom ?? null),
1268
+ )
1269
+ : aliasTargets;
1270
+ for (const t of sortedAliasTargets) {
1271
+ const edgeKey = `${caller.id}|${t.id}`;
1272
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1273
+ const conf =
1274
+ computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
1275
+ if (conf > 0) {
1276
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
1277
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
1278
+ }
1279
+ }
1280
+ }
1281
+ }
588
1282
  }
589
1283
  }
590
1284
 
@@ -602,9 +1296,56 @@ function buildFileCallEdges(
602
1296
  relPath,
603
1297
  typeMap as Map<string, unknown>,
604
1298
  seenCallEdges,
1299
+ importedNames,
605
1300
  );
606
1301
  if (recv) {
607
- allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0]);
1302
+ allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
1303
+ }
1304
+ }
1305
+
1306
+ // Phase 8.5: CHA + RTA dispatch expansion.
1307
+ // For `this`/`self`/`super` calls: resolve through the class hierarchy instead
1308
+ // of relying solely on global name matching.
1309
+ // For typed receiver calls: expand to all instantiated concrete implementations.
1310
+ if (chaCtx && call.receiver) {
1311
+ let chaTargets: ReadonlyArray<{ id: number; file: string }> = [];
1312
+ let isTypedReceiverDispatch = false;
1313
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
1314
+ chaTargets = resolveThisDispatch(
1315
+ call.name,
1316
+ caller.callerName,
1317
+ call.receiver,
1318
+ chaCtx,
1319
+ lookup,
1320
+ relPath,
1321
+ );
1322
+ } else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
1323
+ const typeEntry = typeMap.get(call.receiver);
1324
+ const typeName = typeEntry
1325
+ ? typeof typeEntry === 'string'
1326
+ ? typeEntry
1327
+ : (typeEntry as { type?: string }).type
1328
+ : null;
1329
+ if (typeName) {
1330
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
1331
+ isTypedReceiverDispatch = true;
1332
+ }
1333
+ }
1334
+ for (const t of chaTargets) {
1335
+ const edgeKey = `${caller.id}|${t.id}`;
1336
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1337
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
1338
+ // — file proximity is not meaningful for virtual dispatch confidence.
1339
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
1340
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
1341
+ const conf = isTypedReceiverDispatch
1342
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
1343
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1344
+ if (conf > 0) {
1345
+ seenCallEdges.add(edgeKey);
1346
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
1347
+ }
1348
+ }
608
1349
  }
609
1350
  }
610
1351
  }
@@ -632,7 +1373,7 @@ function buildClassHierarchyEdges(
632
1373
  );
633
1374
  if (sourceRow) {
634
1375
  for (const t of targetRows) {
635
- allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
1376
+ allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0, null]);
636
1377
  }
637
1378
  }
638
1379
  }
@@ -646,13 +1387,56 @@ function buildClassHierarchyEdges(
646
1387
  );
647
1388
  if (sourceRow) {
648
1389
  for (const t of targetRows) {
649
- allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
1390
+ allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0, null]);
650
1391
  }
651
1392
  }
652
1393
  }
653
1394
  }
654
1395
  }
655
1396
 
1397
+ // ── Native bulk-insert technique back-fill ──────────────────────────────
1398
+
1399
+ /**
1400
+ * After native bulkInsertEdges (which does not write the technique column),
1401
+ * apply technique values from the in-memory row array back to the DB.
1402
+ *
1403
+ * Rows with an explicit technique get a targeted UPDATE by (source_id, target_id).
1404
+ * The catch-all 'ts-native' tag is scoped to only the source_ids present in this
1405
+ * batch — this prevents mis-tagging pre-migration NULL-technique edges from
1406
+ * unchanged files that were never purged and re-inserted.
1407
+ */
1408
+ function applyEdgeTechniquesAfterNativeInsert(
1409
+ db: BetterSqlite3Database,
1410
+ rows: EdgeRowTuple[],
1411
+ ): void {
1412
+ const callRows = rows.filter((r) => r[2] === 'calls');
1413
+ if (callRows.length === 0) return;
1414
+
1415
+ const taggedRows = callRows.filter((r) => r[5] != null);
1416
+ // Collect distinct source IDs for this batch so the catch-all UPDATE is scoped
1417
+ // to edges inserted in the current run, not the entire table.
1418
+ const sourceIds = [...new Set(callRows.map((r) => r[0]))];
1419
+ // Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
1420
+ const CHUNK_SIZE = 500;
1421
+
1422
+ const tx = db.transaction(() => {
1423
+ if (taggedRows.length > 0) {
1424
+ const stmt = db.prepare(
1425
+ "UPDATE edges SET technique = ? WHERE kind = 'calls' AND source_id = ? AND target_id = ? AND technique IS NULL",
1426
+ );
1427
+ for (const r of taggedRows) stmt.run(r[5], r[0], r[1]);
1428
+ }
1429
+ for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
1430
+ const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
1431
+ const placeholders = chunk.map(() => '?').join(',');
1432
+ db.prepare(
1433
+ `UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL AND source_id IN (${placeholders})`,
1434
+ ).run(...chunk);
1435
+ }
1436
+ });
1437
+ tx();
1438
+ }
1439
+
656
1440
  // ── Reverse-dep edge reconnection (#932, #933) ─────────────────────────
657
1441
 
658
1442
  /**
@@ -685,6 +1469,7 @@ function reconnectReverseDepEdges(ctx: PipelineContext): void {
685
1469
  saved.edgeKind,
686
1470
  saved.confidence,
687
1471
  saved.dynamic,
1472
+ saved.technique,
688
1473
  ]);
689
1474
  } else {
690
1475
  // Target was removed or renamed in the changed file — edge is stale
@@ -704,6 +1489,8 @@ function reconnectReverseDepEdges(ctx: PipelineContext): void {
704
1489
  const ok = ctx.nativeDb.bulkInsertEdges(nativeEdges);
705
1490
  if (!ok) {
706
1491
  batchInsertEdges(db, reconnectedRows);
1492
+ } else {
1493
+ applyEdgeTechniquesAfterNativeInsert(db, reconnectedRows);
707
1494
  }
708
1495
  } else {
709
1496
  batchInsertEdges(db, reconnectedRows);
@@ -724,7 +1511,7 @@ function reconnectReverseDepEdges(ctx: PipelineContext): void {
724
1511
  * their import targets. Falls back to loading ALL nodes for full builds or
725
1512
  * larger incremental changes.
726
1513
  */
727
- const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
1514
+ const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant','variable')`;
728
1515
 
729
1516
  function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolean } {
730
1517
  const { db, fileSymbols, isFullBuild, batchResolved } = ctx;
@@ -803,8 +1590,34 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
803
1590
  addLazyFallback(ctx, scopedLoad);
804
1591
 
805
1592
  const t0 = performance.now();
1593
+
1594
+ // Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
1595
+ // Runs before call-edge construction so the accurate types are available
1596
+ // for method-call resolution. Gated on config so users can opt out.
1597
+ //
1598
+ // Skip for small incremental builds: TypeScript program creation requires
1599
+ // loading the entire tsconfig file list (~700ms startup on the codegraph
1600
+ // corpus), which dominates the 1-file rebuild time. Native engine bypasses
1601
+ // this entirely via the Rust orchestrator; WASM/JS engines need this gate
1602
+ // to match native's effective behaviour on tiny incremental changes.
1603
+ // Mirrors the smallFilesThreshold gates for nativeDb and native call-edges.
1604
+ const isSmallIncremental =
1605
+ !ctx.isFullBuild && ctx.fileSymbols.size <= ctx.config.build.smallFilesThreshold;
1606
+ if (ctx.config.build.typescriptResolver && !isSmallIncremental) {
1607
+ await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
1608
+ }
1609
+
806
1610
  const native = engineName === 'native' ? loadNative() : null;
807
1611
 
1612
+ // Phase 8.2: Augment typeMaps with cross-file return-type propagation before
1613
+ // the transaction opens. This is pure in-memory mutation (no DB I/O) and must
1614
+ // run outside the transaction to avoid leaving ctx.fileSymbols in a partial
1615
+ // state if the transaction rolls back unexpectedly.
1616
+ propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
1617
+ // Phase 8.5: Build CHA context after propagation so typeMap confidence values
1618
+ // (used for RTA seeding) reflect any cross-file propagated types.
1619
+ const chaCtx = buildChaContext(ctx.fileSymbols);
1620
+
808
1621
  // Phase 1: Compute edges inside a better-sqlite3 transaction.
809
1622
  // Barrel-edge deletion lives here so that the JS path (which also inserts
810
1623
  // edges in this transaction) keeps deletion + insertion atomic.
@@ -853,8 +1666,21 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
853
1666
  (ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
854
1667
  if (useNativeCallEdges) {
855
1668
  buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native!);
1669
+ // The native engine receives all pts bindings (paramBindings,
1670
+ // fnRefBindings, thisCallBindings, objectRestParamBindings, …) through
1671
+ // NativeFileEntry and runs the same points-to solver as the JS path, so
1672
+ // no pts post-passes are needed here. Only capabilities that remain
1673
+ // JS-only run as post-passes below.
1674
+ const sharedLookup = makeContextLookup(ctx, getNodeIdStmt);
1675
+ // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1676
+ // getter/setter functions registered via Object.defineProperty.
1677
+ buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1678
+ // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
1679
+ // The native Rust engine has no knowledge of the CHA context, so this/self
1680
+ // calls and interface dispatch are not expanded to concrete implementations.
1681
+ buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
856
1682
  } else {
857
- buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
1683
+ buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
858
1684
  }
859
1685
 
860
1686
  // When using native edge insert, skip JS insert here — do it after tx commits.
@@ -881,6 +1707,8 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
881
1707
  if (!ok) {
882
1708
  debug('Native bulkInsertEdges failed — falling back to JS batchInsertEdges');
883
1709
  batchInsertEdges(ctx.db, allEdgeRows);
1710
+ } else {
1711
+ applyEdgeTechniquesAfterNativeInsert(ctx.db, allEdgeRows);
884
1712
  }
885
1713
  }
886
1714
 
@@ -893,5 +1721,12 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
893
1721
  reconnectReverseDepEdges(ctx);
894
1722
  }
895
1723
 
1724
+ // Phase 4: CHA post-pass — expand virtual-dispatch edges for class hierarchies
1725
+ // and interface implementations. Runs after all call + hierarchy edges are
1726
+ // committed so the DB is consistent.
1727
+ // Note: the native orchestrator success path runs this independently in
1728
+ // tryNativeOrchestrator; this phase covers the WASM and native-fallback paths.
1729
+ runChaPostPass(db);
1730
+
896
1731
  ctx.timing.edgesMs = performance.now() - t0;
897
1732
  }