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
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SYNTH_PROGRESS_STEPS = exports.SYNTH_PASSES = void 0;
3
4
  exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
4
5
  const generated_detection_1 = require("../extraction/generated-detection");
5
6
  const strip_comments_1 = require("./strip-comments");
6
7
  const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
7
8
  const goframe_synthesizer_1 = require("./goframe-synthesizer");
8
- const cooperative_yield_1 = require("./cooperative-yield");
9
9
  const arkts_entry_1 = require("./frameworks/arkts-entry");
10
+ const cooperative_yield_1 = require("./cooperative-yield");
10
11
  const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
11
12
  const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
12
13
  const MAX_CALLBACKS_PER_CHANNEL = 40;
@@ -41,6 +42,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
41
42
  const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
42
43
  const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
43
44
  const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
45
+ // The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
46
+ // trailing-closure syntax, so ONLY those languages can ever contribute a
47
+ // dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
48
+ // Swift dispatcher's field name) would be a wrong edge, not a missed one.
49
+ // Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
50
+ // in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
51
+ // where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
52
+ // 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
53
+ const CC_LANGUAGES = new Set(['swift', 'kotlin']);
44
54
  function kebabToPascal(s) {
45
55
  return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
46
56
  }
@@ -76,6 +86,35 @@ function sliceLines(content, startLine, endLine) {
76
86
  return null;
77
87
  return content.split('\n').slice(startLine - 1, endLine).join('\n');
78
88
  }
89
+ /**
90
+ * Per-match line resolver over `src`, 1-based at `baseLine`. The inline
91
+ * `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
92
+ * which goes quadratic on a match-dense source (a generated function full of
93
+ * `.push(` calls re-scanned tens of thousands of times was most of the #1235
94
+ * indexing wedge). Builds the newline index once — lazily, since most sources
95
+ * never produce a match — then answers each call with a binary search.
96
+ */
97
+ function makeLineAt(src, baseLine) {
98
+ let nl = null;
99
+ return (idx) => {
100
+ if (!nl) {
101
+ nl = [];
102
+ for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
103
+ nl.push(i);
104
+ }
105
+ // Count newlines strictly before idx.
106
+ let lo = 0;
107
+ let hi = nl.length;
108
+ while (lo < hi) {
109
+ const mid = (lo + hi) >> 1;
110
+ if (nl[mid] < idx)
111
+ lo = mid + 1;
112
+ else
113
+ hi = mid;
114
+ }
115
+ return baseLine + lo;
116
+ };
117
+ }
79
118
  function registrarField(src) {
80
119
  const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
81
120
  return m ? m[1] : null;
@@ -213,13 +252,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
213
252
  arr.push({ node, line: absLine });
214
253
  registrars.set(field, arr);
215
254
  };
216
- // Slices EVERY method/function's source (no cheap name-gate), so on a repo
217
- // with a huge file this is the heaviest synthesis pass — yield mid-scan so it
218
- // can't wedge the #850 watchdog on its own (#1091).
255
+ // Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
256
+ // on a repo with a huge file this is the heaviest synthesis pass — yield
257
+ // mid-scan (and mid-match-loop below: a single generated function dense with
258
+ // matches must not starve the watchdog either) so it can't wedge the #850
259
+ // watchdog on its own (#1091, #1235).
219
260
  let scanned = 0;
261
+ let matchTick = 0;
220
262
  for (const m of methodAndFunctionNodes(queries)) {
221
263
  if ((++scanned & 127) === 0)
222
264
  await onYield();
265
+ if (!CC_LANGUAGES.has(m.language))
266
+ continue;
223
267
  const content = ctx.readFile(m.filePath);
224
268
  const src = content && sliceLines(content, m.startLine, m.endLine);
225
269
  if (!src)
@@ -228,11 +272,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
228
272
  const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
229
273
  if (!hasForEach && !hasAppend)
230
274
  continue;
231
- const lineAt = (idx) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1;
275
+ const lineAt = makeLineAt(src, m.startLine ?? 1);
232
276
  if (hasForEach) {
233
277
  CC_DISPATCH_RE.lastIndex = 0;
234
278
  let d;
235
279
  while ((d = CC_DISPATCH_RE.exec(src))) {
280
+ if ((++matchTick & 255) === 0)
281
+ await onYield();
236
282
  const arr = dispatchers.get(d[1]) ?? [];
237
283
  if (!arr.some((n) => n.node.id === m.id))
238
284
  arr.push({ node: m, line: lineAt(d.index) });
@@ -242,12 +288,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
242
288
  if (hasAppend) {
243
289
  CC_APPEND_WRITE_RE.lastIndex = 0;
244
290
  let w;
245
- while ((w = CC_APPEND_WRITE_RE.exec(src)))
291
+ while ((w = CC_APPEND_WRITE_RE.exec(src))) {
292
+ if ((++matchTick & 255) === 0)
293
+ await onYield();
246
294
  addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
295
+ }
247
296
  CC_APPEND_DIRECT_RE.lastIndex = 0;
248
297
  let a;
249
- while ((a = CC_APPEND_DIRECT_RE.exec(src)))
298
+ while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
299
+ if ((++matchTick & 255) === 0)
300
+ await onYield();
250
301
  addReg(a[1], m, lineAt(a.index));
302
+ }
251
303
  }
252
304
  }
253
305
  const edges = [];
@@ -277,10 +329,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
277
329
  }
278
330
  /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
279
331
  async function eventEmitterEdges(ctx, onYield) {
332
+ let scannedFiles = 0;
280
333
  const emitsByEvent = new Map(); // event → dispatcher node ids
281
334
  const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
282
335
  let scanned = 0;
283
336
  for (const file of ctx.getAllFiles()) {
337
+ if ((++scannedFiles & 15) === 0)
338
+ await onYield();
284
339
  if ((++scanned & 255) === 0)
285
340
  await onYield(); // #1091: yield mid-scan on huge graphs
286
341
  const content = ctx.readFile(file);
@@ -291,7 +346,7 @@ async function eventEmitterEdges(ctx, onYield) {
291
346
  if (!hasEmit && !hasOn)
292
347
  continue;
293
348
  const nodesInFile = ctx.getNodesInFile(file);
294
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
349
+ const lineOf = makeLineAt(content, 1);
295
350
  if (hasEmit) {
296
351
  EMIT_RE.lastIndex = 0;
297
352
  let m;
@@ -354,10 +409,32 @@ async function eventEmitterEdges(ctx, onYield) {
354
409
  * `this.setState`). Over-approximation (all setState methods reach render) is
355
410
  * accepted — it's reachability-correct, like the callback channels.
356
411
  */
357
- function reactRenderEdges(queries, ctx) {
412
+ async function reactRenderEdges(queries, ctx, onYield) {
413
+ let scanned255 = 0;
358
414
  const edges = [];
359
415
  const seen = new Set();
360
- for (const cls of queries.getNodesByKind('class')) {
416
+ // A class can only emit here if it CONTAINS a method named `render` — so one
417
+ // indexed name lookup bounds the candidate set up front, and the class scan
418
+ // below skips everything else before its per-class edge/node queries. On a
419
+ // repo with few/no render methods (any non-React codebase) this collapses
420
+ // the pass from every-class fan-out to ~zero DB work, with identical output:
421
+ // the skipped classes fail the same `render` check today, just after paying
422
+ // for their children. (Not a language gate: `render` + `this.setState(` in
423
+ // Java — e.g. Litho — legitimately matches today and still does.)
424
+ const renderOwners = new Set();
425
+ for (const n of ctx.getNodesByName('render')) {
426
+ if (n.kind !== 'method')
427
+ continue;
428
+ for (const e of queries.getIncomingEdges(n.id, ['contains']))
429
+ renderOwners.add(e.source);
430
+ }
431
+ if (renderOwners.size === 0)
432
+ return edges;
433
+ for (const cls of queries.iterateNodesByKind('class')) {
434
+ if ((++scanned255 & 63) === 0)
435
+ await onYield();
436
+ if (!renderOwners.has(cls.id))
437
+ continue;
361
438
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
362
439
  .map((e) => queries.getNodeById(e.target))
363
440
  .filter((n) => !!n && n.kind === 'method');
@@ -397,10 +474,13 @@ function reactRenderEdges(queries, ctx) {
397
474
  * body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
398
475
  * Flutter State classes. Over-approximation accepted (reachability-correct).
399
476
  */
400
- function flutterBuildEdges(queries, ctx) {
477
+ async function flutterBuildEdges(queries, ctx, onYield) {
478
+ let scanned255 = 0;
401
479
  const edges = [];
402
480
  const seen = new Set();
403
- for (const cls of queries.getNodesByKind('class')) {
481
+ for (const cls of queries.iterateNodesByKind('class')) {
482
+ if ((++scanned255 & 63) === 0)
483
+ await onYield();
404
484
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
405
485
  .map((e) => queries.getNodeById(e.target))
406
486
  .filter((n) => !!n && n.kind === 'method');
@@ -431,6 +511,279 @@ function flutterBuildEdges(queries, ctx) {
431
511
  }
432
512
  return edges;
433
513
  }
514
+ /**
515
+ * Reactive ArkUI property decorators: assigning a property carrying one of
516
+ * these re-runs the owning struct's `build()`. Covers both state models —
517
+ * V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
518
+ * (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
519
+ * the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
520
+ * data classes, not struct properties).
521
+ */
522
+ const ARKUI_REACTIVE_DECORATORS = new Set([
523
+ 'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
524
+ 'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
525
+ 'Local', 'Provider', 'Consumer',
526
+ ]);
527
+ /** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
528
+ const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
529
+ /**
530
+ * Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
531
+ * flutter-build). Assigning a reactive-decorated property (`@State count`,
532
+ * `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
533
+ * hop is framework-internal — no static edge — so "onClick → markAllDone →
534
+ * this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
535
+ * for each arkts struct with a `build()` method and at least one reactive
536
+ * property, link every sibling method whose body ASSIGNS (or array-mutates)
537
+ * one of those properties → `build`. Assignment-gated on the struct's OWN
538
+ * reactive property names — a method that merely reads state, or a struct
539
+ * with no reactive properties, gets nothing (this is the precision line the
540
+ * all-sibling-methods design would erase).
541
+ */
542
+ async function arkuiStateBuildEdges(queries, ctx, onYield) {
543
+ let scanned255 = 0;
544
+ const edges = [];
545
+ const seen = new Set();
546
+ for (const struct of queries.iterateNodesByKind('struct')) {
547
+ if ((++scanned255 & 63) === 0)
548
+ await onYield();
549
+ if (struct.language !== 'arkts')
550
+ continue;
551
+ const children = queries.getOutgoingEdges(struct.id, ['contains'])
552
+ .map((e) => queries.getNodeById(e.target))
553
+ .filter((n) => !!n);
554
+ const build = children.find((n) => n.kind === 'method' && n.name === 'build');
555
+ if (!build)
556
+ continue;
557
+ const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
558
+ if (reactiveProps.length === 0)
559
+ continue;
560
+ const propAlternation = reactiveProps
561
+ .map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
562
+ .join('|');
563
+ // `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
564
+ // `=(?!=)` keeps `this.done == x` comparisons out.
565
+ const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
566
+ let added = 0;
567
+ for (const m of children) {
568
+ if (added >= MAX_CALLBACKS_PER_CHANNEL)
569
+ break;
570
+ if (m.kind !== 'method' || m.id === build.id)
571
+ continue;
572
+ const content = ctx.readFile(m.filePath);
573
+ const src = content && sliceLines(content, m.startLine, m.endLine);
574
+ if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
575
+ continue;
576
+ const key = `${m.id}>${build.id}`;
577
+ if (seen.has(key))
578
+ continue;
579
+ seen.add(key);
580
+ edges.push({
581
+ source: m.id, target: build.id, kind: 'calls', line: m.startLine,
582
+ provenance: 'heuristic',
583
+ metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
584
+ });
585
+ added++;
586
+ }
587
+ }
588
+ return edges;
589
+ }
590
+ /** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
591
+ const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
592
+ /** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
593
+ const ARKUI_EMITTER_FANOUT_CAP = 8;
594
+ /**
595
+ * Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
596
+ * bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
597
+ * framework-internal, so an order flow riding it (OrangeShopping's
598
+ * add-to-cart) dead-ends at the emit. Link emit-site enclosing
599
+ * function/method → on/once-site enclosing function/method when both
600
+ * reference the SAME statically-recoverable event key.
601
+ *
602
+ * Key recovery, per call site (comment-stripped enclosing-file source): the
603
+ * first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
604
+ * a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
605
+ * level. Precision scoping learned from the samples monorepo (thousands of
606
+ * unrelated samples, most using eventId 1): NUMERIC keys pair within the same
607
+ * FILE only; NAMED keys pair within the same workspace module directory (or
608
+ * the whole project when it declares no modules — the single-app case), both
609
+ * behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
610
+ * handling — their bodies' calls already attribute to the registering method,
611
+ * so targeting that method keeps the chain connected.
612
+ */
613
+ async function arkuiEmitterEdges(ctx, onYield) {
614
+ let scannedFiles = 0;
615
+ // bucket key -> emit sites / handler sites
616
+ const emits = new Map();
617
+ const handlers = new Map();
618
+ const moduleDirs = (() => {
619
+ const ws = ctx.getWorkspacePackages?.();
620
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
621
+ })();
622
+ const moduleScopeOf = (file) => {
623
+ for (const dir of moduleDirs) {
624
+ if (file === dir || file.startsWith(dir + '/'))
625
+ return dir;
626
+ }
627
+ return '';
628
+ };
629
+ for (const file of ctx.getAllFiles()) {
630
+ if ((++scannedFiles & 15) === 0)
631
+ await onYield();
632
+ if (!file.endsWith('.ets'))
633
+ continue;
634
+ const content = ctx.readFile(file);
635
+ if (!content || !content.includes('emitter.'))
636
+ continue;
637
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
638
+ const nodes = ctx.getNodesInFile(file)
639
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
640
+ ARKUI_EMITTER_CALL_RE.lastIndex = 0;
641
+ let m;
642
+ while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
643
+ const verb = m[1];
644
+ const arg = m[2].trim();
645
+ const line = safe.slice(0, m.index).split('\n').length;
646
+ const encl = nodes
647
+ .filter((n) => n.startLine <= line && n.endLine >= line)
648
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
649
+ if (!encl)
650
+ continue;
651
+ // Recover the event key from the first argument.
652
+ let key = null;
653
+ const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
654
+ const token = idLit ?? arg;
655
+ if (token !== undefined) {
656
+ if (/^\d+$/.test(token)) {
657
+ key = `num:${file}:${token}`; // numeric: same-file only
658
+ }
659
+ else if (token.includes('.')) {
660
+ key = `name:${moduleScopeOf(file)}:${token}`;
661
+ }
662
+ else {
663
+ // Local variable — chase its same-file declaration one level:
664
+ // `let x = new EventsId(K)` / `const x = K`.
665
+ const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
666
+ const decl = safe.match(declRe);
667
+ const inner = (decl?.[1] ?? decl?.[2])?.trim();
668
+ if (inner && /^\d+$/.test(inner))
669
+ key = `num:${file}:${inner}`;
670
+ else if (inner && /^[\w$.]+$/.test(inner))
671
+ key = `name:${moduleScopeOf(file)}:${inner}`;
672
+ }
673
+ }
674
+ if (!key)
675
+ continue;
676
+ const site = { nodeId: encl.id, file, line };
677
+ if (verb === 'emit') {
678
+ (emits.get(key) ?? emits.set(key, []).get(key)).push(site);
679
+ }
680
+ else {
681
+ (handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
682
+ }
683
+ }
684
+ }
685
+ const edges = [];
686
+ const seen = new Set();
687
+ for (const [key, emitSites] of emits) {
688
+ const handlerSites = handlers.get(key);
689
+ if (!handlerSites)
690
+ continue;
691
+ if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
692
+ continue;
693
+ const eventLabel = key.slice(key.lastIndexOf(':') + 1);
694
+ for (const e of emitSites)
695
+ for (const h of handlerSites) {
696
+ if (e.nodeId === h.nodeId)
697
+ continue;
698
+ const dedupe = `${e.nodeId}>${h.nodeId}`;
699
+ if (seen.has(dedupe))
700
+ continue;
701
+ seen.add(dedupe);
702
+ edges.push({
703
+ source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
704
+ provenance: 'heuristic',
705
+ metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
706
+ });
707
+ }
708
+ }
709
+ return edges;
710
+ }
711
+ /** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
712
+ const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
713
+ /**
714
+ * Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
715
+ * 'pages/Detail' })` reaches the `@Entry struct` of
716
+ * `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
717
+ * static edge — so "tap → openDetail → ???" ends at the router call. Bridge
718
+ * literal urls to the page struct: the url resolves against the standard
719
+ * `src/main/ets/` layout (what main_pages.json entries name); candidates
720
+ * prefer the caller's own workspace module (routes are module-scoped), and
721
+ * anything still ambiguous is dropped rather than guessed. Only `@Entry`
722
+ * structs qualify as targets — the decorator is what makes a file a page.
723
+ */
724
+ async function arkuiRouterEdges(ctx, onYield) {
725
+ const edges = [];
726
+ const seen = new Set();
727
+ const allFiles = ctx.getAllFiles();
728
+ const moduleDirs = (() => {
729
+ const ws = ctx.getWorkspacePackages?.();
730
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
731
+ })();
732
+ const moduleScopeOf = (file) => {
733
+ for (const dir of moduleDirs) {
734
+ if (file === dir || file.startsWith(dir + '/'))
735
+ return dir;
736
+ }
737
+ return '';
738
+ };
739
+ let scannedFiles = 0;
740
+ for (const file of allFiles) {
741
+ if ((++scannedFiles & 15) === 0)
742
+ await onYield();
743
+ if (!file.endsWith('.ets'))
744
+ continue;
745
+ const content = ctx.readFile(file);
746
+ if (!content || !content.includes('router.'))
747
+ continue;
748
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
749
+ const nodes = ctx.getNodesInFile(file)
750
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
751
+ ARKUI_ROUTER_RE.lastIndex = 0;
752
+ let m;
753
+ while ((m = ARKUI_ROUTER_RE.exec(safe))) {
754
+ const url = m[1];
755
+ const line = safe.slice(0, m.index).split('\n').length;
756
+ const encl = nodes
757
+ .filter((n) => n.startLine <= line && n.endLine >= line)
758
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
759
+ if (!encl)
760
+ continue;
761
+ const suffix = `/src/main/ets/${url}.ets`;
762
+ let candidates = allFiles.filter((f) => f.endsWith(suffix));
763
+ if (candidates.length > 1) {
764
+ const scope = moduleScopeOf(file);
765
+ const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
766
+ if (sameModule.length > 0)
767
+ candidates = sameModule;
768
+ }
769
+ if (candidates.length !== 1)
770
+ continue; // ambiguous or unresolved — never guess
771
+ const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
772
+ if (!page)
773
+ continue;
774
+ const key = `${encl.id}>${page.id}`;
775
+ if (seen.has(key))
776
+ continue;
777
+ seen.add(key);
778
+ edges.push({
779
+ source: encl.id, target: page.id, kind: 'calls', line,
780
+ provenance: 'heuristic',
781
+ metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
782
+ });
783
+ }
784
+ }
785
+ return edges;
786
+ }
434
787
  /**
435
788
  * Phase 4c: C++ virtual override. A call through a base/interface pointer
436
789
  * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
@@ -441,14 +794,17 @@ function flutterBuildEdges(queries, ctx) {
441
794
  * implementation(s). Over-approximation accepted (reachability-correct); capped
442
795
  * per class and gated to C++ to avoid touching other languages' dispatch.
443
796
  */
444
- function cppOverrideEdges(queries) {
797
+ async function cppOverrideEdges(queries, onYield) {
798
+ let scanned255 = 0;
445
799
  const edges = [];
446
800
  const seen = new Set();
447
801
  const methodsOf = (classId) => queries
448
802
  .getOutgoingEdges(classId, ['contains'])
449
803
  .map((e) => queries.getNodeById(e.target))
450
804
  .filter((n) => !!n && n.kind === 'method');
451
- for (const cls of queries.getNodesByKind('class')) {
805
+ for (const cls of queries.iterateNodesByKind('class')) {
806
+ if ((++scanned255 & 63) === 0)
807
+ await onYield();
452
808
  const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
453
809
  if (subMethods.length === 0)
454
810
  continue;
@@ -502,6 +858,7 @@ function cppOverrideEdges(queries) {
502
858
  // or an `object` (Scala) so the loop also iterates those kinds.
503
859
  const IFACE_OVERRIDE_LANGS = new Set([
504
860
  'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
861
+ 'arkts',
505
862
  ]);
506
863
  /**
507
864
  * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
@@ -515,7 +872,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
515
872
  * with the other dispatch synthesizers; capped per interface. Empty interfaces
516
873
  * (`any`) are skipped so they don't match every struct.
517
874
  */
518
- function goImplementsEdges(queries) {
875
+ async function goImplementsEdges(queries, onYield) {
876
+ let scanned255 = 0;
519
877
  const edges = [];
520
878
  const seen = new Set();
521
879
  const methodNameSet = (id) => new Set(queries
@@ -523,11 +881,24 @@ function goImplementsEdges(queries) {
523
881
  .map((e) => queries.getNodeById(e.target))
524
882
  .filter((n) => !!n && n.kind === 'method')
525
883
  .map((n) => n.name));
526
- const goStructs = queries.getNodesByKind('struct').filter((s) => s.language === 'go');
884
+ // Materializes GO structs only (the pass is language-gated by the caller),
885
+ // never the whole struct kind — that array is O(nodes) on struct-heavy
886
+ // repos like the Linux kernel (#1212).
887
+ const goStructs = [];
888
+ for (const s of queries.iterateNodesByKind('struct')) {
889
+ if ((++scanned255 & 63) === 0)
890
+ await onYield();
891
+ if (s.language === 'go')
892
+ goStructs.push(s);
893
+ }
527
894
  const structMethods = new Map();
528
895
  for (const s of goStructs)
529
896
  structMethods.set(s.id, methodNameSet(s.id));
530
- for (const iface of queries.getNodesByKind('interface')) {
897
+ for (const iface of queries.iterateNodesByKind('interface')) {
898
+ if ((++scanned255 & 63) === 0)
899
+ await onYield();
900
+ if ((++scanned255 & 63) === 0)
901
+ await onYield();
531
902
  if (iface.language !== 'go')
532
903
  continue;
533
904
  const want = methodNameSet(iface.id);
@@ -587,7 +958,8 @@ function goImplementsEdges(queries) {
587
958
  * matching the same-file edges extraction already emits). Skips methods that
588
959
  * already have a type parent (the same-file case). (#583, cross-file half)
589
960
  */
590
- function goCrossFileMethodContainsEdges(queries) {
961
+ async function goCrossFileMethodContainsEdges(queries, onYield) {
962
+ let scanned255 = 0;
591
963
  const edges = [];
592
964
  const seen = new Set();
593
965
  const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
@@ -595,7 +967,11 @@ function goCrossFileMethodContainsEdges(queries) {
595
967
  const i = p.replace(/\\/g, '/').lastIndexOf('/');
596
968
  return i >= 0 ? p.slice(0, i) : '';
597
969
  };
598
- for (const method of queries.getNodesByKind('method')) {
970
+ for (const method of queries.iterateNodesByKind('method')) {
971
+ if ((++scanned255 & 63) === 0)
972
+ await onYield();
973
+ if ((++scanned255 & 63) === 0)
974
+ await onYield();
599
975
  if (method.language !== 'go')
600
976
  continue;
601
977
  // The receiver type is encoded in the method's qualifiedName as `Recv::name`
@@ -665,13 +1041,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
665
1041
  function kmpKindsCompatible(a, b) {
666
1042
  return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
667
1043
  }
668
- function kotlinExpectActualEdges(queries) {
1044
+ async function kotlinExpectActualEdges(queries, onYield) {
1045
+ let scanned255 = 0;
669
1046
  const edges = [];
670
1047
  const seen = new Set();
671
- const actuals = queries
672
- .getAllNodes()
673
- .filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
674
- for (const act of actuals) {
1048
+ // SQL-side language+decorator pre-filter, streamed. The old
1049
+ // `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
1050
+ // just to find kotlin `actual` declarations — on a 2M-node graph that alone
1051
+ // exceeded Node's default heap and killed the index (#1212). The LIKE
1052
+ // pre-filter can over-match (substring), so the exact decorator check stays.
1053
+ for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
1054
+ if ((++scanned255 & 63) === 0)
1055
+ await onYield();
1056
+ if (!act.decorators?.includes('actual'))
1057
+ continue;
675
1058
  let added = 0;
676
1059
  for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
677
1060
  if (added >= MAX_CALLBACKS_PER_CHANNEL)
@@ -705,23 +1088,43 @@ function kotlinExpectActualEdges(queries) {
705
1088
  }
706
1089
  return edges;
707
1090
  }
708
- function interfaceOverrideEdges(queries) {
1091
+ async function interfaceOverrideEdges(queries, onYield) {
1092
+ let scanned255 = 0;
709
1093
  const edges = [];
710
1094
  const seen = new Set();
711
- const methodsOf = (classId) => queries
712
- .getOutgoingEdges(classId, ['contains'])
713
- .map((e) => queries.getNodeById(e.target))
714
- .filter((n) => !!n && n.kind === 'method');
1095
+ // Memoized: a popular base interface's method list is otherwise re-fetched
1096
+ // once per implementer (dubbo-style hub interfaces have hundreds), and the
1097
+ // memo only ever serves reads. Same rows, same order — byte-identical.
1098
+ const methodsMemo = new Map();
1099
+ const methodsOf = (classId) => {
1100
+ const hit = methodsMemo.get(classId);
1101
+ if (hit)
1102
+ return hit;
1103
+ const methods = queries
1104
+ .getOutgoingEdges(classId, ['contains'])
1105
+ .map((e) => queries.getNodeById(e.target))
1106
+ .filter((n) => !!n && n.kind === 'method');
1107
+ methodsMemo.set(classId, methods);
1108
+ return methods;
1109
+ };
715
1110
  // Concrete-side kinds vary by language: `class` covers Java / Kotlin /
716
1111
  // C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value
717
1112
  // types that conform to protocols. Iterate both.
718
1113
  const concreteKinds = ['class', 'struct'];
719
1114
  for (const kind of concreteKinds) {
720
- for (const cls of queries.getNodesByKind(kind)) {
1115
+ for (const cls of queries.iterateNodesByKind(kind)) {
1116
+ if ((++scanned255 & 63) === 0)
1117
+ await onYield();
1118
+ // A class can only emit here if it HAS a supertype edge — check that
1119
+ // (one edge query) before materializing its methods: most classes in a
1120
+ // typical graph extend/implement nothing and skip in one hop.
1121
+ const sups = queries.getOutgoingEdges(cls.id, ['implements', 'extends']);
1122
+ if (sups.length === 0)
1123
+ continue;
721
1124
  const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
722
1125
  if (implMethods.length === 0)
723
1126
  continue;
724
- for (const sup of queries.getOutgoingEdges(cls.id, ['implements', 'extends'])) {
1127
+ for (const sup of sups) {
725
1128
  const base = queries.getNodeById(sup.target);
726
1129
  if (!base || !IFACE_OVERRIDE_LANGS.has(base.language) || base.id === cls.id)
727
1130
  continue;
@@ -791,7 +1194,8 @@ function interfaceOverrideEdges(queries) {
791
1194
  * Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
792
1195
  * stub's source line is the wiring site shown in the trace trail.
793
1196
  */
794
- function goGrpcStubImplEdges(queries) {
1197
+ async function goGrpcStubImplEdges(queries, onYield) {
1198
+ let scanned255 = 0;
795
1199
  const edges = [];
796
1200
  const seen = new Set();
797
1201
  const STUB_RE = /^Unimplemented.*Server$/;
@@ -803,7 +1207,9 @@ function goGrpcStubImplEdges(queries) {
803
1207
  const methodNamesByStruct = new Map();
804
1208
  const methodNodesByStruct = new Map();
805
1209
  const goStructs = [];
806
- for (const s of queries.getNodesByKind('struct')) {
1210
+ for (const s of queries.iterateNodesByKind('struct')) {
1211
+ if ((++scanned255 & 63) === 0)
1212
+ await onYield();
807
1213
  if (s.language !== 'go')
808
1214
  continue;
809
1215
  goStructs.push(s);
@@ -887,11 +1293,14 @@ function goGrpcStubImplEdges(queries) {
887
1293
  * (or nothing) and are dropped.
888
1294
  */
889
1295
  async function reactJsxChildEdges(ctx, onYield) {
1296
+ let scannedFiles = 0;
890
1297
  const edges = [];
891
1298
  const seen = new Set();
892
1299
  const PARENT_KINDS = new Set(['method', 'function', 'component']);
893
1300
  let scanned = 0;
894
1301
  for (const file of ctx.getAllFiles()) {
1302
+ if ((++scannedFiles & 15) === 0)
1303
+ await onYield();
895
1304
  if ((++scanned & 255) === 0)
896
1305
  await onYield(); // #1091: yield mid-scan on huge graphs
897
1306
  const content = ctx.readFile(file);
@@ -941,7 +1350,9 @@ async function reactJsxChildEdges(ctx, onYield) {
941
1350
  * component, handler→function/method) keeps precision; inline arrows / `$emit`
942
1351
  * skipped.
943
1352
  */
944
- function vueTemplateEdges(ctx) {
1353
+ async function vueTemplateEdges(ctx, onYield) {
1354
+ let scannedFiles = 0;
1355
+ let scanned255 = 0;
945
1356
  const edges = [];
946
1357
  const seen = new Set();
947
1358
  const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
@@ -955,12 +1366,16 @@ function vueTemplateEdges(ctx) {
955
1366
  // misses it (flat components match by basename and don't need this). Map each
956
1367
  // nested component's Nuxt name → node so those template usages resolve.
957
1368
  const nuxtComponents = new Map();
958
- for (const c of ctx.getNodesByKind('component')) {
1369
+ for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1370
+ if ((++scanned255 & 63) === 0)
1371
+ await onYield();
959
1372
  const nn = nuxtComponentName(c.filePath);
960
1373
  if (nn && !nuxtComponents.has(nn))
961
1374
  nuxtComponents.set(nn, c);
962
1375
  }
963
1376
  for (const file of ctx.getAllFiles()) {
1377
+ if ((++scannedFiles & 15) === 0)
1378
+ await onYield();
964
1379
  if (!file.endsWith('.vue'))
965
1380
  continue;
966
1381
  const content = ctx.readFile(file);
@@ -1094,18 +1509,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
1094
1509
  // statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
1095
1510
  // is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
1096
1511
  const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
1097
- function rnEventEdges(ctx) {
1512
+ async function rnEventEdges(ctx, onYield) {
1513
+ let scannedFiles = 0;
1098
1514
  // Native dispatchers (source = the native method whose body sends the
1099
1515
  // event) and JS handlers (target = the function/method registered as
1100
1516
  // the listener) keyed by event name.
1101
1517
  const nativeDispatchersByEvent = new Map();
1102
1518
  const jsHandlersByEvent = new Map();
1103
1519
  for (const file of ctx.getAllFiles()) {
1520
+ if ((++scannedFiles & 15) === 0)
1521
+ await onYield();
1104
1522
  const content = ctx.readFile(file);
1105
1523
  if (!content)
1106
1524
  continue;
1107
1525
  const nodesInFile = ctx.getNodesInFile(file);
1108
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
1526
+ const lineOf = makeLineAt(content, 1);
1109
1527
  const addDispatcher = (event, line) => {
1110
1528
  const disp = enclosingFn(nodesInFile, line);
1111
1529
  if (!disp)
@@ -1288,11 +1706,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
1288
1706
  * caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
1289
1707
  * `<file>::<module>.<method>` by the framework extractor.
1290
1708
  */
1291
- function expoCrossPlatformEdges(queries) {
1709
+ async function expoCrossPlatformEdges(queries, onYield) {
1710
+ let scanned255 = 0;
1292
1711
  const edges = [];
1293
1712
  const seen = new Set();
1294
1713
  const byKey = new Map();
1295
- for (const m of queries.getNodesByKind('method')) {
1714
+ for (const m of queries.iterateNodesByKind('method')) {
1715
+ if ((++scanned255 & 63) === 0)
1716
+ await onYield();
1296
1717
  if (!m.id.startsWith('expo-module:'))
1297
1718
  continue;
1298
1719
  const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
@@ -1341,7 +1762,8 @@ function expoCrossPlatformEdges(queries) {
1341
1762
  * `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
1342
1763
  * lines up with the bare Android method name.
1343
1764
  */
1344
- function rnCrossPlatformEdges(queries) {
1765
+ async function rnCrossPlatformEdges(queries, onYield) {
1766
+ let scanned255 = 0;
1345
1767
  const edges = [];
1346
1768
  const seen = new Set();
1347
1769
  const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
@@ -1363,6 +1785,8 @@ function rnCrossPlatformEdges(queries) {
1363
1785
  // below only runs for genuine cross-platform candidates.
1364
1786
  const byName = new Map();
1365
1787
  for (const m of queries.iterateNodesByKind('method')) {
1788
+ if ((++scanned255 & 63) === 0)
1789
+ await onYield();
1366
1790
  if (!NATIVE.has(m.language))
1367
1791
  continue;
1368
1792
  const key = norm(m.name);
@@ -1413,17 +1837,27 @@ function rnCrossPlatformEdges(queries) {
1413
1837
  }
1414
1838
  return edges;
1415
1839
  }
1416
- function fabricNativeImplEdges(ctx) {
1840
+ async function fabricNativeImplEdges(ctx, onYield) {
1841
+ let scanned255 = 0;
1417
1842
  const edges = [];
1418
1843
  const seen = new Set();
1419
1844
  // The Fabric extractor IDs are prefixed `fabric-component:` so we can
1420
- // filter to just those without iterating all `component` nodes.
1421
- const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
1845
+ // filter to just those while streaming never materializing the whole
1846
+ // `component` kind (#1212).
1847
+ const components = [];
1848
+ for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1849
+ if ((++scanned255 & 63) === 0)
1850
+ await onYield();
1851
+ if (n.id.startsWith('fabric-component:'))
1852
+ components.push(n);
1853
+ }
1422
1854
  if (components.length === 0)
1423
1855
  return edges;
1424
1856
  // Pre-index native classes by name for O(1) lookup.
1425
1857
  const nativeClassesByName = new Map();
1426
- for (const n of ctx.getNodesByKind('class')) {
1858
+ for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
1859
+ if ((++scanned255 & 63) === 0)
1860
+ await onYield();
1427
1861
  if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
1428
1862
  continue;
1429
1863
  const arr = nativeClassesByName.get(n.name);
@@ -1476,12 +1910,29 @@ function fabricNativeImplEdges(ctx) {
1476
1910
  * same simple name) are dropped. We need-not bridge by package because Java
1477
1911
  * mapper interfaces are typically uniquely named within a project.
1478
1912
  */
1479
- function mybatisJavaXmlEdges(queries) {
1913
+ async function mybatisJavaXmlEdges(queries, onYield) {
1914
+ let scanned255 = 0;
1480
1915
  const edges = [];
1481
1916
  const seen = new Set();
1917
+ // Collect the XML side FIRST (mapper `<select id=…>` statements extracted as
1918
+ // xml-language method nodes): if the project has none — every Java+XML repo
1919
+ // that doesn't use MyBatis — return before paying the full java-method
1920
+ // stream below. Same rowid stream order as matching inline, so the edge
1921
+ // output is byte-identical when mappers do exist.
1922
+ const xmlMethods = [];
1923
+ for (const m of queries.iterateNodesByKind('method')) {
1924
+ if ((++scanned255 & 63) === 0)
1925
+ await onYield();
1926
+ if (m.language === 'xml')
1927
+ xmlMethods.push(m);
1928
+ }
1929
+ if (xmlMethods.length === 0)
1930
+ return edges;
1482
1931
  // Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
1483
1932
  const javaIndex = new Map();
1484
1933
  for (const m of queries.iterateNodesByKind('method')) {
1934
+ if ((++scanned255 & 63) === 0)
1935
+ await onYield();
1485
1936
  if (m.language !== 'java' && m.language !== 'kotlin')
1486
1937
  continue;
1487
1938
  const parts = m.qualifiedName.split('::');
@@ -1496,9 +1947,9 @@ function mybatisJavaXmlEdges(queries) {
1496
1947
  else
1497
1948
  javaIndex.set(key, [m]);
1498
1949
  }
1499
- for (const xml of queries.iterateNodesByKind('method')) {
1500
- if (xml.language !== 'xml')
1501
- continue;
1950
+ for (const xml of xmlMethods) {
1951
+ if ((++scanned255 & 63) === 0)
1952
+ await onYield();
1502
1953
  // Qualified name: `<namespace>::<id>`. Extract the simple class name.
1503
1954
  const colonIdx = xml.qualifiedName.lastIndexOf('::');
1504
1955
  if (colonIdx < 0)
@@ -1610,10 +2061,14 @@ function goHandlerIdent(expr) {
1610
2061
  const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
1611
2062
  return m ? m[1] : null;
1612
2063
  }
1613
- function ginMiddlewareChainEdges(queries, ctx) {
2064
+ async function ginMiddlewareChainEdges(queries, ctx, onYield) {
2065
+ let scanned255 = 0;
2066
+ let scannedFiles = 0;
1614
2067
  // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
1615
2068
  const dispatchers = [];
1616
2069
  for (const n of queries.iterateNodesByKind('method')) {
2070
+ if ((++scanned255 & 63) === 0)
2071
+ await onYield();
1617
2072
  if (n.language !== 'go')
1618
2073
  continue;
1619
2074
  const content = ctx.readFile(n.filePath);
@@ -1628,6 +2083,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
1628
2083
  // closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
1629
2084
  const registered = new Map(); // name → registeredAt (file:line)
1630
2085
  for (const file of ctx.getAllFiles()) {
2086
+ if ((++scannedFiles & 15) === 0)
2087
+ await onYield();
1631
2088
  if (!file.endsWith('.go'))
1632
2089
  continue;
1633
2090
  const content = ctx.readFile(file);
@@ -1683,10 +2140,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
1683
2140
  * clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
1684
2141
  * definition isn't orphaned, and editing the form surfaces its code-behind unit.
1685
2142
  */
1686
- function pascalFormEdges(ctx) {
2143
+ async function pascalFormEdges(ctx, onYield) {
2144
+ let scannedFiles = 0;
1687
2145
  const edges = [];
1688
2146
  const allFiles = new Set(ctx.getAllFiles());
1689
2147
  for (const file of allFiles) {
2148
+ if ((++scannedFiles & 255) === 0)
2149
+ await onYield();
1690
2150
  if (!/\.(dfm|fmx)$/i.test(file))
1691
2151
  continue;
1692
2152
  const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
@@ -1722,12 +2182,15 @@ function pascalFormEdges(ctx) {
1722
2182
  * a loader's data shows the page it feeds) and the page's dependencies include
1723
2183
  * its loader.
1724
2184
  */
1725
- function svelteKitLoadEdges(ctx) {
2185
+ async function svelteKitLoadEdges(ctx, onYield) {
2186
+ let scannedFiles = 0;
1726
2187
  const edges = [];
1727
2188
  const allFiles = new Set(ctx.getAllFiles());
1728
2189
  const HOOKS = new Set(['load', 'actions']);
1729
2190
  const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
1730
2191
  for (const file of allFiles) {
2192
+ if ((++scannedFiles & 255) === 0)
2193
+ await onYield();
1731
2194
  const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
1732
2195
  if (!m)
1733
2196
  continue;
@@ -1760,91 +2223,6 @@ function svelteKitLoadEdges(ctx) {
1760
2223
  }
1761
2224
  return edges;
1762
2225
  }
1763
- /** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
1764
- function arktsEntryEdges(queries, ctx) {
1765
- const edges = [];
1766
- const seen = new Set();
1767
- const add = (source, target, synthesizedBy, extra = {}) => {
1768
- const key = `${source}>${target}`;
1769
- if (seen.has(key))
1770
- return;
1771
- seen.add(key);
1772
- const targetNode = queries.getNodeById(target);
1773
- edges.push({
1774
- source,
1775
- target,
1776
- kind: 'calls',
1777
- provenance: 'heuristic',
1778
- metadata: {
1779
- synthesizedBy,
1780
- registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
1781
- ...extra,
1782
- },
1783
- });
1784
- };
1785
- for (const m of methodAndFunctionNodes(queries)) {
1786
- if (m.language !== 'arkts')
1787
- continue;
1788
- const content = ctx.readFile(m.filePath);
1789
- if (!content)
1790
- continue;
1791
- const lines = content.split('\n');
1792
- const start = Math.max((m.startLine ?? 1) - 1, 0);
1793
- const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
1794
- const body = lines.slice(start, end).join('\n');
1795
- const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
1796
- if (!loadMatch)
1797
- continue;
1798
- const page = loadMatch[1];
1799
- const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
1800
- if (pageComponent) {
1801
- const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
1802
- if (aboutToAppear) {
1803
- add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
1804
- }
1805
- }
1806
- }
1807
- for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
1808
- if (cls.language !== 'arkts')
1809
- continue;
1810
- const methods = queries
1811
- .getOutgoingEdges(cls.id, ['contains'])
1812
- .map((e) => queries.getNodeById(e.target))
1813
- .filter((n) => !!n && n.kind === 'method');
1814
- const onCreate = methods.find((n) => n.name === 'onCreate');
1815
- const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
1816
- if (onCreate && onWindowStageCreate) {
1817
- add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
1818
- }
1819
- }
1820
- for (const comp of [
1821
- ...queries.getNodesByKind('component'),
1822
- ...queries.getNodesByKind('class'),
1823
- ...queries.getNodesByKind('struct'),
1824
- ]) {
1825
- if (comp.language !== 'arkts')
1826
- continue;
1827
- const owner = comp.kind === 'component'
1828
- ? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
1829
- queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
1830
- comp
1831
- : comp;
1832
- const methods = queries
1833
- .getOutgoingEdges(owner.id, ['contains'])
1834
- .map((e) => queries.getNodeById(e.target))
1835
- .filter((n) => !!n && n.kind === 'method');
1836
- const build = methods.find((n) => n.name === 'build');
1837
- if (!build)
1838
- continue;
1839
- for (const life of ['aboutToAppear', 'onPageShow']) {
1840
- const lifeMethod = methods.find((n) => n.name === life);
1841
- if (lifeMethod) {
1842
- add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
1843
- }
1844
- }
1845
- }
1846
- return edges;
1847
- }
1848
2226
  /**
1849
2227
  * Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
1850
2228
  * (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
@@ -1861,10 +2239,13 @@ function arktsEntryEdges(queries, ctx) {
1861
2239
  const THUNK_DECL_RE = /create(?:Async)?Thunk/;
1862
2240
  const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
1863
2241
  const THUNK_FANOUT_CAP = 24;
1864
- function reduxThunkEdges(queries, ctx) {
2242
+ async function reduxThunkEdges(queries, ctx, onYield) {
2243
+ let scanned255 = 0;
1865
2244
  const edges = [];
1866
2245
  const seen = new Set();
1867
2246
  for (const node of queries.iterateNodesByKind('constant')) {
2247
+ if ((++scanned255 & 63) === 0)
2248
+ await onYield();
1868
2249
  // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
1869
2250
  // avoids reading every constant's body on a large repo.
1870
2251
  if (!node.signature || !THUNK_DECL_RE.test(node.signature))
@@ -1996,10 +2377,13 @@ function resolveRegistryHandler(ctx, name, chained) {
1996
2377
  return cands.find((n) => n.kind === 'method') ?? null;
1997
2378
  }
1998
2379
  async function objectRegistryEdges(ctx, onYield) {
2380
+ let scannedFiles = 0;
1999
2381
  const edges = [];
2000
2382
  const seen = new Set();
2001
2383
  let scanned = 0;
2002
2384
  for (const file of ctx.getAllFiles()) {
2385
+ if ((++scannedFiles & 15) === 0)
2386
+ await onYield();
2003
2387
  if ((++scanned & 255) === 0)
2004
2388
  await onYield(); // #1091: yield mid-scan on huge graphs
2005
2389
  if (!REGISTRY_JS_EXT.test(file))
@@ -2111,10 +2495,13 @@ function rtkEndpointNameFromHook(hook) {
2111
2495
  return null;
2112
2496
  return mid.charAt(0).toLowerCase() + mid.slice(1);
2113
2497
  }
2114
- function rtkQueryEdges(queries, ctx) {
2498
+ async function rtkQueryEdges(queries, ctx, onYield) {
2499
+ let scanned255 = 0;
2115
2500
  const edges = [];
2116
2501
  const seen = new Set();
2117
2502
  for (const hook of queries.iterateNodesByKind('function')) {
2503
+ if ((++scanned255 & 63) === 0)
2504
+ await onYield();
2118
2505
  // Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
2119
2506
  if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
2120
2507
  continue;
@@ -2158,10 +2545,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
2158
2545
  const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
2159
2546
  const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
2160
2547
  const PINIA_FANOUT_CAP = 80;
2161
- function piniaStoreEdges(ctx) {
2548
+ async function piniaStoreEdges(ctx, onYield) {
2549
+ let scannedFiles = 0;
2162
2550
  // 1. Map each `const useXStore = defineStore(...)` factory → its store file.
2163
2551
  const factoryFile = new Map();
2164
2552
  for (const file of ctx.getAllFiles()) {
2553
+ if ((++scannedFiles & 15) === 0)
2554
+ await onYield();
2165
2555
  if (!PINIA_CONSUMER_EXT.test(file))
2166
2556
  continue;
2167
2557
  const content = ctx.readFile(file);
@@ -2177,6 +2567,8 @@ function piniaStoreEdges(ctx) {
2177
2567
  const edges = [];
2178
2568
  const seen = new Set();
2179
2569
  for (const file of ctx.getAllFiles()) {
2570
+ if ((++scannedFiles & 15) === 0)
2571
+ await onYield();
2180
2572
  if (!PINIA_CONSUMER_EXT.test(file))
2181
2573
  continue;
2182
2574
  const content = ctx.readFile(file);
@@ -2249,7 +2641,8 @@ const VUEX_FANOUT_CAP = 120;
2249
2641
  function pathHasSegment(filePath, seg) {
2250
2642
  return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
2251
2643
  }
2252
- function vuexDispatchEdges(ctx) {
2644
+ async function vuexDispatchEdges(ctx, onYield) {
2645
+ let scannedFiles = 0;
2253
2646
  const storeFileCache = new Map();
2254
2647
  const isStoreFile = (file) => {
2255
2648
  let v = storeFileCache.get(file);
@@ -2287,6 +2680,8 @@ function vuexDispatchEdges(ctx) {
2287
2680
  const edges = [];
2288
2681
  const seen = new Set();
2289
2682
  for (const file of ctx.getAllFiles()) {
2683
+ if ((++scannedFiles & 15) === 0)
2684
+ await onYield();
2290
2685
  if (!PINIA_CONSUMER_EXT.test(file))
2291
2686
  continue;
2292
2687
  const content = ctx.readFile(file);
@@ -2346,7 +2741,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
2346
2741
  const CELERY_PY_EXT = /\.py$/;
2347
2742
  const CELERY_FANOUT_CAP = 80;
2348
2743
  const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
2349
- function celeryDispatchEdges(ctx) {
2744
+ async function celeryDispatchEdges(ctx, onYield) {
2745
+ let scannedFiles = 0;
2350
2746
  // Memoize the decorator check per task-candidate node: it reads the file and scans a few
2351
2747
  // lines above the def. Only called on names that are actually `.delay`/`.apply_async`
2352
2748
  // receivers, so the candidate set stays small.
@@ -2390,6 +2786,8 @@ function celeryDispatchEdges(ctx) {
2390
2786
  const edges = [];
2391
2787
  const seen = new Set();
2392
2788
  for (const file of ctx.getAllFiles()) {
2789
+ if ((++scannedFiles & 15) === 0)
2790
+ await onYield();
2393
2791
  if (!CELERY_PY_EXT.test(file))
2394
2792
  continue;
2395
2793
  const content = ctx.readFile(file);
@@ -2468,15 +2866,24 @@ function springFirstParamType(sig) {
2468
2866
  const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
2469
2867
  return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
2470
2868
  }
2471
- function springEventEdges(ctx) {
2869
+ async function springEventEdges(ctx, onYield) {
2870
+ let scannedFiles = 0;
2472
2871
  // Pass 1 — event-type → listener methods, scanning only event-relevant files.
2872
+ // This is the ONLY full read sweep: publisher files are recorded here so
2873
+ // pass 2 re-reads just those instead of every .java file again (#1212 —
2874
+ // the double full-repo read was one of the tail's longest unyielded spans).
2473
2875
  const listeners = new Map();
2876
+ const publisherFiles = [];
2474
2877
  for (const file of ctx.getAllFiles()) {
2878
+ if ((++scannedFiles & 15) === 0)
2879
+ await onYield();
2475
2880
  if (!SPRING_JAVA_EXT.test(file))
2476
2881
  continue;
2477
2882
  const content = ctx.readFile(file);
2478
2883
  if (!content)
2479
2884
  continue;
2885
+ if (content.includes('.publishEvent('))
2886
+ publisherFiles.push(file);
2480
2887
  const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
2481
2888
  const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
2482
2889
  if (!hasAnno && !hasAppListener)
@@ -2516,12 +2923,13 @@ function springEventEdges(ctx) {
2516
2923
  }
2517
2924
  if (!listeners.size)
2518
2925
  return [];
2519
- // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
2926
+ // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
2927
+ // XEvent. Only the publisher files recorded in pass 1 are (re-)read.
2520
2928
  const edges = [];
2521
2929
  const seen = new Set();
2522
- for (const file of ctx.getAllFiles()) {
2523
- if (!SPRING_JAVA_EXT.test(file))
2524
- continue;
2930
+ for (const file of publisherFiles) {
2931
+ if ((++scannedFiles & 15) === 0)
2932
+ await onYield();
2525
2933
  const content = ctx.readFile(file);
2526
2934
  if (!content || !content.includes('.publishEvent('))
2527
2935
  continue;
@@ -2606,10 +3014,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
2606
3014
  }
2607
3015
  return declType;
2608
3016
  }
2609
- function mediatrDispatchEdges(ctx) {
3017
+ async function mediatrDispatchEdges(ctx, onYield) {
3018
+ let scannedFiles = 0;
2610
3019
  // Pass 1 — request/notification type → the Handle method of each handler class.
2611
3020
  const handlers = new Map();
2612
3021
  for (const file of ctx.getAllFiles()) {
3022
+ if ((++scannedFiles & 15) === 0)
3023
+ await onYield();
2613
3024
  if (!MEDIATR_CS_EXT.test(file))
2614
3025
  continue;
2615
3026
  const content = ctx.readFile(file);
@@ -2643,6 +3054,8 @@ function mediatrDispatchEdges(ctx) {
2643
3054
  const edges = [];
2644
3055
  const seen = new Set();
2645
3056
  for (const file of ctx.getAllFiles()) {
3057
+ if ((++scannedFiles & 15) === 0)
3058
+ await onYield();
2646
3059
  if (!MEDIATR_CS_EXT.test(file))
2647
3060
  continue;
2648
3061
  const content = ctx.readFile(file);
@@ -2706,7 +3119,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
2706
3119
  const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
2707
3120
  const SIDEKIQ_RB_EXT = /\.rb$/;
2708
3121
  const SIDEKIQ_FANOUT_CAP = 80;
2709
- function sidekiqDispatchEdges(ctx) {
3122
+ async function sidekiqDispatchEdges(ctx, onYield) {
3123
+ let scannedFiles = 0;
2710
3124
  // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
2711
3125
  // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
2712
3126
  const performCache = new Map();
@@ -2743,6 +3157,8 @@ function sidekiqDispatchEdges(ctx) {
2743
3157
  const edges = [];
2744
3158
  const seen = new Set();
2745
3159
  for (const file of ctx.getAllFiles()) {
3160
+ if ((++scannedFiles & 15) === 0)
3161
+ await onYield();
2746
3162
  if (!SIDEKIQ_RB_EXT.test(file))
2747
3163
  continue;
2748
3164
  const content = ctx.readFile(file);
@@ -2778,7 +3194,415 @@ function sidekiqDispatchEdges(ctx) {
2778
3194
  }
2779
3195
  return edges;
2780
3196
  }
2781
- // ── Laravel events (PHP) ──────────────────────────────────────────────────────
3197
+ // ── Erlang behaviour-callback dispatch ────────────────────────────────────────
3198
+ // An Erlang behaviour is a compile-checked callback contract: the behaviour
3199
+ // module declares `-callback init(...) -> ...`, implementers declare
3200
+ // `-behaviour(B)` and export the callbacks, and the framework side dispatches
3201
+ // through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
3202
+ // `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
3203
+ // deliberately leaves var-module calls silent (no static target), so the flow
3204
+ // breaks at exactly the hop agents ask about (request → handler init). Bridge:
3205
+ //
3206
+ // dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
3207
+ // declaring `fn` with the SITE's arity — provided exactly ONE in-repo
3208
+ // behaviour declares (fn, arity); a name+arity collision across behaviours
3209
+ // bails (silent beats wrong) — and the implementer defines and exports `fn`.
3210
+ //
3211
+ // Behaviours are discovered by scanning every Erlang file for `-callback`
3212
+ // declarations (not just `implements` targets), so a behaviour with zero
3213
+ // implementers still participates in the ambiguity gate. Fan-out control: a
3214
+ // mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
3215
+ // hundreds of edges per site that READ as complete coverage while being
3216
+ // arbitrary — above the cap the site is skipped entirely and the boundary
3217
+ // stays visibly dynamic (explore's boundary announcer covers it) instead of
3218
+ // silently truncated.
3219
+ const ERLANG_EXT = /\.(?:erl|hrl)$/;
3220
+ // `Var:fn(` — variable (capitalized) module, lowercase function, immediate
3221
+ // open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
3222
+ // (static remote call, already linked), and mid-word matches.
3223
+ const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
3224
+ const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
3225
+ const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
3226
+ /**
3227
+ * Argument count of the call/declaration whose `(` sits at `openIdx` —
3228
+ * top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
3229
+ * (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
3230
+ * so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
3231
+ */
3232
+ function erlangArityAt(src, openIdx) {
3233
+ let depth = 1;
3234
+ let commas = 0;
3235
+ let sawArg = false;
3236
+ const limit = Math.min(src.length, openIdx + 4000);
3237
+ for (let i = openIdx + 1; i < limit; i++) {
3238
+ const ch = src[i];
3239
+ if (ch === '"' || ch === "'") {
3240
+ i++;
3241
+ while (i < limit && src[i] !== ch) {
3242
+ if (src[i] === '\\')
3243
+ i++;
3244
+ i++;
3245
+ }
3246
+ sawArg = true;
3247
+ continue;
3248
+ }
3249
+ if (ch === '$') {
3250
+ i++;
3251
+ if (src[i] === '\\')
3252
+ i++;
3253
+ sawArg = true;
3254
+ continue;
3255
+ }
3256
+ if (ch === '(' || ch === '[' || ch === '{') {
3257
+ depth++;
3258
+ sawArg = true;
3259
+ continue;
3260
+ }
3261
+ if (ch === ')' || ch === ']' || ch === '}') {
3262
+ depth--;
3263
+ if (depth === 0)
3264
+ return sawArg ? commas + 1 : 0;
3265
+ continue;
3266
+ }
3267
+ if (ch === ',' && depth === 1) {
3268
+ commas++;
3269
+ continue;
3270
+ }
3271
+ if (!/\s/.test(ch))
3272
+ sawArg = true;
3273
+ }
3274
+ return -1;
3275
+ }
3276
+ /**
3277
+ * Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
3278
+ * DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
3279
+ * and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
3280
+ * config) — the connection happens by option-path unification inside the
3281
+ * module-system evaluator, so there is no static call/import edge to follow
3282
+ * and flow questions ("how does services.yabai.enable become a launchd
3283
+ * service?") go dark at the module boundary.
3284
+ *
3285
+ * This pass links each config-write binding to the option declaration whose
3286
+ * path is the longest static-segment prefix of the write path. Precision gates:
3287
+ * - only STATIC segments participate: plain identifiers, plus quoted segments
3288
+ * (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
3289
+ * only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
3290
+ * so dynamic paths never match beyond their static head;
3291
+ * - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
3292
+ * every package's `meta = { ... }` attrset to nixos's `options.meta`;
3293
+ * - a prefix declared in more than one file is ambiguous → no edge (a wrong
3294
+ * edge is worse than none);
3295
+ * - writes physically inside an options block are declaration internals
3296
+ * (types, defaults, examples), never config writes → excluded.
3297
+ * Both declaration spellings register: flat (`options.a.b = ...`) by name, and
3298
+ * nested (`options = { a.b = ...; }`) by line-span containment.
3299
+ */
3300
+ function nixLeadingPlainSegments(name) {
3301
+ const segs = [];
3302
+ let i = 0;
3303
+ const n = name.length;
3304
+ while (i < n) {
3305
+ if (name[i] === '"') {
3306
+ // Quoted segment — an opaque verbatim token (quotes kept, so it can
3307
+ // never collide with a plain identifier). `NSGlobalDomain."com.apple.
3308
+ // mouse.tapBehavior"` must match ITS OWN quoted declaration, not
3309
+ // whichever sibling registered the shared plain prefix first.
3310
+ let j = i + 1;
3311
+ while (j < n && name[j] !== '"') {
3312
+ if (name[j] === '\\')
3313
+ j++;
3314
+ j++;
3315
+ }
3316
+ if (j >= n)
3317
+ return segs; // unterminated — stop at the static head
3318
+ const tok = name.slice(i, j + 1);
3319
+ if (tok.includes('${'))
3320
+ return segs; // interpolated → dynamic → stop
3321
+ segs.push(tok);
3322
+ i = j + 1;
3323
+ if (i >= n)
3324
+ break;
3325
+ if (name[i] !== '.')
3326
+ return segs;
3327
+ i++;
3328
+ continue;
3329
+ }
3330
+ let j = i;
3331
+ while (j < n && name[j] !== '.') {
3332
+ if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
3333
+ return segs;
3334
+ j++;
3335
+ }
3336
+ const seg = name.slice(i, j);
3337
+ if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
3338
+ return segs;
3339
+ segs.push(seg);
3340
+ i = j + 1;
3341
+ }
3342
+ return segs;
3343
+ }
3344
+ async function nixOptionPathEdges(queries, onYield) {
3345
+ let scanned255 = 0;
3346
+ // One streaming pass over nix bindings (variables + the odd function-valued
3347
+ // option); memory stays O(bindings-kept), not O(all nodes) (#610).
3348
+ const byFile = new Map();
3349
+ let scanned = 0;
3350
+ for (const kind of ['variable', 'function']) {
3351
+ for (const node of queries.iterateNodesByKind(kind)) {
3352
+ if ((++scanned255 & 63) === 0)
3353
+ await onYield();
3354
+ if ((++scanned & 0x3fff) === 0 && onYield)
3355
+ await onYield();
3356
+ if (node.language !== 'nix')
3357
+ continue;
3358
+ const segs = nixLeadingPlainSegments(node.name);
3359
+ if (segs.length === 0)
3360
+ continue;
3361
+ const rec = {
3362
+ id: node.id,
3363
+ filePath: node.filePath,
3364
+ startLine: node.startLine,
3365
+ endLine: node.endLine,
3366
+ segs,
3367
+ };
3368
+ const arr = byFile.get(node.filePath);
3369
+ if (arr)
3370
+ arr.push(rec);
3371
+ else
3372
+ byFile.set(node.filePath, [rec]);
3373
+ }
3374
+ }
3375
+ // Per file: walk bindings outermost-first with a stack of active option
3376
+ // spans, composing nested declaration paths (`options = { services.foo = {
3377
+ // enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
3378
+ // An `options` binding nested inside another option span is a SUBMODULE's
3379
+ // own namespace (`attrsOf (submodule { options = ...; })`) — its internals
3380
+ // are not globally addressable, so the sentinel blocks registration below it
3381
+ // while still excluding the region from write candidates.
3382
+ const SUBMODULE = 'submodule';
3383
+ const decls = new Map();
3384
+ const writes = [];
3385
+ const register = (path, rec) => {
3386
+ if (path.length < 2 || path.includes(SUBMODULE))
3387
+ return;
3388
+ const key = path.join('.');
3389
+ const arr = decls.get(key);
3390
+ if (arr)
3391
+ arr.push(rec);
3392
+ else
3393
+ decls.set(key, [rec]);
3394
+ };
3395
+ for (const recs of byFile.values()) {
3396
+ recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
3397
+ const stack = [];
3398
+ for (const rec of recs) {
3399
+ while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
3400
+ stack.pop();
3401
+ // Strict containment at line granularity: a one-line nested binding is
3402
+ // indistinguishable from its container, so it stays unclassified (rare
3403
+ // in module code, where option blocks are multi-line).
3404
+ const enclosing = stack.length > 0 &&
3405
+ rec.startLine >= stack[stack.length - 1].start &&
3406
+ rec.endLine <= stack[stack.length - 1].end &&
3407
+ !(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
3408
+ ? stack[stack.length - 1]
3409
+ : null;
3410
+ if (rec.segs[0] === 'options') {
3411
+ const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
3412
+ const prefix = enclosing ? [SUBMODULE] : ownPath;
3413
+ register(prefix, rec);
3414
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix });
3415
+ continue;
3416
+ }
3417
+ if (enclosing) {
3418
+ const composed = [...enclosing.prefix, ...rec.segs];
3419
+ register(composed, rec);
3420
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
3421
+ continue;
3422
+ }
3423
+ if (rec.segs.length >= 2) {
3424
+ writes.push(rec);
3425
+ }
3426
+ }
3427
+ }
3428
+ if (decls.size === 0 || writes.length === 0)
3429
+ return [];
3430
+ const edges = [];
3431
+ for (const w of writes) {
3432
+ // `config.services.x = ...` spells the same write with an explicit prefix.
3433
+ const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
3434
+ if (segs.length < 2)
3435
+ continue;
3436
+ // Longest prefix wins; an ambiguous longest match does NOT fall back to a
3437
+ // shorter one (that would link `services.nginx.virtualHosts.x` to
3438
+ // `options.services.nginx` when virtualHosts is the contested path).
3439
+ for (let len = Math.min(segs.length, 6); len >= 2; len--) {
3440
+ const candidates = decls.get(segs.slice(0, len).join('.'));
3441
+ if (!candidates || candidates.length === 0)
3442
+ continue;
3443
+ const files = new Set(candidates.map((c) => c.filePath));
3444
+ if (files.size === 1) {
3445
+ const target = candidates[0];
3446
+ if (target.id !== w.id) {
3447
+ edges.push({
3448
+ source: w.id,
3449
+ target: target.id,
3450
+ kind: 'references',
3451
+ line: w.startLine,
3452
+ provenance: 'heuristic',
3453
+ metadata: {
3454
+ synthesizedBy: 'nix-option-path',
3455
+ optionPath: segs.slice(0, len).join('.'),
3456
+ registeredAt: `${target.filePath}:${target.startLine}`,
3457
+ },
3458
+ });
3459
+ }
3460
+ }
3461
+ break; // longest hit decides, matched or ambiguous
3462
+ }
3463
+ }
3464
+ return edges;
3465
+ }
3466
+ async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
3467
+ let scannedFiles = 0;
3468
+ let scanned255 = 0;
3469
+ // Cheap language gate: no Erlang modules → no cost beyond one streamed
3470
+ // kind scan (never a materialized array of every namespace — #1212).
3471
+ const erlangModules = [];
3472
+ for (const n of queries.iterateNodesByKind('namespace')) {
3473
+ if ((++scanned255 & 63) === 0)
3474
+ await onYield();
3475
+ if (n.language === 'erlang')
3476
+ erlangModules.push(n);
3477
+ }
3478
+ if (erlangModules.length === 0)
3479
+ return [];
3480
+ // Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
3481
+ // its (name, arity) callback set, and the global `name/arity` → declaring
3482
+ // behaviours map that drives the ambiguity gate.
3483
+ const moduleByFile = new Map();
3484
+ for (const ns of erlangModules) {
3485
+ if (!moduleByFile.has(ns.filePath))
3486
+ moduleByFile.set(ns.filePath, ns);
3487
+ }
3488
+ const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
3489
+ const callbackNames = new Set();
3490
+ for (const file of ctx.getAllFiles()) {
3491
+ if ((++scannedFiles & 15) === 0)
3492
+ await onYield();
3493
+ if (!ERLANG_EXT.test(file))
3494
+ continue;
3495
+ const behaviour = moduleByFile.get(file);
3496
+ if (!behaviour)
3497
+ continue; // a .hrl or module-less file can't be a behaviour
3498
+ const content = ctx.readFile(file);
3499
+ if (!content || !content.includes('-callback'))
3500
+ continue;
3501
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3502
+ ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
3503
+ let m;
3504
+ while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
3505
+ const name = m[2].replace(/^'|'$/g, '');
3506
+ const arity = erlangArityAt(safe, m.index + m[0].length - 1);
3507
+ if (arity < 0)
3508
+ continue;
3509
+ const key = `${name}/${arity}`;
3510
+ const arr = declaringBehaviours.get(key);
3511
+ if (arr) {
3512
+ if (!arr.some((b) => b.id === behaviour.id))
3513
+ arr.push(behaviour);
3514
+ }
3515
+ else {
3516
+ declaringBehaviours.set(key, [behaviour]);
3517
+ }
3518
+ callbackNames.add(name);
3519
+ }
3520
+ }
3521
+ if (declaringBehaviours.size === 0)
3522
+ return [];
3523
+ // Implementer target lookup, lazy per (behaviour, fn): implementers come
3524
+ // from the `implements` edges extraction resolved, and the target is the
3525
+ // implementer module's own exported `fn` function node.
3526
+ const targetCache = new Map();
3527
+ const targetsOf = (behaviour, fn) => {
3528
+ const cacheKey = `${behaviour.id}#${fn}`;
3529
+ let targets = targetCache.get(cacheKey);
3530
+ if (targets)
3531
+ return targets;
3532
+ targets = [];
3533
+ for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
3534
+ const impl = queries.getNodeById(e.source);
3535
+ if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
3536
+ continue;
3537
+ const fnNode = ctx
3538
+ .getNodesInFile(impl.filePath)
3539
+ .find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
3540
+ if (fnNode)
3541
+ targets.push(fnNode);
3542
+ }
3543
+ targetCache.set(cacheKey, targets);
3544
+ return targets;
3545
+ };
3546
+ // Pass 2 — dispatch sites. Only files containing a var-module call shape are
3547
+ // scanned in full.
3548
+ const edges = [];
3549
+ const seen = new Set();
3550
+ for (const file of ctx.getAllFiles()) {
3551
+ if ((++scannedFiles & 15) === 0)
3552
+ await onYield();
3553
+ if (!ERLANG_EXT.test(file))
3554
+ continue;
3555
+ const content = ctx.readFile(file);
3556
+ if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
3557
+ continue;
3558
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3559
+ const nodesInFile = ctx.getNodesInFile(file);
3560
+ ERLANG_DISPATCH_RE.lastIndex = 0;
3561
+ let m;
3562
+ while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
3563
+ const fn = m[3];
3564
+ if (!callbackNames.has(fn))
3565
+ continue;
3566
+ const openIdx = m.index + m[0].length - 1;
3567
+ const arity = erlangArityAt(safe, openIdx);
3568
+ if (arity < 0)
3569
+ continue;
3570
+ const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
3571
+ if (!behaviours || behaviours.length !== 1)
3572
+ continue; // unknown or ambiguous
3573
+ const behaviour = behaviours[0];
3574
+ const targets = targetsOf(behaviour, fn);
3575
+ if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
3576
+ continue;
3577
+ const line = safe.slice(0, m.index).split('\n').length;
3578
+ const disp = enclosingFn(nodesInFile, line);
3579
+ if (!disp)
3580
+ continue;
3581
+ for (const target of targets) {
3582
+ if (target.id === disp.id)
3583
+ continue;
3584
+ const key = `${disp.id}>${target.id}`;
3585
+ if (seen.has(key))
3586
+ continue;
3587
+ seen.add(key);
3588
+ edges.push({
3589
+ source: disp.id,
3590
+ target: target.id,
3591
+ kind: 'calls',
3592
+ line,
3593
+ provenance: 'heuristic',
3594
+ metadata: {
3595
+ synthesizedBy: 'erlang-behaviour',
3596
+ via: `${behaviour.name}:${fn}/${arity}`,
3597
+ registeredAt: `${file}:${line}`,
3598
+ },
3599
+ });
3600
+ }
3601
+ }
3602
+ }
3603
+ return edges;
3604
+ }
3605
+ // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2782
3606
  // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2783
3607
  // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
2784
3608
  // class UpdateLastfmNowPlaying { public function handle(PlaybackStarted $event) { … } }
@@ -2824,7 +3648,8 @@ function phpArrayBody(src, openIdx) {
2824
3648
  }
2825
3649
  return null;
2826
3650
  }
2827
- function laravelEventEdges(ctx) {
3651
+ async function laravelEventEdges(ctx, onYield) {
3652
+ let scannedFiles = 0;
2828
3653
  // event short name → its listener `handle` methods (deduped by node id).
2829
3654
  const listeners = new Map();
2830
3655
  const add = (event, handle) => {
@@ -2841,6 +3666,8 @@ function laravelEventEdges(ctx) {
2841
3666
  && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
2842
3667
  // Pass 1 — build the event→handle map from both registration mechanisms.
2843
3668
  for (const file of ctx.getAllFiles()) {
3669
+ if ((++scannedFiles & 15) === 0)
3670
+ await onYield();
2844
3671
  if (!LARAVEL_PHP_EXT.test(file))
2845
3672
  continue;
2846
3673
  const content = ctx.readFile(file);
@@ -2886,6 +3713,8 @@ function laravelEventEdges(ctx) {
2886
3713
  const edges = [];
2887
3714
  const seen = new Set();
2888
3715
  for (const file of ctx.getAllFiles()) {
3716
+ if ((++scannedFiles & 15) === 0)
3717
+ await onYield();
2889
3718
  if (!LARAVEL_PHP_EXT.test(file))
2890
3719
  continue;
2891
3720
  const content = ctx.readFile(file);
@@ -2936,7 +3765,182 @@ function laravelEventEdges(ctx) {
2936
3765
  * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
2937
3766
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
2938
3767
  */
2939
- async function synthesizeCallbackEdges(queries, ctx) {
3768
+ /**
3769
+ * Number of progress steps synthesizeCallbackEdges reports: one per `__mark()`
3770
+ * call (every synthesis pass, plus the dedupe-merge and edge-insert steps).
3771
+ * Cosmetic only — drift just makes the progress bar end early or jump — and a
3772
+ * test pins it to the actual step count (registry passes + the fixed
3773
+ * pre/post marks) so adding a pass without bumping this fails loudly instead
3774
+ * of silently skewing the bar.
3775
+ */
3776
+ const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
3777
+ /** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
3778
+ function arktsEntryEdges(queries, ctx) {
3779
+ const edges = [];
3780
+ const seen = new Set();
3781
+ const add = (source, target, synthesizedBy, extra = {}) => {
3782
+ const key = `${source}>${target}`;
3783
+ if (seen.has(key))
3784
+ return;
3785
+ seen.add(key);
3786
+ const targetNode = queries.getNodeById(target);
3787
+ edges.push({
3788
+ source,
3789
+ target,
3790
+ kind: 'calls',
3791
+ provenance: 'heuristic',
3792
+ metadata: {
3793
+ synthesizedBy,
3794
+ registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
3795
+ ...extra,
3796
+ },
3797
+ });
3798
+ };
3799
+ for (const m of methodAndFunctionNodes(queries)) {
3800
+ if (m.language !== 'arkts')
3801
+ continue;
3802
+ const content = ctx.readFile(m.filePath);
3803
+ if (!content)
3804
+ continue;
3805
+ const lines = content.split('\n');
3806
+ const start = Math.max((m.startLine ?? 1) - 1, 0);
3807
+ const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
3808
+ const body = lines.slice(start, end).join('\n');
3809
+ const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
3810
+ if (!loadMatch)
3811
+ continue;
3812
+ const page = loadMatch[1];
3813
+ const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
3814
+ if (pageComponent) {
3815
+ const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
3816
+ if (aboutToAppear) {
3817
+ add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
3818
+ }
3819
+ }
3820
+ }
3821
+ for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
3822
+ if (cls.language !== 'arkts')
3823
+ continue;
3824
+ const methods = queries
3825
+ .getOutgoingEdges(cls.id, ['contains'])
3826
+ .map((e) => queries.getNodeById(e.target))
3827
+ .filter((n) => !!n && n.kind === 'method');
3828
+ const onCreate = methods.find((n) => n.name === 'onCreate');
3829
+ const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
3830
+ if (onCreate && onWindowStageCreate) {
3831
+ add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
3832
+ }
3833
+ }
3834
+ for (const comp of [
3835
+ ...queries.getNodesByKind('component'),
3836
+ ...queries.getNodesByKind('class'),
3837
+ ...queries.getNodesByKind('struct'),
3838
+ ]) {
3839
+ if (comp.language !== 'arkts')
3840
+ continue;
3841
+ const owner = comp.kind === 'component'
3842
+ ? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
3843
+ queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
3844
+ comp
3845
+ : comp;
3846
+ const methods = queries
3847
+ .getOutgoingEdges(owner.id, ['contains'])
3848
+ .map((e) => queries.getNodeById(e.target))
3849
+ .filter((n) => !!n && n.kind === 'method');
3850
+ const build = methods.find((n) => n.name === 'build');
3851
+ if (!build)
3852
+ continue;
3853
+ for (const life of ['aboutToAppear', 'onPageShow']) {
3854
+ const lifeMethod = methods.find((n) => n.name === life);
3855
+ if (lifeMethod) {
3856
+ add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
3857
+ }
3858
+ }
3859
+ }
3860
+ return edges;
3861
+ }
3862
+ const ALWAYS = () => true;
3863
+ /**
3864
+ * The independent passes, in MERGE ORDER — the first-seen dedup in
3865
+ * synthesizeCallbackEdges follows this array, so reordering entries changes
3866
+ * which duplicate edge wins. The two Go pre-passes (cross-file method
3867
+ * `contains`, implicit `implements`) are NOT here: they persist before these
3868
+ * run because interfaceOverrideEdges reads their edges from the DB.
3869
+ */
3870
+ exports.SYNTH_PASSES = [
3871
+ { name: 'fieldEdges', gate: ALWAYS, run: (q, c, y) => fieldChannelEdges(q, c, y) },
3872
+ { name: 'closureCollEdges', gate: ALWAYS, run: (q, c, y) => closureCollectionEdges(q, c, y) },
3873
+ { name: 'emitterEdges', gate: ALWAYS, run: (_q, c, y) => eventEmitterEdges(c, y) },
3874
+ { name: 'renderEdges', gate: ALWAYS, run: (q, c, y) => reactRenderEdges(q, c, y) },
3875
+ { name: 'jsxEdges', gate: ALWAYS, run: (_q, c, y) => reactJsxChildEdges(c, y) },
3876
+ { name: 'vueEdges', gate: (has) => has('vue'), run: (_q, c, y) => vueTemplateEdges(c, y) },
3877
+ { name: 'svelteKitEdges', gate: (has) => has('svelte'), run: (_q, c, y) => svelteKitLoadEdges(c, y) },
3878
+ { name: 'pascalEdges', gate: ALWAYS, run: (_q, c, y) => pascalFormEdges(c, y) },
3879
+ { name: 'flutterEdges', gate: (has) => has('dart'), run: (q, c, y) => flutterBuildEdges(q, c, y) },
3880
+ { name: 'arkuiStateEdges', gate: (has) => has('arkts'), run: (q, c, y) => arkuiStateBuildEdges(q, c, y) },
3881
+ { name: 'arkuiEmitter', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiEmitterEdges(c, y) },
3882
+ { name: 'arkuiRoutes', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiRouterEdges(c, y) },
3883
+ { name: 'arktsStartupEdges', gate: (has) => has('arkts'), run: async (q, c) => arktsEntryEdges(q, c) },
3884
+ { name: 'cppEdges', gate: (has) => has('cpp'), run: (q, _c, y) => cppOverrideEdges(q, y) },
3885
+ {
3886
+ name: 'ifaceEdges',
3887
+ gate: (has) => has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY),
3888
+ run: (q, _c, y) => interfaceOverrideEdges(q, y),
3889
+ },
3890
+ { name: 'kotlinExpectActual', gate: (has) => has('kotlin'), run: (q, _c, y) => kotlinExpectActualEdges(q, y) },
3891
+ { name: 'goGrpcEdges', gate: (has) => has('go'), run: (q, _c, y) => goGrpcStubImplEdges(q, y) },
3892
+ { name: 'rnEventEdgesList', gate: (has) => has(...JS_FAMILY), run: (_q, c, y) => rnEventEdges(c, y) },
3893
+ { name: 'fabricNativeEdges', gate: ALWAYS, run: (_q, c, y) => fabricNativeImplEdges(c, y) },
3894
+ // Expo module nodes (`expo-module:` ids) are emitted only from .swift/.kt
3895
+ // files, and a pair needs BOTH platforms — so without both languages the
3896
+ // pass's only collection loop is provably empty (it was streaming every
3897
+ // method row on pure-Java repos to find nothing).
3898
+ { name: 'expoXPlatEdges', gate: (has) => has('swift') && has('kotlin'), run: (q, _c, y) => expoCrossPlatformEdges(q, y) },
3899
+ // An RN cross-platform edge requires a JS-language caller on the native
3900
+ // method (`isBridge`) — no JS-family files means no JS-language nodes, so
3901
+ // the result is provably empty.
3902
+ { name: 'rnXPlatEdges', gate: (has) => has(...JS_FAMILY), run: (q, _c, y) => rnCrossPlatformEdges(q, y) },
3903
+ {
3904
+ name: 'mybatisEdges',
3905
+ gate: (has) => has('java', 'kotlin') && has('xml'),
3906
+ run: (q, _c, y) => mybatisJavaXmlEdges(q, y),
3907
+ },
3908
+ { name: 'ginEdges', gate: (has) => has('go'), run: (q, c, y) => ginMiddlewareChainEdges(q, c, y) },
3909
+ { name: 'thunkEdges', gate: (has) => has(...JS_FAMILY), run: (q, c, y) => reduxThunkEdges(q, c, y) },
3910
+ { name: 'registryEdges', gate: ALWAYS, run: (_q, c, y) => objectRegistryEdges(c, y) },
3911
+ { name: 'rtkEdges', gate: (has) => has(...JS_FAMILY), run: (q, c, y) => rtkQueryEdges(q, c, y) },
3912
+ { name: 'piniaEdges', gate: (has) => has('vue', ...JS_FAMILY), run: (_q, c, y) => piniaStoreEdges(c, y) },
3913
+ { name: 'vuexEdges', gate: (has) => has('vue', ...JS_FAMILY), run: (_q, c, y) => vuexDispatchEdges(c, y) },
3914
+ { name: 'celeryEdges', gate: (has) => has('python'), run: (_q, c, y) => celeryDispatchEdges(c, y) },
3915
+ { name: 'springEdges', gate: (has) => has('java'), run: (_q, c, y) => springEventEdges(c, y) },
3916
+ { name: 'mediatrEdges', gate: (has) => has('csharp'), run: (_q, c, y) => mediatrDispatchEdges(c, y) },
3917
+ { name: 'sidekiqEdges', gate: (has) => has('ruby'), run: (_q, c, y) => sidekiqDispatchEdges(c, y) },
3918
+ {
3919
+ name: 'erlangBehaviourEdges',
3920
+ gate: (has) => has('erlang'),
3921
+ run: (q, c, y) => erlangBehaviourDispatchEdges(q, c, y),
3922
+ },
3923
+ { name: 'laravelEdges', gate: (has) => has('php'), run: (_q, c, y) => laravelEventEdges(c, y) },
3924
+ {
3925
+ name: 'cFnPtrEdges',
3926
+ gate: (has) => has('c', 'cpp'),
3927
+ run: (q, c, y, sub) => (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(q, c, y, sub),
3928
+ },
3929
+ { name: 'goframeEdges', gate: (has) => has('go'), run: (_q, c, y) => (0, goframe_synthesizer_1.goframeRouteEdges)(c, y) },
3930
+ { name: 'nixOptionEdges', gate: (has) => has('nix'), run: (q, _c, y) => nixOptionPathEdges(q, y) },
3931
+ ];
3932
+ /** Fixed non-registry steps: goMethodContains, goImplements, dedupe-merge, insertMergedEdges. */
3933
+ const FIXED_SYNTH_STEPS = 4;
3934
+ exports.SYNTH_PROGRESS_STEPS = exports.SYNTH_PASSES.length + FIXED_SYNTH_STEPS;
3935
+ async function synthesizeCallbackEdges(queries, ctx, onProgress,
3936
+ // A live resolver pool to fan the independent passes across (structural type
3937
+ // so this file never imports the pool — resolver-worker imports THIS file).
3938
+ // Null/omitted → the sequential path, byte-identical to the pool path.
3939
+ pool,
3940
+ // WAL-valve writer backstop (WalCheckpointValve.backpressure), called at
3941
+ // pool-idle points in the edge-insert loops below — the passes themselves
3942
+ // only read; every write in this function happens with the pool idle.
3943
+ backpressure) {
2940
3944
  // Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
2941
3945
  // running synchronously on the indexer's main thread. Their AGGREGATE can run
2942
3946
  // for well over a minute on a large repo — long enough for the #850 liveness
@@ -2945,131 +3949,169 @@ async function synthesizeCallbackEdges(queries, ctx) {
2945
3949
  // that itself hangs (a real wedge) never reaches the next yield, so the
2946
3950
  // watchdog still catches that. See ./cooperative-yield.
2947
3951
  const yieldToLoop = (0, cooperative_yield_1.createYielder)();
3952
+ // Synthesis runs AFTER the resolution progress bar reaches 100%, so without
3953
+ // its own progress the UI freezes at "Resolving refs 100%" for the whole
3954
+ // tail — long enough on big repos that users conclude the index hung and
3955
+ // kill it. Report each completed pass; the caller surfaces it as its own
3956
+ // progress phase. Emit 0/total up front so the phase flips immediately.
3957
+ // Emissions are throttled to whole-percent movement (each consumes a UI
3958
+ // message); values may be fractional steps from within-pass reporting.
3959
+ let passesDone = 0;
3960
+ let lastPct = -1;
3961
+ const emit = (value) => {
3962
+ if (!onProgress)
3963
+ return;
3964
+ const v = Math.min(value, exports.SYNTH_PROGRESS_STEPS);
3965
+ const pct = Math.floor((v / exports.SYNTH_PROGRESS_STEPS) * 100);
3966
+ if (pct === lastPct)
3967
+ return;
3968
+ lastPct = pct;
3969
+ onProgress(v, exports.SYNTH_PROGRESS_STEPS);
3970
+ };
3971
+ // A single long pass otherwise parks the bar between steps; a pass that
3972
+ // takes this callback reports a 0..1 fraction of its own work, surfaced
3973
+ // here as fractional progress within its step.
3974
+ const subProgress = (fraction) => emit(passesDone + Math.max(0, Math.min(fraction, 1)));
3975
+ emit(0);
3976
+ // Per-pass wall-clock timing to stderr, opt-in via HOMEGRAPH_SYNTH_TIMINGS
3977
+ // (=1: passes over 250ms; =all: every pass). This is the diagnostic that
3978
+ // located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
3979
+ const markT = { t: Date.now() };
3980
+ const __mark = (label) => {
3981
+ const now = Date.now();
3982
+ const dt = now - markT.t;
3983
+ markT.t = now;
3984
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.HOMEGRAPH_SYNTH_TIMINGS === 'all')) {
3985
+ console.error(`[synth-timing] ${label}: ${dt}ms`);
3986
+ }
3987
+ passesDone++;
3988
+ emit(passesDone);
3989
+ };
3990
+ // Language gating: one indexed DISTINCT over the files table lets a pass
3991
+ // whose own filters reference a specific language/extension be skipped
3992
+ // outright when the project has no such files — its result is provably
3993
+ // empty, so skipping is behavior-identical and the cost drops to zero
3994
+ // (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
3995
+ // Passes without an explicit language filter always run.
3996
+ const langs = queries.getDistinctFileLanguages();
3997
+ const has = (...ls) => ls.some((l) => langs.has(l));
3998
+ const NONE = [];
2948
3999
  // Cross-file Go method→type `contains` edges must be synthesized AND persisted
2949
4000
  // FIRST: a method declared in a different file from its receiver type is
2950
4001
  // otherwise orphaned from the struct, and goImplementsEdges (next) derives a
2951
4002
  // struct's method set from its `contains` edges — so without this it would
2952
4003
  // under-count the interfaces a cross-file struct satisfies. (#583)
2953
- const goMethodContains = goCrossFileMethodContainsEdges(queries);
2954
- if (goMethodContains.length > 0)
2955
- queries.insertEdges(goMethodContains);
4004
+ // Writer-side WAL backstop for the insert loops here (see the param doc):
4005
+ // one fstat when under the valve's hard cap, a parked full backfill past it.
4006
+ const foldIfOver = async () => {
4007
+ const bp = backpressure?.();
4008
+ if (bp)
4009
+ await bp;
4010
+ };
4011
+ const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
4012
+ for (let i = 0; i < goMethodContains.length; i += 2000) {
4013
+ queries.insertEdges(goMethodContains.slice(i, i + 2000));
4014
+ await yieldToLoop();
4015
+ await foldIfOver();
4016
+ }
2956
4017
  await yieldToLoop();
4018
+ __mark('goMethodContains');
2957
4019
  // Go implicit `implements` edges must be synthesized AND persisted next: the
2958
4020
  // interface-dispatch bridge below reads `implements` edges from the DB, and
2959
4021
  // Go has none statically. (Other languages already have static implements
2960
4022
  // edges from extraction, so they don't need this pre-pass.)
2961
- const goImpl = goImplementsEdges(queries);
2962
- if (goImpl.length > 0)
2963
- queries.insertEdges(goImpl);
2964
- await yieldToLoop();
2965
- const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
2966
- await yieldToLoop();
2967
- const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
2968
- await yieldToLoop();
2969
- const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
2970
- await yieldToLoop();
2971
- const renderEdges = reactRenderEdges(queries, ctx);
2972
- await yieldToLoop();
2973
- const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
2974
- await yieldToLoop();
2975
- const vueEdges = vueTemplateEdges(ctx);
2976
- await yieldToLoop();
2977
- const svelteKitEdges = svelteKitLoadEdges(ctx);
2978
- await yieldToLoop();
2979
- const pascalEdges = pascalFormEdges(ctx);
2980
- await yieldToLoop();
2981
- const flutterEdges = flutterBuildEdges(queries, ctx);
2982
- await yieldToLoop();
2983
- const cppEdges = cppOverrideEdges(queries);
2984
- await yieldToLoop();
2985
- const ifaceEdges = interfaceOverrideEdges(queries);
2986
- await yieldToLoop();
2987
- const kotlinExpectActual = kotlinExpectActualEdges(queries);
2988
- await yieldToLoop();
2989
- const goGrpcEdges = goGrpcStubImplEdges(queries);
2990
- await yieldToLoop();
2991
- const rnEventEdgesList = rnEventEdges(ctx);
2992
- await yieldToLoop();
2993
- const fabricNativeEdges = fabricNativeImplEdges(ctx);
2994
- await yieldToLoop();
2995
- const expoXPlatEdges = expoCrossPlatformEdges(queries);
2996
- await yieldToLoop();
2997
- const rnXPlatEdges = rnCrossPlatformEdges(queries);
2998
- await yieldToLoop();
2999
- const mybatisEdges = mybatisJavaXmlEdges(queries);
3000
- await yieldToLoop();
3001
- const ginEdges = ginMiddlewareChainEdges(queries, ctx);
3002
- await yieldToLoop();
3003
- const thunkEdges = reduxThunkEdges(queries, ctx);
3004
- await yieldToLoop();
3005
- const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
3006
- await yieldToLoop();
3007
- const rtkEdges = rtkQueryEdges(queries, ctx);
3008
- await yieldToLoop();
3009
- const piniaEdges = piniaStoreEdges(ctx);
3010
- await yieldToLoop();
3011
- const vuexEdges = vuexDispatchEdges(ctx);
3012
- await yieldToLoop();
3013
- const celeryEdges = celeryDispatchEdges(ctx);
3014
- await yieldToLoop();
3015
- const springEdges = springEventEdges(ctx);
3016
- await yieldToLoop();
3017
- const mediatrEdges = mediatrDispatchEdges(ctx);
3018
- await yieldToLoop();
3019
- const sidekiqEdges = sidekiqDispatchEdges(ctx);
3020
- await yieldToLoop();
3021
- const laravelEdges = laravelEventEdges(ctx);
3022
- await yieldToLoop();
3023
- const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
3024
- await yieldToLoop();
3025
- const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
3026
- await yieldToLoop();
3027
- const arktsStartupEdges = arktsEntryEdges(queries, ctx);
4023
+ const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
4024
+ for (let i = 0; i < goImpl.length; i += 2000) {
4025
+ queries.insertEdges(goImpl.slice(i, i + 2000));
4026
+ await yieldToLoop();
4027
+ await foldIfOver();
4028
+ }
3028
4029
  await yieldToLoop();
4030
+ __mark('goImplements');
4031
+ // Run the independent passes (see SYNTH_PASSES). Their results are merged in
4032
+ // REGISTRY ORDER below regardless of execution order, and none of their edges
4033
+ // persist until that merge — so every pass sees the same committed
4034
+ // post-resolution DB state whether it runs sequentially here or on a resolver
4035
+ // pool worker. With a live pool (already booted on ≥150k-ref repos), passes
4036
+ // fan out across its read-only workers and the per-pass wall-clock comes from
4037
+ // the worker; a pass that fails on a worker falls back to running on the main
4038
+ // thread, so a worker crash isolates to a retry instead of failing synthesis.
4039
+ const passEdges = new Array(exports.SYNTH_PASSES.length).fill(NONE);
4040
+ const markPass = (label, dt) => {
4041
+ if (process.env.HOMEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.HOMEGRAPH_SYNTH_TIMINGS === 'all')) {
4042
+ console.error(`[synth-timing] ${label}: ${dt}ms`);
4043
+ }
4044
+ passesDone++;
4045
+ emit(passesDone);
4046
+ };
4047
+ const runPassOnMain = async (i) => {
4048
+ const pass = exports.SYNTH_PASSES[i];
4049
+ const t0 = Date.now();
4050
+ passEdges[i] = await pass.run(queries, ctx, yieldToLoop, subProgress);
4051
+ await yieldToLoop();
4052
+ markPass(pass.name, Date.now() - t0);
4053
+ };
4054
+ const gatedIn = [];
4055
+ for (let i = 0; i < exports.SYNTH_PASSES.length; i++) {
4056
+ if (exports.SYNTH_PASSES[i].gate(has))
4057
+ gatedIn.push(i);
4058
+ else
4059
+ markPass(exports.SYNTH_PASSES[i].name, 0);
4060
+ }
4061
+ // Above this node count, a pass that OOM-killed its worker must NOT be
4062
+ // retried on the main thread — the retry would OOM the whole process and
4063
+ // take the index with it (the #1212 failure class). Below it, a worker
4064
+ // failure is more likely a transient crash than a memory ceiling, and the
4065
+ // main-thread retry keeps coverage. Skipping loses only that pass's
4066
+ // synthesized edges; the index still completes.
4067
+ const MAIN_RETRY_MAX_NODES = 1_500_000;
4068
+ const graphNodes = queries.getNodeAndEdgeCount().nodes;
4069
+ if (pool && gatedIn.length > 1) {
4070
+ await Promise.all(gatedIn.map(async (i) => {
4071
+ const pass = exports.SYNTH_PASSES[i];
4072
+ try {
4073
+ const out = await pool.runSynthPass(pass.name);
4074
+ passEdges[i] = out.edges;
4075
+ markPass(pass.name, out.ms);
4076
+ }
4077
+ catch (err) {
4078
+ if (graphNodes > MAIN_RETRY_MAX_NODES) {
4079
+ // Worker died at a scale where the main-thread retry is a process
4080
+ // OOM risk: skip the pass, keep the index alive, and say so.
4081
+ console.error(`[synthesis] pass '${pass.name}' failed on a worker at ${graphNodes} nodes — skipped (edges from this pass are absent): ${err instanceof Error ? err.message : String(err)}`);
4082
+ markPass(`${pass.name} (skipped at scale)`, 0);
4083
+ return;
4084
+ }
4085
+ // Worker-side failure (crash, OOM, unknown pass after a version
4086
+ // mismatch): retry this one pass on the main thread.
4087
+ await runPassOnMain(i);
4088
+ }
4089
+ }));
4090
+ }
4091
+ else {
4092
+ for (const i of gatedIn) {
4093
+ await runPassOnMain(i);
4094
+ }
4095
+ }
3029
4096
  const merged = [];
3030
4097
  const seen = new Set();
3031
- for (const e of [
3032
- ...fieldEdges,
3033
- ...closureCollEdges,
3034
- ...emitterEdges,
3035
- ...renderEdges,
3036
- ...jsxEdges,
3037
- ...vueEdges,
3038
- ...svelteKitEdges,
3039
- ...pascalEdges,
3040
- ...flutterEdges,
3041
- ...cppEdges,
3042
- ...ifaceEdges,
3043
- ...kotlinExpectActual,
3044
- ...goGrpcEdges,
3045
- ...rnEventEdgesList,
3046
- ...fabricNativeEdges,
3047
- ...expoXPlatEdges,
3048
- ...rnXPlatEdges,
3049
- ...mybatisEdges,
3050
- ...ginEdges,
3051
- ...thunkEdges,
3052
- ...registryEdges,
3053
- ...rtkEdges,
3054
- ...piniaEdges,
3055
- ...vuexEdges,
3056
- ...celeryEdges,
3057
- ...springEdges,
3058
- ...mediatrEdges,
3059
- ...sidekiqEdges,
3060
- ...laravelEdges,
3061
- ...cFnPtrEdges,
3062
- ...goframeEdges,
3063
- ...arktsStartupEdges,
3064
- ]) {
4098
+ for (const e of passEdges.flat()) {
3065
4099
  const key = `${e.source}>${e.target}`;
3066
4100
  if (seen.has(key))
3067
4101
  continue;
3068
4102
  seen.add(key);
3069
4103
  merged.push(e);
3070
4104
  }
3071
- if (merged.length > 0)
3072
- queries.insertEdges(merged);
4105
+ __mark('dedupe-merge');
4106
+ // Chunked insert with yields: on the Linux kernel the merged synthesized
4107
+ // edge set is ~275k rows, and one transaction for all of them was a 20s
4108
+ // unyielded main-thread span (#1212 follow-up) — the last one in the tail.
4109
+ for (let i = 0; i < merged.length; i += 2000) {
4110
+ queries.insertEdges(merged.slice(i, i + 2000));
4111
+ await yieldToLoop();
4112
+ await foldIfOver();
4113
+ }
4114
+ __mark('insertMergedEdges');
3073
4115
  return merged.length + goImpl.length + goMethodContains.length;
3074
4116
  }
3075
4117
  //# sourceMappingURL=callback-synthesizer.js.map