homegraph 1.2.0 → 1.5.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 (560) hide show
  1. package/README.md +48 -11
  2. package/dist/bin/homegraph.js +298 -72
  3. package/dist/bin/homegraph.js.map +1 -1
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +117 -11
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/db/index.d.ts +207 -6
  8. package/dist/db/index.d.ts.map +1 -1
  9. package/dist/db/index.js +461 -20
  10. package/dist/db/index.js.map +1 -1
  11. package/dist/db/migrations.d.ts +1 -1
  12. package/dist/db/migrations.d.ts.map +1 -1
  13. package/dist/db/migrations.js +19 -1
  14. package/dist/db/migrations.js.map +1 -1
  15. package/dist/db/queries.d.ts +157 -3
  16. package/dist/db/queries.d.ts.map +1 -1
  17. package/dist/db/queries.js +426 -9
  18. package/dist/db/queries.js.map +1 -1
  19. package/dist/db/schema.sql +32 -1
  20. package/dist/db/sqlite-adapter.d.ts +24 -17
  21. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  22. package/dist/db/sqlite-adapter.js +292 -36
  23. package/dist/db/sqlite-adapter.js.map +1 -1
  24. package/dist/db/wal-valve.d.ts +119 -0
  25. package/dist/db/wal-valve.d.ts.map +1 -0
  26. package/dist/db/wal-valve.js +290 -0
  27. package/dist/db/wal-valve.js.map +1 -0
  28. package/dist/directory.d.ts +9 -5
  29. package/dist/directory.d.ts.map +1 -1
  30. package/dist/directory.js +210 -19
  31. package/dist/directory.js.map +1 -1
  32. package/dist/extraction/arkts-batch-worker.js +3 -2
  33. package/dist/extraction/arkts-batch-worker.js.map +1 -1
  34. package/dist/extraction/cfml-extractor.d.ts +107 -0
  35. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  36. package/dist/extraction/cfml-extractor.js +494 -0
  37. package/dist/extraction/cfml-extractor.js.map +1 -0
  38. package/dist/extraction/default-ignore.d.ts +39 -0
  39. package/dist/extraction/default-ignore.d.ts.map +1 -0
  40. package/dist/extraction/default-ignore.js +221 -0
  41. package/dist/extraction/default-ignore.js.map +1 -0
  42. package/dist/extraction/grammars.d.ts +25 -1
  43. package/dist/extraction/grammars.d.ts.map +1 -1
  44. package/dist/extraction/grammars.js +177 -24
  45. package/dist/extraction/grammars.js.map +1 -1
  46. package/dist/extraction/index.d.ts +52 -14
  47. package/dist/extraction/index.d.ts.map +1 -1
  48. package/dist/extraction/index.js +521 -314
  49. package/dist/extraction/index.js.map +1 -1
  50. package/dist/extraction/languages/arkts.d.ts +90 -6
  51. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  52. package/dist/extraction/languages/arkts.js +1579 -128
  53. package/dist/extraction/languages/arkts.js.map +1 -1
  54. package/dist/extraction/languages/c-cpp.d.ts +42 -0
  55. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  56. package/dist/extraction/languages/c-cpp.js +369 -4
  57. package/dist/extraction/languages/c-cpp.js.map +1 -1
  58. package/dist/extraction/languages/cfquery.d.ts +12 -0
  59. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  60. package/dist/extraction/languages/cfquery.js +28 -0
  61. package/dist/extraction/languages/cfquery.js.map +1 -0
  62. package/dist/extraction/languages/cfscript.d.ts +3 -0
  63. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  64. package/dist/extraction/languages/cfscript.js +73 -0
  65. package/dist/extraction/languages/cfscript.js.map +1 -0
  66. package/dist/extraction/languages/cobol.d.ts +33 -0
  67. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  68. package/dist/extraction/languages/cobol.js +499 -0
  69. package/dist/extraction/languages/cobol.js.map +1 -0
  70. package/dist/extraction/languages/erlang.d.ts +3 -0
  71. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  72. package/dist/extraction/languages/erlang.js +350 -0
  73. package/dist/extraction/languages/erlang.js.map +1 -0
  74. package/dist/extraction/languages/index.d.ts.map +1 -1
  75. package/dist/extraction/languages/index.js +16 -0
  76. package/dist/extraction/languages/index.js.map +1 -1
  77. package/dist/extraction/languages/nix.d.ts +3 -0
  78. package/dist/extraction/languages/nix.d.ts.map +1 -0
  79. package/dist/extraction/languages/nix.js +294 -0
  80. package/dist/extraction/languages/nix.js.map +1 -0
  81. package/dist/extraction/languages/solidity.d.ts +3 -0
  82. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  83. package/dist/extraction/languages/solidity.js +293 -0
  84. package/dist/extraction/languages/solidity.js.map +1 -0
  85. package/dist/extraction/languages/terraform.d.ts +3 -0
  86. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  87. package/dist/extraction/languages/terraform.js +641 -0
  88. package/dist/extraction/languages/terraform.js.map +1 -0
  89. package/dist/extraction/languages/vbnet.d.ts +11 -0
  90. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  91. package/dist/extraction/languages/vbnet.js +141 -0
  92. package/dist/extraction/languages/vbnet.js.map +1 -0
  93. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  94. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  95. package/dist/extraction/mybatis-extractor.js +140 -38
  96. package/dist/extraction/mybatis-extractor.js.map +1 -1
  97. package/dist/extraction/parse-pool.d.ts +27 -1
  98. package/dist/extraction/parse-pool.d.ts.map +1 -1
  99. package/dist/extraction/parse-pool.js +75 -7
  100. package/dist/extraction/parse-pool.js.map +1 -1
  101. package/dist/extraction/parse-worker.js +9 -2
  102. package/dist/extraction/parse-worker.js.map +1 -1
  103. package/dist/extraction/store-worker.d.ts +20 -0
  104. package/dist/extraction/store-worker.d.ts.map +1 -0
  105. package/dist/extraction/store-worker.js +102 -0
  106. package/dist/extraction/store-worker.js.map +1 -0
  107. package/dist/extraction/store-writer.d.ts +48 -0
  108. package/dist/extraction/store-writer.d.ts.map +1 -0
  109. package/dist/extraction/store-writer.js +167 -0
  110. package/dist/extraction/store-writer.js.map +1 -0
  111. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  112. package/dist/extraction/tree-sitter-helpers.js +1 -0
  113. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  114. package/dist/extraction/tree-sitter-types.d.ts +3 -1
  115. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  116. package/dist/extraction/tree-sitter.d.ts +38 -0
  117. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  118. package/dist/extraction/tree-sitter.js +801 -10
  119. package/dist/extraction/tree-sitter.js.map +1 -1
  120. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  121. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  122. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  123. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  124. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  125. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  126. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  127. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  128. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  129. package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
  130. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  131. package/dist/extraction/wasm-runtime-flags.js +55 -7
  132. package/dist/extraction/wasm-runtime-flags.js.map +1 -1
  133. package/dist/graph/traversal.d.ts.map +1 -1
  134. package/dist/graph/traversal.js +1 -1
  135. package/dist/graph/traversal.js.map +1 -1
  136. package/dist/index.d.ts +74 -5
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +436 -55
  139. package/dist/index.js.map +1 -1
  140. package/dist/installer/index.d.ts +42 -0
  141. package/dist/installer/index.d.ts.map +1 -1
  142. package/dist/installer/index.js +97 -2
  143. package/dist/installer/index.js.map +1 -1
  144. package/dist/mcp/daemon.d.ts +25 -3
  145. package/dist/mcp/daemon.d.ts.map +1 -1
  146. package/dist/mcp/daemon.js +62 -8
  147. package/dist/mcp/daemon.js.map +1 -1
  148. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  149. package/dist/mcp/dynamic-boundaries.js +2 -1
  150. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  151. package/dist/mcp/early-ppid.d.ts +26 -0
  152. package/dist/mcp/early-ppid.d.ts.map +1 -0
  153. package/dist/mcp/early-ppid.js +29 -0
  154. package/dist/mcp/early-ppid.js.map +1 -0
  155. package/dist/mcp/engine.d.ts +4 -5
  156. package/dist/mcp/engine.d.ts.map +1 -1
  157. package/dist/mcp/engine.js +27 -1
  158. package/dist/mcp/engine.js.map +1 -1
  159. package/dist/mcp/index.d.ts.map +1 -1
  160. package/dist/mcp/index.js +41 -6
  161. package/dist/mcp/index.js.map +1 -1
  162. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  163. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  164. package/dist/mcp/liveness-watchdog.js +73 -13
  165. package/dist/mcp/liveness-watchdog.js.map +1 -1
  166. package/dist/mcp/memory-budget.d.ts +30 -0
  167. package/dist/mcp/memory-budget.d.ts.map +1 -0
  168. package/dist/mcp/memory-budget.js +123 -0
  169. package/dist/mcp/memory-budget.js.map +1 -0
  170. package/dist/mcp/proxy.d.ts.map +1 -1
  171. package/dist/mcp/proxy.js +88 -4
  172. package/dist/mcp/proxy.js.map +1 -1
  173. package/dist/mcp/query-cache.d.ts +35 -4
  174. package/dist/mcp/query-cache.d.ts.map +1 -1
  175. package/dist/mcp/query-cache.js +89 -19
  176. package/dist/mcp/query-cache.js.map +1 -1
  177. package/dist/mcp/query-pool.d.ts +46 -12
  178. package/dist/mcp/query-pool.d.ts.map +1 -1
  179. package/dist/mcp/query-pool.js +100 -23
  180. package/dist/mcp/query-pool.js.map +1 -1
  181. package/dist/mcp/query-worker.d.ts +2 -2
  182. package/dist/mcp/query-worker.js +2 -2
  183. package/dist/mcp/server-instructions.d.ts +4 -27
  184. package/dist/mcp/server-instructions.d.ts.map +1 -1
  185. package/dist/mcp/server-instructions.js +39 -84
  186. package/dist/mcp/server-instructions.js.map +1 -1
  187. package/dist/mcp/session.d.ts +14 -0
  188. package/dist/mcp/session.d.ts.map +1 -1
  189. package/dist/mcp/session.js +28 -1
  190. package/dist/mcp/session.js.map +1 -1
  191. package/dist/mcp/startup-handshake.d.ts +44 -0
  192. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  193. package/dist/mcp/startup-handshake.js +73 -0
  194. package/dist/mcp/startup-handshake.js.map +1 -0
  195. package/dist/mcp/tools.d.ts +104 -4
  196. package/dist/mcp/tools.d.ts.map +1 -1
  197. package/dist/mcp/tools.js +2432 -217
  198. package/dist/mcp/tools.js.map +1 -1
  199. package/dist/mcp/transport.d.ts.map +1 -1
  200. package/dist/mcp/transport.js +18 -0
  201. package/dist/mcp/transport.js.map +1 -1
  202. package/dist/project-config.d.ts +38 -0
  203. package/dist/project-config.d.ts.map +1 -1
  204. package/dist/project-config.js +101 -2
  205. package/dist/project-config.js.map +1 -1
  206. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  207. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  208. package/dist/resolution/c-fnptr-synthesizer.js +178 -146
  209. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  210. package/dist/resolution/callback-synthesizer.d.ts +54 -10
  211. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  212. package/dist/resolution/callback-synthesizer.js +1299 -257
  213. package/dist/resolution/callback-synthesizer.js.map +1 -1
  214. package/dist/resolution/cooperative-yield.d.ts +10 -2
  215. package/dist/resolution/cooperative-yield.d.ts.map +1 -1
  216. package/dist/resolution/cooperative-yield.js +6 -4
  217. package/dist/resolution/cooperative-yield.js.map +1 -1
  218. package/dist/resolution/frameworks/cics.d.ts +20 -0
  219. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  220. package/dist/resolution/frameworks/cics.js +90 -0
  221. package/dist/resolution/frameworks/cics.js.map +1 -0
  222. package/dist/resolution/frameworks/go.d.ts.map +1 -1
  223. package/dist/resolution/frameworks/go.js +29 -6
  224. package/dist/resolution/frameworks/go.js.map +1 -1
  225. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  226. package/dist/resolution/frameworks/index.js +3 -0
  227. package/dist/resolution/frameworks/index.js.map +1 -1
  228. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  229. package/dist/resolution/frameworks/java.js +14 -6
  230. package/dist/resolution/frameworks/java.js.map +1 -1
  231. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  232. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  233. package/dist/resolution/frameworks/terraform.js +277 -0
  234. package/dist/resolution/frameworks/terraform.js.map +1 -0
  235. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  236. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  237. package/dist/resolution/goframe-synthesizer.js +8 -3
  238. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  239. package/dist/resolution/import-resolver.d.ts +9 -3
  240. package/dist/resolution/import-resolver.d.ts.map +1 -1
  241. package/dist/resolution/import-resolver.js +295 -11
  242. package/dist/resolution/import-resolver.js.map +1 -1
  243. package/dist/resolution/index.d.ts +146 -8
  244. package/dist/resolution/index.d.ts.map +1 -1
  245. package/dist/resolution/index.js +1089 -131
  246. package/dist/resolution/index.js.map +1 -1
  247. package/dist/resolution/memory-budget.d.ts +48 -0
  248. package/dist/resolution/memory-budget.d.ts.map +1 -0
  249. package/dist/resolution/memory-budget.js +162 -0
  250. package/dist/resolution/memory-budget.js.map +1 -0
  251. package/dist/resolution/name-matcher.d.ts +12 -4
  252. package/dist/resolution/name-matcher.d.ts.map +1 -1
  253. package/dist/resolution/name-matcher.js +793 -130
  254. package/dist/resolution/name-matcher.js.map +1 -1
  255. package/dist/resolution/resolver-pool.d.ts +106 -0
  256. package/dist/resolution/resolver-pool.d.ts.map +1 -0
  257. package/dist/resolution/resolver-pool.js +344 -0
  258. package/dist/resolution/resolver-pool.js.map +1 -0
  259. package/dist/resolution/resolver-worker.d.ts +17 -0
  260. package/dist/resolution/resolver-worker.d.ts.map +1 -0
  261. package/dist/resolution/resolver-worker.js +141 -0
  262. package/dist/resolution/resolver-worker.js.map +1 -0
  263. package/dist/resolution/strip-comments.d.ts +1 -1
  264. package/dist/resolution/strip-comments.d.ts.map +1 -1
  265. package/dist/resolution/strip-comments.js +49 -0
  266. package/dist/resolution/strip-comments.js.map +1 -1
  267. package/dist/resolution/types.d.ts +32 -0
  268. package/dist/resolution/types.d.ts.map +1 -1
  269. package/dist/resolution/workspace-packages.d.ts +10 -0
  270. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  271. package/dist/resolution/workspace-packages.js +142 -4
  272. package/dist/resolution/workspace-packages.js.map +1 -1
  273. package/dist/search/identifier-segments.d.ts +60 -0
  274. package/dist/search/identifier-segments.d.ts.map +1 -0
  275. package/dist/search/identifier-segments.js +176 -0
  276. package/dist/search/identifier-segments.js.map +1 -0
  277. package/dist/search/query-utils.d.ts +201 -0
  278. package/dist/search/query-utils.d.ts.map +1 -1
  279. package/dist/search/query-utils.js +919 -2
  280. package/dist/search/query-utils.js.map +1 -1
  281. package/dist/spec/build/diff-parser.d.ts +55 -0
  282. package/dist/spec/build/diff-parser.d.ts.map +1 -0
  283. package/dist/spec/build/diff-parser.js +205 -0
  284. package/dist/spec/build/diff-parser.js.map +1 -0
  285. package/dist/spec/build/git-scanner.d.ts +93 -0
  286. package/dist/spec/build/git-scanner.d.ts.map +1 -0
  287. package/dist/spec/build/git-scanner.js +254 -0
  288. package/dist/spec/build/git-scanner.js.map +1 -0
  289. package/dist/spec/build/pipeline.d.ts +54 -0
  290. package/dist/spec/build/pipeline.d.ts.map +1 -0
  291. package/dist/spec/build/pipeline.js +170 -0
  292. package/dist/spec/build/pipeline.js.map +1 -0
  293. package/dist/spec/build/scan.d.ts +48 -0
  294. package/dist/spec/build/scan.d.ts.map +1 -0
  295. package/dist/spec/build/scan.js +89 -0
  296. package/dist/spec/build/scan.js.map +1 -0
  297. package/dist/spec/build/scope-resolver.d.ts +50 -0
  298. package/dist/spec/build/scope-resolver.d.ts.map +1 -0
  299. package/dist/spec/build/scope-resolver.js +105 -0
  300. package/dist/spec/build/scope-resolver.js.map +1 -0
  301. package/dist/spec/build/spec-extractor.d.ts +74 -0
  302. package/dist/spec/build/spec-extractor.d.ts.map +1 -0
  303. package/dist/spec/build/spec-extractor.js +344 -0
  304. package/dist/spec/build/spec-extractor.js.map +1 -0
  305. package/dist/spec/config.d.ts +35 -1
  306. package/dist/spec/config.d.ts.map +1 -1
  307. package/dist/spec/config.js +30 -5
  308. package/dist/spec/config.js.map +1 -1
  309. package/dist/spec/db/fts.d.ts.map +1 -1
  310. package/dist/spec/db/fts.js +19 -50
  311. package/dist/spec/db/fts.js.map +1 -1
  312. package/dist/spec/db/index.d.ts +5 -3
  313. package/dist/spec/db/index.d.ts.map +1 -1
  314. package/dist/spec/db/index.js +16 -1
  315. package/dist/spec/db/index.js.map +1 -1
  316. package/dist/spec/db/persist.d.ts +45 -0
  317. package/dist/spec/db/persist.d.ts.map +1 -0
  318. package/dist/spec/db/persist.js +66 -0
  319. package/dist/spec/db/persist.js.map +1 -0
  320. package/dist/spec/db/relations.d.ts +80 -1
  321. package/dist/spec/db/relations.d.ts.map +1 -1
  322. package/dist/spec/db/relations.js +175 -4
  323. package/dist/spec/db/relations.js.map +1 -1
  324. package/dist/spec/db/schema.d.ts +4 -0
  325. package/dist/spec/db/schema.d.ts.map +1 -1
  326. package/dist/spec/db/schema.js +4 -0
  327. package/dist/spec/db/schema.js.map +1 -1
  328. package/dist/spec/db/schema.sql +5 -1
  329. package/dist/spec/db/spec-node.d.ts +5 -0
  330. package/dist/spec/db/spec-node.d.ts.map +1 -1
  331. package/dist/spec/db/spec-node.js +11 -5
  332. package/dist/spec/db/spec-node.js.map +1 -1
  333. package/dist/spec/db/sql-utils.d.ts +11 -0
  334. package/dist/spec/db/sql-utils.d.ts.map +1 -0
  335. package/dist/spec/db/sql-utils.js +19 -0
  336. package/dist/spec/db/sql-utils.js.map +1 -0
  337. package/dist/spec/evolve/cluster-context.d.ts +30 -0
  338. package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
  339. package/dist/spec/evolve/cluster-context.js +78 -0
  340. package/dist/spec/evolve/cluster-context.js.map +1 -0
  341. package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
  342. package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
  343. package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
  344. package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
  345. package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
  346. package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
  347. package/dist/spec/evolve/commit-spec-persister.js +93 -0
  348. package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
  349. package/dist/spec/evolve/impact-locator.d.ts +23 -7
  350. package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
  351. package/dist/spec/evolve/impact-locator.js +58 -14
  352. package/dist/spec/evolve/impact-locator.js.map +1 -1
  353. package/dist/spec/evolve/pipeline.d.ts +62 -25
  354. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  355. package/dist/spec/evolve/pipeline.js +436 -457
  356. package/dist/spec/evolve/pipeline.js.map +1 -1
  357. package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
  358. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  359. package/dist/spec/evolve/spec-rewriter.js +45 -45
  360. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  361. package/dist/spec/git/commits.d.ts +85 -0
  362. package/dist/spec/git/commits.d.ts.map +1 -0
  363. package/dist/spec/git/commits.js +218 -0
  364. package/dist/spec/git/commits.js.map +1 -0
  365. package/dist/spec/git/exec.d.ts +13 -0
  366. package/dist/spec/git/exec.d.ts.map +1 -0
  367. package/dist/spec/git/exec.js +19 -0
  368. package/dist/spec/git/exec.js.map +1 -0
  369. package/dist/spec/git/index.d.ts +9 -0
  370. package/dist/spec/git/index.d.ts.map +1 -0
  371. package/dist/spec/git/index.js +22 -0
  372. package/dist/spec/git/index.js.map +1 -0
  373. package/dist/spec/git-utils.d.ts +8 -0
  374. package/dist/spec/git-utils.d.ts.map +1 -0
  375. package/dist/spec/git-utils.js +14 -0
  376. package/dist/spec/git-utils.js.map +1 -0
  377. package/dist/spec/graph/index.d.ts +8 -0
  378. package/dist/spec/graph/index.d.ts.map +1 -0
  379. package/dist/spec/graph/index.js +17 -0
  380. package/dist/spec/graph/index.js.map +1 -0
  381. package/dist/spec/graph/queries.d.ts +6 -35
  382. package/dist/spec/graph/queries.d.ts.map +1 -1
  383. package/dist/spec/graph/queries.js +71 -248
  384. package/dist/spec/graph/queries.js.map +1 -1
  385. package/dist/spec/llm/agent-client.d.ts +59 -0
  386. package/dist/spec/llm/agent-client.d.ts.map +1 -0
  387. package/dist/spec/llm/agent-client.js +213 -0
  388. package/dist/spec/llm/agent-client.js.map +1 -0
  389. package/dist/spec/llm/agents/claude-code.d.ts +32 -0
  390. package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
  391. package/dist/spec/llm/agents/claude-code.js +125 -0
  392. package/dist/spec/llm/agents/claude-code.js.map +1 -0
  393. package/dist/spec/llm/agents/codex.d.ts +32 -0
  394. package/dist/spec/llm/agents/codex.d.ts.map +1 -0
  395. package/dist/spec/llm/agents/codex.js +145 -0
  396. package/dist/spec/llm/agents/codex.js.map +1 -0
  397. package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
  398. package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
  399. package/dist/spec/llm/agents/detect-utils.js +91 -0
  400. package/dist/spec/llm/agents/detect-utils.js.map +1 -0
  401. package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
  402. package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
  403. package/dist/spec/llm/agents/deveco-code.js +165 -0
  404. package/dist/spec/llm/agents/deveco-code.js.map +1 -0
  405. package/dist/spec/llm/agents/index.d.ts +31 -0
  406. package/dist/spec/llm/agents/index.d.ts.map +1 -0
  407. package/dist/spec/llm/agents/index.js +65 -0
  408. package/dist/spec/llm/agents/index.js.map +1 -0
  409. package/dist/spec/llm/agents/types.d.ts +76 -0
  410. package/dist/spec/llm/agents/types.d.ts.map +1 -0
  411. package/dist/spec/llm/agents/types.js +15 -0
  412. package/dist/spec/llm/agents/types.js.map +1 -0
  413. package/dist/spec/llm/client.d.ts +31 -11
  414. package/dist/spec/llm/client.d.ts.map +1 -1
  415. package/dist/spec/llm/client.js +143 -81
  416. package/dist/spec/llm/client.js.map +1 -1
  417. package/dist/spec/llm/factory.d.ts +41 -0
  418. package/dist/spec/llm/factory.d.ts.map +1 -0
  419. package/dist/spec/llm/factory.js +80 -0
  420. package/dist/spec/llm/factory.js.map +1 -0
  421. package/dist/spec/llm/prompts.d.ts +24 -8
  422. package/dist/spec/llm/prompts.d.ts.map +1 -1
  423. package/dist/spec/llm/prompts.js +100 -50
  424. package/dist/spec/llm/prompts.js.map +1 -1
  425. package/dist/spec/llm/retry.d.ts +53 -0
  426. package/dist/spec/llm/retry.d.ts.map +1 -0
  427. package/dist/spec/llm/retry.js +153 -0
  428. package/dist/spec/llm/retry.js.map +1 -0
  429. package/dist/spec/mine/clusterer.d.ts +63 -0
  430. package/dist/spec/mine/clusterer.d.ts.map +1 -0
  431. package/dist/spec/mine/clusterer.js +904 -0
  432. package/dist/spec/mine/clusterer.js.map +1 -0
  433. package/dist/spec/mine/clustering/features.d.ts +38 -0
  434. package/dist/spec/mine/clustering/features.d.ts.map +1 -0
  435. package/dist/spec/mine/clustering/features.js +108 -0
  436. package/dist/spec/mine/clustering/features.js.map +1 -0
  437. package/dist/spec/mine/clustering/index.d.ts +62 -0
  438. package/dist/spec/mine/clustering/index.d.ts.map +1 -0
  439. package/dist/spec/mine/clustering/index.js +259 -0
  440. package/dist/spec/mine/clustering/index.js.map +1 -0
  441. package/dist/spec/mine/clustering/leiden.d.ts +61 -0
  442. package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
  443. package/dist/spec/mine/clustering/leiden.js +489 -0
  444. package/dist/spec/mine/clustering/leiden.js.map +1 -0
  445. package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
  446. package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
  447. package/dist/spec/mine/clustering/text-similarity.js +98 -0
  448. package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
  449. package/dist/spec/mine/generator.d.ts +54 -0
  450. package/dist/spec/mine/generator.d.ts.map +1 -0
  451. package/dist/spec/mine/generator.js +267 -0
  452. package/dist/spec/mine/generator.js.map +1 -0
  453. package/dist/spec/mine/persist.d.ts +37 -0
  454. package/dist/spec/mine/persist.d.ts.map +1 -0
  455. package/dist/spec/mine/persist.js +170 -0
  456. package/dist/spec/mine/persist.js.map +1 -0
  457. package/dist/spec/mine/pipeline.d.ts +41 -0
  458. package/dist/spec/mine/pipeline.d.ts.map +1 -0
  459. package/dist/spec/mine/pipeline.js +236 -0
  460. package/dist/spec/mine/pipeline.js.map +1 -0
  461. package/dist/spec/mine/progress-handler.d.ts +22 -0
  462. package/dist/spec/mine/progress-handler.d.ts.map +1 -0
  463. package/dist/spec/mine/progress-handler.js +108 -0
  464. package/dist/spec/mine/progress-handler.js.map +1 -0
  465. package/dist/spec/mine/progress.d.ts +23 -0
  466. package/dist/spec/mine/progress.d.ts.map +1 -0
  467. package/dist/spec/mine/progress.js +12 -0
  468. package/dist/spec/mine/progress.js.map +1 -0
  469. package/dist/spec/mine/scanner.d.ts +59 -0
  470. package/dist/spec/mine/scanner.d.ts.map +1 -0
  471. package/dist/spec/mine/scanner.js +383 -0
  472. package/dist/spec/mine/scanner.js.map +1 -0
  473. package/dist/spec/types.d.ts +32 -0
  474. package/dist/spec/types.d.ts.map +1 -1
  475. package/dist/spec/ui/index.d.ts +9 -0
  476. package/dist/spec/ui/index.d.ts.map +1 -0
  477. package/dist/spec/ui/index.js +16 -0
  478. package/dist/spec/ui/index.js.map +1 -0
  479. package/dist/spec/ui/progress-handler.d.ts +33 -0
  480. package/dist/spec/ui/progress-handler.d.ts.map +1 -0
  481. package/dist/spec/ui/progress-handler.js +132 -0
  482. package/dist/spec/ui/progress-handler.js.map +1 -0
  483. package/dist/spec/ui/progress.d.ts +24 -0
  484. package/dist/spec/ui/progress.d.ts.map +1 -0
  485. package/dist/spec/ui/progress.js +13 -0
  486. package/dist/spec/ui/progress.js.map +1 -0
  487. package/dist/spec/utils/fs.d.ts +45 -0
  488. package/dist/spec/utils/fs.d.ts.map +1 -0
  489. package/dist/spec/utils/fs.js +163 -0
  490. package/dist/spec/utils/fs.js.map +1 -0
  491. package/dist/spec/utils/index.d.ts +12 -0
  492. package/dist/spec/utils/index.d.ts.map +1 -0
  493. package/dist/spec/utils/index.js +26 -0
  494. package/dist/spec/utils/index.js.map +1 -0
  495. package/dist/spec/utils/meta.d.ts +32 -0
  496. package/dist/spec/utils/meta.d.ts.map +1 -0
  497. package/dist/spec/utils/meta.js +122 -0
  498. package/dist/spec/utils/meta.js.map +1 -0
  499. package/dist/spec/utils/truncate.d.ts +86 -0
  500. package/dist/spec/utils/truncate.d.ts.map +1 -0
  501. package/dist/spec/utils/truncate.js +172 -0
  502. package/dist/spec/utils/truncate.js.map +1 -0
  503. package/dist/spec/utils.d.ts +4 -16
  504. package/dist/spec/utils.d.ts.map +1 -1
  505. package/dist/spec/utils.js +6 -58
  506. package/dist/spec/utils.js.map +1 -1
  507. package/dist/sync/git-hooks.d.ts.map +1 -1
  508. package/dist/sync/git-hooks.js +2 -0
  509. package/dist/sync/git-hooks.js.map +1 -1
  510. package/dist/sync/watcher.d.ts +35 -6
  511. package/dist/sync/watcher.d.ts.map +1 -1
  512. package/dist/sync/watcher.js +127 -17
  513. package/dist/sync/watcher.js.map +1 -1
  514. package/dist/sync/worktree.d.ts.map +1 -1
  515. package/dist/sync/worktree.js +5 -0
  516. package/dist/sync/worktree.js.map +1 -1
  517. package/dist/telemetry/index.js +3 -3
  518. package/dist/telemetry/index.js.map +1 -1
  519. package/dist/types.d.ts +22 -1
  520. package/dist/types.d.ts.map +1 -1
  521. package/dist/types.js +10 -0
  522. package/dist/types.js.map +1 -1
  523. package/dist/upgrade/index.d.ts +32 -0
  524. package/dist/upgrade/index.d.ts.map +1 -1
  525. package/dist/upgrade/index.js +162 -14
  526. package/dist/upgrade/index.js.map +1 -1
  527. package/dist/upgrade/remove-binary.d.ts +87 -0
  528. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  529. package/dist/upgrade/remove-binary.js +289 -0
  530. package/dist/upgrade/remove-binary.js.map +1 -0
  531. package/dist/upgrade/update-check.d.ts +92 -0
  532. package/dist/upgrade/update-check.d.ts.map +1 -0
  533. package/dist/upgrade/update-check.js +258 -0
  534. package/dist/upgrade/update-check.js.map +1 -0
  535. package/package.json +6 -2
  536. package/scripts/agent-eval/run-all.sh +6 -0
  537. package/scripts/build-bundle.sh +5 -0
  538. package/scripts/npm-shim.js +8 -1
  539. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  540. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  541. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  542. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  543. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  544. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  545. package/scripts/exp_boundary_eval/data/agents.json +0 -109
  546. package/scripts/exp_boundary_eval/data/experiments.json +0 -140
  547. package/scripts/qa_eval/README.md +0 -407
  548. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  549. package/scripts/qa_eval/agent_runner.py +0 -526
  550. package/scripts/qa_eval/data/.gitignore +0 -4
  551. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  552. package/scripts/qa_eval/eval_metrics.py +0 -274
  553. package/scripts/qa_eval/external_agent.py +0 -976
  554. package/scripts/qa_eval/llm_config.py +0 -92
  555. package/scripts/qa_eval/memory_monitor.py +0 -132
  556. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  557. package/scripts/qa_eval/requirements.txt +0 -2
  558. package/scripts/qa_eval/run_pipeline.py +0 -804
  559. package/scripts/qa_eval/stats_efficiency.py +0 -279
  560. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -47,6 +47,7 @@ const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const name_matcher_1 = require("./name-matcher");
49
49
  const import_resolver_1 = require("./import-resolver");
50
+ const resolver_pool_1 = require("./resolver-pool");
50
51
  const frameworks_1 = require("./frameworks");
51
52
  const callback_synthesizer_1 = require("./callback-synthesizer");
52
53
  const cooperative_yield_1 = require("./cooperative-yield");
@@ -68,6 +69,8 @@ const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'g
68
69
  const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
69
70
  /** The extractor's chained-receiver encoding: `<inner>().<method>`. */
70
71
  const CHAIN_SHAPE = /^(.+)\(\)\.(\w+)$/;
72
+ /** PHP `$this->prop->method()` encoded as `this->prop.method` — no `()`, so CHAIN_SHAPE misses it. */
73
+ const PHP_PROP_SHAPE = /^this->\w+\.\w+$/;
71
74
  /**
72
75
  * Cache size limits. Each per-resolver cache is bounded so memory
73
76
  * stays flat on large codebases (20k+ files). Sizes were chosen to
@@ -240,6 +243,43 @@ class ReferenceResolver {
240
243
  nameCache; // name → nodes cache
241
244
  lowerNameCache; // lower(name) → nodes cache
242
245
  qualifiedNameCache; // qualified_name → nodes cache
246
+ fileLinesCache; // file → split lines cache
247
+ methodMatchCache; // lang\0Type::method → matching method nodes
248
+ // Per-(language, methodName) owner index for getMethodMatches: buckets a
249
+ // method name's candidates by their qualifiedName's last two segments so a
250
+ // (type, method) query is a lookup instead of an O(candidates) filter per
251
+ // methodMatchCache miss. Derived purely from node rows (stable through the
252
+ // resolution loop, same window nameCache relies on); dropped in clearCaches.
253
+ methodOwnerIndexCache = new Map();
254
+ // Generation-tagged memo for getSupertypes. Supertype edges GROW during the
255
+ // resolution loop (batch k persists its implements/extends edges BEFORE
256
+ // batch k+1 fans out — the #1320 ordering), so a plain cache would freeze an
257
+ // early batch's emptier answer and change later batches' outcomes. Within
258
+ // one batch the edge state is fixed by that same ordering, so entries are
259
+ // tagged with a generation that advances at every batch entry point
260
+ // (resolveBatchYielding / resolveListForAdmission) — a stale-gen entry is
261
+ // recomputed, making the memo behavior-identical to no memo at every point
262
+ // in time. On the Swift compiler the unmemoized walk ran 971k times for
263
+ // 565s of combined worker time (~581µs each, recursion-multiplied).
264
+ supertypeGen = 0;
265
+ supertypeMemo = new Map();
266
+ /** Invalidate the getSupertypes memo — call when resolved edges may have advanced. */
267
+ advanceSupertypeGeneration() {
268
+ this.supertypeGen++;
269
+ // Lazy invalidation via the gen tag; bound the map so a long run over many
270
+ // batches doesn't accrete dead entries.
271
+ if (this.supertypeMemo.size > 50_000)
272
+ this.supertypeMemo.clear();
273
+ }
274
+ // Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU.
275
+ // getNodesByKind returns the FULL node list for a kind; it was previously
276
+ // uncached — a per-ref `SELECT * FROM nodes WHERE kind=?` + row-mapping. Called
277
+ // for every dotted call ref by the Spring resolver (constants) and every
278
+ // `hook_` ref by the Drupal resolver (functions), that scan dominated
279
+ // resolution on large repos (#1180). The node set is stable within a
280
+ // resolution pass (same lifetime assumption as nameCache); clearCaches() resets
281
+ // it between passes. Callers must treat the returned array as read-only.
282
+ nodesByKindCache = new Map();
243
283
  knownNames = null; // all known symbol names for fast pre-filtering
244
284
  knownFiles = null;
245
285
  cachesWarmed = false;
@@ -265,6 +305,10 @@ class ReferenceResolver {
265
305
  this.nameCache = new lru_cache_1.LRUCache(limit);
266
306
  this.lowerNameCache = new lru_cache_1.LRUCache(limit);
267
307
  this.qualifiedNameCache = new lru_cache_1.LRUCache(limit);
308
+ // Split-lines arrays are heavier than content strings; refs arrive
309
+ // file-ordered, so a small cache still hits nearly always.
310
+ this.fileLinesCache = new lru_cache_1.LRUCache(contentLimit);
311
+ this.methodMatchCache = new lru_cache_1.LRUCache(limit);
268
312
  this.context = this.createContext();
269
313
  }
270
314
  /**
@@ -320,6 +364,28 @@ class ReferenceResolver {
320
364
  this.knownNames = new Set(this.queries.getAllNodeNames());
321
365
  this.cachesWarmed = true;
322
366
  }
367
+ /**
368
+ * warmCaches for the async resolution entry points: streams the distinct
369
+ * name set with periodic yields instead of one synchronous `.all()`. On a
370
+ * multi-million-node index the DISTINCT scan is a solid multi-second block
371
+ * (measured up to 28s inside `homegraph sync` on the Linux kernel index),
372
+ * long enough to matter to the #850 watchdog on slower hardware. Same
373
+ * result, same memory — only the event loop keeps turning.
374
+ */
375
+ async warmCachesYielding(onYield) {
376
+ if (this.cachesWarmed)
377
+ return;
378
+ this.knownFiles = new Set(this.queries.getAllFilePaths());
379
+ const names = new Set();
380
+ let scanned = 0;
381
+ for (const name of this.queries.iterateNodeNames()) {
382
+ names.add(name);
383
+ if ((++scanned & 8191) === 0)
384
+ await onYield();
385
+ }
386
+ this.knownNames = names;
387
+ this.cachesWarmed = true;
388
+ }
323
389
  /**
324
390
  * Clear internal caches
325
391
  */
@@ -331,9 +397,38 @@ class ReferenceResolver {
331
397
  this.nameCache.clear();
332
398
  this.lowerNameCache.clear();
333
399
  this.qualifiedNameCache.clear();
400
+ this.fileLinesCache.clear();
401
+ this.methodMatchCache.clear();
402
+ this.methodOwnerIndexCache.clear();
403
+ this.supertypeMemo.clear();
404
+ this.supertypeGen++;
405
+ this.nodesByKindCache.clear();
334
406
  this.knownNames = null;
335
407
  this.knownFiles = null;
336
408
  this.cachesWarmed = false;
409
+ // The import-resolver's and name-matcher's per-context memos assume the
410
+ // same stable window as the caches above — drop them together.
411
+ if (this.context) {
412
+ (0, import_resolver_1.clearImportResolverMemos)(this.context);
413
+ (0, name_matcher_1.clearNameMatcherMemos)(this.context);
414
+ }
415
+ }
416
+ /** `readFile` through the LRU content cache (null = read failed, also cached). */
417
+ readFileCached(filePath) {
418
+ if (this.fileCache.has(filePath)) {
419
+ return this.fileCache.get(filePath);
420
+ }
421
+ const fullPath = path.join(this.projectRoot, filePath);
422
+ try {
423
+ const content = fs.readFileSync(fullPath, 'utf-8');
424
+ this.fileCache.set(filePath, content);
425
+ return content;
426
+ }
427
+ catch (error) {
428
+ (0, errors_1.logDebug)('Failed to read file for resolution', { filePath, error: String(error) });
429
+ this.fileCache.set(filePath, null);
430
+ return null;
431
+ }
337
432
  }
338
433
  /**
339
434
  * Create the resolution context
@@ -354,6 +449,76 @@ class ReferenceResolver {
354
449
  this.nameCache.set(name, result);
355
450
  return result;
356
451
  },
452
+ getMethodMatches: (typeName, methodName, language) => {
453
+ const key = `${language} ${typeName}::${methodName}`;
454
+ const cached = this.methodMatchCache.get(key);
455
+ if (cached !== undefined)
456
+ return cached;
457
+ let candidates = this.nameCache.get(methodName);
458
+ if (candidates === undefined) {
459
+ candidates = this.queries.getNodesByName(methodName);
460
+ this.nameCache.set(methodName, candidates);
461
+ }
462
+ const want = `${typeName}::${methodName}`;
463
+ let matches;
464
+ if (typeName.includes('::') || methodName.includes(':')) {
465
+ // Legacy linear filter for the shapes the owner index below can't
466
+ // key exactly: a multi-segment typeName (the endsWith test then
467
+ // spans more than two `::` segments) and ObjC selectors (whose
468
+ // single/empty-keyword colons defeat the segment split). Tiny
469
+ // populations; the per-key memo above still amortizes them.
470
+ matches = [];
471
+ for (const m of candidates) {
472
+ if (m.kind !== 'method')
473
+ continue;
474
+ if (m.language !== language)
475
+ continue;
476
+ const qn = m.qualifiedName;
477
+ if (qn === want || qn.endsWith(`::${want}`))
478
+ matches.push(m);
479
+ }
480
+ }
481
+ else {
482
+ // Owner index: the linear filter above is O(all same-named methods)
483
+ // per CACHE MISS, and on overload-heavy landscapes the distinct
484
+ // (type, method) key space is so large the per-key memo never
485
+ // amortizes — Swift's `init` has tens of thousands of candidates
486
+ // and the compiler repo measured 732µs per failing call, most of it
487
+ // this scan (re-entered once per supertype recursion level, too).
488
+ // Bucket each (language, methodName)'s candidates ONCE by the
489
+ // qualifiedName's last two `::` segments — exactly the span the
490
+ // `qn === want || qn.endsWith('::' + want)` predicate tests for a
491
+ // segment-clean typeName — then every query is a map lookup.
492
+ // Bucket insertion follows candidate order, so each bucket is
493
+ // byte-identical to what the linear filter produced.
494
+ const idxKey = `${language} ${methodName}`;
495
+ let ownerIndex = this.methodOwnerIndexCache.get(idxKey);
496
+ if (!ownerIndex) {
497
+ ownerIndex = new Map();
498
+ for (const m of candidates) {
499
+ if (m.kind !== 'method')
500
+ continue;
501
+ if (m.language !== language)
502
+ continue;
503
+ const qn = m.qualifiedName;
504
+ const i2 = qn.lastIndexOf('::');
505
+ if (i2 < 0)
506
+ continue; // single-segment qn can never match `T::m`
507
+ const i1 = qn.lastIndexOf('::', i2 - 1);
508
+ const bucketKey = i1 < 0 ? qn : qn.slice(i1 + 2);
509
+ const bucket = ownerIndex.get(bucketKey);
510
+ if (bucket)
511
+ bucket.push(m);
512
+ else
513
+ ownerIndex.set(bucketKey, [m]);
514
+ }
515
+ this.methodOwnerIndexCache.set(idxKey, ownerIndex);
516
+ }
517
+ matches = ownerIndex.get(want) ?? [];
518
+ }
519
+ this.methodMatchCache.set(key, matches);
520
+ return matches;
521
+ },
357
522
  getNodesByQualifiedName: (qualifiedName) => {
358
523
  const cached = this.qualifiedNameCache.get(qualifiedName);
359
524
  if (cached !== undefined)
@@ -363,8 +528,18 @@ class ReferenceResolver {
363
528
  return result;
364
529
  },
365
530
  getNodesByKind: (kind) => {
366
- return this.queries.getNodesByKind(kind);
531
+ const cached = this.nodesByKindCache.get(kind);
532
+ if (cached !== undefined)
533
+ return cached;
534
+ const result = this.queries.getNodesByKind(kind);
535
+ this.nodesByKindCache.set(kind, result);
536
+ return result;
367
537
  },
538
+ // Streamed, uncached — synthesizers scan-and-filter whole kinds, and
539
+ // both the materialized array AND the per-kind cache retention are
540
+ // O(nodes) memory (#1212). Per-ref resolvers keep the cached array
541
+ // variant above.
542
+ iterateNodesByKind: (kind) => this.queries.iterateNodesByKind(kind),
368
543
  fileExists: (filePath) => {
369
544
  // Check pre-built known files set first (O(1))
370
545
  if (this.knownFiles) {
@@ -383,21 +558,15 @@ class ReferenceResolver {
383
558
  return false;
384
559
  }
385
560
  },
386
- readFile: (filePath) => {
387
- if (this.fileCache.has(filePath)) {
388
- return this.fileCache.get(filePath);
389
- }
390
- const fullPath = path.join(this.projectRoot, filePath);
391
- try {
392
- const content = fs.readFileSync(fullPath, 'utf-8');
393
- this.fileCache.set(filePath, content);
394
- return content;
395
- }
396
- catch (error) {
397
- (0, errors_1.logDebug)('Failed to read file for resolution', { filePath, error: String(error) });
398
- this.fileCache.set(filePath, null);
399
- return null;
400
- }
561
+ readFile: (filePath) => this.readFileCached(filePath),
562
+ getFileLines: (filePath) => {
563
+ const cached = this.fileLinesCache.get(filePath);
564
+ if (cached !== undefined)
565
+ return cached;
566
+ const source = this.readFileCached(filePath);
567
+ const lines = source === null ? null : source.split(/\r?\n/);
568
+ this.fileLinesCache.set(filePath, lines);
569
+ return lines;
401
570
  },
402
571
  getProjectRoot: () => this.projectRoot,
403
572
  getAllFiles: () => {
@@ -437,20 +606,34 @@ class ReferenceResolver {
437
606
  // Matching by simple name (not id) reconciles a type declared in one node
438
607
  // (`KF::Builder`) with conformance declared in a separate extension node
439
608
  // (`KF.Builder: KFOptionSetter`) — both have name `Builder`.
609
+ // Memoized per batch generation (see supertypeMemo): within a batch the
610
+ // edge state is fixed, and the conformance walk re-queries the same
611
+ // popular supertypes (Swift stdlib protocols especially) thousands of
612
+ // times per batch.
613
+ const memoKey = `${language} ${typeName}`;
614
+ const hit = this.supertypeMemo.get(memoKey);
615
+ if (hit && hit.gen === this.supertypeGen)
616
+ return hit.supers;
440
617
  const typeNodes = this.context
441
618
  .getNodesByName(typeName)
442
619
  .filter((n) => SUPERTYPE_BEARING_KINDS.has(n.kind) && n.language === language);
443
- if (typeNodes.length === 0)
444
- return [];
445
- const supertypes = new Set();
446
- for (const tn of typeNodes) {
447
- for (const edge of this.queries.getOutgoingEdges(tn.id, ['implements', 'extends'])) {
448
- const target = this.queries.getNodeById(edge.target);
449
- if (target?.name && target.name !== typeName)
450
- supertypes.add(target.name);
620
+ let supers;
621
+ if (typeNodes.length === 0) {
622
+ supers = [];
623
+ }
624
+ else {
625
+ const supertypes = new Set();
626
+ for (const tn of typeNodes) {
627
+ for (const edge of this.queries.getOutgoingEdges(tn.id, ['implements', 'extends'])) {
628
+ const target = this.queries.getNodeById(edge.target);
629
+ if (target?.name && target.name !== typeName)
630
+ supertypes.add(target.name);
631
+ }
451
632
  }
633
+ supers = [...supertypes];
452
634
  }
453
- return [...supertypes];
635
+ this.supertypeMemo.set(memoKey, { gen: this.supertypeGen, supers });
636
+ return supers;
454
637
  },
455
638
  getImportMappings: (filePath, language) => {
456
639
  const cacheKey = filePath;
@@ -500,7 +683,7 @@ class ReferenceResolver {
500
683
  // `.ts` index barrel and silently break the chain (#629). Re-key
501
684
  // the parse on the barrel's extension so the chase works no matter
502
685
  // what kind of file imports through it.
503
- const isJsFamily = /\.(?:d\.ts|[cm]?tsx?|[cm]?jsx?)$/i.test(filePath);
686
+ const isJsFamily = /\.(?:d\.ts|[cm]?tsx?|[cm]?jsx?|ets)$/i.test(filePath);
504
687
  const reExports = (0, import_resolver_1.extractReExports)(content, isJsFamily ? 'typescript' : language);
505
688
  this.reExportCache.set(filePath, reExports);
506
689
  return reExports;
@@ -528,12 +711,13 @@ class ReferenceResolver {
528
711
  column: ref.column,
529
712
  filePath: ref.filePath || this.getFilePathFromNodeId(ref.fromNodeId),
530
713
  language: ref.language || this.getLanguageFromNodeId(ref.fromNodeId),
714
+ rowId: ref.rowId,
531
715
  }));
532
716
  const total = refs.length;
533
717
  let lastReportedPercent = -1;
534
718
  for (let i = 0; i < refs.length; i++) {
535
719
  const ref = refs[i]; // Array index is guaranteed to be in bounds
536
- const result = this.resolveOne(ref);
720
+ const result = this.resolveOneTimed(ref);
537
721
  if (result) {
538
722
  resolved.push(result);
539
723
  byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
@@ -667,15 +851,42 @@ class ReferenceResolver {
667
851
  if (this.isBuiltInOrExternal(ref)) {
668
852
  return null;
669
853
  }
854
+ // CFML component paths in inheritance (#1152): `extends="coldbox.system.web.
855
+ // Controller"` names the supertype by its dot-separated path (or `extends=
856
+ // "../base"` by relative file path) — the graph indexes the class under its
857
+ // final segment only, so these die at the fast pre-filter below and never
858
+ // resolved. Handled by a dedicated path-corroborated matcher, gated to
859
+ // inheritance refs only (a dotted `calls` ref is a member-access chain, not
860
+ // a component path). No fallthrough on miss: the full path string can only
861
+ // ever mis-match downstream, and an unresolvable supertype usually lives in
862
+ // an out-of-repo library (mxunit, testbox) — silent beats wrong.
863
+ if ((ref.language === 'cfml' || ref.language === 'cfscript') &&
864
+ (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') &&
865
+ (ref.referenceName.includes('.') || ref.referenceName.includes('/'))) {
866
+ return this.resolveCfmlComponentPath(ref);
867
+ }
670
868
  // Fast pre-filter: skip if no symbol with this name exists anywhere
671
869
  // AND the name doesn't match a local import. The import escape is
672
870
  // necessary because re-export rename chains (`import { login }
673
871
  // from './barrel'` where the barrel has `export { signIn as login }
674
872
  // from './auth'`) intentionally call a name that has no
675
873
  // declaration anywhere — only the renamed upstream symbol does.
676
- if (!this.hasAnyPossibleMatch(ref.referenceName) &&
677
- !this.matchesAnyImport(ref) &&
678
- !this.frameworks.some((f) => f.claimsReference?.(ref.referenceName))) {
874
+ // ArkTS chained-attribute refs carry a leading dot (`.titleStyle`) that
875
+ // routes them to the decorator-gated matcher; the symbol itself is
876
+ // indexed under the bare name, so the existence check strips the dot.
877
+ // Nix static path imports (`import ./x.nix`) name a FILE, not a symbol —
878
+ // they bypass the symbol-existence check and resolve via resolveViaImport.
879
+ const existenceName = ref.language === 'arkts' && ref.referenceName.startsWith('.')
880
+ ? ref.referenceName.slice(1)
881
+ : ref.referenceName;
882
+ const tPre = this.profileStages ? process.hrtime.bigint() : 0n;
883
+ const preFilterPass = (0, import_resolver_1.isNixPathImportRef)(ref) ||
884
+ this.hasAnyPossibleMatch(existenceName) ||
885
+ this.matchesAnyImport(ref) ||
886
+ this.frameworks.some((f) => f.claimsReference?.(ref.referenceName));
887
+ if (this.profileStages)
888
+ this.stageAdd('preFilter', ref, preFilterPass, tPre);
889
+ if (!preFilterPass) {
679
890
  return null;
680
891
  }
681
892
  // Function-as-value refs (#756) get a dedicated, strictly-gated path:
@@ -701,7 +912,10 @@ class ReferenceResolver {
701
912
  // JVM FQN imports skip framework/name-matcher: `import com.example.Bar`
702
913
  // resolves directly through the qualifiedName index, which is unambiguous
703
914
  // even when several `Bar` classes exist in different packages.
915
+ const tJvm = this.profileStages ? process.hrtime.bigint() : 0n;
704
916
  const jvmImport = (0, import_resolver_1.resolveJvmImport)(ref, this.context);
917
+ if (this.profileStages)
918
+ this.stageAdd('jvmImport', ref, !!jvmImport, tJvm);
705
919
  if (jvmImport)
706
920
  return jvmImport;
707
921
  // Razor/Blazor: a markup or `@code` type ref resolves through the file's
@@ -720,16 +934,49 @@ class ReferenceResolver {
720
934
  // JS → native `calls`) — `gateFrameworkLanguage` only drops a type/import
721
935
  // edge between two KNOWN families (see its doc), never a `calls` bridge or
722
936
  // a config↔code edge.
937
+ const tFw = this.profileStages ? process.hrtime.bigint() : 0n;
938
+ let fwEarly = null;
723
939
  for (const framework of this.frameworks) {
724
940
  const result = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
725
941
  if (result) {
726
- if (result.confidence >= 0.9)
727
- return result; // High confidence, return immediately
942
+ if (result.confidence >= 0.9) {
943
+ fwEarly = result; // High confidence, return immediately (below)
944
+ break;
945
+ }
728
946
  candidates.push(result);
729
947
  }
730
948
  }
949
+ if (this.profileStages)
950
+ this.stageAdd('frameworks', ref, fwEarly !== null, tFw);
951
+ if (fwEarly)
952
+ return fwEarly;
731
953
  // Strategy 2: Try import-based resolution
732
- const importResult = this.gateLanguage((0, import_resolver_1.resolveViaImport)(ref, this.context), ref);
954
+ const tImp = this.profileStages ? process.hrtime.bigint() : 0n;
955
+ let importResult = this.gateLanguage((0, import_resolver_1.resolveViaImport)(ref, this.context), ref);
956
+ // ArkTS:
957
+ // - `instantiates`: never via import — `import { Foo }` + a constructor
958
+ // seed would mint a `new Foo` edge for every false CFG constructor in
959
+ // that file (99k instantiates on scene_board). Same-file / unique-class
960
+ // exact-match in matchReference only.
961
+ // - dotted `Class.method`: import → static member (cross-module path;
962
+ // Strategy 1 is same-file only).
963
+ // - bare calls: only when the import local maps to a function/method.
964
+ if (importResult &&
965
+ ref.language === 'arkts' &&
966
+ (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates')) {
967
+ if (ref.referenceKind === 'instantiates') {
968
+ importResult = null;
969
+ }
970
+ else {
971
+ const target = this.queries.getNodeById(importResult.targetNodeId);
972
+ const ok = target &&
973
+ (target.kind === 'function' || target.kind === 'method');
974
+ if (!ok)
975
+ importResult = null;
976
+ }
977
+ }
978
+ if (this.profileStages)
979
+ this.stageAdd('viaImport', ref, !!importResult, tImp);
733
980
  if (importResult) {
734
981
  if (importResult.confidence >= 0.9)
735
982
  return importResult;
@@ -739,13 +986,43 @@ class ReferenceResolver {
739
986
  // If that didn't find the file, do NOT fall back to the symbol
740
987
  // name-matcher — it would mis-connect e.g. "inc/db.php" to an unrelated
741
988
  // db.php elsewhere in the tree (a wrong edge is worse than none, #660).
742
- if ((0, import_resolver_1.isPhpIncludePathRef)(ref)) {
989
+ // Terraform refs are directory-scoped by language semantics — the
990
+ // framework resolver IS the whole rulebook (`var.X` can never legally
991
+ // bind outside its module directory), so the name-matcher's
992
+ // qualified-name fallback would only ever add wrong cross-module edges.
993
+ // Nix static path imports are file references for the same reason —
994
+ // falling through would let "./x.nix" name-match an unrelated node.
995
+ if ((0, import_resolver_1.isPhpIncludePathRef)(ref) || (0, import_resolver_1.isCobolCopybookRef)(ref) || (0, import_resolver_1.isNixPathImportRef)(ref) || ref.language === 'terraform') {
743
996
  return candidates.length > 0
744
997
  ? candidates.reduce((best, curr) => curr.confidence > best.confidence ? curr : best)
745
998
  : null;
746
999
  }
747
1000
  // Strategy 3: Try name matching
748
- const nameResult = this.gateLanguage((0, name_matcher_1.matchReference)(ref, this.context), ref);
1001
+ const tName = this.profileStages ? process.hrtime.bigint() : 0n;
1002
+ let nameResult = this.gateLanguage((0, name_matcher_1.matchReference)(ref, this.context), ref);
1003
+ if (this.profileStages)
1004
+ this.stageAdd('nameMatch', ref, !!nameResult, tName);
1005
+ // Nix has no ambient cross-file namespace — a callee binds lexically
1006
+ // (same file) or through explicit import/callPackage wiring (the import
1007
+ // path above). A cross-file name match is wrong by construction: every
1008
+ // module `inherit (lib) mkOption`s the same nixpkgs helpers, so the
1009
+ // matcher would link each `mkOption` call to whichever file's inherit
1010
+ // binding it happened to pick. Same-file matches only.
1011
+ if (nameResult) {
1012
+ const target = this.queries.getNodeById(nameResult.targetNodeId);
1013
+ if (ref.language === 'nix') {
1014
+ if (!target || target.filePath !== ref.filePath) {
1015
+ nameResult = null;
1016
+ }
1017
+ }
1018
+ else if (target && target.language === 'nix') {
1019
+ // The reverse direction is just as impossible: no other language can
1020
+ // symbolically call into a .nix binding (interop is eval/CLI, never a
1021
+ // linkable symbol) — without this, a Python script's `split()` lands
1022
+ // on some module's `split = ...` binding as a low-confidence match.
1023
+ nameResult = null;
1024
+ }
1025
+ }
749
1026
  if (nameResult) {
750
1027
  candidates.push(nameResult);
751
1028
  }
@@ -758,6 +1035,15 @@ class ReferenceResolver {
758
1035
  CHAIN_SHAPE.test(ref.referenceName)) {
759
1036
  this.deferredChainRefs.push(ref);
760
1037
  }
1038
+ else if (
1039
+ // PHP `$this->prop->method()` (encoded `this->prop.method`): its method
1040
+ // may live on the property's declared supertype, resolvable only once
1041
+ // implements/extends edges exist — defer to the same conformance pass.
1042
+ ref.referenceKind === 'calls' &&
1043
+ ref.language === 'php' &&
1044
+ PHP_PROP_SHAPE.test(ref.referenceName)) {
1045
+ this.deferredChainRefs.push(ref);
1046
+ }
761
1047
  return null;
762
1048
  }
763
1049
  // Return highest confidence candidate
@@ -804,6 +1090,18 @@ class ReferenceResolver {
804
1090
  metadata: {
805
1091
  confidence: ref.confidence,
806
1092
  resolvedBy: ref.resolvedBy,
1093
+ // The ORIGINAL reference text (and kind, when edge-kind promotion
1094
+ // rewrote it — calls→instantiates, extends→implements,
1095
+ // function_ref→references). If this edge's target is later removed
1096
+ // by a re-index, the edge is resurrected as exactly this ref and
1097
+ // re-resolved (#1240 removal case) — a faithful resurrection, so
1098
+ // re-resolution can never bind anywhere a full re-index wouldn't.
1099
+ // Reconstruction from the target node's name instead would strip
1100
+ // receiver/qualifier context (`h.greet` → `greet`) and risk a
1101
+ // wrong rebind; edges without refName (pre-#1240, synthesized) are
1102
+ // deliberately NOT resurrected for the same reason.
1103
+ refName: ref.original.referenceName,
1104
+ ...(ref.original.referenceKind !== kind ? { refKind: ref.original.referenceKind } : {}),
807
1105
  // Uniform marker for function-as-value edges (#756), regardless of
808
1106
  // which strategy resolved them (import vs matchFunctionRef) — lets
809
1107
  // tooling label "callback registration" and lets validation diff
@@ -813,6 +1111,52 @@ class ReferenceResolver {
813
1111
  };
814
1112
  });
815
1113
  }
1114
+ /**
1115
+ * Split resolved refs into rows deletable by id and hand-built refs that
1116
+ * must fall back to the key-tuple delete. Rows loaded from the database
1117
+ * carry their row id and are deleted by exactly that id; the key tuple
1118
+ * omits line/col, so it also removes SIBLING rows — the same caller calling
1119
+ * the same callee at other lines — that a later batch hadn't attempted yet:
1120
+ * when a batch boundary split a caller's same-named call sites, the later
1121
+ * sites' edges were silently never created (#1269).
1122
+ */
1123
+ static partitionResolvedCleanup(resolved) {
1124
+ const rowIds = [];
1125
+ const legacyKeys = [];
1126
+ for (const r of resolved) {
1127
+ if (r.original.rowId != null)
1128
+ rowIds.push(r.original.rowId);
1129
+ else
1130
+ legacyKeys.push({
1131
+ fromNodeId: r.original.fromNodeId,
1132
+ referenceName: r.original.referenceName,
1133
+ referenceKind: r.original.referenceKind,
1134
+ });
1135
+ }
1136
+ return { rowIds, legacyKeys };
1137
+ }
1138
+ /**
1139
+ * Same row-id precision for parking unresolvable refs as status='failed'
1140
+ * (#1240): the key-tuple fallback would flip same-key sibling rows in later
1141
+ * batches to 'failed' before they were ever attempted, and resolution
1142
+ * outcome can differ per call site (receiver-type inference reads the
1143
+ * ref's line), so a sibling must not inherit this row's failure (#1269).
1144
+ */
1145
+ static partitionFailedCleanup(unresolved) {
1146
+ const byRowId = [];
1147
+ const legacyKeys = [];
1148
+ for (const r of unresolved) {
1149
+ if (r.rowId != null)
1150
+ byRowId.push({ rowId: r.rowId, referenceName: r.referenceName });
1151
+ else
1152
+ legacyKeys.push({
1153
+ fromNodeId: r.fromNodeId,
1154
+ referenceName: r.referenceName,
1155
+ referenceKind: r.referenceKind,
1156
+ });
1157
+ }
1158
+ return { byRowId, legacyKeys };
1159
+ }
816
1160
  /**
817
1161
  * Resolve and persist edges to database
818
1162
  */
@@ -826,11 +1170,61 @@ class ReferenceResolver {
826
1170
  }
827
1171
  // Clean up resolved refs from unresolved_refs table so metrics are accurate
828
1172
  if (result.resolved.length > 0) {
829
- this.queries.deleteSpecificResolvedReferences(result.resolved.map((r) => ({
830
- fromNodeId: r.original.fromNodeId,
831
- referenceName: r.original.referenceName,
832
- referenceKind: r.original.referenceKind,
833
- })));
1173
+ const { rowIds, legacyKeys } = ReferenceResolver.partitionResolvedCleanup(result.resolved);
1174
+ this.queries.deleteReferencesByRowIds(rowIds);
1175
+ this.queries.deleteSpecificResolvedReferences(legacyKeys);
1176
+ }
1177
+ // Park unresolvable refs as status='failed' — parity with
1178
+ // resolveAndPersistBatched. Deleting them was wrong (#1240): a ref whose
1179
+ // own file never changes is otherwise gone forever, so when a DIFFERENT
1180
+ // file later gains the export/symbol that would satisfy it, no sync can
1181
+ // recreate the edge — only a full re-index. Failed rows are excluded from
1182
+ // the pending readers, which preserves the #1187 orphan sweep's
1183
+ // invariant in status form: after a COMPLETED pass nothing it processed
1184
+ // is still 'pending', so any pending row at rest belongs to an
1185
+ // interrupted run and the sweep can key off the pending count.
1186
+ if (result.unresolved.length > 0) {
1187
+ const { byRowId, legacyKeys } = ReferenceResolver.partitionFailedCleanup(result.unresolved);
1188
+ this.queries.markReferencesFailedByRowIds(byRowId);
1189
+ this.queries.markReferencesFailed(legacyKeys);
1190
+ }
1191
+ return result;
1192
+ }
1193
+ /**
1194
+ * Yielding counterpart of {@link resolveAndPersist} for a caller-supplied
1195
+ * ref list — used by sync's failed-ref retry pass (#1240). Same persistence
1196
+ * semantics: resolved refs become edges and their rows are deleted;
1197
+ * still-unresolvable refs are (re-)marked failed (a no-op for rows already
1198
+ * in that status). Yields per-ref because sync can run on the daemon's
1199
+ * liveness-watchdog thread (#850/#1091) and a retry set is unbounded when
1200
+ * a large edit lands many popular symbol names at once.
1201
+ */
1202
+ async resolveAndPersistListYielding(refs) {
1203
+ const maybeYield = (0, cooperative_yield_1.createYielder)();
1204
+ const result = await this.resolveBatchYielding(refs, maybeYield);
1205
+ const PERSIST_CHUNK = 1000;
1206
+ const edges = this.createEdges(result.resolved);
1207
+ for (let i = 0; i < edges.length; i += PERSIST_CHUNK) {
1208
+ this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
1209
+ await maybeYield();
1210
+ }
1211
+ const resolvedCleanup = ReferenceResolver.partitionResolvedCleanup(result.resolved);
1212
+ for (let i = 0; i < resolvedCleanup.rowIds.length; i += PERSIST_CHUNK) {
1213
+ this.queries.deleteReferencesByRowIds(resolvedCleanup.rowIds.slice(i, i + PERSIST_CHUNK));
1214
+ await maybeYield();
1215
+ }
1216
+ for (let i = 0; i < resolvedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
1217
+ this.queries.deleteSpecificResolvedReferences(resolvedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
1218
+ await maybeYield();
1219
+ }
1220
+ const failedCleanup = ReferenceResolver.partitionFailedCleanup(result.unresolved);
1221
+ for (let i = 0; i < failedCleanup.byRowId.length; i += PERSIST_CHUNK) {
1222
+ this.queries.markReferencesFailedByRowIds(failedCleanup.byRowId.slice(i, i + PERSIST_CHUNK));
1223
+ await maybeYield();
1224
+ }
1225
+ for (let i = 0; i < failedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
1226
+ this.queries.markReferencesFailed(failedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
1227
+ await maybeYield();
834
1228
  }
835
1229
  return result;
836
1230
  }
@@ -862,11 +1256,15 @@ class ReferenceResolver {
862
1256
  const maybeYield = (0, cooperative_yield_1.createYielder)();
863
1257
  const resolved = [];
864
1258
  for (const ref of deferred) {
865
- // `::`-receiver languages (Rust) split on `::` (matchScopedCallChain);
1259
+ // PHP `this->prop.method` resolves via matchMethodCall (declared-type
1260
+ // inference + resolveMethodOnType conformance walk); `::`-receiver
1261
+ // languages (Rust) split on `::` (matchScopedCallChain); other
866
1262
  // dotted-receiver languages on `.` (matchDottedCallChain).
867
- const chainMatch = SCOPED_CHAIN_LANGUAGES.has(ref.language)
868
- ? (0, name_matcher_1.matchScopedCallChain)(ref, this.context)
869
- : (0, name_matcher_1.matchDottedCallChain)(ref, this.context);
1263
+ const chainMatch = (ref.language === 'php' && PHP_PROP_SHAPE.test(ref.referenceName))
1264
+ ? (0, name_matcher_1.matchMethodCall)(ref, this.context)
1265
+ : SCOPED_CHAIN_LANGUAGES.has(ref.language)
1266
+ ? (0, name_matcher_1.matchScopedCallChain)(ref, this.context)
1267
+ : (0, name_matcher_1.matchDottedCallChain)(ref, this.context);
870
1268
  const match = this.gateLanguage(chainMatch, ref);
871
1269
  if (match)
872
1270
  resolved.push(match);
@@ -882,45 +1280,198 @@ class ReferenceResolver {
882
1280
  return edges.length;
883
1281
  }
884
1282
  /**
885
- * Resolve one batch in smaller sub-chunks, yielding to the event loop between
886
- * them so the #850 liveness heartbeat can fire on a slow/dense batch (#1091).
887
- * Behaviourally identical to a single `resolveAll(batch)`: `warmCaches()` is
888
- * idempotent (guarded) and `resolveOne` is independent per ref, so splitting
889
- * and re-merging changes only timing, never which edges get created. Falls
890
- * through to a plain `resolveAll` when the batch is already small.
1283
+ * Resolve one batch with a yield checkpoint between EVERY ref so the #850
1284
+ * liveness heartbeat can fire on a slow/dense batch (#1091). The checkpoint
1285
+ * granularity is per-ref not per-N-refs because per-ref cost is unbounded
1286
+ * in the worst case (a collision-heavy method name whose candidate set misses
1287
+ * the LRU re-fetches tens of thousands of rows): any fixed N multiplies that
1288
+ * worst case into the watchdog window, which is how v1.2.0 still got killed
1289
+ * at "Resolving refs" on large Java monorepos (#1122). `maybeYield()` is a
1290
+ * ~ns time check when under budget, so per-ref checkpoints cost nothing.
1291
+ * Behaviourally identical to `resolveAll(batch)`: `warmCaches()` is
1292
+ * idempotent (guarded) and `resolveOne` is independent per ref, so yielding
1293
+ * between refs changes only timing, never which edges get created.
891
1294
  */
892
- async resolveBatchYielding(batch, maybeYield, subChunkSize = 500) {
893
- if (batch.length <= subChunkSize)
894
- return this.resolveAll(batch);
1295
+ async resolveBatchYielding(batch, maybeYield) {
1296
+ this.warmCaches();
1297
+ this.advanceSupertypeGeneration();
895
1298
  const resolved = [];
896
1299
  const unresolved = [];
897
1300
  const byMethod = {};
898
- let total = 0;
899
- let resolvedCount = 0;
900
- let unresolvedCount = 0;
901
- for (let i = 0; i < batch.length; i += subChunkSize) {
902
- const chunk = this.resolveAll(batch.slice(i, i + subChunkSize));
903
- for (const r of chunk.resolved)
904
- resolved.push(r);
905
- for (const u of chunk.unresolved)
906
- unresolved.push(u);
907
- total += chunk.stats.total;
908
- resolvedCount += chunk.stats.resolved;
909
- unresolvedCount += chunk.stats.unresolved;
910
- for (const [m, c] of Object.entries(chunk.stats.byMethod)) {
911
- byMethod[m] = (byMethod[m] || 0) + c;
1301
+ for (const raw of batch) {
1302
+ const ref = {
1303
+ fromNodeId: raw.fromNodeId,
1304
+ referenceName: raw.referenceName,
1305
+ referenceKind: raw.referenceKind,
1306
+ line: raw.line,
1307
+ column: raw.column,
1308
+ filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId),
1309
+ language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId),
1310
+ rowId: raw.rowId,
1311
+ };
1312
+ const result = this.resolveOneTimed(ref);
1313
+ if (result) {
1314
+ resolved.push(result);
1315
+ byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
912
1316
  }
913
- await maybeYield();
1317
+ else {
1318
+ unresolved.push(ref);
1319
+ }
1320
+ // Fast-path the per-ref yield check: awaiting the async no-op costs a
1321
+ // microtask hop per ref, which dominates at ~10⁵ refs (see MaybeYield).
1322
+ const y = maybeYield();
1323
+ if (y)
1324
+ await y;
1325
+ }
1326
+ return {
1327
+ resolved,
1328
+ unresolved,
1329
+ stats: {
1330
+ total: batch.length,
1331
+ resolved: resolved.length,
1332
+ unresolved: unresolved.length,
1333
+ byMethod,
1334
+ },
1335
+ };
1336
+ }
1337
+ /**
1338
+ * Resolve a list of refs and return everything the ADMISSION side needs to
1339
+ * persist the outcome: resolutions, failures, the deferred post-pass refs
1340
+ * this run produced (drained, so the caller owns routing them), and stats.
1341
+ * This is the resolver-worker entry point — it runs the exact per-ref loop
1342
+ * of resolveBatchYielding, minus the main-thread yields (worker threads have
1343
+ * no watchdog heartbeat to starve). Results are in input order.
1344
+ */
1345
+ /**
1346
+ * HOMEGRAPH_RESOLVE_PROFILE=1: per-outcome wall-clock histogram of
1347
+ * resolveOne, keyed by the winning strategy (`resolvedBy`) or
1348
+ * `fail:<referenceKind>` — the §7a.2 "profile the per-ref path" probe. The
1349
+ * kernel-scale batch loop is ~430s and CORE-INVARIANT (835.9s pooled-4-on-8
1350
+ * ≈ 812.5s sequential-on-2 for the whole superphase), so the next lever is
1351
+ * which CLASS of ref the time belongs to, not more parallelism. Off by
1352
+ * default: the hrtime pair costs ~100ns/ref only when the env is set.
1353
+ */
1354
+ resolveProfile = process.env.HOMEGRAPH_RESOLVE_PROFILE ? new Map() : null;
1355
+ /**
1356
+ * HOMEGRAPH_RESOLVE_PROFILE=2 additionally attributes time to the
1357
+ * STRATEGIES inside resolveOne (`stage:<name>|<refKind>|hit/miss` rows in
1358
+ * the same histogram) — i.e. WHICH machinery a failing class of refs pays
1359
+ * for, not just that it fails. =1 keeps the per-outcome rows only.
1360
+ */
1361
+ profileStages = process.env.HOMEGRAPH_RESOLVE_PROFILE === '2';
1362
+ stageAdd(stage, ref, hit, t0) {
1363
+ if (!this.resolveProfile)
1364
+ return;
1365
+ const dt = process.hrtime.bigint() - t0;
1366
+ const key = `stage:${stage}|${ref.referenceKind}|${hit ? 'hit' : 'miss'}`;
1367
+ const slot = this.resolveProfile.get(key);
1368
+ if (slot) {
1369
+ slot.n++;
1370
+ slot.ns += dt;
1371
+ }
1372
+ else {
1373
+ this.resolveProfile.set(key, { n: 1, ns: dt });
914
1374
  }
915
- return { resolved, unresolved, stats: { total, resolved: resolvedCount, unresolved: unresolvedCount, byMethod } };
1375
+ }
1376
+ resolveOneTimed(ref) {
1377
+ if (!this.resolveProfile)
1378
+ return this.resolveOne(ref);
1379
+ const t0 = process.hrtime.bigint();
1380
+ const result = this.resolveOne(ref);
1381
+ const dt = process.hrtime.bigint() - t0;
1382
+ const key = result ? result.resolvedBy : `fail:${ref.referenceKind}`;
1383
+ const slot = this.resolveProfile.get(key);
1384
+ if (slot) {
1385
+ slot.n++;
1386
+ slot.ns += dt;
1387
+ }
1388
+ else {
1389
+ this.resolveProfile.set(key, { n: 1, ns: dt });
1390
+ }
1391
+ return result;
1392
+ }
1393
+ /** Dump the HOMEGRAPH_RESOLVE_PROFILE histogram to stderr (no-op when off). */
1394
+ dumpResolveProfile(label) {
1395
+ if (!this.resolveProfile || this.resolveProfile.size === 0)
1396
+ return;
1397
+ const rows = [...this.resolveProfile.entries()]
1398
+ .map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1000000n) }))
1399
+ .sort((a, b) => b.ms - a.ms);
1400
+ for (const r of rows) {
1401
+ console.error(`[resolve-profile] ${label} ${r.k}: n=${r.n} total=${(r.ms / 1000).toFixed(1)}s avg=${((r.ms * 1000) / Math.max(1, r.n)).toFixed(0)}µs`);
1402
+ }
1403
+ // =2 only: this thread's matchReference sub-stage table rides along.
1404
+ (0, name_matcher_1.dumpNameMatcherProfile)(label);
1405
+ }
1406
+ resolveListForAdmission(refs) {
1407
+ this.warmCaches();
1408
+ this.advanceSupertypeGeneration();
1409
+ const resolved = [];
1410
+ const unresolved = [];
1411
+ const byMethod = {};
1412
+ for (const raw of refs) {
1413
+ const ref = {
1414
+ fromNodeId: raw.fromNodeId,
1415
+ referenceName: raw.referenceName,
1416
+ referenceKind: raw.referenceKind,
1417
+ line: raw.line,
1418
+ column: raw.column,
1419
+ filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId),
1420
+ language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId),
1421
+ rowId: raw.rowId,
1422
+ };
1423
+ const result = this.resolveOneTimed(ref);
1424
+ if (result) {
1425
+ resolved.push(result);
1426
+ byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
1427
+ }
1428
+ else {
1429
+ unresolved.push(ref);
1430
+ }
1431
+ }
1432
+ return {
1433
+ resolved,
1434
+ unresolved,
1435
+ deferredChain: this.deferredChainRefs.splice(0),
1436
+ deferredThisMember: this.deferredThisMemberRefs.splice(0),
1437
+ byMethod,
1438
+ };
1439
+ }
1440
+ /**
1441
+ * The resolver's live ResolutionContext — resolver-pool workers use it to
1442
+ * run synthesis passes against their own read-only connection.
1443
+ */
1444
+ getResolutionContext() {
1445
+ return this.context;
1446
+ }
1447
+ /**
1448
+ * Re-queue deferred post-pass refs produced by resolver workers, preserving
1449
+ * their admission order so resolveChainedCallsViaConformance /
1450
+ * resolveDeferredThisMemberRefs process them exactly as the sequential path
1451
+ * would have.
1452
+ */
1453
+ appendDeferredFromWorkers(deferredChain, deferredThisMember) {
1454
+ this.deferredChainRefs.push(...deferredChain);
1455
+ this.deferredThisMemberRefs.push(...deferredThisMember);
916
1456
  }
917
1457
  /**
918
1458
  * Resolve and persist in batches to keep memory bounded.
919
1459
  * Processes unresolved references in chunks, persisting edges and cleaning
920
1460
  * up resolved refs after each batch to avoid accumulating large arrays.
921
1461
  */
922
- async resolveAndPersistBatched(onProgress, batchSize = 5000) {
923
- this.warmCaches();
1462
+ async resolveAndPersistBatched(onProgress, batchSize = 5000, onSynthesisProgress,
1463
+ // When provided, big batches fan out across a read-only resolver-worker
1464
+ // pool with results admitted in canonical order (see resolver-pool.ts).
1465
+ // Sequential fallback on any pool failure. HOMEGRAPH_NO_PARALLEL_RESOLVE=1
1466
+ // disables entirely. bulkEdgeLoad hooks (when provided) bracket the batch
1467
+ // loop with drop/recreate of the non-unique edge indexes on big runs —
1468
+ // see DatabaseConnection.beginBulkEdgeLoad. backpressure (when provided)
1469
+ // is the WAL valve's writer-side backstop (WalCheckpointValve.backpressure):
1470
+ // called at pool-idle boundaries so a full backfill can actually complete —
1471
+ // the valve's timer-driven passive passes stay perpetually partial against
1472
+ // the pool's continuous reads, which is how a kernel-scale resolution grew
1473
+ // a 22GB WAL on a 4.6GB DB (migration plan §7a.1).
1474
+ parallel) {
924
1475
  // Resolution runs on the indexer's MAIN thread, and the #850 liveness
925
1476
  // watchdog SIGKILLs a process whose event loop stalls past its window (60s
926
1477
  // by default). A single dense batch's resolveAll — or the synthesis pass
@@ -928,6 +1479,20 @@ class ReferenceResolver {
928
1479
  // (#1091). A shared yielder lets both give the watchdog heartbeat a regular
929
1480
  // window to fire; see ./cooperative-yield.
930
1481
  const maybeYield = (0, cooperative_yield_1.createYielder)();
1482
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS) {
1483
+ console.error(`[pool-timing] backpressure hook: ${parallel?.backpressure ? 'present' : 'absent'}`);
1484
+ }
1485
+ // HOMEGRAPH_RESOLVE_PROFILE loop-stage attribution: the §7a.2 kernel-scale
1486
+ // histogram showed resolveOne owns only ~93s of the ~436s batch loop —
1487
+ // these counters name where the other ~340s goes (reads, edge build+insert,
1488
+ // deletes/marks, the per-batch count guard).
1489
+ const loopProf = process.env.HOMEGRAPH_RESOLVE_PROFILE
1490
+ ? { read: 0, settle: 0, backpressure: 0, recycle: 0, createEdges: 0, insertEdges: 0, deletes: 0, marks: 0, countGuard: 0 }
1491
+ : null;
1492
+ const lp = (k, t0) => { if (loopProf)
1493
+ loopProf[k] = (loopProf[k] ?? 0) + (Date.now() - t0); };
1494
+ let tLp = 0;
1495
+ await this.warmCachesYielding(maybeYield);
931
1496
  const total = this.queries.getUnresolvedReferencesCount();
932
1497
  let processed = 0;
933
1498
  const aggregateStats = {
@@ -936,74 +1501,370 @@ class ReferenceResolver {
936
1501
  unresolved: 0,
937
1502
  byMethod: {},
938
1503
  };
939
- // Process in batches. We always read from offset 0 because resolved refs
940
- // are deleted after each batch, shifting the remaining rows forward.
1504
+ // Parallel pool, started immediately but never awaited up front: early
1505
+ // batches run sequentially while the workers boot (module load + readonly
1506
+ // DB open + framework detect + cache warm ≈ hundreds of ms), and the loop
1507
+ // switches to fan-out the moment the pool reports ready — so pool boot
1508
+ // costs zero wall-clock. Any failure downgrades to sequential permanently.
1509
+ let pool = null;
1510
+ let poolReady = false;
1511
+ // True once pool creation has been attempted by EITHER engage site (the
1512
+ // up-front ref-count gate or the adaptive projection below) — a pool that
1513
+ // failed or was destroyed must stay down (downgrade is permanent), and
1514
+ // tryCreate's sizing probes shouldn't re-run every batch on hosts that
1515
+ // declined.
1516
+ let poolEngageTried = false;
1517
+ const createPool = (t0, why) => {
1518
+ poolEngageTried = true;
1519
+ if (!parallel)
1520
+ return null;
1521
+ const p = resolver_pool_1.ResolverPool.tryCreate(parallel.dbPath, this.projectRoot);
1522
+ p?.ready().then(() => {
1523
+ poolReady = true;
1524
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1525
+ console.error(`[pool-timing] pool ready after ${Date.now() - t0}ms (${why})`);
1526
+ }, () => {
1527
+ void p.destroy().catch(() => undefined);
1528
+ if (pool === p)
1529
+ pool = null;
1530
+ });
1531
+ return p;
1532
+ };
1533
+ if (parallel && total >= (0, resolver_pool_1.minRefsForPool)()) {
1534
+ pool = createPool(Date.now(), 'ref-count');
1535
+ }
1536
+ // Adaptive engagement bar (see the batch-loop hook): projected remaining
1537
+ // sequential settle above this boots the pool mid-loop. Boot is async and
1538
+ // fan-out waits for ready, so a marginal engage costs background boot
1539
+ // only; the bar just needs to clear the fan-out's own overhead class.
1540
+ const ADAPTIVE_ENGAGE_SETTLE_MS = 400;
1541
+ let adaptiveSeqMs = 0;
1542
+ let adaptiveSeqRefs = 0;
1543
+ // Process in PIPELINED batches (double-buffer). The enumeration is the
1544
+ // head of the pending set in rowid order; every ref a persisted batch
1545
+ // processed leaves the pending set (resolved rows are deleted,
1546
+ // unresolvable ones flip to status='failed'), shifting the remaining
1547
+ // pending rows forward.
941
1548
  let prevRemaining = Number.POSITIVE_INFINITY;
942
- while (true) {
943
- const batch = this.queries.getUnresolvedReferencesBatch(0, batchSize);
944
- if (batch.length === 0)
945
- break;
946
- const result = await this.resolveBatchYielding(batch, maybeYield);
947
- // Persist edges immediately
948
- const edges = this.createEdges(result.resolved);
949
- if (edges.length > 0) {
950
- this.queries.insertEdges(edges);
1549
+ // Cadence for the worker connection recycling below — ~8 batches
1550
+ // 40k refs between recycles keeps the WAL shallow at kernel scale
1551
+ // while a small sync never recycles at all. (25 recovered only half
1552
+ // the write tax — the WAL re-deepened between recycles; reopens are
1553
+ // sub-millisecond so the shorter cadence is ~free.)
1554
+ const RECYCLE_EVERY_BATCHES = 8;
1555
+ let batchesSinceRecycle = 0;
1556
+ // Begin one batch: fan out to the pool when it's ready and the batch is
1557
+ // big enough — workers then resolve batch k+1 WHILE the main thread
1558
+ // persists batch k (persist measured at ~58% of resolution wall on a
1559
+ // 255k-ref repo, all of it previously spent with the pool idle).
1560
+ // Sequential batches stay lazy: they run on the main thread at settle
1561
+ // time, where an early start would only contend with the persist.
1562
+ const beginBatch = (batch) => {
1563
+ if (pool && poolReady && resolver_pool_1.ResolverPool.worthParallel(batch.length)) {
1564
+ return {
1565
+ mode: 'pool',
1566
+ settled: pool.resolveBatch(batch).then((out) => ({ ok: true, out }), (err) => ({ ok: false, err })),
1567
+ };
951
1568
  }
952
- // Clean up resolved refs so they don't appear in the next batch
953
- if (result.resolved.length > 0) {
954
- this.queries.deleteSpecificResolvedReferences(result.resolved.map((r) => ({
955
- fromNodeId: r.original.fromNodeId,
956
- referenceName: r.original.referenceName,
957
- referenceKind: r.original.referenceKind,
958
- })));
959
- }
960
- // Delete unresolvable refs from this batch to avoid re-processing them
961
- if (result.unresolved.length > 0) {
962
- this.queries.deleteSpecificResolvedReferences(result.unresolved.map((r) => ({
963
- fromNodeId: r.fromNodeId,
964
- referenceName: r.referenceName,
965
- referenceKind: r.referenceKind,
966
- })));
1569
+ return { mode: 'seq' };
1570
+ };
1571
+ // Settle an in-flight batch to a ResolutionResult. Deferred post-pass refs
1572
+ // are appended HERE, in loop order — never inside the fan-out promise — so
1573
+ // admission order stays exactly the sequential order even while a later
1574
+ // batch resolves concurrently. A pool failure downgrades to sequential
1575
+ // permanently and re-resolves this batch on the main thread.
1576
+ const settleBatch = async (inFlight, batch) => {
1577
+ if (inFlight.mode === 'pool') {
1578
+ const settled = await inFlight.settled;
1579
+ if (settled.ok) {
1580
+ this.appendDeferredFromWorkers(settled.out.deferredChain, settled.out.deferredThisMember);
1581
+ return {
1582
+ resolved: settled.out.resolved,
1583
+ unresolved: settled.out.unresolved,
1584
+ stats: {
1585
+ total: batch.length,
1586
+ resolved: settled.out.resolved.length,
1587
+ unresolved: settled.out.unresolved.length,
1588
+ byMethod: settled.out.byMethod,
1589
+ },
1590
+ };
1591
+ }
1592
+ (0, errors_1.logDebug)('Parallel resolution failed; falling back to sequential', {
1593
+ error: settled.err instanceof Error ? settled.err.message : String(settled.err),
1594
+ });
1595
+ if (pool)
1596
+ await pool.destroy().catch(() => undefined);
1597
+ pool = null;
967
1598
  }
968
- // Aggregate stats
969
- aggregateStats.total += result.stats.total;
970
- aggregateStats.resolved += result.stats.resolved;
971
- aggregateStats.unresolved += result.stats.unresolved;
972
- for (const [method, count] of Object.entries(result.stats.byMethod)) {
973
- aggregateStats.byMethod[method] = (aggregateStats.byMethod[method] || 0) + count;
1599
+ return this.resolveBatchYielding(batch, maybeYield);
1600
+ };
1601
+ // Bulk edge load: on big runs, drop the non-unique edge indexes for the
1602
+ // duration of the batch loop (the identity index stays — OR IGNORE dedup
1603
+ // and the source-keyed supertype-walk reads both live on it). Recreated in
1604
+ // the inner finally BEFORE synthesis, whose passes read kind-keyed.
1605
+ // Measured on a 224k-edge resolution set: insert 2.8s → 1.1s + 0.3s
1606
+ // recreate. Same ref-count gate as the pool so small syncs never pay the
1607
+ // recreate cost.
1608
+ let bulkEdgesActive = false;
1609
+ if (parallel?.bulkEdgeLoad && total >= (0, resolver_pool_1.minRefsForPool)()) {
1610
+ try {
1611
+ parallel.bulkEdgeLoad.begin();
1612
+ bulkEdgesActive = true;
974
1613
  }
975
- processed += batch.length;
976
- onProgress?.(processed, total);
977
- // Yield so progress UI can render between batches
978
- await new Promise(resolve => setImmediate(resolve));
979
- // If nothing was resolved or removed in this batch, we'd loop forever
980
- // on the same rows. Break to avoid infinite loop.
981
- if (result.resolved.length === 0 && result.unresolved.length === batch.length) {
982
- break;
1614
+ catch { /* keep the indexes; inserts just pay the per-row maintenance */ }
1615
+ }
1616
+ // Same gate for the ref-index window: the loop's deletes stop maintaining
1617
+ // the five sync-path unresolved_refs indexes, and the end-of-loop rebuild
1618
+ // is near-free (only failed refs survive the loop).
1619
+ let bulkRefsActive = false;
1620
+ if (parallel?.refIndexLoad && total >= (0, resolver_pool_1.minRefsForPool)()) {
1621
+ try {
1622
+ parallel.refIndexLoad.begin();
1623
+ bulkRefsActive = true;
983
1624
  }
984
- // Non-progress guard (defense-in-depth). Because we re-read from offset 0
985
- // each pass, the unresolved_refs table MUST shrink every iteration — both
986
- // resolved and unresolved refs are deleted above. If it didn't shrink, a
987
- // resolver returned a match whose `original.referenceName` differs from the
988
- // stored row, so the keyed delete no-ops, and we'd re-read + re-resolve +
989
- // re-insert the same rows forever (the runaway that grew a 99-file repo to
990
- // 5M edges / 1.4 GB before the Go-fallback fix). Stop rather than grow the
991
- // graph without bound.
992
- const remaining = this.queries.getUnresolvedReferencesCount();
993
- if (remaining >= prevRemaining)
994
- break;
995
- prevRemaining = remaining;
1625
+ catch { /* keep the indexes; deletes just pay the per-row maintenance */ }
996
1626
  }
997
- // Dynamic-edge synthesis: now that all base `calls` edges are persisted,
998
- // synthesize observer/callback dispatch edges (dispatcher → registered
999
- // callbacks) that static parsing leaves out. Best-effort — never fail the
1000
- // index on it. See docs/design/callback-edge-synthesis.md.
1001
1627
  try {
1002
- aggregateStats.byMethod['callback-synthesis'] = await (0, callback_synthesizer_1.synthesizeCallbackEdges)(this.queries, this.context);
1628
+ try {
1629
+ tLp = Date.now();
1630
+ let batch = this.queries.getUnresolvedReferencesBatchAfter(0, batchSize);
1631
+ lp('read', tLp);
1632
+ let inFlight = batch.length > 0 ? beginBatch(batch) : null;
1633
+ while (batch.length > 0 && inFlight) {
1634
+ // Prefetch the NEXT batch before this one persists: this batch's rows
1635
+ // are still pending (nothing has mutated the table since they were
1636
+ // read), so seeking past this batch's last row id in the same rowid
1637
+ // enumeration yields the following batch (keyset — OFFSET re-walked the
1638
+ // accumulated failed prefix every read, 54.6s at kernel scale, §7a.2).
1639
+ tLp = Date.now();
1640
+ const nextBatch = this.queries.getUnresolvedReferencesBatchAfter(batch[batch.length - 1].rowId, batchSize);
1641
+ lp('read', tLp);
1642
+ const tBatch = Date.now();
1643
+ const result = await settleBatch(inFlight, batch);
1644
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1645
+ console.error(`[pool-timing] batch ${inFlight.mode}: ${batch.length} refs in ${Date.now() - tBatch}ms`);
1646
+ lp('settle', tBatch);
1647
+ // Adaptive pool engagement: the fixed ref-count gate can't see PER-REF
1648
+ // cost, and settle rates differ ~9× by language (56k Rust refs cost
1649
+ // more sequential settle than 154k Go refs — 36µs vs 4µs measured on
1650
+ // tokio/prometheus). After each sequential batch, project the remaining
1651
+ // settle from the observed rate and boot the pool mid-loop when it
1652
+ // clears the bar. The loop already switches to fan-out only when the
1653
+ // async boot reports ready, admission order is mode-independent, and
1654
+ // 2-core/low-memory hosts still decline inside tryCreate's sizing —
1655
+ // so the switch changes wall-clock, never the graph.
1656
+ if (inFlight.mode === 'seq' && parallel && pool === null && !poolEngageTried) {
1657
+ adaptiveSeqMs += Date.now() - tBatch;
1658
+ adaptiveSeqRefs += batch.length;
1659
+ const remaining = total - processed - batch.length;
1660
+ const projectedMs = (adaptiveSeqMs / Math.max(1, adaptiveSeqRefs)) * Math.max(0, remaining);
1661
+ if (projectedMs >= ADAPTIVE_ENGAGE_SETTLE_MS) {
1662
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS) {
1663
+ console.error(`[pool-timing] adaptive engage: projected ${Math.round(projectedMs)}ms sequential settle over ${remaining} remaining refs`);
1664
+ }
1665
+ pool = createPool(Date.now(), 'adaptive');
1666
+ }
1667
+ }
1668
+ // WAL-valve backstop at the ONE pool-idle boundary of the double-buffer
1669
+ // (this batch settled, the next not yet fanned out): past the hard cap
1670
+ // the writer parks for a full backfill here, where the pool's readers
1671
+ // are all between statements — so the backfill completes, readers
1672
+ // re-enter at SQLite's backfilled mark, and the next persist commit
1673
+ // WRAPS the WAL instead of growing it. No-op (one fstat) under the cap.
1674
+ tLp = Date.now();
1675
+ const bp = parallel?.backpressure?.();
1676
+ if (bp)
1677
+ await bp;
1678
+ lp('backpressure', tLp);
1679
+ // Recycle the workers' read connections periodically at this same
1680
+ // worker-idle boundary (batch k settled, batch k+1 not yet fanned
1681
+ // out): a long-lived reader pins WAL checkpoint progress, and the
1682
+ // deep WAL that accumulates behind it taxes the writer's OWN page
1683
+ // operations — the §7a.6 writes-under-readers finding (deletes
1684
+ // 42.6s → 118.8s from 0 to 4 attached readers; an aggressive valve
1685
+ // recovered the writes but paid +129s in full-park folds). Releasing
1686
+ // the read marks every ~25 batches lets the existing checkpoints
1687
+ // advance instead, at ~milliseconds of reopen cost. A failed recycle
1688
+ // downgrades to sequential permanently, same as a failed fan-out.
1689
+ if (pool && poolReady && ++batchesSinceRecycle >= RECYCLE_EVERY_BATCHES) {
1690
+ batchesSinceRecycle = 0;
1691
+ tLp = Date.now();
1692
+ try {
1693
+ await pool.recycleWorkers();
1694
+ }
1695
+ catch (err) {
1696
+ (0, errors_1.logDebug)('Worker connection recycle failed; falling back to sequential', {
1697
+ error: err instanceof Error ? err.message : String(err),
1698
+ });
1699
+ await pool.destroy().catch(() => undefined);
1700
+ pool = null;
1701
+ }
1702
+ lp('recycle', tLp);
1703
+ }
1704
+ // Persist in bounded sub-transactions with yields between: a whole
1705
+ // batch's edge insert / keyed deletes are otherwise one solid
1706
+ // synchronous span each on a multi-GB index, sitting BETWEEN the
1707
+ // per-ref yields — the last unyielded stretch of the resolution loop.
1708
+ // Crash semantics are unchanged (already several transactions): edges
1709
+ // land before their refs are deleted, so a kill mid-way re-resolves
1710
+ // the remainder idempotently on the next run/sweep (#1187).
1711
+ const PERSIST_CHUNK = 1000;
1712
+ const tPersist = Date.now();
1713
+ // Persist edges BEFORE fanning out the next batch: later batches read
1714
+ // this batch's edges — resolveMethodOnType walks supertype chains over
1715
+ // `extends`/`implements` edges that earlier batches resolved, so a
1716
+ // receiver typed as a subclass only reaches a method declared on its
1717
+ // base class if those edges are visible. (Validated on dubbo: fanning
1718
+ // out first downgraded exactly those supertype-method resolutions from
1719
+ // the 0.9 typed-receiver path to the 0.65 word-overlap fallback.)
1720
+ tLp = Date.now();
1721
+ const edges = this.createEdges(result.resolved);
1722
+ lp('createEdges', tLp);
1723
+ tLp = Date.now();
1724
+ for (let i = 0; i < edges.length; i += PERSIST_CHUNK) {
1725
+ this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
1726
+ await maybeYield();
1727
+ }
1728
+ lp('insertEdges', tLp);
1729
+ // NOW fan the next batch out — workers see exactly the edge state the
1730
+ // sequential baseline would (every batch ≤ this one committed), while
1731
+ // the main thread spends the REST of the persist (ref deletes + failed
1732
+ // parking below) overlapped with their resolution — the double-buffer.
1733
+ const nextInFlight = nextBatch.length > 0 ? beginBatch(nextBatch) : null;
1734
+ // Clean up resolved refs so they don't appear in the next batch —
1735
+ // by row id, so a same-key sibling ref in a LATER batch (same caller
1736
+ // calling the same callee at another line) is left pending for its own
1737
+ // attempt instead of being swept out with this batch's rows (#1269).
1738
+ tLp = Date.now();
1739
+ let removedThisBatch = 0;
1740
+ const resolvedCleanup = ReferenceResolver.partitionResolvedCleanup(result.resolved);
1741
+ for (let i = 0; i < resolvedCleanup.rowIds.length; i += PERSIST_CHUNK) {
1742
+ removedThisBatch += this.queries.deleteReferencesByRowIds(resolvedCleanup.rowIds.slice(i, i + PERSIST_CHUNK));
1743
+ await maybeYield();
1744
+ }
1745
+ for (let i = 0; i < resolvedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
1746
+ removedThisBatch += this.queries.deleteSpecificResolvedReferences(resolvedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
1747
+ await maybeYield();
1748
+ }
1749
+ lp('deletes', tLp);
1750
+ // Park unresolvable refs from this batch as status='failed' so they
1751
+ // leave the pending set (the batch reader and non-progress guard below
1752
+ // only see pending rows) but stay retryable when a later sync adds a
1753
+ // symbol that could satisfy them (#1240).
1754
+ tLp = Date.now();
1755
+ const failedCleanup = ReferenceResolver.partitionFailedCleanup(result.unresolved);
1756
+ for (let i = 0; i < failedCleanup.byRowId.length; i += PERSIST_CHUNK) {
1757
+ removedThisBatch += this.queries.markReferencesFailedByRowIds(failedCleanup.byRowId.slice(i, i + PERSIST_CHUNK));
1758
+ await maybeYield();
1759
+ }
1760
+ for (let i = 0; i < failedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
1761
+ removedThisBatch += this.queries.markReferencesFailed(failedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
1762
+ await maybeYield();
1763
+ }
1764
+ lp('marks', tLp);
1765
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1766
+ console.error(`[pool-timing] batch persist: ${Date.now() - tPersist}ms`);
1767
+ // Aggregate stats
1768
+ aggregateStats.total += result.stats.total;
1769
+ aggregateStats.resolved += result.stats.resolved;
1770
+ aggregateStats.unresolved += result.stats.unresolved;
1771
+ for (const [method, count] of Object.entries(result.stats.byMethod)) {
1772
+ aggregateStats.byMethod[method] = (aggregateStats.byMethod[method] || 0) + count;
1773
+ }
1774
+ processed += batch.length;
1775
+ onProgress?.(processed, total);
1776
+ // Yield so progress UI can render between batches
1777
+ await new Promise(resolve => setImmediate(resolve));
1778
+ // NOTE: there used to be an extra early break here when a batch resolved
1779
+ // nothing (`result.unresolved.length === batch.length`). That was wrong:
1780
+ // an all-unresolvable batch still DELETES its rows (progress), yet the
1781
+ // break abandoned every batch after it in the same run — on a repo whose
1782
+ // first 5000 refs are all external/stdlib calls, resolution stopped at
1783
+ // batch one and left the rest of the table as permanent orphans (#1187).
1784
+ // The count-based guard below catches the true no-progress case.
1785
+ // Non-progress guard (defense-in-depth). Each iteration enumerates from
1786
+ // the head of the pending set, so the PENDING population MUST shrink
1787
+ // every iteration — resolved refs are deleted and unresolvable ones are
1788
+ // marked failed above, and both leave the pending set the batch reader
1789
+ // sees. If it didn't shrink, a resolver returned a match whose
1790
+ // `original.referenceName` differs from the stored row, so the keyed
1791
+ // delete/update no-ops, and we'd re-read + re-resolve + re-insert the
1792
+ // same rows forever (the runaway that grew a 99-file repo to 5M edges /
1793
+ // 1.4 GB before the Go-fallback fix). Stop rather than grow the graph
1794
+ // without bound. (An in-flight prefetched batch is abandoned unsettled —
1795
+ // fan-out has no side effects until settleBatch appends its results.)
1796
+ // Non-progress signal, now O(1): `changes` summed across this batch's
1797
+ // deletes + failed-parks is the DIRECT evidence the guard's old count
1798
+ // diff inferred — a resolver returning a mismatched name makes the keyed
1799
+ // cleanup no-op, which shows up here as zero removals. The per-batch
1800
+ // COUNT(*) it replaces walked every remaining pending row — O(N²/batch)
1801
+ // over a run, 93.9s of the kernel-scale batch loop (§7a.2). A REAL count
1802
+ // runs only on the suspicious path (claimed-work batch removed nothing —
1803
+ // e.g. every row was a sibling a legacy-key sweep already consumed),
1804
+ // where it arbitrates stop-vs-continue exactly as before.
1805
+ if (removedThisBatch <= 0 && batch.length > 0) {
1806
+ tLp = Date.now();
1807
+ const remaining = this.queries.getUnresolvedReferencesCount();
1808
+ lp('countGuard', tLp);
1809
+ if (remaining >= prevRemaining)
1810
+ break;
1811
+ prevRemaining = remaining;
1812
+ }
1813
+ // Advance the pipeline: the prefetched batch (already fanned out when
1814
+ // the pool is on) becomes the current one.
1815
+ batch = nextBatch;
1816
+ inFlight = nextInFlight;
1817
+ }
1818
+ }
1819
+ finally {
1820
+ // Recreate the edge indexes BEFORE synthesis (kind-keyed reads) and on
1821
+ // any error path. A crash before this line is healed by the next
1822
+ // DatabaseConnection open (schema.sql re-applies IF NOT EXISTS).
1823
+ if (bulkRefsActive) {
1824
+ const tRef = Date.now();
1825
+ await parallel.refIndexLoad.end();
1826
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1827
+ console.error(`[phase-timing] ref-index-recreate: ${Date.now() - tRef}ms`);
1828
+ }
1829
+ if (bulkEdgesActive) {
1830
+ const tIdx = Date.now();
1831
+ await parallel.bulkEdgeLoad.end();
1832
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1833
+ console.error(`[phase-timing] edge-index-recreate: ${Date.now() - tIdx}ms`);
1834
+ // The recreate just wrote every non-unique edge index into the WAL
1835
+ // (multi-GB at kernel scale) with the pool idle — fold before the
1836
+ // synthesis passes pin readers against it for minutes.
1837
+ const bp = parallel?.backpressure?.();
1838
+ if (bp)
1839
+ await bp;
1840
+ }
1841
+ }
1842
+ // Dynamic-edge synthesis: now that all base `calls` edges are persisted,
1843
+ // synthesize observer/callback dispatch edges (dispatcher → registered
1844
+ // callbacks) that static parsing leaves out. Best-effort — never fail the
1845
+ // index on it. The pool (when it survived resolution) is REUSED to fan the
1846
+ // independent passes across its read-only workers — that's why its destroy
1847
+ // lives in the finally below, after synthesis, not at the end of the batch
1848
+ // loop. See docs/design/callback-edge-synthesis.md.
1849
+ const tSynth = Date.now();
1850
+ try {
1851
+ aggregateStats.byMethod['callback-synthesis'] = await (0, callback_synthesizer_1.synthesizeCallbackEdges)(this.queries, this.context, onSynthesisProgress, pool, parallel?.backpressure);
1852
+ }
1853
+ catch {
1854
+ // synthesis is additive and optional; ignore failures
1855
+ }
1856
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
1857
+ console.error(`[phase-timing] callback-synthesis: ${Date.now() - tSynth}ms`);
1858
+ }
1859
+ finally {
1860
+ if (pool)
1861
+ await pool.destroy().catch(() => undefined);
1003
1862
  }
1004
- catch {
1005
- // synthesis is additive and optional; ignore failures
1863
+ if (loopProf) {
1864
+ const parts = Object.entries(loopProf).map(([k, v]) => `${k}=${(v / 1000).toFixed(1)}s`).join(' ');
1865
+ console.error(`[resolve-profile] loop-stages ${parts}`);
1006
1866
  }
1867
+ this.dumpResolveProfile('main');
1007
1868
  return {
1008
1869
  resolved: [],
1009
1870
  unresolved: [],
@@ -1022,11 +1883,18 @@ class ReferenceResolver {
1022
1883
  isBuiltInOrExternal(ref) {
1023
1884
  const name = ref.referenceName;
1024
1885
  const isJsTs = ref.language === 'typescript' || ref.language === 'javascript'
1025
- || ref.language === 'tsx' || ref.language === 'jsx';
1886
+ || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'arkts';
1026
1887
  // JavaScript/TypeScript built-ins
1027
1888
  if (isJsTs && JS_BUILT_INS.has(name)) {
1028
1889
  return true;
1029
1890
  }
1891
+ // ArkTS resource-reference intrinsics — `$r('app.string.x')` /
1892
+ // `$rawfile('x.png')` are framework-provided and appear dozens of times
1893
+ // per UI file; without this they can resolve to a stray same-named
1894
+ // symbol (e.g. a checked-in hvigor wrapper's `$r`).
1895
+ if (ref.language === 'arkts' && (name === '$r' || name === '$rawfile')) {
1896
+ return true;
1897
+ }
1030
1898
  // Common JS/TS library calls (console.log, Math.floor, JSON.parse)
1031
1899
  if (isJsTs && (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.'))) {
1032
1900
  return true;
@@ -1191,6 +2059,96 @@ class ReferenceResolver {
1191
2059
  const target = found.values().next().value;
1192
2060
  return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
1193
2061
  }
2062
+ /**
2063
+ * Resolve a CFML inheritance reference written as a component path (#1152).
2064
+ * Two forms exist in real code:
2065
+ *
2066
+ * - Dotted: `extends="coldbox.system.web.Controller"` — dots are directory
2067
+ * separators from the webroot or a CFML mapping. Mappings live in server
2068
+ * config / Application.cfc, so the leading segments may not exist in the
2069
+ * repo at all (in the coldbox repo itself the path is `system/web/
2070
+ * Controller.cfc` — the `coldbox.` root IS the repo). Matched by final
2071
+ * segment (the class), corroborated right-to-left against the candidate's
2072
+ * parent directories.
2073
+ * - Relative: `extends="../base"` / `extends="./base"` (the FW/1 style) —
2074
+ * resolved against the referencing file's own directory.
2075
+ *
2076
+ * Conservative by design: a candidate needs at least one corroborating
2077
+ * directory segment (a dotted path whose only same-named class sits in an
2078
+ * unrelated directory is almost always an out-of-repo library supertype —
2079
+ * mxunit/testbox/coldbox-as-dependency), and a corroboration tie yields no
2080
+ * edge. Directory comparison is case-insensitive (CFML path resolution is);
2081
+ * the class segment itself is matched exactly, which real code satisfies —
2082
+ * dotted paths are written to match the on-disk file name.
2083
+ */
2084
+ resolveCfmlComponentPath(ref) {
2085
+ const cfmlCandidates = (name) => this.context
2086
+ .getNodesByName(name)
2087
+ .filter((n) => (n.kind === 'class' || n.kind === 'interface') &&
2088
+ (n.language === 'cfml' || n.language === 'cfscript'));
2089
+ const norm = (p) => p.replace(/\\/g, '/').toLowerCase();
2090
+ // Relative-path form: `../base`, `./base`, `sub/thing` — resolve against
2091
+ // the referencing file's directory and require an exact (case-insensitive)
2092
+ // file match.
2093
+ if (ref.referenceName.includes('/')) {
2094
+ const rel = ref.referenceName.replace(/\.cfc$/i, '');
2095
+ const fromDir = ref.filePath.replace(/\\/g, '/').split('/').slice(0, -1);
2096
+ const parts = [...fromDir];
2097
+ for (const seg of rel.split('/')) {
2098
+ if (seg === '' || seg === '.')
2099
+ continue;
2100
+ if (seg === '..') {
2101
+ if (parts.length === 0)
2102
+ return null; // escapes the project root
2103
+ parts.pop();
2104
+ }
2105
+ else {
2106
+ parts.push(seg);
2107
+ }
2108
+ }
2109
+ const wantPath = norm(parts.join('/') + '.cfc');
2110
+ const className = parts[parts.length - 1];
2111
+ if (!className)
2112
+ return null;
2113
+ const target = cfmlCandidates(className).find((c) => norm(c.filePath) === wantPath);
2114
+ return target
2115
+ ? { original: ref, targetNodeId: target.id, confidence: 0.95, resolvedBy: 'file-path' }
2116
+ : null;
2117
+ }
2118
+ // Dotted form.
2119
+ const segments = ref.referenceName.split('.').map((s) => s.trim()).filter(Boolean);
2120
+ if (segments.length < 2)
2121
+ return null;
2122
+ const className = segments[segments.length - 1];
2123
+ const dirSegments = segments.slice(0, -1);
2124
+ let best = null;
2125
+ let bestScore = 0;
2126
+ let tie = false;
2127
+ for (const cand of cfmlCandidates(className)) {
2128
+ const dirs = cand.filePath.replace(/\\/g, '/').split('/').slice(0, -1);
2129
+ // Count matching directory segments right-to-left: for
2130
+ // `coldbox.system.web.Controller` vs `system/web/Controller.cfc`,
2131
+ // `web` and `system` match, then the repo root ends the run → score 2.
2132
+ let score = 0;
2133
+ while (score < dirSegments.length &&
2134
+ score < dirs.length &&
2135
+ dirSegments[dirSegments.length - 1 - score].toLowerCase() ===
2136
+ dirs[dirs.length - 1 - score].toLowerCase()) {
2137
+ score++;
2138
+ }
2139
+ if (score > bestScore) {
2140
+ best = cand;
2141
+ bestScore = score;
2142
+ tie = false;
2143
+ }
2144
+ else if (score === bestScore && score > 0) {
2145
+ tie = true;
2146
+ }
2147
+ }
2148
+ if (!best || bestScore === 0 || tie)
2149
+ return null;
2150
+ return { original: ref, targetNodeId: best.id, confidence: 0.9, resolvedBy: 'qualified-name' };
2151
+ }
1194
2152
  /**
1195
2153
  * Resolve a `this.<member>` function-as-value reference (#756/#808) to the
1196
2154
  * ENCLOSING CLASS's own member — never a same-named symbol elsewhere. The