@optave/codegraph 3.12.0 → 3.15.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 (524) hide show
  1. package/README.md +83 -46
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +38 -40
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/b2.d.ts +7 -0
  6. package/dist/ast-analysis/rules/b2.d.ts.map +1 -0
  7. package/dist/ast-analysis/rules/b2.js +240 -0
  8. package/dist/ast-analysis/rules/b2.js.map +1 -0
  9. package/dist/ast-analysis/rules/b3.d.ts +6 -0
  10. package/dist/ast-analysis/rules/b3.d.ts.map +1 -0
  11. package/dist/ast-analysis/rules/b3.js +105 -0
  12. package/dist/ast-analysis/rules/b3.js.map +1 -0
  13. package/dist/ast-analysis/rules/b4.d.ts +9 -0
  14. package/dist/ast-analysis/rules/b4.d.ts.map +1 -0
  15. package/dist/ast-analysis/rules/b4.js +361 -0
  16. package/dist/ast-analysis/rules/b4.js.map +1 -0
  17. package/dist/ast-analysis/rules/b5.d.ts +4 -0
  18. package/dist/ast-analysis/rules/b5.d.ts.map +1 -0
  19. package/dist/ast-analysis/rules/b5.js +52 -0
  20. package/dist/ast-analysis/rules/b5.js.map +1 -0
  21. package/dist/ast-analysis/rules/c.d.ts +4 -0
  22. package/dist/ast-analysis/rules/c.d.ts.map +1 -0
  23. package/dist/ast-analysis/rules/c.js +143 -0
  24. package/dist/ast-analysis/rules/c.js.map +1 -0
  25. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  26. package/dist/ast-analysis/rules/index.js +34 -0
  27. package/dist/ast-analysis/rules/index.js.map +1 -1
  28. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  29. package/dist/ast-analysis/rules/javascript.js +3 -0
  30. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  31. package/dist/ast-analysis/shared.d.ts.map +1 -1
  32. package/dist/ast-analysis/shared.js +2 -0
  33. package/dist/ast-analysis/shared.js.map +1 -1
  34. package/dist/ast-analysis/visitor-utils.d.ts +1 -0
  35. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  36. package/dist/ast-analysis/visitor-utils.js +5 -0
  37. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  38. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  39. package/dist/ast-analysis/visitor.js +60 -47
  40. package/dist/ast-analysis/visitor.js.map +1 -1
  41. package/dist/ast-analysis/visitors/cfg-visitor.d.ts.map +1 -1
  42. package/dist/ast-analysis/visitors/cfg-visitor.js +126 -76
  43. package/dist/ast-analysis/visitors/cfg-visitor.js.map +1 -1
  44. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  45. package/dist/ast-analysis/visitors/complexity-visitor.js +27 -15
  46. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  47. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  48. package/dist/ast-analysis/visitors/dataflow-visitor.js +54 -21
  49. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  50. package/dist/cli/commands/audit.d.ts.map +1 -1
  51. package/dist/cli/commands/audit.js +2 -1
  52. package/dist/cli/commands/audit.js.map +1 -1
  53. package/dist/cli/commands/batch.d.ts.map +1 -1
  54. package/dist/cli/commands/batch.js +1 -0
  55. package/dist/cli/commands/batch.js.map +1 -1
  56. package/dist/cli/commands/build.d.ts.map +1 -1
  57. package/dist/cli/commands/build.js +6 -1
  58. package/dist/cli/commands/build.js.map +1 -1
  59. package/dist/cli/commands/config.d.ts +3 -0
  60. package/dist/cli/commands/config.d.ts.map +1 -0
  61. package/dist/cli/commands/config.js +275 -0
  62. package/dist/cli/commands/config.js.map +1 -0
  63. package/dist/cli/commands/roles.d.ts.map +1 -1
  64. package/dist/cli/commands/roles.js +6 -1
  65. package/dist/cli/commands/roles.js.map +1 -1
  66. package/dist/cli/commands/triage.js +1 -1
  67. package/dist/cli/commands/triage.js.map +1 -1
  68. package/dist/cli/index.d.ts.map +1 -1
  69. package/dist/cli/index.js +10 -0
  70. package/dist/cli/index.js.map +1 -1
  71. package/dist/cli/shared/options.d.ts +2 -1
  72. package/dist/cli/shared/options.d.ts.map +1 -1
  73. package/dist/cli/shared/options.js +11 -1
  74. package/dist/cli/shared/options.js.map +1 -1
  75. package/dist/cli/types.d.ts +2 -0
  76. package/dist/cli/types.d.ts.map +1 -1
  77. package/dist/db/better-sqlite3.d.ts +2 -1
  78. package/dist/db/better-sqlite3.d.ts.map +1 -1
  79. package/dist/db/better-sqlite3.js.map +1 -1
  80. package/dist/db/connection.d.ts +7 -1
  81. package/dist/db/connection.d.ts.map +1 -1
  82. package/dist/db/connection.js +20 -5
  83. package/dist/db/connection.js.map +1 -1
  84. package/dist/db/index.d.ts +1 -1
  85. package/dist/db/index.d.ts.map +1 -1
  86. package/dist/db/index.js +1 -1
  87. package/dist/db/index.js.map +1 -1
  88. package/dist/db/migrations.d.ts.map +1 -1
  89. package/dist/db/migrations.js +69 -1
  90. package/dist/db/migrations.js.map +1 -1
  91. package/dist/db/repository/build-stmts.d.ts.map +1 -1
  92. package/dist/db/repository/build-stmts.js +18 -0
  93. package/dist/db/repository/build-stmts.js.map +1 -1
  94. package/dist/db/repository/dataflow.d.ts +5 -0
  95. package/dist/db/repository/dataflow.d.ts.map +1 -1
  96. package/dist/db/repository/dataflow.js +14 -0
  97. package/dist/db/repository/dataflow.js.map +1 -1
  98. package/dist/db/repository/index.d.ts +1 -1
  99. package/dist/db/repository/index.d.ts.map +1 -1
  100. package/dist/db/repository/index.js +1 -1
  101. package/dist/db/repository/index.js.map +1 -1
  102. package/dist/db/repository/native-repository.d.ts.map +1 -1
  103. package/dist/db/repository/native-repository.js +47 -34
  104. package/dist/db/repository/native-repository.js.map +1 -1
  105. package/dist/domain/analysis/context.d.ts +2 -2
  106. package/dist/domain/analysis/dependencies.d.ts +2 -2
  107. package/dist/domain/analysis/diff-impact.d.ts +2 -2
  108. package/dist/domain/analysis/fn-impact.d.ts +3 -1
  109. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  110. package/dist/domain/analysis/fn-impact.js +4 -0
  111. package/dist/domain/analysis/fn-impact.js.map +1 -1
  112. package/dist/domain/analysis/implementations.d.ts +2 -2
  113. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  114. package/dist/domain/analysis/module-map.js +32 -5
  115. package/dist/domain/analysis/module-map.js.map +1 -1
  116. package/dist/domain/analysis/roles.d.ts +7 -1
  117. package/dist/domain/analysis/roles.d.ts.map +1 -1
  118. package/dist/domain/analysis/roles.js +16 -0
  119. package/dist/domain/analysis/roles.js.map +1 -1
  120. package/dist/domain/analysis/symbol-lookup.d.ts +4 -4
  121. package/dist/domain/graph/builder/call-resolver.d.ts +29 -13
  122. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  123. package/dist/domain/graph/builder/call-resolver.js +125 -205
  124. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  125. package/dist/domain/graph/builder/cha.d.ts +9 -1
  126. package/dist/domain/graph/builder/cha.d.ts.map +1 -1
  127. package/dist/domain/graph/builder/cha.js +17 -2
  128. package/dist/domain/graph/builder/cha.js.map +1 -1
  129. package/dist/domain/graph/builder/context.d.ts +1 -0
  130. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  131. package/dist/domain/graph/builder/context.js.map +1 -1
  132. package/dist/domain/graph/builder/helpers.d.ts +24 -1
  133. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  134. package/dist/domain/graph/builder/helpers.js +174 -65
  135. package/dist/domain/graph/builder/helpers.js.map +1 -1
  136. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  137. package/dist/domain/graph/builder/incremental.js +166 -97
  138. package/dist/domain/graph/builder/incremental.js.map +1 -1
  139. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  140. package/dist/domain/graph/builder/pipeline.js +46 -5
  141. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  142. package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
  143. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  144. package/dist/domain/graph/builder/stages/build-edges.js +554 -538
  145. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  146. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  147. package/dist/domain/graph/builder/stages/collect-files.js +10 -7
  148. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  149. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  150. package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
  151. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  152. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  153. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  154. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  155. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  156. package/dist/domain/graph/builder/stages/native-orchestrator.js +952 -343
  157. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  158. package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
  159. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  160. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -1
  161. package/dist/domain/graph/resolver/points-to.js +105 -57
  162. package/dist/domain/graph/resolver/points-to.js.map +1 -1
  163. package/dist/domain/graph/resolver/strategy.d.ts +61 -0
  164. package/dist/domain/graph/resolver/strategy.d.ts.map +1 -0
  165. package/dist/domain/graph/resolver/strategy.js +222 -0
  166. package/dist/domain/graph/resolver/strategy.js.map +1 -0
  167. package/dist/domain/graph/watcher.d.ts.map +1 -1
  168. package/dist/domain/graph/watcher.js +16 -9
  169. package/dist/domain/graph/watcher.js.map +1 -1
  170. package/dist/domain/parser.d.ts +16 -5
  171. package/dist/domain/parser.d.ts.map +1 -1
  172. package/dist/domain/parser.js +58 -17
  173. package/dist/domain/parser.js.map +1 -1
  174. package/dist/domain/queries.d.ts +1 -1
  175. package/dist/domain/queries.d.ts.map +1 -1
  176. package/dist/domain/queries.js +1 -1
  177. package/dist/domain/queries.js.map +1 -1
  178. package/dist/domain/wasm-worker-entry.js +13 -2
  179. package/dist/domain/wasm-worker-entry.js.map +1 -1
  180. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  181. package/dist/domain/wasm-worker-pool.js +26 -5
  182. package/dist/domain/wasm-worker-pool.js.map +1 -1
  183. package/dist/domain/wasm-worker-protocol.d.ts +8 -0
  184. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  185. package/dist/extractors/cpp.d.ts.map +1 -1
  186. package/dist/extractors/cpp.js +42 -1
  187. package/dist/extractors/cpp.js.map +1 -1
  188. package/dist/extractors/cuda.d.ts.map +1 -1
  189. package/dist/extractors/cuda.js +42 -1
  190. package/dist/extractors/cuda.js.map +1 -1
  191. package/dist/extractors/dart.js +48 -3
  192. package/dist/extractors/dart.js.map +1 -1
  193. package/dist/extractors/groovy.js +62 -3
  194. package/dist/extractors/groovy.js.map +1 -1
  195. package/dist/extractors/helpers.d.ts +15 -2
  196. package/dist/extractors/helpers.d.ts.map +1 -1
  197. package/dist/extractors/helpers.js +45 -1
  198. package/dist/extractors/helpers.js.map +1 -1
  199. package/dist/extractors/java.d.ts.map +1 -1
  200. package/dist/extractors/java.js +85 -8
  201. package/dist/extractors/java.js.map +1 -1
  202. package/dist/extractors/javascript.d.ts.map +1 -1
  203. package/dist/extractors/javascript.js +686 -169
  204. package/dist/extractors/javascript.js.map +1 -1
  205. package/dist/extractors/kotlin.js +58 -3
  206. package/dist/extractors/kotlin.js.map +1 -1
  207. package/dist/extractors/objc.js +25 -2
  208. package/dist/extractors/objc.js.map +1 -1
  209. package/dist/extractors/scala.js +62 -2
  210. package/dist/extractors/scala.js.map +1 -1
  211. package/dist/extractors/swift.js +52 -3
  212. package/dist/extractors/swift.js.map +1 -1
  213. package/dist/features/audit.js +26 -23
  214. package/dist/features/audit.js.map +1 -1
  215. package/dist/features/boundaries.d.ts.map +1 -1
  216. package/dist/features/boundaries.js +12 -9
  217. package/dist/features/boundaries.js.map +1 -1
  218. package/dist/features/cfg.d.ts.map +1 -1
  219. package/dist/features/cfg.js +25 -18
  220. package/dist/features/cfg.js.map +1 -1
  221. package/dist/features/check.d.ts.map +1 -1
  222. package/dist/features/check.js +18 -5
  223. package/dist/features/check.js.map +1 -1
  224. package/dist/features/communities.d.ts +4 -2
  225. package/dist/features/communities.d.ts.map +1 -1
  226. package/dist/features/communities.js +6 -4
  227. package/dist/features/communities.js.map +1 -1
  228. package/dist/features/dataflow.d.ts +60 -0
  229. package/dist/features/dataflow.d.ts.map +1 -1
  230. package/dist/features/dataflow.js +530 -6
  231. package/dist/features/dataflow.js.map +1 -1
  232. package/dist/features/manifesto.d.ts.map +1 -1
  233. package/dist/features/manifesto.js +59 -72
  234. package/dist/features/manifesto.js.map +1 -1
  235. package/dist/features/sequence.d.ts.map +1 -1
  236. package/dist/features/sequence.js +27 -22
  237. package/dist/features/sequence.js.map +1 -1
  238. package/dist/features/snapshot.d.ts.map +1 -1
  239. package/dist/features/snapshot.js +36 -28
  240. package/dist/features/snapshot.js.map +1 -1
  241. package/dist/features/structure-query.d.ts +1 -1
  242. package/dist/features/structure-query.d.ts.map +1 -1
  243. package/dist/features/structure-query.js +6 -6
  244. package/dist/features/structure-query.js.map +1 -1
  245. package/dist/features/structure.d.ts.map +1 -1
  246. package/dist/features/structure.js +150 -62
  247. package/dist/features/structure.js.map +1 -1
  248. package/dist/features/triage.d.ts.map +1 -1
  249. package/dist/features/triage.js +18 -11
  250. package/dist/features/triage.js.map +1 -1
  251. package/dist/graph/algorithms/bfs.d.ts +1 -1
  252. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  253. package/dist/graph/algorithms/bfs.js +14 -13
  254. package/dist/graph/algorithms/bfs.js.map +1 -1
  255. package/dist/graph/algorithms/tarjan.d.ts.map +1 -1
  256. package/dist/graph/algorithms/tarjan.js +5 -0
  257. package/dist/graph/algorithms/tarjan.js.map +1 -1
  258. package/dist/graph/builders/dependency.js +28 -22
  259. package/dist/graph/builders/dependency.js.map +1 -1
  260. package/dist/graph/classifiers/roles.d.ts +10 -1
  261. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  262. package/dist/graph/classifiers/roles.js +60 -6
  263. package/dist/graph/classifiers/roles.js.map +1 -1
  264. package/dist/index.d.ts +1 -1
  265. package/dist/index.d.ts.map +1 -1
  266. package/dist/index.js +1 -1
  267. package/dist/index.js.map +1 -1
  268. package/dist/infrastructure/config.d.ts +87 -4
  269. package/dist/infrastructure/config.d.ts.map +1 -1
  270. package/dist/infrastructure/config.js +424 -22
  271. package/dist/infrastructure/config.js.map +1 -1
  272. package/dist/infrastructure/registry.d.ts +27 -7
  273. package/dist/infrastructure/registry.d.ts.map +1 -1
  274. package/dist/infrastructure/registry.js +79 -5
  275. package/dist/infrastructure/registry.js.map +1 -1
  276. package/dist/infrastructure/update-check.d.ts.map +1 -1
  277. package/dist/infrastructure/update-check.js +49 -31
  278. package/dist/infrastructure/update-check.js.map +1 -1
  279. package/dist/mcp/server.d.ts +2 -10
  280. package/dist/mcp/server.d.ts.map +1 -1
  281. package/dist/mcp/server.js.map +1 -1
  282. package/dist/mcp/tools/ast-query.d.ts +1 -1
  283. package/dist/mcp/tools/ast-query.d.ts.map +1 -1
  284. package/dist/mcp/tools/audit.d.ts +1 -1
  285. package/dist/mcp/tools/audit.d.ts.map +1 -1
  286. package/dist/mcp/tools/batch-query.d.ts +1 -1
  287. package/dist/mcp/tools/batch-query.d.ts.map +1 -1
  288. package/dist/mcp/tools/branch-compare.d.ts +1 -1
  289. package/dist/mcp/tools/branch-compare.d.ts.map +1 -1
  290. package/dist/mcp/tools/brief.d.ts +1 -1
  291. package/dist/mcp/tools/brief.d.ts.map +1 -1
  292. package/dist/mcp/tools/cfg.d.ts +1 -1
  293. package/dist/mcp/tools/cfg.d.ts.map +1 -1
  294. package/dist/mcp/tools/check.d.ts +1 -1
  295. package/dist/mcp/tools/check.d.ts.map +1 -1
  296. package/dist/mcp/tools/co-changes.d.ts +1 -1
  297. package/dist/mcp/tools/co-changes.d.ts.map +1 -1
  298. package/dist/mcp/tools/code-owners.d.ts +1 -1
  299. package/dist/mcp/tools/code-owners.d.ts.map +1 -1
  300. package/dist/mcp/tools/communities.d.ts +1 -1
  301. package/dist/mcp/tools/communities.d.ts.map +1 -1
  302. package/dist/mcp/tools/complexity.d.ts +1 -1
  303. package/dist/mcp/tools/complexity.d.ts.map +1 -1
  304. package/dist/mcp/tools/context.d.ts +1 -1
  305. package/dist/mcp/tools/context.d.ts.map +1 -1
  306. package/dist/mcp/tools/dataflow.d.ts +1 -1
  307. package/dist/mcp/tools/dataflow.d.ts.map +1 -1
  308. package/dist/mcp/tools/diff-impact.d.ts +1 -1
  309. package/dist/mcp/tools/diff-impact.d.ts.map +1 -1
  310. package/dist/mcp/tools/execution-flow.d.ts +1 -1
  311. package/dist/mcp/tools/execution-flow.d.ts.map +1 -1
  312. package/dist/mcp/tools/export-graph.d.ts +1 -1
  313. package/dist/mcp/tools/export-graph.d.ts.map +1 -1
  314. package/dist/mcp/tools/file-deps.d.ts +1 -1
  315. package/dist/mcp/tools/file-deps.d.ts.map +1 -1
  316. package/dist/mcp/tools/file-exports.d.ts +1 -1
  317. package/dist/mcp/tools/file-exports.d.ts.map +1 -1
  318. package/dist/mcp/tools/find-cycles.d.ts +1 -1
  319. package/dist/mcp/tools/find-cycles.d.ts.map +1 -1
  320. package/dist/mcp/tools/fn-impact.d.ts +1 -1
  321. package/dist/mcp/tools/fn-impact.d.ts.map +1 -1
  322. package/dist/mcp/tools/impact-analysis.d.ts +1 -1
  323. package/dist/mcp/tools/impact-analysis.d.ts.map +1 -1
  324. package/dist/mcp/tools/implementations.d.ts +1 -1
  325. package/dist/mcp/tools/implementations.d.ts.map +1 -1
  326. package/dist/mcp/tools/index.d.ts +2 -5
  327. package/dist/mcp/tools/index.d.ts.map +1 -1
  328. package/dist/mcp/tools/index.js.map +1 -1
  329. package/dist/mcp/tools/interfaces.d.ts +1 -1
  330. package/dist/mcp/tools/interfaces.d.ts.map +1 -1
  331. package/dist/mcp/tools/list-functions.d.ts +1 -1
  332. package/dist/mcp/tools/list-functions.d.ts.map +1 -1
  333. package/dist/mcp/tools/list-repos.d.ts +1 -1
  334. package/dist/mcp/tools/list-repos.d.ts.map +1 -1
  335. package/dist/mcp/tools/module-map.d.ts +1 -1
  336. package/dist/mcp/tools/module-map.d.ts.map +1 -1
  337. package/dist/mcp/tools/node-roles.d.ts +1 -1
  338. package/dist/mcp/tools/node-roles.d.ts.map +1 -1
  339. package/dist/mcp/tools/path.d.ts +1 -1
  340. package/dist/mcp/tools/path.d.ts.map +1 -1
  341. package/dist/mcp/tools/query.d.ts +1 -1
  342. package/dist/mcp/tools/query.d.ts.map +1 -1
  343. package/dist/mcp/tools/semantic-search.d.ts +1 -1
  344. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  345. package/dist/mcp/tools/sequence.d.ts +1 -1
  346. package/dist/mcp/tools/sequence.d.ts.map +1 -1
  347. package/dist/mcp/tools/structure.d.ts +1 -1
  348. package/dist/mcp/tools/structure.d.ts.map +1 -1
  349. package/dist/mcp/tools/symbol-children.d.ts +1 -1
  350. package/dist/mcp/tools/symbol-children.d.ts.map +1 -1
  351. package/dist/mcp/tools/triage.d.ts +1 -1
  352. package/dist/mcp/tools/triage.d.ts.map +1 -1
  353. package/dist/mcp/tools/where.d.ts +1 -1
  354. package/dist/mcp/tools/where.d.ts.map +1 -1
  355. package/dist/mcp/types.d.ts +19 -0
  356. package/dist/mcp/types.d.ts.map +1 -0
  357. package/dist/mcp/types.js +6 -0
  358. package/dist/mcp/types.js.map +1 -0
  359. package/dist/presentation/queries-cli/index.d.ts +1 -1
  360. package/dist/presentation/queries-cli/index.d.ts.map +1 -1
  361. package/dist/presentation/queries-cli/index.js +1 -1
  362. package/dist/presentation/queries-cli/index.js.map +1 -1
  363. package/dist/presentation/queries-cli/overview.d.ts +1 -0
  364. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  365. package/dist/presentation/queries-cli/overview.js +20 -1
  366. package/dist/presentation/queries-cli/overview.js.map +1 -1
  367. package/dist/presentation/queries-cli.d.ts +1 -1
  368. package/dist/presentation/queries-cli.d.ts.map +1 -1
  369. package/dist/presentation/queries-cli.js +1 -1
  370. package/dist/presentation/queries-cli.js.map +1 -1
  371. package/dist/presentation/structure.d.ts +1 -1
  372. package/dist/presentation/structure.d.ts.map +1 -1
  373. package/dist/presentation/structure.js +2 -2
  374. package/dist/presentation/structure.js.map +1 -1
  375. package/dist/presentation/viewer.d.ts.map +1 -1
  376. package/dist/presentation/viewer.js +45 -32
  377. package/dist/presentation/viewer.js.map +1 -1
  378. package/dist/shared/constants.d.ts +21 -0
  379. package/dist/shared/constants.d.ts.map +1 -1
  380. package/dist/shared/constants.js +25 -0
  381. package/dist/shared/constants.js.map +1 -1
  382. package/dist/shared/normalize.d.ts.map +1 -1
  383. package/dist/shared/normalize.js +12 -22
  384. package/dist/shared/normalize.js.map +1 -1
  385. package/dist/shared/paginate.d.ts +4 -17
  386. package/dist/shared/paginate.d.ts.map +1 -1
  387. package/dist/shared/paginate.js.map +1 -1
  388. package/dist/types.d.ts +113 -1
  389. package/dist/types.d.ts.map +1 -1
  390. package/grammars/tree-sitter-erlang.wasm +0 -0
  391. package/grammars/tree-sitter-gleam.wasm +0 -0
  392. package/package.json +7 -8
  393. package/src/ast-analysis/engine.ts +43 -63
  394. package/src/ast-analysis/rules/b2.ts +263 -0
  395. package/src/ast-analysis/rules/b3.ts +127 -0
  396. package/src/ast-analysis/rules/b4.ts +378 -0
  397. package/src/ast-analysis/rules/b5.ts +65 -0
  398. package/src/ast-analysis/rules/c.ts +157 -0
  399. package/src/ast-analysis/rules/index.ts +34 -0
  400. package/src/ast-analysis/rules/javascript.ts +3 -0
  401. package/src/ast-analysis/shared.ts +2 -0
  402. package/src/ast-analysis/visitor-utils.ts +5 -0
  403. package/src/ast-analysis/visitor.ts +82 -52
  404. package/src/ast-analysis/visitors/cfg-visitor.ts +198 -84
  405. package/src/ast-analysis/visitors/complexity-visitor.ts +44 -16
  406. package/src/ast-analysis/visitors/dataflow-visitor.ts +68 -29
  407. package/src/cli/commands/audit.ts +2 -1
  408. package/src/cli/commands/batch.ts +1 -0
  409. package/src/cli/commands/build.ts +6 -1
  410. package/src/cli/commands/config.ts +353 -0
  411. package/src/cli/commands/roles.ts +6 -1
  412. package/src/cli/commands/triage.ts +1 -1
  413. package/src/cli/index.ts +10 -0
  414. package/src/cli/shared/options.ts +11 -1
  415. package/src/cli/types.ts +2 -0
  416. package/src/db/better-sqlite3.ts +5 -4
  417. package/src/db/connection.ts +23 -5
  418. package/src/db/index.ts +1 -0
  419. package/src/db/migrations.ts +69 -1
  420. package/src/db/repository/build-stmts.ts +30 -0
  421. package/src/db/repository/dataflow.ts +16 -0
  422. package/src/db/repository/index.ts +1 -1
  423. package/src/db/repository/native-repository.ts +56 -40
  424. package/src/domain/analysis/fn-impact.ts +4 -0
  425. package/src/domain/analysis/module-map.ts +38 -6
  426. package/src/domain/analysis/roles.ts +23 -0
  427. package/src/domain/graph/builder/call-resolver.ts +156 -218
  428. package/src/domain/graph/builder/cha.ts +18 -1
  429. package/src/domain/graph/builder/context.ts +1 -0
  430. package/src/domain/graph/builder/helpers.ts +205 -67
  431. package/src/domain/graph/builder/incremental.ts +249 -119
  432. package/src/domain/graph/builder/pipeline.ts +59 -6
  433. package/src/domain/graph/builder/stages/build-edges.ts +783 -652
  434. package/src/domain/graph/builder/stages/collect-files.ts +12 -6
  435. package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
  436. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  437. package/src/domain/graph/builder/stages/native-orchestrator.ts +1214 -398
  438. package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
  439. package/src/domain/graph/resolver/points-to.ts +182 -59
  440. package/src/domain/graph/resolver/strategy.ts +265 -0
  441. package/src/domain/graph/watcher.ts +19 -9
  442. package/src/domain/parser.ts +57 -16
  443. package/src/domain/queries.ts +1 -1
  444. package/src/domain/wasm-worker-entry.ts +13 -2
  445. package/src/domain/wasm-worker-pool.ts +29 -4
  446. package/src/domain/wasm-worker-protocol.ts +5 -0
  447. package/src/extractors/cpp.ts +44 -1
  448. package/src/extractors/cuda.ts +44 -1
  449. package/src/extractors/dart.ts +48 -3
  450. package/src/extractors/groovy.ts +62 -2
  451. package/src/extractors/helpers.ts +48 -2
  452. package/src/extractors/java.ts +88 -8
  453. package/src/extractors/javascript.ts +693 -167
  454. package/src/extractors/kotlin.ts +57 -3
  455. package/src/extractors/objc.ts +25 -1
  456. package/src/extractors/scala.ts +63 -1
  457. package/src/extractors/swift.ts +46 -3
  458. package/src/features/audit.ts +43 -34
  459. package/src/features/boundaries.ts +17 -9
  460. package/src/features/cfg.ts +31 -22
  461. package/src/features/check.ts +21 -5
  462. package/src/features/communities.ts +28 -19
  463. package/src/features/dataflow.ts +755 -6
  464. package/src/features/manifesto.ts +76 -75
  465. package/src/features/sequence.ts +29 -23
  466. package/src/features/snapshot.ts +36 -25
  467. package/src/features/structure-query.ts +7 -7
  468. package/src/features/structure.ts +185 -55
  469. package/src/features/triage.ts +28 -15
  470. package/src/graph/algorithms/bfs.ts +13 -12
  471. package/src/graph/algorithms/tarjan.ts +5 -0
  472. package/src/graph/builders/dependency.ts +35 -23
  473. package/src/graph/classifiers/roles.ts +74 -7
  474. package/src/index.ts +5 -1
  475. package/src/infrastructure/config.ts +511 -23
  476. package/src/infrastructure/registry.ts +117 -12
  477. package/src/infrastructure/update-check.ts +55 -33
  478. package/src/mcp/server.ts +2 -8
  479. package/src/mcp/tools/ast-query.ts +1 -1
  480. package/src/mcp/tools/audit.ts +1 -1
  481. package/src/mcp/tools/batch-query.ts +1 -1
  482. package/src/mcp/tools/branch-compare.ts +1 -1
  483. package/src/mcp/tools/brief.ts +1 -1
  484. package/src/mcp/tools/cfg.ts +1 -1
  485. package/src/mcp/tools/check.ts +1 -1
  486. package/src/mcp/tools/co-changes.ts +1 -1
  487. package/src/mcp/tools/code-owners.ts +1 -1
  488. package/src/mcp/tools/communities.ts +1 -1
  489. package/src/mcp/tools/complexity.ts +1 -1
  490. package/src/mcp/tools/context.ts +1 -1
  491. package/src/mcp/tools/dataflow.ts +1 -1
  492. package/src/mcp/tools/diff-impact.ts +1 -1
  493. package/src/mcp/tools/execution-flow.ts +1 -1
  494. package/src/mcp/tools/export-graph.ts +1 -1
  495. package/src/mcp/tools/file-deps.ts +1 -1
  496. package/src/mcp/tools/file-exports.ts +1 -1
  497. package/src/mcp/tools/find-cycles.ts +1 -1
  498. package/src/mcp/tools/fn-impact.ts +1 -1
  499. package/src/mcp/tools/impact-analysis.ts +1 -1
  500. package/src/mcp/tools/implementations.ts +1 -1
  501. package/src/mcp/tools/index.ts +2 -5
  502. package/src/mcp/tools/interfaces.ts +1 -1
  503. package/src/mcp/tools/list-functions.ts +1 -1
  504. package/src/mcp/tools/list-repos.ts +1 -1
  505. package/src/mcp/tools/module-map.ts +1 -1
  506. package/src/mcp/tools/node-roles.ts +1 -1
  507. package/src/mcp/tools/path.ts +1 -1
  508. package/src/mcp/tools/query.ts +1 -1
  509. package/src/mcp/tools/semantic-search.ts +1 -1
  510. package/src/mcp/tools/sequence.ts +1 -1
  511. package/src/mcp/tools/structure.ts +1 -1
  512. package/src/mcp/tools/symbol-children.ts +1 -1
  513. package/src/mcp/tools/triage.ts +1 -1
  514. package/src/mcp/tools/where.ts +1 -1
  515. package/src/mcp/types.ts +21 -0
  516. package/src/presentation/queries-cli/index.ts +1 -1
  517. package/src/presentation/queries-cli/overview.ts +35 -1
  518. package/src/presentation/queries-cli.ts +1 -0
  519. package/src/presentation/structure.ts +3 -3
  520. package/src/presentation/viewer.ts +98 -87
  521. package/src/shared/constants.ts +26 -0
  522. package/src/shared/normalize.ts +13 -22
  523. package/src/shared/paginate.ts +4 -18
  524. package/src/types.ts +127 -1
@@ -11,21 +11,21 @@
11
11
  * The orchestrator-selection strategy lives here so `pipeline.ts` stays a thin
12
12
  * top-level controller: detect changes, try native, fall back to JS stages.
13
13
  */
14
+ import { execFileSync } from 'node:child_process';
14
15
  import path from 'node:path';
15
16
  import { performance } from 'node:perf_hooks';
16
17
  import { acquireAdvisoryLock, closeDbPair, openDb, purgeFilesData, releaseAdvisoryLock, setBuildMeta, } from '../../../../db/index.js';
17
18
  import { debug, info, warn } from '../../../../infrastructure/logger.js';
18
19
  import { loadNative } from '../../../../infrastructure/native.js';
19
20
  import { semverCompare } from '../../../../infrastructure/update-check.js';
20
- import { normalizePath } from '../../../../shared/constants.js';
21
+ import { normalizePath, TS_NATIVE_CONFIDENCE_FLOOR } from '../../../../shared/constants.js';
21
22
  import { toErrorMessage } from '../../../../shared/errors.js';
22
23
  import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
23
- import { classifyNativeDrops, formatDropExtensionSummary, getInstalledWasmExtensions, NATIVE_SUPPORTED_EXTENSIONS, parseFilesWasmForBackfill, } from '../../../parser.js';
24
+ import { classifyNativeDrops, formatDropExtensionSummary, getInstalledWasmExtensions, NATIVE_SUPPORTED_EXTENSIONS, parseFilesWasmForBackfill, patchDataflowResult, } from '../../../parser.js';
24
25
  import { computeConfidence } from '../../resolve.js';
25
26
  import { resolveThisDispatch } from '../cha.js';
26
- import { batchInsertEdges, batchInsertNodes, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
27
+ import { batchInsertEdges, batchInsertNodes, CHA_DISPATCH_PENALTY, CHA_TYPED_DISPATCH_CONFIDENCE, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
27
28
  import { NativeDbProxy } from '../native-db-proxy.js';
28
- import { CHA_DISPATCH_PENALTY } from './build-edges.js';
29
29
  import { closeNativeDb } from './native-db-lifecycle.js';
30
30
  // ── Native orchestrator helpers ───────────────────────────────────────
31
31
  /** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
@@ -167,6 +167,162 @@ async function runPostNativeStructure(ctx, allFileSymbols, isFullBuild, changedF
167
167
  }
168
168
  return performance.now() - structureStart;
169
169
  }
170
+ /**
171
+ * P6: Build dataflow_vertices and inter-procedural edges after the Rust
172
+ * orchestrator completes.
173
+ *
174
+ * The Rust pipeline writes flows_to/returns/mutates edges directly to the DB
175
+ * but never writes to dataflow_vertices or dataflow_summary. This pass re-runs
176
+ * the Rust dataflow visitor (via extractDataflowAnalysis — fast, no re-parse)
177
+ * to get the DataflowResult and calls buildDataflowVerticesFromMap.
178
+ *
179
+ * Languages for which Rust has no dataflow rules return null from
180
+ * extractDataflowAnalysis and are silently skipped here. A follow-up issue
181
+ * (#1614 adjacent) will add WASM fallback for those languages.
182
+ */
183
+ async function runDataflowVertexPass(ctx, changedFiles) {
184
+ if (ctx.opts.dataflow === false)
185
+ return;
186
+ const native = loadNative();
187
+ if (!native?.extractDataflowAnalysis)
188
+ return;
189
+ // Determine which files to process: changed files for incremental, all for full builds.
190
+ let filesToProcess;
191
+ if (changedFiles && changedFiles.length > 0) {
192
+ filesToProcess = changedFiles;
193
+ }
194
+ else {
195
+ // Full build: scope to files that need vertex extraction rather than scanning every
196
+ // file in the project. Two categories:
197
+ // (a) Non-native language files — NATIVE_SUPPORTED_EXTENSIONS doesn't cover them,
198
+ // so extractDataflowAnalysis returns null; the wasmStubs path calls buildDataflowEdges
199
+ // which writes both edges AND vertices for those files.
200
+ // (b) Native-language files with dataflow edges already written by the Rust orchestrator
201
+ // (flows_to/returns/mutates) — those need vertex rows to connect them.
202
+ //
203
+ // Skipping native-language files with no dataflow edges is safe: extractDataflowAnalysis
204
+ // would return argFlows=[], assignments=[], mutations=[] for them, producing zero vertices
205
+ // and zero inter-procedural edges. Excluding them avoids O(n_total_files) re-analysis on
206
+ // every full build (codegraph itself: ~2000 files, ~50-80% with no dataflow edges).
207
+ const filesWithDataflow = new Set(ctx.db
208
+ .prepare(`SELECT DISTINCT n.file
209
+ FROM dataflow d
210
+ JOIN nodes n ON n.id = d.source_id
211
+ WHERE n.file IS NOT NULL`)
212
+ .all().map((r) => r.file));
213
+ filesToProcess = ctx.db
214
+ .prepare(`SELECT DISTINCT file FROM nodes WHERE file IS NOT NULL AND kind != 'directory'`)
215
+ .all()
216
+ .map((r) => r.file)
217
+ .filter((f) => {
218
+ const ext = path.extname(f).toLowerCase();
219
+ // Non-native files: always include (WASM handles them via wasmStubs path).
220
+ if (!NATIVE_SUPPORTED_EXTENSIONS.has(ext))
221
+ return true;
222
+ // Native files: only include when Rust wrote dataflow edges for them.
223
+ return filesWithDataflow.has(f);
224
+ });
225
+ }
226
+ // Split files into two buckets:
227
+ // nativeDataflow — Rust extracted data (vertex-only pass; edges already in DB)
228
+ // wasmStubs — Rust returned null (WASM will handle edges + vertices)
229
+ const nativeDataflow = new Map();
230
+ const wasmStubs = new Map();
231
+ const absPaths = filesToProcess.map((relPath) => path.join(ctx.rootDir, relPath));
232
+ // Batch the per-file dataflow extraction into one NAPI call so the parses run
233
+ // across the rayon thread pool instead of serially on the event loop — this is
234
+ // the dominant cost of a native full build (#perf). Older addons predate the
235
+ // batch export, so fall back to the per-file path when it is unavailable.
236
+ let batchResults = null;
237
+ if (typeof native.extractDataflowAnalysisBatch === 'function') {
238
+ try {
239
+ batchResults = native.extractDataflowAnalysisBatch(absPaths);
240
+ }
241
+ catch {
242
+ batchResults = null; // fall through to per-file extraction below
243
+ }
244
+ }
245
+ for (let i = 0; i < filesToProcess.length; i++) {
246
+ const relPath = filesToProcess[i];
247
+ let result = null;
248
+ if (batchResults) {
249
+ result = batchResults[i] ?? null;
250
+ }
251
+ else {
252
+ let source;
253
+ try {
254
+ source = readFileSafe(absPaths[i]);
255
+ }
256
+ catch {
257
+ // Unreadable file — mirror batch-path behaviour and route to WASM.
258
+ wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
259
+ continue;
260
+ }
261
+ if (!source) {
262
+ // Empty file — same treatment as batch returning null.
263
+ wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
264
+ continue;
265
+ }
266
+ try {
267
+ result = native.extractDataflowAnalysis(source, absPaths[i]);
268
+ }
269
+ catch {
270
+ // Language-specific parse failure — fall through to WASM.
271
+ }
272
+ }
273
+ if (result) {
274
+ // Normalise the native DataflowResult: Rust emits `bindingType: string | null`
275
+ // (flat) while the TS dataflow layer expects `binding: { type, index? }` (object).
276
+ // patchNativeResult handles this via patchDataflow for the full parse path;
277
+ // extractDataflowAnalysis(Batch) is a vertex-only fast path that bypasses
278
+ // patchNativeResult, so we apply the same normalisation here.
279
+ patchDataflowResult(result);
280
+ nativeDataflow.set(relPath, result);
281
+ }
282
+ else {
283
+ // Rust has no dataflow rules for this language; WASM fallback will handle
284
+ // both edge insertion and vertex extraction. Since Rust inserted 0 dataflow
285
+ // edges for these files, there is no risk of duplicates.
286
+ wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
287
+ }
288
+ }
289
+ const { buildExtToLangMap } = (await import('../../../../ast-analysis/shared.js'));
290
+ const { buildDataflowVerticesFromMap, buildDataflowEdges, collectCallerStitchCandidates, collectFuncIdsForFiles, } = (await import('../../../../features/dataflow.js'));
291
+ // Rust-supported languages: build vertices only (edges already written by Rust orchestrator).
292
+ if (nativeDataflow.size > 0) {
293
+ // P4: On incremental builds, unchanged caller files' arg_in edges were deleted when
294
+ // the changed files' param vertices were purged and recreated. Re-collect stitch
295
+ // candidates from those caller files so buildInterproceduralStitch can reconnect them.
296
+ // Skip on full builds (changedFiles absent/empty) — nativeDataflow covers all files.
297
+ let p4Candidates = [];
298
+ let p4Captures = [];
299
+ if (changedFiles && changedFiles.length > 0) {
300
+ const changedSet = new Set(changedFiles);
301
+ const totalFilesInDb = ctx.db.prepare(`SELECT COUNT(DISTINCT file) AS n FROM nodes`).get().n;
302
+ // Only run P4 when this is a real incremental build (not all files changed).
303
+ if (nativeDataflow.size < totalFilesInDb) {
304
+ const changedFuncIds = collectFuncIdsForFiles(ctx.db, changedSet);
305
+ if (changedFuncIds.length > 0) {
306
+ const extra = await collectCallerStitchCandidates(ctx.db, changedFuncIds, changedSet, ctx.rootDir, buildExtToLangMap(), null, // parsers — lazily loaded inside collectCallerStitchCandidates
307
+ null);
308
+ p4Candidates = extra.candidates;
309
+ p4Captures = extra.captures;
310
+ }
311
+ }
312
+ }
313
+ const interCount = buildDataflowVerticesFromMap(ctx.db, nativeDataflow, p4Candidates.length > 0 ? p4Candidates : undefined, p4Captures.length > 0 ? p4Captures : undefined);
314
+ if (interCount > 0) {
315
+ info(`Dataflow (native orchestrator): ${interCount} inter-procedural edges inserted${p4Candidates.length > 0 ? ` (P4: ${p4Candidates.length} re-stitch candidate(s) from unchanged callers)` : ''}`);
316
+ }
317
+ }
318
+ // Rust-unsupported languages: run the full WASM extraction (edges + vertices).
319
+ // wasmStubs entries have no `.dataflow` property, so the native bulk-insert
320
+ // fast path in buildDataflowEdges is always skipped for them — WASM runs
321
+ // both edge insertion and vertex extraction end-to-end.
322
+ if (wasmStubs.size > 0) {
323
+ await buildDataflowEdges(ctx.db, wasmStubs, ctx.rootDir);
324
+ }
325
+ }
170
326
  /**
171
327
  * JS fallback for AST/complexity/CFG/dataflow analysis after native orchestrator.
172
328
  * Used when the Rust addon doesn't include analysis persistence (older addon
@@ -245,34 +401,9 @@ async function runPostNativeAnalysis(ctx, allFileSymbols, changedFiles) {
245
401
  }
246
402
  return timing;
247
403
  }
248
- /**
249
- * Phase 8.5: CHA expansion post-pass for the native orchestrator path.
250
- *
251
- * The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
252
- * where `worker: IWorker`) to the interface method declaration only. This
253
- * post-pass reads the class hierarchy (via `implements`/`extends` edges) and
254
- * instantiated types (via `calls` edges to class nodes) from the DB and expands
255
- * each call to an interface/abstract method to ALL RTA-filtered concrete
256
- * implementations.
257
- *
258
- * Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
259
- * which WASM-re-parses JS/TS files to obtain raw call site receiver info.
260
- *
261
- * Returns the count of newly inserted CHA edges plus the set of files containing
262
- * the new edges' endpoints, so the caller can scope role re-classification to the
263
- * nodes whose fan-in/out actually changed. A zero count means no edges were added
264
- * and role re-classification is unnecessary.
265
- */
266
- function runPostNativeCha(db) {
267
- const affectedFiles = new Set();
268
- const empty = { newEdgeCount: 0, affectedFiles };
269
- // Fast guard: no hierarchy edges → no CHA work
270
- const hasHierarchy = db
271
- .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
272
- .get();
273
- if (!hasHierarchy)
274
- return empty;
275
- // Build implementors map: parent/interface name → [child/implementing class names]
404
+ // ── CHA post-pass helpers ────────────────────────────────────────────────────
405
+ /** Build implementors map: parent/interface name [child/implementing class names]. */
406
+ function buildChaImplementorsMap(db) {
276
407
  const hierarchyRows = db
277
408
  .prepare(`
278
409
  SELECT src.name AS child_name, tgt.name AS parent_name
@@ -292,12 +423,18 @@ function runPostNativeCha(db) {
292
423
  if (!list.includes(row.child_name))
293
424
  list.push(row.child_name);
294
425
  }
295
- if (implementors.size === 0)
296
- return empty;
297
- // RTA: collect class names that are actually instantiated via `new X()`.
298
- // Primary query targets `class`-kind nodes (the canonical schema).
299
- // Fallback also matches `constructor`/`function`-kind nodes because some native
300
- // engine versions record constructor calls against those kinds instead of `class`.
426
+ return implementors;
427
+ }
428
+ /**
429
+ * Build RTA set: class names actually instantiated via `new X()`.
430
+ * Primary query targets `class`-kind nodes (the canonical schema).
431
+ * Fallback also matches `constructor`/`function`-kind nodes because some native
432
+ * engine versions record constructor calls against those kinds instead of `class`.
433
+ * Returns `{ instantiated, noRtaEvidence }` where `noRtaEvidence` means no
434
+ * constructor-call evidence exists — skip RTA filtering so interface dispatch
435
+ * still produces edges.
436
+ */
437
+ function buildChaRtaSet(db) {
301
438
  let rtaRows = db
302
439
  .prepare(`
303
440
  SELECT DISTINCT tgt.name
@@ -319,27 +456,118 @@ function runPostNativeCha(db) {
319
456
  .all();
320
457
  }
321
458
  const instantiated = new Set(rtaRows.map((r) => r.name));
322
- // noRtaEvidence: true when no constructor-call evidence exists in the DB (e.g. graph
323
- // built by an older native engine that doesn't emit constructor call edges at all).
324
- // In that case we skip RTA filtering so interface dispatch still produces edges —
325
- // all instantiated implementors are admitted rather than silently dropping everything.
326
459
  const noRtaEvidence = instantiated.size === 0;
327
460
  if (noRtaEvidence) {
328
461
  debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
329
462
  }
330
- // Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
331
- // Include the caller node's file so confidence can be computed file-pair-aware,
332
- // matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
333
- const callToMethods = db
463
+ return { instantiated, noRtaEvidence };
464
+ }
465
+ /**
466
+ * Determine CHA candidate scope for incremental builds.
467
+ *
468
+ * Gate A: did a changed file add/change a class hierarchy node?
469
+ * A new `extends`/`implements` edge means a previously-untracked implementor
470
+ * is now in the hierarchy — unchanged call sites in OTHER files may gain new
471
+ * valid expansions, so the full scan is required.
472
+ * Note: *removed* class nodes are safe — Rust's `purge_changed_files` runs
473
+ * before this post-pass and deletes stale nodes and their hierarchy edges, so
474
+ * Gate A queries the post-purge DB. A deleted class returns no row here, which
475
+ * is correct: its stale CHA edges were already cleaned up by the Rust purge.
476
+ *
477
+ * Gate B: did a changed file add new RTA evidence (`new ConcreteX()`)?
478
+ * A new `calls` edge to a class/constructor/function-kind target means the
479
+ * instantiated set grew — previously RTA-filtered expansions in unchanged
480
+ * caller files become admissible, so the full scan is required.
481
+ * (`constructor`/`function` cover the older native engine fallback schema.)
482
+ *
483
+ * Returns `true` when the scan should be scoped to changed-file sources only.
484
+ * Returns `false` (full scan) when changedFiles is null, empty, or either gate fires.
485
+ */
486
+ function computeChaScope(db, changedFiles) {
487
+ if (changedFiles === null || changedFiles.length === 0)
488
+ return false;
489
+ const CHUNK_SIZE = 500;
490
+ let gateAFired = false;
491
+ for (let i = 0; i < changedFiles.length && !gateAFired; i += CHUNK_SIZE) {
492
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
493
+ const ph = chunk.map(() => '?').join(',');
494
+ const row = db
495
+ .prepare(`SELECT 1 FROM nodes
496
+ WHERE file IN (${ph})
497
+ AND kind IN ('class', 'interface', 'trait', 'struct', 'record')
498
+ LIMIT 1`)
499
+ .get(...chunk);
500
+ if (row)
501
+ gateAFired = true;
502
+ }
503
+ let gateBFired = false;
504
+ if (!gateAFired) {
505
+ for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
506
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
507
+ const ph = chunk.map(() => '?').join(',');
508
+ const row = db
509
+ .prepare(`SELECT 1 FROM edges e
510
+ JOIN nodes src ON e.source_id = src.id
511
+ JOIN nodes tgt ON e.target_id = tgt.id
512
+ WHERE e.kind = 'calls'
513
+ AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
514
+ AND src.file IN (${ph})
515
+ LIMIT 1`)
516
+ .get(...chunk);
517
+ if (row)
518
+ gateBFired = true;
519
+ }
520
+ }
521
+ if (!gateAFired && !gateBFired) {
522
+ debug(`runPostNativeCha: neither gate fired — scoping candidate scan to ${changedFiles.length} changed file(s)`);
523
+ return true;
524
+ }
525
+ debug(`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`);
526
+ return false;
527
+ }
528
+ /**
529
+ * Fetch call→method rows that are candidates for CHA expansion.
530
+ * When `scopeToChangedFiles` is true, restricts to source nodes in `changedFiles`.
531
+ */
532
+ function fetchChaCallToMethods(db, changedFiles, scopeToChangedFiles) {
533
+ if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
534
+ const CHUNK_SIZE = 500;
535
+ const rows = [];
536
+ for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
537
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
538
+ const ph = chunk.map(() => '?').join(',');
539
+ const chunkRows = db
540
+ .prepare(`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
541
+ FROM edges e
542
+ JOIN nodes tgt ON e.target_id = tgt.id
543
+ JOIN nodes src ON e.source_id = src.id
544
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
545
+ AND INSTR(tgt.name, '.') > 0
546
+ AND (e.technique IS NULL OR e.technique != 'cha-expanded')
547
+ AND src.file IN (${ph})`)
548
+ .all(...chunk);
549
+ rows.push(...chunkRows);
550
+ }
551
+ return rows;
552
+ }
553
+ return db
334
554
  .prepare(`
335
- SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
555
+ SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
336
556
  FROM edges e
337
557
  JOIN nodes tgt ON e.target_id = tgt.id
338
558
  JOIN nodes src ON e.source_id = src.id
339
559
  WHERE e.kind = 'calls' AND tgt.kind = 'method'
340
560
  AND INSTR(tgt.name, '.') > 0
561
+ AND (e.technique IS NULL OR e.technique != 'cha-expanded')
341
562
  `)
342
563
  .all();
564
+ }
565
+ /**
566
+ * BFS-expand CHA call edges and insert new edges into the DB.
567
+ * Returns `{ newEdgeCount, affectedFiles }` for role re-classification scoping.
568
+ */
569
+ function expandChaEdges(db, callToMethods, implementors, instantiated, noRtaEvidence) {
570
+ const affectedFiles = new Set();
343
571
  // Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
344
572
  // call edge in the DB (which would be O(all edges)) for large codebases.
345
573
  const seen = new Set();
@@ -384,17 +612,14 @@ function runPostNativeCha(db) {
384
612
  const qualifiedName = `${cls}.${methodSuffix}`;
385
613
  const methodNodes = findMethodStmt.all(qualifiedName);
386
614
  for (const methodNode of methodNodes) {
615
+ if (methodNode.id === source_id)
616
+ continue; // skip self-loops
387
617
  const key = `${source_id}|${methodNode.id}`;
388
618
  if (seen.has(key))
389
619
  continue;
390
620
  seen.add(key);
391
- // Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY)
392
- // Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
393
- const conf = computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
394
- CHA_DISPATCH_PENALTY;
395
- if (conf <= 0)
396
- continue;
397
- newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
621
+ const conf = CHA_TYPED_DISPATCH_CONFIDENCE;
622
+ newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha-expanded']);
398
623
  newEdgeCount++;
399
624
  if (caller_file)
400
625
  affectedFiles.add(caller_file);
@@ -409,34 +634,72 @@ function runPostNativeCha(db) {
409
634
  }
410
635
  if (newEdges.length > 0) {
411
636
  db.transaction(() => batchInsertEdges(db, newEdges))();
637
+ // Account for post-pass edges excluded from the build summary line (#1452),
638
+ // mirroring the this/super dispatch post-pass insertion log.
639
+ debug(`CHA expansion post-pass: inserted ${newEdgeCount} edge(s)`);
412
640
  }
413
641
  return { newEdgeCount, affectedFiles };
414
642
  }
415
- // Extensions where `this`/`super` dispatch can occur (JS/TS family)
416
- const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
417
643
  /**
418
- * Phase 8.5: this/super dispatch post-pass for the native orchestrator path.
644
+ * Phase 8.6: CHA expansion post-pass for the native orchestrator path.
419
645
  *
420
- * The Rust build pipeline resolves typed receiver calls but does NOT persist raw
421
- * unresolved call site receiver info (e.g. `this`, `super`) to the DB. This
422
- * hybrid post-pass re-parses JS/TS/TSX files via WASM to collect call sites with
423
- * `this`/`super` receivers, then resolves them through the class hierarchy stored
424
- * in DB `extends` edges mirroring what `buildChaPostPass` does on the WASM path.
646
+ * The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
647
+ * where `worker: IWorker`) to the interface method declaration only. This
648
+ * post-pass reads the class hierarchy (via `implements`/`extends` edges) and
649
+ * instantiated types (via `calls` edges to class nodes) from the DB and expands
650
+ * each call to an interface/abstract method to ALL RTA-filtered concrete
651
+ * implementations.
652
+ *
653
+ * Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
654
+ * which WASM-re-parses JS/TS files to obtain raw call site receiver info.
425
655
  *
426
- * Only runs when `extends` edges exist in the DB; if there is no inheritance
427
- * hierarchy there is nothing to resolve via `this`/`super` dispatch.
656
+ * `changedFiles` controls candidate scoping on incremental builds:
657
+ * - null → full build; scan all call→method edges (existing behaviour).
658
+ * - array → incremental; two cheap gate queries decide scope:
659
+ * Gate A: any class/interface/trait/struct/record nodes in changed files?
660
+ * If yes, a new implementor may have appeared — full scan required.
661
+ * Gate B: any `calls` edges from changed-file sources targeting
662
+ * class/constructor/function-kind nodes? If yes, the RTA set may
663
+ * have grown (also covers the older-schema fallback where
664
+ * constructor calls target `constructor`/`function` nodes instead
665
+ * of `class` nodes) — full scan required.
666
+ * If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
667
+ * (safe because no hierarchy or RTA evidence changed).
668
+ *
669
+ * Returns the count of newly inserted CHA edges plus the set of files containing
670
+ * the new edges' endpoints, so the caller can scope role re-classification to the
671
+ * nodes whose fan-in/out actually changed. A zero count means no edges were added
672
+ * and role re-classification is unnecessary.
428
673
  */
429
- async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild) {
430
- const t0 = Date.now();
431
- const targetIds = new Set();
432
- // Files containing endpoints of newly inserted edges — lets the caller scope
433
- // role re-classification to the nodes whose fan-in/out actually changed.
674
+ function runPostNativeCha(db, changedFiles) {
434
675
  const affectedFiles = new Set();
435
- // Fast guard: need at least one extends edge for this/super to have meaning
436
- const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
676
+ const empty = { newEdgeCount: 0, affectedFiles };
677
+ // Fast guard: no hierarchy edges no CHA work
678
+ const hasHierarchy = db
679
+ .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
680
+ .get();
681
+ if (!hasHierarchy)
682
+ return empty;
683
+ const implementors = buildChaImplementorsMap(db);
684
+ if (implementors.size === 0)
685
+ return empty;
686
+ const { instantiated, noRtaEvidence } = buildChaRtaSet(db);
687
+ const scopeToChangedFiles = computeChaScope(db, changedFiles);
688
+ const callToMethods = fetchChaCallToMethods(db, changedFiles, scopeToChangedFiles);
689
+ return expandChaEdges(db, callToMethods, implementors, instantiated, noRtaEvidence);
690
+ }
691
+ // Extensions where `this`/`super` dispatch can occur (JS/TS family)
692
+ const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
693
+ // ── this/super dispatch post-pass helpers ───────────────────────────────────
694
+ /**
695
+ * Build parents map: child class → direct parent class (from `extends` edges).
696
+ * May be empty when only func-prop methods exist (no class inheritance) —
697
+ * resolveThisDispatch handles that case via direct class-prefix lookup.
698
+ */
699
+ function buildThisDispatchParentsMap(db, hasExtends) {
700
+ const parents = new Map();
437
701
  if (!hasExtends)
438
- return { elapsedMs: 0, targetIds, affectedFiles };
439
- // Build parents map: child class → direct parent class (from `extends` edges)
702
+ return parents;
440
703
  const parentRows = db
441
704
  .prepare(`
442
705
  SELECT src.name AS child_name, tgt.name AS parent_name
@@ -446,31 +709,22 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
446
709
  WHERE e.kind = 'extends'
447
710
  `)
448
711
  .all();
449
- const parents = new Map();
450
712
  for (const row of parentRows) {
451
713
  if (!parents.has(row.child_name))
452
714
  parents.set(row.child_name, row.parent_name);
453
715
  }
454
- if (parents.size === 0)
455
- return { elapsedMs: 0, targetIds, affectedFiles };
456
- const chaCtx = {
457
- implementors: new Map(), // not needed for this/super resolution
458
- parents,
459
- instantiatedTypes: new Set(), // not needed for this/super resolution
460
- };
461
- // Determine which files to re-parse.
462
- //
463
- // On a full build we do NOT re-parse every JS/TS file that would WASM-parse
464
- // the entire project on top of the native pass, causing a massive regression
465
- // (measured: +358% ms/file on codegraph itself). Instead we restrict to files
466
- // that are part of the class inheritance hierarchy: both subclass files (which
467
- // contain `super.X()` calls dispatching to a parent) and parent-class files
468
- // (whose method bodies contain `this.X()` calls that CHA must resolve). Any
469
- // file not in the hierarchy has no `extends` relationship, so `this`/`super`
470
- // calls in it either resolve locally (same-class dispatch, already handled by
471
- // the direct-call edge) or have no class context — and will be skipped by
472
- // `resolveThisDispatch` anyway.
473
- let relFiles;
716
+ return parents;
717
+ }
718
+ /**
719
+ * Determine the set of relative file paths to re-parse for this/super dispatch.
720
+ *
721
+ * On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
722
+ * the entire project on top of the native pass, causing a massive regression
723
+ * (measured: +358% ms/file on codegraph itself). Instead we restrict to files
724
+ * that are part of the class inheritance hierarchy OR that contain dot-named
725
+ * method nodes (func-prop assignments whose bodies may call `this.sibling()`).
726
+ */
727
+ function selectThisDispatchFiles(db, changedFiles, isFullBuild) {
474
728
  if (isFullBuild || !changedFiles) {
475
729
  const rows = db
476
730
  .prepare(`
@@ -484,71 +738,51 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
484
738
  FROM edges e
485
739
  JOIN nodes tgt ON e.target_id = tgt.id
486
740
  WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
741
+ UNION
742
+ -- Files with func-prop method definitions (e.g. f.h = function(){this.g()}).
743
+ -- Only include files where the method's owner prefix is NOT a known class name —
744
+ -- this keeps the re-parse set small (func-prop files only, not all class-method files).
745
+ -- AND name IS NOT NULL guards the NOT IN sub-select: if any class node had a NULL
746
+ -- name the entire NOT IN clause would silently return no rows (SQL NULL semantics).
747
+ SELECT n.file AS file
748
+ FROM nodes n
749
+ WHERE n.kind = 'method'
750
+ AND INSTR(n.name, '.') > 0
751
+ AND n.file IS NOT NULL
752
+ AND SUBSTR(n.name, 1, INSTR(n.name, '.') - 1) NOT IN (
753
+ SELECT name FROM nodes WHERE kind IN ('class', 'struct', 'interface', 'type')
754
+ AND name IS NOT NULL
755
+ )
487
756
  )
488
757
  `)
489
758
  .all();
490
- relFiles = rows
759
+ return rows
491
760
  .map((r) => r.file)
492
761
  .filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
493
762
  }
494
- else {
495
- // NOTE: Only files explicitly listed in changedFiles are re-parsed.
496
- // If a parent-class method is replaced (new node ID) but the child file is
497
- // unchanged, the stale super.method() edge is not refreshed here. A full
498
- // rebuild (isFullBuild=true) is required to recover in that scenario.
499
- relFiles = changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
500
- }
501
- if (relFiles.length === 0)
502
- return { elapsedMs: 0, targetIds, affectedFiles };
503
- // DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
504
- const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
505
- const lookup = {
506
- byName: (name) => findByNameStmt.all(name),
507
- byNameAndFile: (name, file) => findByNameStmt.all(name).filter((n) => n.file === file),
508
- isBarrel: () => false,
509
- resolveBarrel: () => null,
510
- nodeId: () => undefined,
511
- };
512
- // Seed seen-pairs from existing call edges on source nodes in our file set
513
- const seen = new Set();
514
- const CHUNK = 500;
515
- for (let i = 0; i < relFiles.length; i += CHUNK) {
516
- const chunk = relFiles.slice(i, i + CHUNK);
517
- const ph = chunk.map(() => '?').join(',');
518
- const rows = db
519
- .prepare(`SELECT e.source_id, e.target_id
520
- FROM edges e
521
- JOIN nodes n ON e.source_id = n.id
522
- WHERE e.kind = 'calls' AND n.file IN (${ph})`)
523
- .all(...chunk);
524
- for (const r of rows)
525
- seen.add(`${r.source_id}|${r.target_id}`);
526
- }
527
- // Find the innermost containing method/function for a call at `line` in `file`.
528
- // COALESCE maps NULL end_line to a large sentinel so unbounded nodes sort last
529
- // (SQLite ASC orders NULLs first, so a raw `end_line - line` would pick them first).
530
- const findCallerByLineStmt = db.prepare(`
531
- SELECT id, name FROM nodes
532
- WHERE file = ? AND kind IN ('method', 'function')
533
- AND line <= ? AND (end_line IS NULL OR end_line >= ?)
534
- ORDER BY COALESCE(end_line - line, 999999999) ASC
535
- LIMIT 1
536
- `);
537
- // Re-parse the files to obtain raw call sites with receiver info. Only
538
- // `calls` (with receivers) are consumed here.
539
- //
540
- // The native engine is preferred: this pass only runs after a native
541
- // orchestrator build, so the addon is already loaded and re-parses the
542
- // hierarchy file set in single-digit milliseconds with the same
543
- // receiver-annotated call sites as the WASM extractor. Booting the WASM
544
- // runtime here instead cost ~40–110ms per full build (in-process
545
- // web-tree-sitter + grammar init dominated) — part of the v3.12.0
546
- // publish-gate regression. Files the native engine cannot parse (extension
547
- // outside NATIVE_SUPPORTED_EXTENSIONS, e.g. .mts/.cts) and native parse
548
- // failures fall back to the WASM backfill path so the sweep stays complete.
549
- const absFiles = relFiles.map((f) => path.join(rootDir, f));
550
- const nativeAbs = absFiles.filter((f) => NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
763
+ // NOTE: Only files explicitly listed in changedFiles are re-parsed.
764
+ // If a parent-class method is replaced (new node ID) but the child file is
765
+ // unchanged, the stale super.method() edge is not refreshed here. A full
766
+ // rebuild (isFullBuild=true) is required to recover in that scenario.
767
+ return changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
768
+ }
769
+ /**
770
+ * Re-parse files via native (preferred) + WASM fallback to obtain call sites
771
+ * with receiver info. Returns a map of relPath → calls array.
772
+ *
773
+ * The native engine is preferred: this pass only runs after a native
774
+ * orchestrator build, so the addon is already loaded and re-parses the
775
+ * hierarchy file set in single-digit milliseconds with the same
776
+ * receiver-annotated call sites as the WASM extractor. Booting the WASM
777
+ * runtime here instead cost ~40–110ms per full build (in-process
778
+ * web-tree-sitter + grammar init dominated) part of the v3.12.0
779
+ * publish-gate regression. Files the native engine cannot parse (extension
780
+ * outside NATIVE_SUPPORTED_EXTENSIONS, e.g. .mts/.cts) and native parse
781
+ * failures fall back to the WASM backfill path so the sweep stays complete.
782
+ */
783
+ async function parseFilesForThisDispatch(absFiles, rootDir) {
551
784
  const callsByRel = new Map();
785
+ const nativeAbs = absFiles.filter((f) => NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
552
786
  // Track native-supported files that returned null (per-file parse error) so
553
787
  // they can be included in the WASM fallback set below, ensuring no file's
554
788
  // this/super call sites are silently discarded.
@@ -592,7 +826,23 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
592
826
  for (const [relPath, symbols] of wasmResults) {
593
827
  callsByRel.set(relPath, symbols.calls ?? []);
594
828
  }
829
+ return { callsByRel, wasmResults };
830
+ }
831
+ /** Emit this/super dispatch edges from re-parsed call sites. */
832
+ function emitThisDispatchEdges(db, callsByRel, chaCtx, lookup, seen) {
833
+ // Find the innermost containing method/function for a call at `line` in `file`.
834
+ // COALESCE maps NULL end_line to a large sentinel so unbounded nodes sort last
835
+ // (SQLite ASC orders NULLs first, so a raw `end_line - line` would pick them first).
836
+ const findCallerByLineStmt = db.prepare(`
837
+ SELECT id, name FROM nodes
838
+ WHERE file = ? AND kind IN ('method', 'function')
839
+ AND line <= ? AND (end_line IS NULL OR end_line >= ?)
840
+ ORDER BY COALESCE(end_line - line, 999999999) ASC
841
+ LIMIT 1
842
+ `);
595
843
  const newEdges = [];
844
+ const targetIds = new Set();
845
+ const affectedFiles = new Set();
596
846
  for (const [relPath, calls] of callsByRel) {
597
847
  for (const call of calls) {
598
848
  // Only 'this' and 'super' are class-instance receivers in JS/TS.
@@ -603,8 +853,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
603
853
  const callerRow = findCallerByLineStmt.get(relPath, call.line, call.line);
604
854
  if (!callerRow)
605
855
  continue;
606
- const targets = resolveThisDispatch(call.name, callerRow.name, call.receiver, chaCtx, lookup);
856
+ const targets = resolveThisDispatch(call.name, callerRow.name, call.receiver, chaCtx, lookup, relPath);
607
857
  for (const t of targets) {
858
+ if (t.id === callerRow.id)
859
+ continue; // skip self-loops
608
860
  const key = `${callerRow.id}|${t.id}`;
609
861
  if (seen.has(key))
610
862
  continue;
@@ -612,7 +864,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
612
864
  const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
613
865
  if (conf <= 0)
614
866
  continue;
615
- newEdges.push([callerRow.id, t.id, 'calls', conf, 0, 'cha']);
867
+ // Tag super-dispatch edges distinctly so runPostNativeCha can exclude them
868
+ // from further CHA expansion (super calls are not virtual dispatch).
869
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
870
+ newEdges.push([callerRow.id, t.id, 'calls', conf, 0, technique]);
616
871
  targetIds.add(t.id);
617
872
  affectedFiles.add(relPath);
618
873
  if (t.file)
@@ -620,11 +875,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
620
875
  }
621
876
  }
622
877
  }
623
- if (newEdges.length > 0) {
624
- db.transaction(() => batchInsertEdges(db, newEdges))();
625
- debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
626
- }
627
- // Free WASM parse trees — mirrors the cleanup in backfillNativeDroppedFiles
878
+ return { newEdges, targetIds, affectedFiles };
879
+ }
880
+ /** Free WASM parse trees after this-dispatch post-pass to prevent memory leaks. */
881
+ function cleanupThisDispatchWasmTrees(wasmResults) {
628
882
  for (const [, symbols] of wasmResults) {
629
883
  const tree = symbols._tree;
630
884
  if (tree && typeof tree.delete === 'function') {
@@ -638,10 +892,89 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
638
892
  symbols._tree = undefined;
639
893
  symbols._langId = undefined;
640
894
  }
641
- return { elapsedMs: Date.now() - t0, targetIds, affectedFiles };
895
+ }
896
+ /**
897
+ * Phase 8.5: this/super dispatch post-pass for the native orchestrator path.
898
+ *
899
+ * The Rust build pipeline resolves typed receiver calls but does NOT persist raw
900
+ * unresolved call site receiver info (e.g. `this`, `super`) to the DB. This
901
+ * hybrid post-pass re-parses JS/TS/TSX files via WASM to collect call sites with
902
+ * `this`/`super` receivers, then resolves them through the class hierarchy stored
903
+ * in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
904
+ *
905
+ * Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
906
+ * these use `this` to reference sibling properties on the same object (`f`), so
907
+ * `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
908
+ * (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
909
+ *
910
+ * Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
911
+ * nodes exist (func-prop assignments); skips only when neither is present.
912
+ */
913
+ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild) {
914
+ const t0 = performance.now();
915
+ // Fast guard: need at least one extends edge (class inheritance) OR a dot-named
916
+ // method node (func-prop assignment: `f.h = function() { this.g() }`) for
917
+ // this/super dispatch to produce any edges.
918
+ const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
919
+ const hasFuncPropMethod = db
920
+ .prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
921
+ .get();
922
+ const emptyResult = {
923
+ elapsedMs: 0,
924
+ targetIds: new Set(),
925
+ affectedFiles: new Set(),
926
+ };
927
+ if (!hasExtends && !hasFuncPropMethod)
928
+ return emptyResult;
929
+ const parents = buildThisDispatchParentsMap(db, hasExtends);
930
+ // Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
931
+ // intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
932
+ // treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
933
+ // up `f.g` directly via lookup.byName(), without traversing the parents chain.
934
+ const chaCtx = {
935
+ implementors: new Map(), // not needed for this/super resolution
936
+ parents,
937
+ instantiatedTypes: new Set(), // not needed for this/super resolution
938
+ };
939
+ const relFiles = selectThisDispatchFiles(db, changedFiles, isFullBuild);
940
+ if (relFiles.length === 0)
941
+ return emptyResult;
942
+ // DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
943
+ const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
944
+ const lookup = {
945
+ byName: (name) => findByNameStmt.all(name),
946
+ byNameAndFile: (name, file) => findByNameStmt.all(name).filter((n) => n.file === file),
947
+ isBarrel: () => false,
948
+ resolveBarrel: () => null,
949
+ nodeId: () => undefined,
950
+ };
951
+ // Seed seen-pairs from existing call edges on source nodes in our file set
952
+ const seen = new Set();
953
+ const CHUNK = 500;
954
+ for (let i = 0; i < relFiles.length; i += CHUNK) {
955
+ const chunk = relFiles.slice(i, i + CHUNK);
956
+ const ph = chunk.map(() => '?').join(',');
957
+ const rows = db
958
+ .prepare(`SELECT e.source_id, e.target_id
959
+ FROM edges e
960
+ JOIN nodes n ON e.source_id = n.id
961
+ WHERE e.kind = 'calls' AND n.file IN (${ph})`)
962
+ .all(...chunk);
963
+ for (const r of rows)
964
+ seen.add(`${r.source_id}|${r.target_id}`);
965
+ }
966
+ const absFiles = relFiles.map((f) => path.join(rootDir, f));
967
+ const { callsByRel, wasmResults } = await parseFilesForThisDispatch(absFiles, rootDir);
968
+ const { newEdges, targetIds, affectedFiles } = emitThisDispatchEdges(db, callsByRel, chaCtx, lookup, seen);
969
+ if (newEdges.length > 0) {
970
+ db.transaction(() => batchInsertEdges(db, newEdges))();
971
+ debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
972
+ }
973
+ cleanupThisDispatchWasmTrees(wasmResults);
974
+ return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
642
975
  }
643
976
  /** Format timing result from native orchestrator phases + JS post-processing. */
644
- function formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs) {
977
+ function formatNativeTimingResult(p, structurePatchMs, analysisTiming, postPass) {
645
978
  return {
646
979
  phases: {
647
980
  setupMs: +(p.setupMs ?? 0).toFixed(1),
@@ -653,7 +986,11 @@ function formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispa
653
986
  edgesMs: +(p.edgesMs ?? 0).toFixed(1),
654
987
  structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
655
988
  rolesMs: +(p.rolesMs ?? 0).toFixed(1),
656
- thisDispatchMs: +thisDispatchMs.toFixed(1),
989
+ gapDetectMs: +postPass.gapDetectMs.toFixed(1),
990
+ chaMs: +postPass.chaMs.toFixed(1),
991
+ thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
992
+ reclassifyMs: +postPass.reclassifyMs.toFixed(1),
993
+ techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
657
994
  astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
658
995
  complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
659
996
  cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
@@ -736,6 +1073,57 @@ function groupByExtension(relPaths) {
736
1073
  }
737
1074
  return buckets;
738
1075
  }
1076
+ /**
1077
+ * Return the subset of relative paths that are gitignored in `rootDir`.
1078
+ *
1079
+ * Runs `git check-ignore --stdin` with all candidate paths piped in. Any
1080
+ * path that git echoes back is gitignored. Fails silently (returns an empty
1081
+ * set) when git is unavailable, the directory is not a git repo, or the
1082
+ * check-ignore call throws — the gap-detection logic handles those cases
1083
+ * gracefully without this filter.
1084
+ *
1085
+ * Uses relative paths (forward-slash separated) as both input and output so
1086
+ * the result set can be matched directly against the `expected` set in
1087
+ * `detectDroppedLanguageGap` without any further path manipulation.
1088
+ */
1089
+ function queryGitIgnoredFiles(rootDir, relPaths) {
1090
+ const ignored = new Set();
1091
+ const paths = [...relPaths];
1092
+ if (paths.length === 0)
1093
+ return ignored;
1094
+ try {
1095
+ const stdin = paths.join('\n');
1096
+ const output = execFileSync('git', ['check-ignore', '--stdin'], {
1097
+ cwd: rootDir,
1098
+ input: stdin,
1099
+ encoding: 'utf-8',
1100
+ maxBuffer: 100 * 1024 * 1024,
1101
+ // git check-ignore exits with 1 when none of the paths are ignored —
1102
+ // that is not an error for our purposes. stdio: 'pipe' lets us capture
1103
+ // stdout without swallowing stderr, and the try/catch handles the
1104
+ // non-zero exit from execFileSync when ALL paths are non-ignored
1105
+ // (exit code 1 from git check-ignore means "no matches").
1106
+ stdio: ['pipe', 'pipe', 'pipe'],
1107
+ });
1108
+ for (const line of output.split('\n')) {
1109
+ const trimmed = normalizePath(line.trim());
1110
+ if (trimmed)
1111
+ ignored.add(trimmed);
1112
+ }
1113
+ }
1114
+ catch (e) {
1115
+ // Exit code 1 means no paths were ignored — not an error. Any other
1116
+ // failure (git unavailable, not a repo, etc.) is silently swallowed
1117
+ // so the caller proceeds with the unfiltered set.
1118
+ const exitCode = e.status;
1119
+ if (exitCode !== 1) {
1120
+ debug(`queryGitIgnoredFiles: git check-ignore failed: ${toErrorMessage(e)}`);
1121
+ }
1122
+ // On exit code 1, output is empty so ignored stays empty — correct.
1123
+ // On other errors we also proceed with the empty set (safe degradation).
1124
+ }
1125
+ return ignored;
1126
+ }
739
1127
  /**
740
1128
  * Detect files the native orchestrator silently dropped.
741
1129
  *
@@ -765,7 +1153,15 @@ function groupByExtension(relPaths) {
765
1153
  */
766
1154
  function detectDroppedLanguageGap(ctx) {
767
1155
  const collected = collectFilesUtil(ctx.rootDir, [], ctx.config, new Set());
768
- const expected = new Set(collected.files.map((f) => normalizePath(path.relative(ctx.rootDir, f))));
1156
+ const expectedRaw = collected.files.map((f) => normalizePath(path.relative(ctx.rootDir, f)));
1157
+ // The native Rust engine uses the `ignore` crate with git_ignore(true), so it
1158
+ // respects .gitignore and never processes gitignored files. The JS collectFiles
1159
+ // walker has no gitignore awareness, so without this filter gitignored files
1160
+ // (e.g. NAPI-RS generated crates/codegraph-core/index.js / index.d.ts) appear
1161
+ // in `expected` but not in the DB, causing a spurious "native extractor bug"
1162
+ // WARN and triggering an unnecessary WASM backfill (#1626).
1163
+ const gitIgnored = queryGitIgnoredFiles(ctx.rootDir, expectedRaw);
1164
+ const expected = new Set(gitIgnored.size > 0 ? expectedRaw.filter((r) => !gitIgnored.has(r)) : expectedRaw);
769
1165
  const existingNodeRows = ctx.db
770
1166
  .prepare("SELECT DISTINCT file FROM nodes WHERE kind = 'file'")
771
1167
  .all();
@@ -803,58 +1199,71 @@ function detectDroppedLanguageGap(ctx) {
803
1199
  });
804
1200
  return { missingRel, missingAbs, staleRel };
805
1201
  }
1202
+ // ── backfillNativeDroppedFiles helpers ───────────────────────────────────────
1203
+ /** Purge stale WASM-only files deleted from disk (#1073). */
1204
+ function purgeStaleWasmOnlyFiles(db, staleRel) {
1205
+ // `computeWasmOnlyStaleFiles` guarantees every path here has an extension
1206
+ // outside NATIVE_SUPPORTED_EXTENSIONS, so `classifyNativeDrops` would
1207
+ // always bucket 100% into `unsupported-by-native`. Build the extension
1208
+ // summary directly to avoid a redundant classification pass.
1209
+ const staleByExt = groupByExtension(staleRel);
1210
+ info(`Detected ${staleRel.length} deleted WASM-only file(s) across ${staleByExt.size} extension(s) the native orchestrator skipped; purging stale rows:${formatDropExtensionSummary(staleByExt)}`);
1211
+ purgeFilesData(db, staleRel);
1212
+ }
806
1213
  /**
807
- * Backfill files that the native orchestrator silently dropped during parse.
808
- * Falls back to WASM + inserts file/symbol nodes so engine counts match (#967).
809
- *
810
- * Also purges stale rows for WASM-only files deleted from disk (#1073), which
811
- * Rust's `detect_removed_files` filter (#1070) skips.
812
- *
813
- * Accepts a pre-computed `gap` from `detectDroppedLanguageGap` so the caller
814
- * can use the same scan for both gating and the actual backfill — avoiding
815
- * a redundant fs walk when the orchestrator's signals already triggered.
1214
+ * Classify and log dropped file buckets.
1215
+ * Three-way split of native-extractor-failure files:
1216
+ * realFailureBuckets — WASM found symbols → real Rust extractor bug (WARN)
1217
+ * emptyFileBuckets — WASM parsed but found 0 symbols gitignored/empty (debug)
1218
+ * wasmSkipBuckets — WASM skipped entirely → no file-node insert (debug)
816
1219
  */
817
- async function backfillNativeDroppedFiles(ctx, gap) {
818
- const { missingRel, missingAbs, staleRel } = gap;
819
- if (missingAbs.length === 0 && staleRel.length === 0)
820
- return;
821
- // Now that we know there's work to do, hand off to better-sqlite3 (needed
822
- // for the INSERT path below).
823
- if (ctx.nativeFirstProxy) {
824
- closeNativeDb(ctx, 'pre-parity-backfill');
825
- ctx.db = openDb(ctx.dbPath);
826
- ctx.nativeFirstProxy = false;
827
- }
828
- const dbConn = ctx.db;
829
- // Purge WASM-only files that were deleted from disk (#1073). Rust's
830
- // detect_removed_files skips them and the insert path below never visits
831
- // them, so without this their rows would persist across rebuilds until the
832
- // next full rebuild reset the DB.
833
- if (staleRel.length > 0) {
834
- // `computeWasmOnlyStaleFiles` guarantees every path here has an extension
835
- // outside NATIVE_SUPPORTED_EXTENSIONS, so `classifyNativeDrops` would
836
- // always bucket 100% into `unsupported-by-native`. Build the extension
837
- // summary directly to avoid a redundant classification pass.
838
- const staleByExt = groupByExtension(staleRel);
839
- info(`Detected ${staleRel.length} deleted WASM-only file(s) across ${staleByExt.size} extension(s) the native orchestrator skipped; purging stale rows:${formatDropExtensionSummary(staleByExt)}`);
840
- purgeFilesData(dbConn, staleRel);
841
- }
842
- if (missingAbs.length === 0)
843
- return;
844
- // Classify drops so users see per-extension reasons instead of just a count
845
- // (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
846
- // extractor); `native-extractor-failure` indicates a real native bug since
847
- // the language IS supported by the addon yet the file was dropped anyway.
1220
+ function classifyAndLogDroppedFiles(missingRel, wasmParsedFiles, wasmFoundSymbols) {
848
1221
  const { byReason, totals } = classifyNativeDrops(missingRel);
849
1222
  if (totals['unsupported-by-native'] > 0) {
850
1223
  const buckets = byReason['unsupported-by-native'];
851
1224
  info(`Native orchestrator skipped ${totals['unsupported-by-native']} file(s) across ${buckets.size} extension(s) in languages without a Rust extractor; backfilling via WASM:${formatDropExtensionSummary(buckets)}`);
852
1225
  }
853
1226
  if (totals['native-extractor-failure'] > 0) {
854
- const buckets = byReason['native-extractor-failure'];
855
- warn(`Native orchestrator dropped ${totals['native-extractor-failure']} file(s) across ${buckets.size} extension(s) in natively-supported languages — likely a Rust extractor bug. Backfilling via WASM:${formatDropExtensionSummary(buckets)}`);
1227
+ const allFailurePaths = byReason['native-extractor-failure'];
1228
+ const realFailureBuckets = new Map();
1229
+ const emptyFileBuckets = new Map();
1230
+ const wasmSkipBuckets = new Map();
1231
+ for (const [ext, paths] of allFailurePaths) {
1232
+ for (const relPath of paths) {
1233
+ let bucket;
1234
+ if (wasmFoundSymbols.has(relPath)) {
1235
+ bucket = realFailureBuckets;
1236
+ }
1237
+ else if (wasmParsedFiles.has(relPath)) {
1238
+ bucket = emptyFileBuckets;
1239
+ }
1240
+ else {
1241
+ bucket = wasmSkipBuckets;
1242
+ }
1243
+ let list = bucket.get(ext);
1244
+ if (!list) {
1245
+ list = [];
1246
+ bucket.set(ext, list);
1247
+ }
1248
+ list.push(relPath);
1249
+ }
1250
+ }
1251
+ if (realFailureBuckets.size > 0) {
1252
+ const realCount = [...realFailureBuckets.values()].reduce((s, a) => s + a.length, 0);
1253
+ warn(`Native orchestrator dropped ${realCount} file(s) across ${realFailureBuckets.size} extension(s) in natively-supported languages — likely a Rust extractor bug. Backfilling via WASM:${formatDropExtensionSummary(realFailureBuckets)}`);
1254
+ }
1255
+ if (emptyFileBuckets.size > 0) {
1256
+ const emptyCount = [...emptyFileBuckets.values()].reduce((s, a) => s + a.length, 0);
1257
+ debug(`Native orchestrator skipped ${emptyCount} file(s) in natively-supported languages that also produced 0 symbols via WASM (likely gitignored or empty); backfilling file nodes:${formatDropExtensionSummary(emptyFileBuckets)}`);
1258
+ }
1259
+ if (wasmSkipBuckets.size > 0) {
1260
+ const skipCount = [...wasmSkipBuckets.values()].reduce((s, a) => s + a.length, 0);
1261
+ debug(`Native orchestrator skipped ${skipCount} file(s) in natively-supported languages that WASM also could not parse (unregistered extension or parse error); no file-node inserted:${formatDropExtensionSummary(wasmSkipBuckets)}`);
1262
+ }
856
1263
  }
857
- const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
1264
+ }
1265
+ /** Insert node rows for all backfilled files and mark exported symbols. */
1266
+ function insertBackfilledNodes(db, wasmResults) {
858
1267
  const rows = [];
859
1268
  const exportKeys = [];
860
1269
  for (const [relPath, symbols] of wasmResults) {
@@ -886,7 +1295,6 @@ async function backfillNativeDroppedFiles(ctx, gap) {
886
1295
  exportKeys.push([exp.name, exp.kind, relPath, exp.line]);
887
1296
  }
888
1297
  }
889
- const db = dbConn;
890
1298
  batchInsertNodes(db, rows);
891
1299
  // Mark exported symbols in batches — mirrors insertDefinitionsAndExports.
892
1300
  if (exportKeys.length > 0) {
@@ -909,17 +1317,21 @@ async function backfillNativeDroppedFiles(ctx, gap) {
909
1317
  updateStmt.run(...vals);
910
1318
  }
911
1319
  }
912
- // Persist file_hashes rows for every backfilled file. The Rust orchestrator
913
- // only hashes files it parsed itself, so without this step files in
914
- // optional-language extensions (e.g. .clj when no Rust extractor exists)
915
- // would be missing from `file_hashes` — permanently breaking the JS-side
916
- // fast-skip pre-flight (#1054), which rejects on `collected file missing
917
- // from file_hashes` and forces every no-op rebuild back through the full
918
- // ~2s native pipeline (#1068).
919
- //
920
- // Iterates `missingRel` (every collected file the Rust orchestrator
921
- // dropped), not `wasmResults`, so files that produced zero symbols still
922
- // get a row.
1320
+ }
1321
+ /**
1322
+ * Persist file_hashes rows for every backfilled file.
1323
+ *
1324
+ * The Rust orchestrator only hashes files it parsed itself, so without this
1325
+ * step files in optional-language extensions (e.g. .clj when no Rust extractor
1326
+ * exists) would be missing from `file_hashes` — permanently breaking the JS-side
1327
+ * fast-skip pre-flight (#1054), which rejects on `collected file missing
1328
+ * from file_hashes` and forces every no-op rebuild back through the full
1329
+ * ~2s native pipeline (#1068).
1330
+ *
1331
+ * Iterates `missingRel` (every collected file the Rust orchestrator dropped),
1332
+ * not `wasmResults`, so files that produced zero symbols still get a row.
1333
+ */
1334
+ function backfillFileHashes(db, missingRel, missingAbs) {
923
1335
  try {
924
1336
  const upsertHash = db.prepare('INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)');
925
1337
  const writeHashes = db.transaction(() => {
@@ -949,6 +1361,69 @@ async function backfillNativeDroppedFiles(ctx, gap) {
949
1361
  catch (e) {
950
1362
  debug(`backfillNativeDroppedFiles: file_hashes write failed (table may not exist): ${toErrorMessage(e)}`);
951
1363
  }
1364
+ }
1365
+ /**
1366
+ * Backfill files that the native orchestrator silently dropped during parse.
1367
+ * Falls back to WASM + inserts file/symbol nodes so engine counts match (#967).
1368
+ *
1369
+ * Also purges stale rows for WASM-only files deleted from disk (#1073), which
1370
+ * Rust's `detect_removed_files` filter (#1070) skips.
1371
+ *
1372
+ * Accepts a pre-computed `gap` from `detectDroppedLanguageGap` so the caller
1373
+ * can use the same scan for both gating and the actual backfill — avoiding
1374
+ * a redundant fs walk when the orchestrator's signals already triggered.
1375
+ */
1376
+ async function backfillNativeDroppedFiles(ctx, gap) {
1377
+ const { missingRel, missingAbs, staleRel } = gap;
1378
+ if (missingAbs.length === 0 && staleRel.length === 0)
1379
+ return;
1380
+ // Now that we know there's work to do, hand off to better-sqlite3 (needed
1381
+ // for the INSERT path below).
1382
+ if (ctx.nativeFirstProxy) {
1383
+ closeNativeDb(ctx, 'pre-parity-backfill');
1384
+ ctx.db = openDb(ctx.dbPath);
1385
+ ctx.nativeFirstProxy = false;
1386
+ }
1387
+ const dbConn = ctx.db;
1388
+ // Purge WASM-only files that were deleted from disk (#1073). Rust's
1389
+ // detect_removed_files skips them and the insert path below never visits
1390
+ // them, so without this their rows would persist across rebuilds until the
1391
+ // next full rebuild reset the DB.
1392
+ if (staleRel.length > 0) {
1393
+ purgeStaleWasmOnlyFiles(dbConn, staleRel);
1394
+ }
1395
+ if (missingAbs.length === 0)
1396
+ return;
1397
+ // Parse all missing files via WASM first so we can distinguish real native
1398
+ // extractor failures (WASM finds symbols but native didn't) from files the
1399
+ // Rust engine legitimately skipped (gitignored artifacts, empty declaration
1400
+ // files, etc. where WASM also produces 0 symbols). Both categories are
1401
+ // backfilled — only the former triggers a WARN (#1566).
1402
+ const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
1403
+ // Build two sets from wasmResults:
1404
+ // wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
1405
+ // wasmFoundSymbols — subset where WASM found ≥1 symbol
1406
+ // Files absent from wasmParsedFiles were skipped by WASM entirely (extension
1407
+ // not in _extToLang, wasmExtractSymbols returned null, or a read error).
1408
+ // Those files do NOT end up in the batchInsertNodes loop below.
1409
+ const wasmParsedFiles = new Set();
1410
+ const wasmFoundSymbols = new Set();
1411
+ for (const [relPath, symbols] of wasmResults) {
1412
+ wasmParsedFiles.add(relPath);
1413
+ if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
1414
+ wasmFoundSymbols.add(relPath);
1415
+ }
1416
+ }
1417
+ // Classify drops so users see per-extension reasons instead of just a count
1418
+ // (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
1419
+ // extractor); `native-extractor-failure` indicates a real native bug since
1420
+ // the language IS supported by the addon yet WASM found symbols the native
1421
+ // engine should have extracted. Files where both engines produce 0 symbols
1422
+ // are legitimately empty (e.g. gitignored napi-generated declaration stubs)
1423
+ // and logged at debug level only.
1424
+ classifyAndLogDroppedFiles(missingRel, wasmParsedFiles, wasmFoundSymbols);
1425
+ insertBackfilledNodes(dbConn, wasmResults);
1426
+ backfillFileHashes(dbConn, missingRel, missingAbs);
952
1427
  // Free WASM parse trees from the inline backfill path (#1058).
953
1428
  // `parseFilesWasmInline` sets `symbols._tree` (a live web-tree-sitter Tree
954
1429
  // backed by WASM linear memory) on every result, but these symbols are
@@ -957,23 +1432,14 @@ async function backfillNativeDroppedFiles(ctx, gap) {
957
1432
  // sees them. Without this, trees leak WASM memory until process exit —
958
1433
  // bounded per run but cumulative across in-process integration tests.
959
1434
  // Mirrors the cleanup discipline established for #931.
960
- for (const [, symbols] of wasmResults) {
961
- const tree = symbols._tree;
962
- if (tree && typeof tree.delete === 'function') {
963
- try {
964
- tree.delete();
965
- }
966
- catch {
967
- /* ignore cleanup errors */
968
- }
969
- }
970
- symbols._tree = undefined;
971
- symbols._langId = undefined;
972
- }
1435
+ cleanupThisDispatchWasmTrees(wasmResults);
973
1436
  }
974
1437
  /**
975
1438
  * Backfill the `technique` column on `calls` edges written by the native Rust
976
- * orchestrator, which does not write the column itself.
1439
+ * orchestrator, which does not write the column itself. Also lifts any
1440
+ * resolved ts-native edge whose confidence is below TS_NATIVE_CONFIDENCE_FLOOR
1441
+ * to that floor value so that the name-lookup quality of the native resolver is
1442
+ * reflected in the call-confidence metric.
977
1443
  *
978
1444
  * For full builds, all `calls` edges in the DB are new so a global UPDATE is
979
1445
  * correct. For incremental builds, only changed-file source nodes are updated
@@ -988,6 +1454,10 @@ function backfillEdgeTechniquesAfterNativeOrchestrator(db, isFullBuild, changedF
988
1454
  }
989
1455
  if (isFullBuild || !changedFiles) {
990
1456
  db.prepare("UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL").run();
1457
+ // Lift resolved ts-native edges below the confidence floor.
1458
+ db.prepare(`UPDATE edges SET confidence = ?
1459
+ WHERE kind = 'calls' AND technique = 'ts-native'
1460
+ AND confidence > 0 AND confidence < ?`).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR);
991
1461
  return;
992
1462
  }
993
1463
  // Incremental: scope to source nodes whose file is one of the changed files.
@@ -1002,10 +1472,205 @@ function backfillEdgeTechniquesAfterNativeOrchestrator(db, isFullBuild, changedF
1002
1472
  AND source_id IN (
1003
1473
  SELECT id FROM nodes WHERE file IN (${placeholders})
1004
1474
  )`).run(...chunk);
1475
+ // Lift resolved ts-native edges below the confidence floor for this chunk.
1476
+ db.prepare(`UPDATE edges SET confidence = ?
1477
+ WHERE kind = 'calls' AND technique = 'ts-native'
1478
+ AND confidence > 0 AND confidence < ?
1479
+ AND source_id IN (
1480
+ SELECT id FROM nodes WHERE file IN (${placeholders})
1481
+ )`).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR, ...chunk);
1005
1482
  }
1006
1483
  });
1007
1484
  tx();
1008
1485
  }
1486
+ // ── tryNativeOrchestrator helpers ────────────────────────────────────────────
1487
+ /**
1488
+ * Open NativeDatabase on demand — deferred from setupPipeline to skip the
1489
+ * ~60ms cost on no-op/early-exit builds.
1490
+ *
1491
+ * Closes the better-sqlite3 connection first to avoid dual-connection WAL
1492
+ * corruption. On setup failure, falls back to reopening better-sqlite3 and
1493
+ * leaves ctx.nativeDb undefined so the caller falls through to the JS pipeline.
1494
+ */
1495
+ function openNativeDatabase(ctx) {
1496
+ if (ctx.nativeDb || !ctx.nativeAvailable)
1497
+ return;
1498
+ const native = loadNative();
1499
+ if (!native?.NativeDatabase)
1500
+ return;
1501
+ try {
1502
+ // Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
1503
+ // Uses raw close() instead of closeDb() intentionally — the advisory lock
1504
+ // is kept and transferred to the NativeDbProxy below, not released here.
1505
+ ctx.db.close();
1506
+ acquireAdvisoryLock(ctx.dbPath);
1507
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
1508
+ ctx.nativeDb.initSchema();
1509
+ // Replace ctx.db with a NativeDbProxy so post-native JS fallback
1510
+ // (structure, analysis) can use it without reopening better-sqlite3.
1511
+ const proxy = new NativeDbProxy(ctx.nativeDb);
1512
+ proxy.__lockPath = `${ctx.dbPath}.lock`;
1513
+ ctx.db = proxy;
1514
+ ctx.nativeFirstProxy = true;
1515
+ }
1516
+ catch (err) {
1517
+ warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
1518
+ try {
1519
+ ctx.nativeDb?.close();
1520
+ }
1521
+ catch (e) {
1522
+ debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
1523
+ }
1524
+ ctx.nativeDb = undefined;
1525
+ ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
1526
+ releaseAdvisoryLock(`${ctx.dbPath}.lock`);
1527
+ // Reopen better-sqlite3 for JS pipeline fallback
1528
+ ctx.db = openDb(ctx.dbPath);
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Coordinate all post-native edge-writing post-passes, role re-classification,
1533
+ * and technique backfill. Returns timing data for the build result.
1534
+ *
1535
+ * Post-passes run before structure/analysis so role classification sees the
1536
+ * complete graph including CHA + this/super dispatch edges.
1537
+ */
1538
+ async function runPostNativePasses(ctx, result) {
1539
+ // Engine parity: the native orchestrator silently drops files whose
1540
+ // Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
1541
+ // stale native binaries). WASM handles those — backfill via WASM so both
1542
+ // engines process the same file set (#967).
1543
+ //
1544
+ // Detect the gap once (fs walk + 2 DB queries) and use it for both gating
1545
+ // and the backfill itself. On quiet incrementals we still pay the walk so
1546
+ // we can detect brand-new files in dropped-language extensions — a gap that
1547
+ // the orchestrator's `detect_removed_files` filter (#1070) leaves open
1548
+ // (#1083, #1091). The pre-check is cheap because the expensive part (WASM
1549
+ // re-parse of the missing set) is gated below.
1550
+ const gapDetectStart = performance.now();
1551
+ const gap = detectDroppedLanguageGap(ctx);
1552
+ const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
1553
+ if (backfillHappened) {
1554
+ await backfillNativeDroppedFiles(ctx, gap);
1555
+ }
1556
+ const gapDetectMs = performance.now() - gapDetectStart;
1557
+ // Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
1558
+ // whose raw receiver info the Rust pipeline does not persist to DB.
1559
+ // Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
1560
+ // (technique='cha') are in the DB when runPostNativeCha expands them to sibling
1561
+ // class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
1562
+ const { elapsedMs: thisDispatchMs, targetIds: thisDispatchTargetIds, affectedFiles: thisDispatchAffectedFiles, } = await runPostNativeThisDispatch(ctx.db, ctx.rootDir, result.changedFiles, !!result.isFullBuild);
1563
+ // Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
1564
+ // Returns the affected files so role re-classification below can be scoped to
1565
+ // the nodes whose fan-in/out actually changed.
1566
+ //
1567
+ // Runs AFTER this/super dispatch so super.method() edges are already in the DB.
1568
+ // The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
1569
+ // of those edges in subsequent incremental builds, while 'cha'-tagged edges from
1570
+ // this/super dispatch remain eligible for expansion here.
1571
+ //
1572
+ // Function-as-object-property methods (`fn.method = function() {}`) are extracted
1573
+ // natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
1574
+ // no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
1575
+ const chaStart = performance.now();
1576
+ const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(ctx.db,
1577
+ // null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
1578
+ result.isFullBuild ? null : (result.changedFiles ?? null));
1579
+ const chaMs = performance.now() - chaStart;
1580
+ // Role re-classification after the Rust orchestrator build.
1581
+ //
1582
+ // Two reasons to re-classify:
1583
+ //
1584
+ // 1. Post-pass edges (CHA, this-dispatch): the Rust orchestrator classifies
1585
+ // roles before these passes add edges, so fan-in/out for their endpoints
1586
+ // is stale. On incremental builds, scope to the affected files for speed.
1587
+ //
1588
+ // 2. hasActiveFileSiblings parity: the Rust classifier does not implement the
1589
+ // JS hasActiveFileSiblings heuristic. That heuristic promotes functions with
1590
+ // fan_in=0 but fan_out>0 to 'leaf' when their file has other connected
1591
+ // callables — preventing false dead-unresolved classifications for functions
1592
+ // like `main` or `square` that call others but are never called themselves.
1593
+ // On full builds, always run a full JS re-classification so the Rust roles
1594
+ // are replaced by the canonical JS classifier output (#1659).
1595
+ //
1596
+ // Strategy:
1597
+ // - Full build: always run full JS classifyNodeRoles(db, null).
1598
+ // - Incremental build with post-pass edges: run scoped re-classification
1599
+ // for the affected files (same as before). The full-build pass already
1600
+ // produced correct JS roles for all unchanged files on the previous build.
1601
+ // - Incremental build with no post-pass edges: skip re-classification
1602
+ // (Rust roles on unchanged files are not stale, and the heuristic gap
1603
+ // was corrected on the last full build).
1604
+ let reclassifyMs = 0;
1605
+ const needsFullReclassify = !!result.isFullBuild;
1606
+ const needsScopedReclassify = !needsFullReclassify && (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0);
1607
+ if (needsFullReclassify || needsScopedReclassify) {
1608
+ let scopedFiles = null;
1609
+ if (needsScopedReclassify) {
1610
+ const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
1611
+ // When edges were inserted but all their endpoint nodes have null `file`
1612
+ // columns (rare but possible), affectedFiles stays empty even though
1613
+ // fan-in/out changed. Fall back to full-graph re-classification in that
1614
+ // case — scoped classification with an empty set would be a no-op, leaving
1615
+ // roles stale for those nodes.
1616
+ scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
1617
+ }
1618
+ const reclassifyStart = performance.now();
1619
+ try {
1620
+ const { classifyNodeRoles } = (await import('../../../../features/structure.js'));
1621
+ classifyNodeRoles(ctx.db, scopedFiles);
1622
+ debug(scopedFiles
1623
+ ? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
1624
+ : 'Post-pass role re-classification complete (full graph)');
1625
+ }
1626
+ catch (err) {
1627
+ debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
1628
+ }
1629
+ reclassifyMs = performance.now() - reclassifyStart;
1630
+ }
1631
+ // Backfill the `technique` column on `calls` edges written by the Rust
1632
+ // orchestrator, which does not write the column. Runs after all edge-writing
1633
+ // phases (including the WASM dropped-language backfill, CHA post-pass, and
1634
+ // this/super dispatch) so every new edge in this build cycle gets a label.
1635
+ const techniqueBackfillStart = performance.now();
1636
+ backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
1637
+ const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
1638
+ // Re-count nodes/edges now that all edge-writing post-passes have run: the
1639
+ // Rust orchestrator captured its counts before the JS post-passes added
1640
+ // edges, so both its summary and build_meta under-report (#1452).
1641
+ //
1642
+ // Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
1643
+ // COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
1644
+ // NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
1645
+ // Rust orchestrator's counts are still accurate — no re-count needed.
1646
+ let finalNodeCount = result.nodeCount ?? 0;
1647
+ let finalEdgeCount = result.edgeCount ?? 0;
1648
+ const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
1649
+ if (postPassWroteData) {
1650
+ try {
1651
+ const counts = ctx.db
1652
+ .prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
1653
+ .get();
1654
+ if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
1655
+ finalNodeCount = counts.n;
1656
+ finalEdgeCount = counts.e;
1657
+ setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
1658
+ }
1659
+ }
1660
+ catch (err) {
1661
+ debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
1662
+ }
1663
+ }
1664
+ info(`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`);
1665
+ return {
1666
+ gapDetectMs,
1667
+ chaMs,
1668
+ thisDispatchMs,
1669
+ reclassifyMs,
1670
+ techniqueBackfillMs,
1671
+ backfillHappened,
1672
+ };
1673
+ }
1009
1674
  /**
1010
1675
  * Try the native build orchestrator.
1011
1676
  *
@@ -1026,53 +1691,46 @@ export async function tryNativeOrchestrator(ctx) {
1026
1691
  debug(`Skipping native orchestrator: ${skipReason}`);
1027
1692
  return undefined;
1028
1693
  }
1029
- // Open NativeDatabase on demand — deferred from setupPipeline to skip the
1030
- // ~60ms cost on no-op/early-exit builds. Close the better-sqlite3 connection
1031
- // first to avoid dual-connection WAL corruption.
1032
- if (!ctx.nativeDb && ctx.nativeAvailable) {
1033
- const native = loadNative();
1034
- if (native?.NativeDatabase) {
1035
- try {
1036
- // Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
1037
- // Uses raw close() instead of closeDb() intentionally — the advisory lock
1038
- // is kept and transferred to the NativeDbProxy below, not released here.
1039
- ctx.db.close();
1040
- acquireAdvisoryLock(ctx.dbPath);
1041
- ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
1042
- ctx.nativeDb.initSchema();
1043
- // Replace ctx.db with a NativeDbProxy so post-native JS fallback
1044
- // (structure, analysis) can use it without reopening better-sqlite3.
1045
- const proxy = new NativeDbProxy(ctx.nativeDb);
1046
- proxy.__lockPath = `${ctx.dbPath}.lock`;
1047
- ctx.db = proxy;
1048
- ctx.nativeFirstProxy = true;
1049
- }
1050
- catch (err) {
1051
- warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
1052
- try {
1053
- ctx.nativeDb?.close();
1054
- }
1055
- catch (e) {
1056
- debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
1057
- }
1058
- ctx.nativeDb = undefined;
1059
- ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
1060
- releaseAdvisoryLock(`${ctx.dbPath}.lock`);
1061
- // Reopen better-sqlite3 for JS pipeline fallback
1062
- ctx.db = openDb(ctx.dbPath);
1063
- }
1064
- }
1065
- }
1694
+ openNativeDatabase(ctx);
1066
1695
  if (!ctx.nativeDb?.buildGraph)
1067
1696
  return undefined;
1068
- const resultJson = ctx.nativeDb.buildGraph(ctx.rootDir, JSON.stringify(ctx.config), JSON.stringify(ctx.aliases), JSON.stringify(ctx.opts));
1697
+ // The previous full build's clear_all_graph_data() sets PRAGMA foreign_keys = ON
1698
+ // on the native connection. Older native binaries (< v3.14) do not delete
1699
+ // dataflow_vertices / dataflow_summary / call_edge_id rows before purging
1700
+ // nodes/edges during incremental builds, so FK enforcement causes the purge
1701
+ // statements to fail silently — leaving stale nodes and edges that then get
1702
+ // duplicated when the barrel-candidate re-parse re-inserts them (issue #1644).
1703
+ // Disabling FK before buildGraph() lets the purge succeed; FK is restored in
1704
+ // a finally block so post-passes (gap-repair, structure patch) retain FK protection
1705
+ // even if buildGraph() throws.
1706
+ try {
1707
+ ctx.nativeDb.exec('PRAGMA foreign_keys = OFF');
1708
+ }
1709
+ catch {
1710
+ // exec may not exist on very old addon versions — safe to ignore
1711
+ }
1712
+ let resultJson;
1713
+ try {
1714
+ resultJson = ctx.nativeDb.buildGraph(ctx.rootDir, JSON.stringify(ctx.config), JSON.stringify(ctx.aliases), JSON.stringify(ctx.opts));
1715
+ }
1716
+ finally {
1717
+ // Restore FK enforcement so any subsequent writes to this connection
1718
+ // (gap-repair, structure patch) retain FK protection — even if buildGraph()
1719
+ // throws.
1720
+ try {
1721
+ ctx.nativeDb.exec('PRAGMA foreign_keys = ON');
1722
+ }
1723
+ catch {
1724
+ // safe to ignore on very old addon versions
1725
+ }
1726
+ }
1069
1727
  const result = JSON.parse(resultJson);
1070
1728
  if (result.earlyExit) {
1071
1729
  info('No changes detected');
1072
1730
  // Even on no-op rebuilds, dropped-language files added since the last
1073
1731
  // full build are still missing from `nodes`/`file_hashes` (#1083), and
1074
1732
  // WASM-only files deleted from disk leave stale rows behind (#1073).
1075
- // The orchestrator's file_collector skipped them, so its earlyExit
1733
+ // The orchestrator's collect_files skipped them, so its earlyExit
1076
1734
  // doesn't imply DB consistency. Run the gap repair before returning.
1077
1735
  const gap = detectDroppedLanguageGap(ctx);
1078
1736
  if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
@@ -1110,7 +1768,9 @@ export async function tryNativeOrchestrator(ctx) {
1110
1768
  schema_version: String(ctx.schemaVersion),
1111
1769
  built_at: new Date().toISOString(),
1112
1770
  });
1113
- info(`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`);
1771
+ // The build summary is logged after the JS edge-writing post-passes below
1772
+ // (dropped-language backfill, CHA, this/super dispatch) so the reported
1773
+ // counts include their edges (#1452).
1114
1774
  // ── Post-native structure + analysis ──────────────────────────────
1115
1775
  let analysisTiming = {
1116
1776
  astMs: +(p.astMs ?? 0),
@@ -1143,78 +1803,17 @@ export async function tryNativeOrchestrator(ctx) {
1143
1803
  ctx.nativeFirstProxy = false;
1144
1804
  }
1145
1805
  else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
1146
- // DB reopen failed — return partial result
1147
- return formatNativeTimingResult(p, 0, analysisTiming, 0);
1148
- }
1149
- }
1150
- // ── Edge-writing post-passes (run before structure so roles see full graph) ──
1151
- // Engine parity: the native orchestrator silently drops files whose
1152
- // Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
1153
- // stale native binaries). WASM handles those — backfill via WASM so both
1154
- // engines process the same file set (#967).
1155
- //
1156
- // Detect the gap once (fs walk + 2 DB queries, ~20–30ms) and use it for
1157
- // both gating and the backfill itself. On dirty incrementals/full builds
1158
- // the orchestrator signals trigger backfill, so the walk happens once
1159
- // (instead of redundantly inside backfill). On quiet incrementals we
1160
- // still pay the walk so we can detect brand-new files in dropped-language
1161
- // extensions — a gap that the orchestrator's `detect_removed_files`
1162
- // filter (#1070) leaves open (#1083, #1091). The pre-check is cheap
1163
- // because the expensive part (WASM re-parse of the missing set) is
1164
- // gated below.
1165
- const removedCount = result.removedCount ?? 0;
1166
- const changedCount = result.changedCount ?? 0;
1167
- const gap = detectDroppedLanguageGap(ctx);
1168
- if (result.isFullBuild ||
1169
- removedCount > 0 ||
1170
- changedCount > 0 ||
1171
- gap.missingAbs.length > 0 ||
1172
- gap.staleRel.length > 0) {
1173
- await backfillNativeDroppedFiles(ctx, gap);
1174
- }
1175
- // Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
1176
- // Returns the affected files so role re-classification below can be scoped to
1177
- // the nodes whose fan-in/out actually changed.
1178
- //
1179
- // Function-as-object-property methods (`fn.method = function() {}`) are extracted
1180
- // natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
1181
- // no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
1182
- const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(ctx.db);
1183
- // Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
1184
- // whose raw receiver info the Rust pipeline does not persist to DB.
1185
- const { elapsedMs: thisDispatchMs, targetIds: thisDispatchTargetIds, affectedFiles: thisDispatchAffectedFiles, } = await runPostNativeThisDispatch(ctx.db, ctx.rootDir, result.changedFiles, !!result.isFullBuild);
1186
- // Role re-classification after JS edge-writing post-passes.
1187
- // The Rust orchestrator classifies roles before these post-passes (CHA,
1188
- // this-dispatch) add edges, so roles for the edge endpoints are stale.
1189
- // Scoped to the files containing those endpoints: a new edge only changes
1190
- // fan-in/out for its own source and target nodes, so re-classifying their
1191
- // files restores correctness without re-running the classifier over the
1192
- // whole graph (which cost ~130ms per build on codegraph itself and was a
1193
- // major part of the v3.12.0 native full-build benchmark regression).
1194
- if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
1195
- const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
1196
- // When edges were inserted but all their endpoint nodes have null `file`
1197
- // columns (rare but possible), affectedFiles stays empty even though
1198
- // fan-in/out changed. Fall back to full-graph re-classification in that
1199
- // case — scoped classification with an empty set would be a no-op, leaving
1200
- // roles stale for those nodes.
1201
- const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
1202
- try {
1203
- const { classifyNodeRoles } = (await import('../../../../features/structure.js'));
1204
- classifyNodeRoles(ctx.db, scopedFiles);
1205
- debug(scopedFiles
1206
- ? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
1207
- : 'Post-pass role re-classification complete (full graph — null-file endpoints)');
1208
- }
1209
- catch (err) {
1210
- debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
1806
+ // DB reopen failed — return partial result (no post-pass phases completed)
1807
+ return formatNativeTimingResult(p, 0, analysisTiming, {
1808
+ gapDetectMs: 0,
1809
+ chaMs: 0,
1810
+ thisDispatchMs: 0,
1811
+ reclassifyMs: 0,
1812
+ techniqueBackfillMs: 0,
1813
+ });
1211
1814
  }
1212
1815
  }
1213
- // Backfill the `technique` column on `calls` edges written by the Rust
1214
- // orchestrator, which does not write the column. Runs after all edge-writing
1215
- // phases (including the WASM dropped-language backfill, CHA post-pass, and
1216
- // this/super dispatch) so every new edge in this build cycle gets a label.
1217
- backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
1816
+ const postPassTimings = await runPostNativePasses(ctx, result);
1218
1817
  // ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
1219
1818
  // Reconstruct fileSymbols once for both structure and analysis to avoid two
1220
1819
  // expensive DB scans. The DB handoff above already ensured ctx.db is a proper
@@ -1228,7 +1827,17 @@ export async function tryNativeOrchestrator(ctx) {
1228
1827
  analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
1229
1828
  }
1230
1829
  }
1830
+ // P6: Vertex extraction for the analysisComplete=true path.
1831
+ // When needsAnalysisFallback=false (the normal native case), runPostNativeAnalysis
1832
+ // was skipped, so buildDataflowEdges never ran and dataflow_vertices were never
1833
+ // populated. Re-run the Rust dataflow visitor per file (fast — no re-parse) to
1834
+ // get the DataflowResult, then build vertices and inter-procedural edges.
1835
+ // Languages where Rust has no dataflow rules are silently skipped; a WASM
1836
+ // fallback for those is tracked in issue #1614.
1837
+ if (ctx.opts.dataflow !== false && !needsAnalysisFallback) {
1838
+ await runDataflowVertexPass(ctx, result.changedFiles);
1839
+ }
1231
1840
  closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
1232
- return formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs);
1841
+ return formatNativeTimingResult(p, structurePatchMs, analysisTiming, postPassTimings);
1233
1842
  }
1234
1843
  //# sourceMappingURL=native-orchestrator.js.map