@aroman22/codegraph-vba-darwin-arm64 1.5.2 → 1.6.1

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 (335) hide show
  1. package/bin/codegraph +5 -0
  2. package/lib/dist/bin/codegraph.d.ts +1 -1
  3. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  4. package/lib/dist/bin/codegraph.js +307 -81
  5. package/lib/dist/bin/codegraph.js.map +1 -1
  6. package/lib/dist/bin/command-supervision.d.ts +38 -1
  7. package/lib/dist/bin/command-supervision.d.ts.map +1 -1
  8. package/lib/dist/bin/command-supervision.js +22 -3
  9. package/lib/dist/bin/command-supervision.js.map +1 -1
  10. package/lib/dist/db/index.d.ts +84 -5
  11. package/lib/dist/db/index.d.ts.map +1 -1
  12. package/lib/dist/db/index.js +209 -12
  13. package/lib/dist/db/index.js.map +1 -1
  14. package/lib/dist/db/migrations.d.ts +1 -1
  15. package/lib/dist/db/migrations.d.ts.map +1 -1
  16. package/lib/dist/db/migrations.js +80 -3
  17. package/lib/dist/db/migrations.js.map +1 -1
  18. package/lib/dist/db/queries.d.ts +116 -4
  19. package/lib/dist/db/queries.d.ts.map +1 -1
  20. package/lib/dist/db/queries.js +294 -7
  21. package/lib/dist/db/queries.js.map +1 -1
  22. package/lib/dist/db/schema.sql +43 -1
  23. package/lib/dist/db/wal-valve.d.ts +106 -0
  24. package/lib/dist/db/wal-valve.d.ts.map +1 -0
  25. package/lib/dist/db/wal-valve.js +208 -0
  26. package/lib/dist/db/wal-valve.js.map +1 -0
  27. package/lib/dist/directory.d.ts +9 -5
  28. package/lib/dist/directory.d.ts.map +1 -1
  29. package/lib/dist/directory.js +210 -19
  30. package/lib/dist/directory.js.map +1 -1
  31. package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
  32. package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
  33. package/lib/dist/extraction/cfml-extractor.js +494 -0
  34. package/lib/dist/extraction/cfml-extractor.js.map +1 -0
  35. package/lib/dist/extraction/grammars.d.ts +25 -1
  36. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  37. package/lib/dist/extraction/grammars.js +177 -23
  38. package/lib/dist/extraction/grammars.js.map +1 -1
  39. package/lib/dist/extraction/index.d.ts +47 -2
  40. package/lib/dist/extraction/index.d.ts.map +1 -1
  41. package/lib/dist/extraction/index.js +568 -51
  42. package/lib/dist/extraction/index.js.map +1 -1
  43. package/lib/dist/extraction/languages/arkts.d.ts +3 -0
  44. package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
  45. package/lib/dist/extraction/languages/arkts.js +127 -0
  46. package/lib/dist/extraction/languages/arkts.js.map +1 -0
  47. package/lib/dist/extraction/languages/c-cpp.d.ts +98 -0
  48. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  49. package/lib/dist/extraction/languages/c-cpp.js +563 -1
  50. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  51. package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
  52. package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
  53. package/lib/dist/extraction/languages/cfquery.js +28 -0
  54. package/lib/dist/extraction/languages/cfquery.js.map +1 -0
  55. package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
  56. package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
  57. package/lib/dist/extraction/languages/cfscript.js +73 -0
  58. package/lib/dist/extraction/languages/cfscript.js.map +1 -0
  59. package/lib/dist/extraction/languages/cobol.d.ts +33 -0
  60. package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
  61. package/lib/dist/extraction/languages/cobol.js +499 -0
  62. package/lib/dist/extraction/languages/cobol.js.map +1 -0
  63. package/lib/dist/extraction/languages/erlang.d.ts +3 -0
  64. package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
  65. package/lib/dist/extraction/languages/erlang.js +350 -0
  66. package/lib/dist/extraction/languages/erlang.js.map +1 -0
  67. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  68. package/lib/dist/extraction/languages/index.js +18 -0
  69. package/lib/dist/extraction/languages/index.js.map +1 -1
  70. package/lib/dist/extraction/languages/nix.d.ts +3 -0
  71. package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
  72. package/lib/dist/extraction/languages/nix.js +294 -0
  73. package/lib/dist/extraction/languages/nix.js.map +1 -0
  74. package/lib/dist/extraction/languages/solidity.d.ts +3 -0
  75. package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
  76. package/lib/dist/extraction/languages/solidity.js +293 -0
  77. package/lib/dist/extraction/languages/solidity.js.map +1 -0
  78. package/lib/dist/extraction/languages/terraform.d.ts +3 -0
  79. package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
  80. package/lib/dist/extraction/languages/terraform.js +641 -0
  81. package/lib/dist/extraction/languages/terraform.js.map +1 -0
  82. package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
  83. package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
  84. package/lib/dist/extraction/languages/vbnet.js +141 -0
  85. package/lib/dist/extraction/languages/vbnet.js.map +1 -0
  86. package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
  87. package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  88. package/lib/dist/extraction/mybatis-extractor.js +140 -38
  89. package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
  90. package/lib/dist/extraction/parse-pool.d.ts +28 -1
  91. package/lib/dist/extraction/parse-pool.d.ts.map +1 -1
  92. package/lib/dist/extraction/parse-pool.js +76 -7
  93. package/lib/dist/extraction/parse-pool.js.map +1 -1
  94. package/lib/dist/extraction/parse-worker.js +11 -4
  95. package/lib/dist/extraction/parse-worker.js.map +1 -1
  96. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  97. package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
  98. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  99. package/lib/dist/extraction/tree-sitter-types.d.ts +20 -1
  100. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  101. package/lib/dist/extraction/tree-sitter.d.ts +60 -1
  102. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  103. package/lib/dist/extraction/tree-sitter.js +905 -20
  104. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  105. package/lib/dist/extraction/vba/call-sweep.d.ts +3 -0
  106. package/lib/dist/extraction/vba/call-sweep.d.ts.map +1 -0
  107. package/lib/dist/extraction/vba/call-sweep.js +206 -0
  108. package/lib/dist/extraction/vba/call-sweep.js.map +1 -0
  109. package/lib/dist/extraction/vba/calls.d.ts +55 -0
  110. package/lib/dist/extraction/vba/calls.d.ts.map +1 -0
  111. package/lib/dist/extraction/vba/calls.js +439 -0
  112. package/lib/dist/extraction/vba/calls.js.map +1 -0
  113. package/lib/dist/extraction/vba/constants.d.ts +42 -0
  114. package/lib/dist/extraction/vba/constants.d.ts.map +1 -0
  115. package/lib/dist/extraction/vba/constants.js +112 -0
  116. package/lib/dist/extraction/vba/constants.js.map +1 -0
  117. package/lib/dist/extraction/vba/context.d.ts +175 -0
  118. package/lib/dist/extraction/vba/context.d.ts.map +1 -0
  119. package/lib/dist/extraction/vba/context.js +268 -0
  120. package/lib/dist/extraction/vba/context.js.map +1 -0
  121. package/lib/dist/extraction/vba/controls.d.ts +24 -0
  122. package/lib/dist/extraction/vba/controls.d.ts.map +1 -0
  123. package/lib/dist/extraction/vba/controls.js +89 -0
  124. package/lib/dist/extraction/vba/controls.js.map +1 -0
  125. package/lib/dist/extraction/vba/declarations.d.ts +3 -0
  126. package/lib/dist/extraction/vba/declarations.d.ts.map +1 -0
  127. package/lib/dist/extraction/vba/declarations.js +154 -0
  128. package/lib/dist/extraction/vba/declarations.js.map +1 -0
  129. package/lib/dist/extraction/vba/dims.d.ts +3 -0
  130. package/lib/dist/extraction/vba/dims.d.ts.map +1 -0
  131. package/lib/dist/extraction/vba/dims.js +186 -0
  132. package/lib/dist/extraction/vba/dims.js.map +1 -0
  133. package/lib/dist/extraction/vba/docmd.d.ts +16 -0
  134. package/lib/dist/extraction/vba/docmd.d.ts.map +1 -0
  135. package/lib/dist/extraction/vba/docmd.js +179 -0
  136. package/lib/dist/extraction/vba/docmd.js.map +1 -0
  137. package/lib/dist/extraction/vba/enums-consts.d.ts +15 -0
  138. package/lib/dist/extraction/vba/enums-consts.d.ts.map +1 -0
  139. package/lib/dist/extraction/vba/enums-consts.js +178 -0
  140. package/lib/dist/extraction/vba/enums-consts.js.map +1 -0
  141. package/lib/dist/extraction/vba/implements.d.ts +3 -0
  142. package/lib/dist/extraction/vba/implements.d.ts.map +1 -0
  143. package/lib/dist/extraction/vba/implements.js +52 -0
  144. package/lib/dist/extraction/vba/implements.js.map +1 -0
  145. package/lib/dist/extraction/vba/procedures.d.ts +3 -0
  146. package/lib/dist/extraction/vba/procedures.d.ts.map +1 -0
  147. package/lib/dist/extraction/vba/procedures.js +239 -0
  148. package/lib/dist/extraction/vba/procedures.js.map +1 -0
  149. package/lib/dist/extraction/vba/sql-wrapper.d.ts +17 -0
  150. package/lib/dist/extraction/vba/sql-wrapper.d.ts.map +1 -0
  151. package/lib/dist/extraction/vba/sql-wrapper.js +212 -0
  152. package/lib/dist/extraction/vba/sql-wrapper.js.map +1 -0
  153. package/lib/dist/extraction/vba/tempvars.d.ts +10 -0
  154. package/lib/dist/extraction/vba/tempvars.d.ts.map +1 -0
  155. package/lib/dist/extraction/vba/tempvars.js +138 -0
  156. package/lib/dist/extraction/vba/tempvars.js.map +1 -0
  157. package/lib/dist/extraction/vba/text-utils.d.ts +73 -0
  158. package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -0
  159. package/lib/dist/extraction/vba/text-utils.js +195 -0
  160. package/lib/dist/extraction/vba/text-utils.js.map +1 -0
  161. package/lib/dist/extraction/vba-extractor.d.ts +3 -951
  162. package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
  163. package/lib/dist/extraction/vba-extractor.js +46 -2934
  164. package/lib/dist/extraction/vba-extractor.js.map +1 -1
  165. package/lib/dist/extraction/vba-preprocess.d.ts +1 -1
  166. package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
  167. package/lib/dist/extraction/vba-preprocess.js +300 -88
  168. package/lib/dist/extraction/vba-preprocess.js.map +1 -1
  169. package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  170. package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  171. package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  172. package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  173. package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  174. package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  175. package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  176. package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  177. package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  178. package/lib/dist/graph/traversal.d.ts.map +1 -1
  179. package/lib/dist/graph/traversal.js +76 -17
  180. package/lib/dist/graph/traversal.js.map +1 -1
  181. package/lib/dist/index.d.ts +81 -1
  182. package/lib/dist/index.d.ts.map +1 -1
  183. package/lib/dist/index.js +460 -10
  184. package/lib/dist/index.js.map +1 -1
  185. package/lib/dist/installer/index.d.ts +42 -0
  186. package/lib/dist/installer/index.d.ts.map +1 -1
  187. package/lib/dist/installer/index.js +97 -2
  188. package/lib/dist/installer/index.js.map +1 -1
  189. package/lib/dist/mcp/daemon.d.ts +35 -3
  190. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  191. package/lib/dist/mcp/daemon.js +101 -9
  192. package/lib/dist/mcp/daemon.js.map +1 -1
  193. package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  194. package/lib/dist/mcp/dynamic-boundaries.js +2 -1
  195. package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
  196. package/lib/dist/mcp/early-ppid.d.ts +26 -0
  197. package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
  198. package/lib/dist/mcp/early-ppid.js +29 -0
  199. package/lib/dist/mcp/early-ppid.js.map +1 -0
  200. package/lib/dist/mcp/index.d.ts.map +1 -1
  201. package/lib/dist/mcp/index.js +29 -4
  202. package/lib/dist/mcp/index.js.map +1 -1
  203. package/lib/dist/mcp/liveness-watchdog.d.ts +18 -1
  204. package/lib/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  205. package/lib/dist/mcp/liveness-watchdog.js +73 -13
  206. package/lib/dist/mcp/liveness-watchdog.js.map +1 -1
  207. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  208. package/lib/dist/mcp/proxy.js +30 -4
  209. package/lib/dist/mcp/proxy.js.map +1 -1
  210. package/lib/dist/mcp/query-pool.d.ts +14 -0
  211. package/lib/dist/mcp/query-pool.d.ts.map +1 -1
  212. package/lib/dist/mcp/query-pool.js +18 -0
  213. package/lib/dist/mcp/query-pool.js.map +1 -1
  214. package/lib/dist/mcp/session.d.ts +14 -0
  215. package/lib/dist/mcp/session.d.ts.map +1 -1
  216. package/lib/dist/mcp/session.js +28 -1
  217. package/lib/dist/mcp/session.js.map +1 -1
  218. package/lib/dist/mcp/startup-handshake.d.ts +44 -0
  219. package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
  220. package/lib/dist/mcp/startup-handshake.js +73 -0
  221. package/lib/dist/mcp/startup-handshake.js.map +1 -0
  222. package/lib/dist/mcp/tools.d.ts +22 -0
  223. package/lib/dist/mcp/tools.d.ts.map +1 -1
  224. package/lib/dist/mcp/tools.js +289 -23
  225. package/lib/dist/mcp/tools.js.map +1 -1
  226. package/lib/dist/mcp/transport.d.ts.map +1 -1
  227. package/lib/dist/mcp/transport.js +18 -0
  228. package/lib/dist/mcp/transport.js.map +1 -1
  229. package/lib/dist/project-config.d.ts +44 -0
  230. package/lib/dist/project-config.d.ts.map +1 -1
  231. package/lib/dist/project-config.js +151 -2
  232. package/lib/dist/project-config.js.map +1 -1
  233. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  234. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  235. package/lib/dist/resolution/c-fnptr-synthesizer.js +170 -146
  236. package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  237. package/lib/dist/resolution/callback-synthesizer.d.ts +1 -1
  238. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  239. package/lib/dist/resolution/callback-synthesizer.js +1109 -97
  240. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  241. package/lib/dist/resolution/cooperative-yield.d.ts +32 -0
  242. package/lib/dist/resolution/cooperative-yield.d.ts.map +1 -0
  243. package/lib/dist/resolution/cooperative-yield.js +42 -0
  244. package/lib/dist/resolution/cooperative-yield.js.map +1 -0
  245. package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
  246. package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
  247. package/lib/dist/resolution/frameworks/cics.js +90 -0
  248. package/lib/dist/resolution/frameworks/cics.js.map +1 -0
  249. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  250. package/lib/dist/resolution/frameworks/index.js +6 -0
  251. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  252. package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
  253. package/lib/dist/resolution/frameworks/java.js +14 -6
  254. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  255. package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
  256. package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  257. package/lib/dist/resolution/frameworks/terraform.js +277 -0
  258. package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
  259. package/lib/dist/resolution/goframe-synthesizer.d.ts +2 -1
  260. package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  261. package/lib/dist/resolution/goframe-synthesizer.js +8 -3
  262. package/lib/dist/resolution/goframe-synthesizer.js.map +1 -1
  263. package/lib/dist/resolution/import-resolver.d.ts +7 -0
  264. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  265. package/lib/dist/resolution/import-resolver.js +129 -4
  266. package/lib/dist/resolution/import-resolver.js.map +1 -1
  267. package/lib/dist/resolution/index.d.ts +64 -2
  268. package/lib/dist/resolution/index.d.ts.map +1 -1
  269. package/lib/dist/resolution/index.js +476 -58
  270. package/lib/dist/resolution/index.js.map +1 -1
  271. package/lib/dist/resolution/name-matcher.d.ts +22 -3
  272. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  273. package/lib/dist/resolution/name-matcher.js +613 -39
  274. package/lib/dist/resolution/name-matcher.js.map +1 -1
  275. package/lib/dist/resolution/strip-comments.d.ts +1 -1
  276. package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
  277. package/lib/dist/resolution/strip-comments.js +49 -0
  278. package/lib/dist/resolution/strip-comments.js.map +1 -1
  279. package/lib/dist/resolution/types.d.ts +29 -0
  280. package/lib/dist/resolution/types.d.ts.map +1 -1
  281. package/lib/dist/resolution/workspace-packages.d.ts +10 -0
  282. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
  283. package/lib/dist/resolution/workspace-packages.js +142 -4
  284. package/lib/dist/resolution/workspace-packages.js.map +1 -1
  285. package/lib/dist/search/identifier-segments.d.ts +60 -0
  286. package/lib/dist/search/identifier-segments.d.ts.map +1 -0
  287. package/lib/dist/search/identifier-segments.js +176 -0
  288. package/lib/dist/search/identifier-segments.js.map +1 -0
  289. package/lib/dist/sync/git-hooks.d.ts.map +1 -1
  290. package/lib/dist/sync/git-hooks.js +2 -0
  291. package/lib/dist/sync/git-hooks.js.map +1 -1
  292. package/lib/dist/sync/watcher.d.ts +10 -5
  293. package/lib/dist/sync/watcher.d.ts.map +1 -1
  294. package/lib/dist/sync/watcher.js +51 -14
  295. package/lib/dist/sync/watcher.js.map +1 -1
  296. package/lib/dist/sync/worktree.d.ts +9 -0
  297. package/lib/dist/sync/worktree.d.ts.map +1 -1
  298. package/lib/dist/sync/worktree.js +45 -0
  299. package/lib/dist/sync/worktree.js.map +1 -1
  300. package/lib/dist/types.d.ts +25 -2
  301. package/lib/dist/types.d.ts.map +1 -1
  302. package/lib/dist/types.js +10 -0
  303. package/lib/dist/types.js.map +1 -1
  304. package/lib/dist/upgrade/index.d.ts +34 -2
  305. package/lib/dist/upgrade/index.d.ts.map +1 -1
  306. package/lib/dist/upgrade/index.js +164 -15
  307. package/lib/dist/upgrade/index.js.map +1 -1
  308. package/lib/dist/upgrade/remove-binary.d.ts +87 -0
  309. package/lib/dist/upgrade/remove-binary.d.ts.map +1 -0
  310. package/lib/dist/upgrade/remove-binary.js +289 -0
  311. package/lib/dist/upgrade/remove-binary.js.map +1 -0
  312. package/lib/dist/upgrade/update-check.d.ts +92 -0
  313. package/lib/dist/upgrade/update-check.d.ts.map +1 -0
  314. package/lib/dist/upgrade/update-check.js +258 -0
  315. package/lib/dist/upgrade/update-check.js.map +1 -0
  316. package/lib/node_modules/.modules.yaml +1 -1
  317. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  318. package/lib/package.json +1 -1
  319. package/package.json +1 -1
  320. package/lib/dist/reasoning/config.d.ts +0 -45
  321. package/lib/dist/reasoning/config.d.ts.map +0 -1
  322. package/lib/dist/reasoning/config.js +0 -171
  323. package/lib/dist/reasoning/config.js.map +0 -1
  324. package/lib/dist/reasoning/credentials.d.ts +0 -5
  325. package/lib/dist/reasoning/credentials.d.ts.map +0 -1
  326. package/lib/dist/reasoning/credentials.js +0 -83
  327. package/lib/dist/reasoning/credentials.js.map +0 -1
  328. package/lib/dist/reasoning/login.d.ts +0 -21
  329. package/lib/dist/reasoning/login.d.ts.map +0 -1
  330. package/lib/dist/reasoning/login.js +0 -85
  331. package/lib/dist/reasoning/login.js.map +0 -1
  332. package/lib/dist/reasoning/reasoner.d.ts +0 -43
  333. package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
  334. package/lib/dist/reasoning/reasoner.js +0 -308
  335. package/lib/dist/reasoning/reasoner.js.map +0 -1
@@ -5,6 +5,7 @@ const generated_detection_1 = require("../extraction/generated-detection");
5
5
  const strip_comments_1 = require("./strip-comments");
6
6
  const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
7
7
  const goframe_synthesizer_1 = require("./goframe-synthesizer");
8
+ const cooperative_yield_1 = require("./cooperative-yield");
8
9
  const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
9
10
  const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
10
11
  const MAX_CALLBACKS_PER_CHANNEL = 40;
@@ -39,6 +40,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
39
40
  const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
40
41
  const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
41
42
  const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
43
+ // The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
44
+ // trailing-closure syntax, so ONLY those languages can ever contribute a
45
+ // dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
46
+ // Swift dispatcher's field name) would be a wrong edge, not a missed one.
47
+ // Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
48
+ // in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
49
+ // where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
50
+ // 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
51
+ const CC_LANGUAGES = new Set(['swift', 'kotlin']);
42
52
  function kebabToPascal(s) {
43
53
  return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
44
54
  }
@@ -74,6 +84,35 @@ function sliceLines(content, startLine, endLine) {
74
84
  return null;
75
85
  return content.split('\n').slice(startLine - 1, endLine).join('\n');
76
86
  }
87
+ /**
88
+ * Per-match line resolver over `src`, 1-based at `baseLine`. The inline
89
+ * `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
90
+ * which goes quadratic on a match-dense source (a generated function full of
91
+ * `.push(` calls re-scanned tens of thousands of times was most of the #1235
92
+ * indexing wedge). Builds the newline index once — lazily, since most sources
93
+ * never produce a match — then answers each call with a binary search.
94
+ */
95
+ function makeLineAt(src, baseLine) {
96
+ let nl = null;
97
+ return (idx) => {
98
+ if (!nl) {
99
+ nl = [];
100
+ for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
101
+ nl.push(i);
102
+ }
103
+ // Count newlines strictly before idx.
104
+ let lo = 0;
105
+ let hi = nl.length;
106
+ while (lo < hi) {
107
+ const mid = (lo + hi) >> 1;
108
+ if (nl[mid] < idx)
109
+ lo = mid + 1;
110
+ else
111
+ hi = mid;
112
+ }
113
+ return baseLine + lo;
114
+ };
115
+ }
77
116
  function registrarField(src) {
78
117
  const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
79
118
  return m ? m[1] : null;
@@ -113,10 +152,13 @@ function* methodAndFunctionNodes(queries) {
113
152
  yield* queries.iterateNodesByKind('function');
114
153
  }
115
154
  /** Phase 1: field-backed observer channels (registrar/dispatcher share a store). */
116
- function fieldChannelEdges(queries, ctx) {
155
+ async function fieldChannelEdges(queries, ctx, onYield) {
117
156
  const registrars = [];
118
157
  const dispatchers = [];
158
+ let scanned = 0;
119
159
  for (const m of methodAndFunctionNodes(queries)) {
160
+ if ((++scanned & 255) === 0)
161
+ await onYield(); // #1091: yield mid-scan on huge graphs
120
162
  const isReg = REGISTRAR_NAME.test(m.name);
121
163
  const isDisp = DISPATCHER_NAME.test(m.name);
122
164
  if (!isReg && !isDisp)
@@ -197,7 +239,7 @@ function fieldChannelEdges(queries, ctx) {
197
239
  * subclass `DataRequest.validate`), bounded by a fan-out cap so a generic field
198
240
  * name shared across unrelated classes can't fan out into noise.
199
241
  */
200
- function closureCollectionEdges(queries, ctx) {
242
+ async function closureCollectionEdges(queries, ctx, onYield) {
201
243
  const dispatchers = new Map(); // field → dispatcher methods + forEach line
202
244
  const registrars = new Map(); // field → registrar methods + append line
203
245
  const addReg = (field, node, absLine) => {
@@ -208,7 +250,18 @@ function closureCollectionEdges(queries, ctx) {
208
250
  arr.push({ node, line: absLine });
209
251
  registrars.set(field, arr);
210
252
  };
253
+ // Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
254
+ // on a repo with a huge file this is the heaviest synthesis pass — yield
255
+ // mid-scan (and mid-match-loop below: a single generated function dense with
256
+ // matches must not starve the watchdog either) so it can't wedge the #850
257
+ // watchdog on its own (#1091, #1235).
258
+ let scanned = 0;
259
+ let matchTick = 0;
211
260
  for (const m of methodAndFunctionNodes(queries)) {
261
+ if ((++scanned & 127) === 0)
262
+ await onYield();
263
+ if (!CC_LANGUAGES.has(m.language))
264
+ continue;
212
265
  const content = ctx.readFile(m.filePath);
213
266
  const src = content && sliceLines(content, m.startLine, m.endLine);
214
267
  if (!src)
@@ -217,11 +270,13 @@ function closureCollectionEdges(queries, ctx) {
217
270
  const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
218
271
  if (!hasForEach && !hasAppend)
219
272
  continue;
220
- const lineAt = (idx) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1;
273
+ const lineAt = makeLineAt(src, m.startLine ?? 1);
221
274
  if (hasForEach) {
222
275
  CC_DISPATCH_RE.lastIndex = 0;
223
276
  let d;
224
277
  while ((d = CC_DISPATCH_RE.exec(src))) {
278
+ if ((++matchTick & 255) === 0)
279
+ await onYield();
225
280
  const arr = dispatchers.get(d[1]) ?? [];
226
281
  if (!arr.some((n) => n.node.id === m.id))
227
282
  arr.push({ node: m, line: lineAt(d.index) });
@@ -231,12 +286,18 @@ function closureCollectionEdges(queries, ctx) {
231
286
  if (hasAppend) {
232
287
  CC_APPEND_WRITE_RE.lastIndex = 0;
233
288
  let w;
234
- while ((w = CC_APPEND_WRITE_RE.exec(src)))
289
+ while ((w = CC_APPEND_WRITE_RE.exec(src))) {
290
+ if ((++matchTick & 255) === 0)
291
+ await onYield();
235
292
  addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
293
+ }
236
294
  CC_APPEND_DIRECT_RE.lastIndex = 0;
237
295
  let a;
238
- while ((a = CC_APPEND_DIRECT_RE.exec(src)))
296
+ while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
297
+ if ((++matchTick & 255) === 0)
298
+ await onYield();
239
299
  addReg(a[1], m, lineAt(a.index));
300
+ }
240
301
  }
241
302
  }
242
303
  const edges = [];
@@ -265,10 +326,16 @@ function closureCollectionEdges(queries, ctx) {
265
326
  return edges;
266
327
  }
267
328
  /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
268
- function eventEmitterEdges(ctx) {
329
+ async function eventEmitterEdges(ctx, onYield) {
330
+ let scannedFiles = 0;
269
331
  const emitsByEvent = new Map(); // event → dispatcher node ids
270
332
  const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
333
+ let scanned = 0;
271
334
  for (const file of ctx.getAllFiles()) {
335
+ if ((++scannedFiles & 15) === 0)
336
+ await onYield();
337
+ if ((++scanned & 255) === 0)
338
+ await onYield(); // #1091: yield mid-scan on huge graphs
272
339
  const content = ctx.readFile(file);
273
340
  if (!content)
274
341
  continue;
@@ -277,7 +344,7 @@ function eventEmitterEdges(ctx) {
277
344
  if (!hasEmit && !hasOn)
278
345
  continue;
279
346
  const nodesInFile = ctx.getNodesInFile(file);
280
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
347
+ const lineOf = makeLineAt(content, 1);
281
348
  if (hasEmit) {
282
349
  EMIT_RE.lastIndex = 0;
283
350
  let m;
@@ -340,10 +407,13 @@ function eventEmitterEdges(ctx) {
340
407
  * `this.setState`). Over-approximation (all setState methods reach render) is
341
408
  * accepted — it's reachability-correct, like the callback channels.
342
409
  */
343
- function reactRenderEdges(queries, ctx) {
410
+ async function reactRenderEdges(queries, ctx, onYield) {
411
+ let scanned255 = 0;
344
412
  const edges = [];
345
413
  const seen = new Set();
346
- for (const cls of queries.getNodesByKind('class')) {
414
+ for (const cls of queries.iterateNodesByKind('class')) {
415
+ if ((++scanned255 & 63) === 0)
416
+ await onYield();
347
417
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
348
418
  .map((e) => queries.getNodeById(e.target))
349
419
  .filter((n) => !!n && n.kind === 'method');
@@ -383,10 +453,13 @@ function reactRenderEdges(queries, ctx) {
383
453
  * body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
384
454
  * Flutter State classes. Over-approximation accepted (reachability-correct).
385
455
  */
386
- function flutterBuildEdges(queries, ctx) {
456
+ async function flutterBuildEdges(queries, ctx, onYield) {
457
+ let scanned255 = 0;
387
458
  const edges = [];
388
459
  const seen = new Set();
389
- for (const cls of queries.getNodesByKind('class')) {
460
+ for (const cls of queries.iterateNodesByKind('class')) {
461
+ if ((++scanned255 & 63) === 0)
462
+ await onYield();
390
463
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
391
464
  .map((e) => queries.getNodeById(e.target))
392
465
  .filter((n) => !!n && n.kind === 'method');
@@ -417,6 +490,279 @@ function flutterBuildEdges(queries, ctx) {
417
490
  }
418
491
  return edges;
419
492
  }
493
+ /**
494
+ * Reactive ArkUI property decorators: assigning a property carrying one of
495
+ * these re-runs the owning struct's `build()`. Covers both state models —
496
+ * V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
497
+ * (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
498
+ * the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
499
+ * data classes, not struct properties).
500
+ */
501
+ const ARKUI_REACTIVE_DECORATORS = new Set([
502
+ 'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
503
+ 'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
504
+ 'Local', 'Provider', 'Consumer',
505
+ ]);
506
+ /** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
507
+ const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
508
+ /**
509
+ * Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
510
+ * flutter-build). Assigning a reactive-decorated property (`@State count`,
511
+ * `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
512
+ * hop is framework-internal — no static edge — so "onClick → markAllDone →
513
+ * this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
514
+ * for each arkts struct with a `build()` method and at least one reactive
515
+ * property, link every sibling method whose body ASSIGNS (or array-mutates)
516
+ * one of those properties → `build`. Assignment-gated on the struct's OWN
517
+ * reactive property names — a method that merely reads state, or a struct
518
+ * with no reactive properties, gets nothing (this is the precision line the
519
+ * all-sibling-methods design would erase).
520
+ */
521
+ async function arkuiStateBuildEdges(queries, ctx, onYield) {
522
+ let scanned255 = 0;
523
+ const edges = [];
524
+ const seen = new Set();
525
+ for (const struct of queries.iterateNodesByKind('struct')) {
526
+ if ((++scanned255 & 63) === 0)
527
+ await onYield();
528
+ if (struct.language !== 'arkts')
529
+ continue;
530
+ const children = queries.getOutgoingEdges(struct.id, ['contains'])
531
+ .map((e) => queries.getNodeById(e.target))
532
+ .filter((n) => !!n);
533
+ const build = children.find((n) => n.kind === 'method' && n.name === 'build');
534
+ if (!build)
535
+ continue;
536
+ const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
537
+ if (reactiveProps.length === 0)
538
+ continue;
539
+ const propAlternation = reactiveProps
540
+ .map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
541
+ .join('|');
542
+ // `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
543
+ // `=(?!=)` keeps `this.done == x` comparisons out.
544
+ const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
545
+ let added = 0;
546
+ for (const m of children) {
547
+ if (added >= MAX_CALLBACKS_PER_CHANNEL)
548
+ break;
549
+ if (m.kind !== 'method' || m.id === build.id)
550
+ continue;
551
+ const content = ctx.readFile(m.filePath);
552
+ const src = content && sliceLines(content, m.startLine, m.endLine);
553
+ if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
554
+ continue;
555
+ const key = `${m.id}>${build.id}`;
556
+ if (seen.has(key))
557
+ continue;
558
+ seen.add(key);
559
+ edges.push({
560
+ source: m.id, target: build.id, kind: 'calls', line: m.startLine,
561
+ provenance: 'heuristic',
562
+ metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
563
+ });
564
+ added++;
565
+ }
566
+ }
567
+ return edges;
568
+ }
569
+ /** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
570
+ const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
571
+ /** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
572
+ const ARKUI_EMITTER_FANOUT_CAP = 8;
573
+ /**
574
+ * Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
575
+ * bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
576
+ * framework-internal, so an order flow riding it (OrangeShopping's
577
+ * add-to-cart) dead-ends at the emit. Link emit-site enclosing
578
+ * function/method → on/once-site enclosing function/method when both
579
+ * reference the SAME statically-recoverable event key.
580
+ *
581
+ * Key recovery, per call site (comment-stripped enclosing-file source): the
582
+ * first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
583
+ * a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
584
+ * level. Precision scoping learned from the samples monorepo (thousands of
585
+ * unrelated samples, most using eventId 1): NUMERIC keys pair within the same
586
+ * FILE only; NAMED keys pair within the same workspace module directory (or
587
+ * the whole project when it declares no modules — the single-app case), both
588
+ * behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
589
+ * handling — their bodies' calls already attribute to the registering method,
590
+ * so targeting that method keeps the chain connected.
591
+ */
592
+ async function arkuiEmitterEdges(ctx, onYield) {
593
+ let scannedFiles = 0;
594
+ // bucket key -> emit sites / handler sites
595
+ const emits = new Map();
596
+ const handlers = new Map();
597
+ const moduleDirs = (() => {
598
+ const ws = ctx.getWorkspacePackages?.();
599
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
600
+ })();
601
+ const moduleScopeOf = (file) => {
602
+ for (const dir of moduleDirs) {
603
+ if (file === dir || file.startsWith(dir + '/'))
604
+ return dir;
605
+ }
606
+ return '';
607
+ };
608
+ for (const file of ctx.getAllFiles()) {
609
+ if ((++scannedFiles & 15) === 0)
610
+ await onYield();
611
+ if (!file.endsWith('.ets'))
612
+ continue;
613
+ const content = ctx.readFile(file);
614
+ if (!content || !content.includes('emitter.'))
615
+ continue;
616
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
617
+ const nodes = ctx.getNodesInFile(file)
618
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
619
+ ARKUI_EMITTER_CALL_RE.lastIndex = 0;
620
+ let m;
621
+ while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
622
+ const verb = m[1];
623
+ const arg = m[2].trim();
624
+ const line = safe.slice(0, m.index).split('\n').length;
625
+ const encl = nodes
626
+ .filter((n) => n.startLine <= line && n.endLine >= line)
627
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
628
+ if (!encl)
629
+ continue;
630
+ // Recover the event key from the first argument.
631
+ let key = null;
632
+ const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
633
+ const token = idLit ?? arg;
634
+ if (token !== undefined) {
635
+ if (/^\d+$/.test(token)) {
636
+ key = `num:${file}:${token}`; // numeric: same-file only
637
+ }
638
+ else if (token.includes('.')) {
639
+ key = `name:${moduleScopeOf(file)}:${token}`;
640
+ }
641
+ else {
642
+ // Local variable — chase its same-file declaration one level:
643
+ // `let x = new EventsId(K)` / `const x = K`.
644
+ const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
645
+ const decl = safe.match(declRe);
646
+ const inner = (decl?.[1] ?? decl?.[2])?.trim();
647
+ if (inner && /^\d+$/.test(inner))
648
+ key = `num:${file}:${inner}`;
649
+ else if (inner && /^[\w$.]+$/.test(inner))
650
+ key = `name:${moduleScopeOf(file)}:${inner}`;
651
+ }
652
+ }
653
+ if (!key)
654
+ continue;
655
+ const site = { nodeId: encl.id, file, line };
656
+ if (verb === 'emit') {
657
+ (emits.get(key) ?? emits.set(key, []).get(key)).push(site);
658
+ }
659
+ else {
660
+ (handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
661
+ }
662
+ }
663
+ }
664
+ const edges = [];
665
+ const seen = new Set();
666
+ for (const [key, emitSites] of emits) {
667
+ const handlerSites = handlers.get(key);
668
+ if (!handlerSites)
669
+ continue;
670
+ if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
671
+ continue;
672
+ const eventLabel = key.slice(key.lastIndexOf(':') + 1);
673
+ for (const e of emitSites)
674
+ for (const h of handlerSites) {
675
+ if (e.nodeId === h.nodeId)
676
+ continue;
677
+ const dedupe = `${e.nodeId}>${h.nodeId}`;
678
+ if (seen.has(dedupe))
679
+ continue;
680
+ seen.add(dedupe);
681
+ edges.push({
682
+ source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
683
+ provenance: 'heuristic',
684
+ metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
685
+ });
686
+ }
687
+ }
688
+ return edges;
689
+ }
690
+ /** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
691
+ const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
692
+ /**
693
+ * Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
694
+ * 'pages/Detail' })` reaches the `@Entry struct` of
695
+ * `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
696
+ * static edge — so "tap → openDetail → ???" ends at the router call. Bridge
697
+ * literal urls to the page struct: the url resolves against the standard
698
+ * `src/main/ets/` layout (what main_pages.json entries name); candidates
699
+ * prefer the caller's own workspace module (routes are module-scoped), and
700
+ * anything still ambiguous is dropped rather than guessed. Only `@Entry`
701
+ * structs qualify as targets — the decorator is what makes a file a page.
702
+ */
703
+ async function arkuiRouterEdges(ctx, onYield) {
704
+ const edges = [];
705
+ const seen = new Set();
706
+ const allFiles = ctx.getAllFiles();
707
+ const moduleDirs = (() => {
708
+ const ws = ctx.getWorkspacePackages?.();
709
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
710
+ })();
711
+ const moduleScopeOf = (file) => {
712
+ for (const dir of moduleDirs) {
713
+ if (file === dir || file.startsWith(dir + '/'))
714
+ return dir;
715
+ }
716
+ return '';
717
+ };
718
+ let scannedFiles = 0;
719
+ for (const file of allFiles) {
720
+ if ((++scannedFiles & 15) === 0)
721
+ await onYield();
722
+ if (!file.endsWith('.ets'))
723
+ continue;
724
+ const content = ctx.readFile(file);
725
+ if (!content || !content.includes('router.'))
726
+ continue;
727
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
728
+ const nodes = ctx.getNodesInFile(file)
729
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
730
+ ARKUI_ROUTER_RE.lastIndex = 0;
731
+ let m;
732
+ while ((m = ARKUI_ROUTER_RE.exec(safe))) {
733
+ const url = m[1];
734
+ const line = safe.slice(0, m.index).split('\n').length;
735
+ const encl = nodes
736
+ .filter((n) => n.startLine <= line && n.endLine >= line)
737
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
738
+ if (!encl)
739
+ continue;
740
+ const suffix = `/src/main/ets/${url}.ets`;
741
+ let candidates = allFiles.filter((f) => f.endsWith(suffix));
742
+ if (candidates.length > 1) {
743
+ const scope = moduleScopeOf(file);
744
+ const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
745
+ if (sameModule.length > 0)
746
+ candidates = sameModule;
747
+ }
748
+ if (candidates.length !== 1)
749
+ continue; // ambiguous or unresolved — never guess
750
+ const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
751
+ if (!page)
752
+ continue;
753
+ const key = `${encl.id}>${page.id}`;
754
+ if (seen.has(key))
755
+ continue;
756
+ seen.add(key);
757
+ edges.push({
758
+ source: encl.id, target: page.id, kind: 'calls', line,
759
+ provenance: 'heuristic',
760
+ metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
761
+ });
762
+ }
763
+ }
764
+ return edges;
765
+ }
420
766
  /**
421
767
  * Phase 4c: C++ virtual override. A call through a base/interface pointer
422
768
  * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
@@ -427,14 +773,17 @@ function flutterBuildEdges(queries, ctx) {
427
773
  * implementation(s). Over-approximation accepted (reachability-correct); capped
428
774
  * per class and gated to C++ to avoid touching other languages' dispatch.
429
775
  */
430
- function cppOverrideEdges(queries) {
776
+ async function cppOverrideEdges(queries, onYield) {
777
+ let scanned255 = 0;
431
778
  const edges = [];
432
779
  const seen = new Set();
433
780
  const methodsOf = (classId) => queries
434
781
  .getOutgoingEdges(classId, ['contains'])
435
782
  .map((e) => queries.getNodeById(e.target))
436
783
  .filter((n) => !!n && n.kind === 'method');
437
- for (const cls of queries.getNodesByKind('class')) {
784
+ for (const cls of queries.iterateNodesByKind('class')) {
785
+ if ((++scanned255 & 63) === 0)
786
+ await onYield();
438
787
  const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
439
788
  if (subMethods.length === 0)
440
789
  continue;
@@ -488,6 +837,7 @@ function cppOverrideEdges(queries) {
488
837
  // or an `object` (Scala) so the loop also iterates those kinds.
489
838
  const IFACE_OVERRIDE_LANGS = new Set([
490
839
  'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
840
+ 'arkts',
491
841
  ]);
492
842
  /**
493
843
  * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
@@ -501,7 +851,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
501
851
  * with the other dispatch synthesizers; capped per interface. Empty interfaces
502
852
  * (`any`) are skipped so they don't match every struct.
503
853
  */
504
- function goImplementsEdges(queries) {
854
+ async function goImplementsEdges(queries, onYield) {
855
+ let scanned255 = 0;
505
856
  const edges = [];
506
857
  const seen = new Set();
507
858
  const methodNameSet = (id) => new Set(queries
@@ -509,11 +860,24 @@ function goImplementsEdges(queries) {
509
860
  .map((e) => queries.getNodeById(e.target))
510
861
  .filter((n) => !!n && n.kind === 'method')
511
862
  .map((n) => n.name));
512
- const goStructs = queries.getNodesByKind('struct').filter((s) => s.language === 'go');
863
+ // Materializes GO structs only (the pass is language-gated by the caller),
864
+ // never the whole struct kind — that array is O(nodes) on struct-heavy
865
+ // repos like the Linux kernel (#1212).
866
+ const goStructs = [];
867
+ for (const s of queries.iterateNodesByKind('struct')) {
868
+ if ((++scanned255 & 63) === 0)
869
+ await onYield();
870
+ if (s.language === 'go')
871
+ goStructs.push(s);
872
+ }
513
873
  const structMethods = new Map();
514
874
  for (const s of goStructs)
515
875
  structMethods.set(s.id, methodNameSet(s.id));
516
- for (const iface of queries.getNodesByKind('interface')) {
876
+ for (const iface of queries.iterateNodesByKind('interface')) {
877
+ if ((++scanned255 & 63) === 0)
878
+ await onYield();
879
+ if ((++scanned255 & 63) === 0)
880
+ await onYield();
517
881
  if (iface.language !== 'go')
518
882
  continue;
519
883
  const want = methodNameSet(iface.id);
@@ -573,7 +937,8 @@ function goImplementsEdges(queries) {
573
937
  * matching the same-file edges extraction already emits). Skips methods that
574
938
  * already have a type parent (the same-file case). (#583, cross-file half)
575
939
  */
576
- function goCrossFileMethodContainsEdges(queries) {
940
+ async function goCrossFileMethodContainsEdges(queries, onYield) {
941
+ let scanned255 = 0;
577
942
  const edges = [];
578
943
  const seen = new Set();
579
944
  const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
@@ -581,7 +946,11 @@ function goCrossFileMethodContainsEdges(queries) {
581
946
  const i = p.replace(/\\/g, '/').lastIndexOf('/');
582
947
  return i >= 0 ? p.slice(0, i) : '';
583
948
  };
584
- for (const method of queries.getNodesByKind('method')) {
949
+ for (const method of queries.iterateNodesByKind('method')) {
950
+ if ((++scanned255 & 63) === 0)
951
+ await onYield();
952
+ if ((++scanned255 & 63) === 0)
953
+ await onYield();
585
954
  if (method.language !== 'go')
586
955
  continue;
587
956
  // The receiver type is encoded in the method's qualifiedName as `Recv::name`
@@ -651,13 +1020,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
651
1020
  function kmpKindsCompatible(a, b) {
652
1021
  return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
653
1022
  }
654
- function kotlinExpectActualEdges(queries) {
1023
+ async function kotlinExpectActualEdges(queries, onYield) {
1024
+ let scanned255 = 0;
655
1025
  const edges = [];
656
1026
  const seen = new Set();
657
- const actuals = queries
658
- .getAllNodes()
659
- .filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
660
- for (const act of actuals) {
1027
+ // SQL-side language+decorator pre-filter, streamed. The old
1028
+ // `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
1029
+ // just to find kotlin `actual` declarations — on a 2M-node graph that alone
1030
+ // exceeded Node's default heap and killed the index (#1212). The LIKE
1031
+ // pre-filter can over-match (substring), so the exact decorator check stays.
1032
+ for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
1033
+ if ((++scanned255 & 63) === 0)
1034
+ await onYield();
1035
+ if (!act.decorators?.includes('actual'))
1036
+ continue;
661
1037
  let added = 0;
662
1038
  for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
663
1039
  if (added >= MAX_CALLBACKS_PER_CHANNEL)
@@ -691,7 +1067,8 @@ function kotlinExpectActualEdges(queries) {
691
1067
  }
692
1068
  return edges;
693
1069
  }
694
- function interfaceOverrideEdges(queries) {
1070
+ async function interfaceOverrideEdges(queries, onYield) {
1071
+ let scanned255 = 0;
695
1072
  const edges = [];
696
1073
  const seen = new Set();
697
1074
  const methodsOf = (classId) => queries
@@ -703,7 +1080,9 @@ function interfaceOverrideEdges(queries) {
703
1080
  // types that conform to protocols. Iterate both.
704
1081
  const concreteKinds = ['class', 'struct'];
705
1082
  for (const kind of concreteKinds) {
706
- for (const cls of queries.getNodesByKind(kind)) {
1083
+ for (const cls of queries.iterateNodesByKind(kind)) {
1084
+ if ((++scanned255 & 63) === 0)
1085
+ await onYield();
707
1086
  const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
708
1087
  if (implMethods.length === 0)
709
1088
  continue;
@@ -777,7 +1156,8 @@ function interfaceOverrideEdges(queries) {
777
1156
  * Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
778
1157
  * stub's source line is the wiring site shown in the trace trail.
779
1158
  */
780
- function goGrpcStubImplEdges(queries) {
1159
+ async function goGrpcStubImplEdges(queries, onYield) {
1160
+ let scanned255 = 0;
781
1161
  const edges = [];
782
1162
  const seen = new Set();
783
1163
  const STUB_RE = /^Unimplemented.*Server$/;
@@ -789,7 +1169,9 @@ function goGrpcStubImplEdges(queries) {
789
1169
  const methodNamesByStruct = new Map();
790
1170
  const methodNodesByStruct = new Map();
791
1171
  const goStructs = [];
792
- for (const s of queries.getNodesByKind('struct')) {
1172
+ for (const s of queries.iterateNodesByKind('struct')) {
1173
+ if ((++scanned255 & 63) === 0)
1174
+ await onYield();
793
1175
  if (s.language !== 'go')
794
1176
  continue;
795
1177
  goStructs.push(s);
@@ -872,11 +1254,17 @@ function goGrpcStubImplEdges(queries) {
872
1254
  * component/function/class node — TS generics like `Array<Foo>` resolve to a type
873
1255
  * (or nothing) and are dropped.
874
1256
  */
875
- function reactJsxChildEdges(ctx) {
1257
+ async function reactJsxChildEdges(ctx, onYield) {
1258
+ let scannedFiles = 0;
876
1259
  const edges = [];
877
1260
  const seen = new Set();
878
1261
  const PARENT_KINDS = new Set(['method', 'function', 'component']);
1262
+ let scanned = 0;
879
1263
  for (const file of ctx.getAllFiles()) {
1264
+ if ((++scannedFiles & 15) === 0)
1265
+ await onYield();
1266
+ if ((++scanned & 255) === 0)
1267
+ await onYield(); // #1091: yield mid-scan on huge graphs
880
1268
  const content = ctx.readFile(file);
881
1269
  if (!content || (!content.includes('</') && !content.includes('/>')))
882
1270
  continue; // JSX-file gate
@@ -924,7 +1312,9 @@ function reactJsxChildEdges(ctx) {
924
1312
  * component, handler→function/method) keeps precision; inline arrows / `$emit`
925
1313
  * skipped.
926
1314
  */
927
- function vueTemplateEdges(ctx) {
1315
+ async function vueTemplateEdges(ctx, onYield) {
1316
+ let scannedFiles = 0;
1317
+ let scanned255 = 0;
928
1318
  const edges = [];
929
1319
  const seen = new Set();
930
1320
  const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
@@ -938,12 +1328,16 @@ function vueTemplateEdges(ctx) {
938
1328
  // misses it (flat components match by basename and don't need this). Map each
939
1329
  // nested component's Nuxt name → node so those template usages resolve.
940
1330
  const nuxtComponents = new Map();
941
- for (const c of ctx.getNodesByKind('component')) {
1331
+ for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1332
+ if ((++scanned255 & 63) === 0)
1333
+ await onYield();
942
1334
  const nn = nuxtComponentName(c.filePath);
943
1335
  if (nn && !nuxtComponents.has(nn))
944
1336
  nuxtComponents.set(nn, c);
945
1337
  }
946
1338
  for (const file of ctx.getAllFiles()) {
1339
+ if ((++scannedFiles & 15) === 0)
1340
+ await onYield();
947
1341
  if (!file.endsWith('.vue'))
948
1342
  continue;
949
1343
  const content = ctx.readFile(file);
@@ -1077,18 +1471,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
1077
1471
  // statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
1078
1472
  // is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
1079
1473
  const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
1080
- function rnEventEdges(ctx) {
1474
+ async function rnEventEdges(ctx, onYield) {
1475
+ let scannedFiles = 0;
1081
1476
  // Native dispatchers (source = the native method whose body sends the
1082
1477
  // event) and JS handlers (target = the function/method registered as
1083
1478
  // the listener) keyed by event name.
1084
1479
  const nativeDispatchersByEvent = new Map();
1085
1480
  const jsHandlersByEvent = new Map();
1086
1481
  for (const file of ctx.getAllFiles()) {
1482
+ if ((++scannedFiles & 15) === 0)
1483
+ await onYield();
1087
1484
  const content = ctx.readFile(file);
1088
1485
  if (!content)
1089
1486
  continue;
1090
1487
  const nodesInFile = ctx.getNodesInFile(file);
1091
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
1488
+ const lineOf = makeLineAt(content, 1);
1092
1489
  const addDispatcher = (event, line) => {
1093
1490
  const disp = enclosingFn(nodesInFile, line);
1094
1491
  if (!disp)
@@ -1271,11 +1668,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
1271
1668
  * caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
1272
1669
  * `<file>::<module>.<method>` by the framework extractor.
1273
1670
  */
1274
- function expoCrossPlatformEdges(queries) {
1671
+ async function expoCrossPlatformEdges(queries, onYield) {
1672
+ let scanned255 = 0;
1275
1673
  const edges = [];
1276
1674
  const seen = new Set();
1277
1675
  const byKey = new Map();
1278
- for (const m of queries.getNodesByKind('method')) {
1676
+ for (const m of queries.iterateNodesByKind('method')) {
1677
+ if ((++scanned255 & 63) === 0)
1678
+ await onYield();
1279
1679
  if (!m.id.startsWith('expo-module:'))
1280
1680
  continue;
1281
1681
  const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
@@ -1324,7 +1724,8 @@ function expoCrossPlatformEdges(queries) {
1324
1724
  * `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
1325
1725
  * lines up with the bare Android method name.
1326
1726
  */
1327
- function rnCrossPlatformEdges(queries) {
1727
+ async function rnCrossPlatformEdges(queries, onYield) {
1728
+ let scanned255 = 0;
1328
1729
  const edges = [];
1329
1730
  const seen = new Set();
1330
1731
  const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
@@ -1346,6 +1747,8 @@ function rnCrossPlatformEdges(queries) {
1346
1747
  // below only runs for genuine cross-platform candidates.
1347
1748
  const byName = new Map();
1348
1749
  for (const m of queries.iterateNodesByKind('method')) {
1750
+ if ((++scanned255 & 63) === 0)
1751
+ await onYield();
1349
1752
  if (!NATIVE.has(m.language))
1350
1753
  continue;
1351
1754
  const key = norm(m.name);
@@ -1396,17 +1799,27 @@ function rnCrossPlatformEdges(queries) {
1396
1799
  }
1397
1800
  return edges;
1398
1801
  }
1399
- function fabricNativeImplEdges(ctx) {
1802
+ async function fabricNativeImplEdges(ctx, onYield) {
1803
+ let scanned255 = 0;
1400
1804
  const edges = [];
1401
1805
  const seen = new Set();
1402
1806
  // The Fabric extractor IDs are prefixed `fabric-component:` so we can
1403
- // filter to just those without iterating all `component` nodes.
1404
- const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
1807
+ // filter to just those while streaming never materializing the whole
1808
+ // `component` kind (#1212).
1809
+ const components = [];
1810
+ for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1811
+ if ((++scanned255 & 63) === 0)
1812
+ await onYield();
1813
+ if (n.id.startsWith('fabric-component:'))
1814
+ components.push(n);
1815
+ }
1405
1816
  if (components.length === 0)
1406
1817
  return edges;
1407
1818
  // Pre-index native classes by name for O(1) lookup.
1408
1819
  const nativeClassesByName = new Map();
1409
- for (const n of ctx.getNodesByKind('class')) {
1820
+ for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
1821
+ if ((++scanned255 & 63) === 0)
1822
+ await onYield();
1410
1823
  if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
1411
1824
  continue;
1412
1825
  const arr = nativeClassesByName.get(n.name);
@@ -1459,12 +1872,15 @@ function fabricNativeImplEdges(ctx) {
1459
1872
  * same simple name) are dropped. We need-not bridge by package because Java
1460
1873
  * mapper interfaces are typically uniquely named within a project.
1461
1874
  */
1462
- function mybatisJavaXmlEdges(queries) {
1875
+ async function mybatisJavaXmlEdges(queries, onYield) {
1876
+ let scanned255 = 0;
1463
1877
  const edges = [];
1464
1878
  const seen = new Set();
1465
1879
  // Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
1466
1880
  const javaIndex = new Map();
1467
1881
  for (const m of queries.iterateNodesByKind('method')) {
1882
+ if ((++scanned255 & 63) === 0)
1883
+ await onYield();
1468
1884
  if (m.language !== 'java' && m.language !== 'kotlin')
1469
1885
  continue;
1470
1886
  const parts = m.qualifiedName.split('::');
@@ -1480,6 +1896,8 @@ function mybatisJavaXmlEdges(queries) {
1480
1896
  javaIndex.set(key, [m]);
1481
1897
  }
1482
1898
  for (const xml of queries.iterateNodesByKind('method')) {
1899
+ if ((++scanned255 & 63) === 0)
1900
+ await onYield();
1483
1901
  if (xml.language !== 'xml')
1484
1902
  continue;
1485
1903
  // Qualified name: `<namespace>::<id>`. Extract the simple class name.
@@ -1593,10 +2011,14 @@ function goHandlerIdent(expr) {
1593
2011
  const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
1594
2012
  return m ? m[1] : null;
1595
2013
  }
1596
- function ginMiddlewareChainEdges(queries, ctx) {
2014
+ async function ginMiddlewareChainEdges(queries, ctx, onYield) {
2015
+ let scanned255 = 0;
2016
+ let scannedFiles = 0;
1597
2017
  // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
1598
2018
  const dispatchers = [];
1599
2019
  for (const n of queries.iterateNodesByKind('method')) {
2020
+ if ((++scanned255 & 63) === 0)
2021
+ await onYield();
1600
2022
  if (n.language !== 'go')
1601
2023
  continue;
1602
2024
  const content = ctx.readFile(n.filePath);
@@ -1611,6 +2033,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
1611
2033
  // closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
1612
2034
  const registered = new Map(); // name → registeredAt (file:line)
1613
2035
  for (const file of ctx.getAllFiles()) {
2036
+ if ((++scannedFiles & 15) === 0)
2037
+ await onYield();
1614
2038
  if (!file.endsWith('.go'))
1615
2039
  continue;
1616
2040
  const content = ctx.readFile(file);
@@ -1666,10 +2090,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
1666
2090
  * clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
1667
2091
  * definition isn't orphaned, and editing the form surfaces its code-behind unit.
1668
2092
  */
1669
- function pascalFormEdges(ctx) {
2093
+ async function pascalFormEdges(ctx, onYield) {
2094
+ let scannedFiles = 0;
1670
2095
  const edges = [];
1671
2096
  const allFiles = new Set(ctx.getAllFiles());
1672
2097
  for (const file of allFiles) {
2098
+ if ((++scannedFiles & 255) === 0)
2099
+ await onYield();
1673
2100
  if (!/\.(dfm|fmx)$/i.test(file))
1674
2101
  continue;
1675
2102
  const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
@@ -1705,12 +2132,15 @@ function pascalFormEdges(ctx) {
1705
2132
  * a loader's data shows the page it feeds) and the page's dependencies include
1706
2133
  * its loader.
1707
2134
  */
1708
- function svelteKitLoadEdges(ctx) {
2135
+ async function svelteKitLoadEdges(ctx, onYield) {
2136
+ let scannedFiles = 0;
1709
2137
  const edges = [];
1710
2138
  const allFiles = new Set(ctx.getAllFiles());
1711
2139
  const HOOKS = new Set(['load', 'actions']);
1712
2140
  const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
1713
2141
  for (const file of allFiles) {
2142
+ if ((++scannedFiles & 255) === 0)
2143
+ await onYield();
1714
2144
  const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
1715
2145
  if (!m)
1716
2146
  continue;
@@ -1759,10 +2189,13 @@ function svelteKitLoadEdges(ctx) {
1759
2189
  const THUNK_DECL_RE = /create(?:Async)?Thunk/;
1760
2190
  const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
1761
2191
  const THUNK_FANOUT_CAP = 24;
1762
- function reduxThunkEdges(queries, ctx) {
2192
+ async function reduxThunkEdges(queries, ctx, onYield) {
2193
+ let scanned255 = 0;
1763
2194
  const edges = [];
1764
2195
  const seen = new Set();
1765
2196
  for (const node of queries.iterateNodesByKind('constant')) {
2197
+ if ((++scanned255 & 63) === 0)
2198
+ await onYield();
1766
2199
  // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
1767
2200
  // avoids reading every constant's body on a large repo.
1768
2201
  if (!node.signature || !THUNK_DECL_RE.test(node.signature))
@@ -1893,10 +2326,16 @@ function resolveRegistryHandler(ctx, name, chained) {
1893
2326
  // entry resolving to the global URL constant).
1894
2327
  return cands.find((n) => n.kind === 'method') ?? null;
1895
2328
  }
1896
- function objectRegistryEdges(ctx) {
2329
+ async function objectRegistryEdges(ctx, onYield) {
2330
+ let scannedFiles = 0;
1897
2331
  const edges = [];
1898
2332
  const seen = new Set();
2333
+ let scanned = 0;
1899
2334
  for (const file of ctx.getAllFiles()) {
2335
+ if ((++scannedFiles & 15) === 0)
2336
+ await onYield();
2337
+ if ((++scanned & 255) === 0)
2338
+ await onYield(); // #1091: yield mid-scan on huge graphs
1900
2339
  if (!REGISTRY_JS_EXT.test(file))
1901
2340
  continue;
1902
2341
  const content = ctx.readFile(file);
@@ -2006,10 +2445,13 @@ function rtkEndpointNameFromHook(hook) {
2006
2445
  return null;
2007
2446
  return mid.charAt(0).toLowerCase() + mid.slice(1);
2008
2447
  }
2009
- function rtkQueryEdges(queries, ctx) {
2448
+ async function rtkQueryEdges(queries, ctx, onYield) {
2449
+ let scanned255 = 0;
2010
2450
  const edges = [];
2011
2451
  const seen = new Set();
2012
2452
  for (const hook of queries.iterateNodesByKind('function')) {
2453
+ if ((++scanned255 & 63) === 0)
2454
+ await onYield();
2013
2455
  // Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
2014
2456
  if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
2015
2457
  continue;
@@ -2053,10 +2495,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
2053
2495
  const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
2054
2496
  const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
2055
2497
  const PINIA_FANOUT_CAP = 80;
2056
- function piniaStoreEdges(ctx) {
2498
+ async function piniaStoreEdges(ctx, onYield) {
2499
+ let scannedFiles = 0;
2057
2500
  // 1. Map each `const useXStore = defineStore(...)` factory → its store file.
2058
2501
  const factoryFile = new Map();
2059
2502
  for (const file of ctx.getAllFiles()) {
2503
+ if ((++scannedFiles & 15) === 0)
2504
+ await onYield();
2060
2505
  if (!PINIA_CONSUMER_EXT.test(file))
2061
2506
  continue;
2062
2507
  const content = ctx.readFile(file);
@@ -2072,6 +2517,8 @@ function piniaStoreEdges(ctx) {
2072
2517
  const edges = [];
2073
2518
  const seen = new Set();
2074
2519
  for (const file of ctx.getAllFiles()) {
2520
+ if ((++scannedFiles & 15) === 0)
2521
+ await onYield();
2075
2522
  if (!PINIA_CONSUMER_EXT.test(file))
2076
2523
  continue;
2077
2524
  const content = ctx.readFile(file);
@@ -2144,7 +2591,8 @@ const VUEX_FANOUT_CAP = 120;
2144
2591
  function pathHasSegment(filePath, seg) {
2145
2592
  return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
2146
2593
  }
2147
- function vuexDispatchEdges(ctx) {
2594
+ async function vuexDispatchEdges(ctx, onYield) {
2595
+ let scannedFiles = 0;
2148
2596
  const storeFileCache = new Map();
2149
2597
  const isStoreFile = (file) => {
2150
2598
  let v = storeFileCache.get(file);
@@ -2182,6 +2630,8 @@ function vuexDispatchEdges(ctx) {
2182
2630
  const edges = [];
2183
2631
  const seen = new Set();
2184
2632
  for (const file of ctx.getAllFiles()) {
2633
+ if ((++scannedFiles & 15) === 0)
2634
+ await onYield();
2185
2635
  if (!PINIA_CONSUMER_EXT.test(file))
2186
2636
  continue;
2187
2637
  const content = ctx.readFile(file);
@@ -2241,7 +2691,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
2241
2691
  const CELERY_PY_EXT = /\.py$/;
2242
2692
  const CELERY_FANOUT_CAP = 80;
2243
2693
  const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
2244
- function celeryDispatchEdges(ctx) {
2694
+ async function celeryDispatchEdges(ctx, onYield) {
2695
+ let scannedFiles = 0;
2245
2696
  // Memoize the decorator check per task-candidate node: it reads the file and scans a few
2246
2697
  // lines above the def. Only called on names that are actually `.delay`/`.apply_async`
2247
2698
  // receivers, so the candidate set stays small.
@@ -2285,6 +2736,8 @@ function celeryDispatchEdges(ctx) {
2285
2736
  const edges = [];
2286
2737
  const seen = new Set();
2287
2738
  for (const file of ctx.getAllFiles()) {
2739
+ if ((++scannedFiles & 15) === 0)
2740
+ await onYield();
2288
2741
  if (!CELERY_PY_EXT.test(file))
2289
2742
  continue;
2290
2743
  const content = ctx.readFile(file);
@@ -2363,15 +2816,24 @@ function springFirstParamType(sig) {
2363
2816
  const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
2364
2817
  return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
2365
2818
  }
2366
- function springEventEdges(ctx) {
2819
+ async function springEventEdges(ctx, onYield) {
2820
+ let scannedFiles = 0;
2367
2821
  // Pass 1 — event-type → listener methods, scanning only event-relevant files.
2822
+ // This is the ONLY full read sweep: publisher files are recorded here so
2823
+ // pass 2 re-reads just those instead of every .java file again (#1212 —
2824
+ // the double full-repo read was one of the tail's longest unyielded spans).
2368
2825
  const listeners = new Map();
2826
+ const publisherFiles = [];
2369
2827
  for (const file of ctx.getAllFiles()) {
2828
+ if ((++scannedFiles & 15) === 0)
2829
+ await onYield();
2370
2830
  if (!SPRING_JAVA_EXT.test(file))
2371
2831
  continue;
2372
2832
  const content = ctx.readFile(file);
2373
2833
  if (!content)
2374
2834
  continue;
2835
+ if (content.includes('.publishEvent('))
2836
+ publisherFiles.push(file);
2375
2837
  const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
2376
2838
  const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
2377
2839
  if (!hasAnno && !hasAppListener)
@@ -2411,12 +2873,13 @@ function springEventEdges(ctx) {
2411
2873
  }
2412
2874
  if (!listeners.size)
2413
2875
  return [];
2414
- // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
2876
+ // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
2877
+ // XEvent. Only the publisher files recorded in pass 1 are (re-)read.
2415
2878
  const edges = [];
2416
2879
  const seen = new Set();
2417
- for (const file of ctx.getAllFiles()) {
2418
- if (!SPRING_JAVA_EXT.test(file))
2419
- continue;
2880
+ for (const file of publisherFiles) {
2881
+ if ((++scannedFiles & 15) === 0)
2882
+ await onYield();
2420
2883
  const content = ctx.readFile(file);
2421
2884
  if (!content || !content.includes('.publishEvent('))
2422
2885
  continue;
@@ -2501,10 +2964,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
2501
2964
  }
2502
2965
  return declType;
2503
2966
  }
2504
- function mediatrDispatchEdges(ctx) {
2967
+ async function mediatrDispatchEdges(ctx, onYield) {
2968
+ let scannedFiles = 0;
2505
2969
  // Pass 1 — request/notification type → the Handle method of each handler class.
2506
2970
  const handlers = new Map();
2507
2971
  for (const file of ctx.getAllFiles()) {
2972
+ if ((++scannedFiles & 15) === 0)
2973
+ await onYield();
2508
2974
  if (!MEDIATR_CS_EXT.test(file))
2509
2975
  continue;
2510
2976
  const content = ctx.readFile(file);
@@ -2538,6 +3004,8 @@ function mediatrDispatchEdges(ctx) {
2538
3004
  const edges = [];
2539
3005
  const seen = new Set();
2540
3006
  for (const file of ctx.getAllFiles()) {
3007
+ if ((++scannedFiles & 15) === 0)
3008
+ await onYield();
2541
3009
  if (!MEDIATR_CS_EXT.test(file))
2542
3010
  continue;
2543
3011
  const content = ctx.readFile(file);
@@ -2601,7 +3069,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
2601
3069
  const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
2602
3070
  const SIDEKIQ_RB_EXT = /\.rb$/;
2603
3071
  const SIDEKIQ_FANOUT_CAP = 80;
2604
- function sidekiqDispatchEdges(ctx) {
3072
+ async function sidekiqDispatchEdges(ctx, onYield) {
3073
+ let scannedFiles = 0;
2605
3074
  // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
2606
3075
  // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
2607
3076
  const performCache = new Map();
@@ -2638,6 +3107,8 @@ function sidekiqDispatchEdges(ctx) {
2638
3107
  const edges = [];
2639
3108
  const seen = new Set();
2640
3109
  for (const file of ctx.getAllFiles()) {
3110
+ if ((++scannedFiles & 15) === 0)
3111
+ await onYield();
2641
3112
  if (!SIDEKIQ_RB_EXT.test(file))
2642
3113
  continue;
2643
3114
  const content = ctx.readFile(file);
@@ -2673,6 +3144,414 @@ function sidekiqDispatchEdges(ctx) {
2673
3144
  }
2674
3145
  return edges;
2675
3146
  }
3147
+ // ── Erlang behaviour-callback dispatch ────────────────────────────────────────
3148
+ // An Erlang behaviour is a compile-checked callback contract: the behaviour
3149
+ // module declares `-callback init(...) -> ...`, implementers declare
3150
+ // `-behaviour(B)` and export the callbacks, and the framework side dispatches
3151
+ // through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
3152
+ // `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
3153
+ // deliberately leaves var-module calls silent (no static target), so the flow
3154
+ // breaks at exactly the hop agents ask about (request → handler init). Bridge:
3155
+ //
3156
+ // dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
3157
+ // declaring `fn` with the SITE's arity — provided exactly ONE in-repo
3158
+ // behaviour declares (fn, arity); a name+arity collision across behaviours
3159
+ // bails (silent beats wrong) — and the implementer defines and exports `fn`.
3160
+ //
3161
+ // Behaviours are discovered by scanning every Erlang file for `-callback`
3162
+ // declarations (not just `implements` targets), so a behaviour with zero
3163
+ // implementers still participates in the ambiguity gate. Fan-out control: a
3164
+ // mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
3165
+ // hundreds of edges per site that READ as complete coverage while being
3166
+ // arbitrary — above the cap the site is skipped entirely and the boundary
3167
+ // stays visibly dynamic (explore's boundary announcer covers it) instead of
3168
+ // silently truncated.
3169
+ const ERLANG_EXT = /\.(?:erl|hrl)$/;
3170
+ // `Var:fn(` — variable (capitalized) module, lowercase function, immediate
3171
+ // open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
3172
+ // (static remote call, already linked), and mid-word matches.
3173
+ const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
3174
+ const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
3175
+ const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
3176
+ /**
3177
+ * Argument count of the call/declaration whose `(` sits at `openIdx` —
3178
+ * top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
3179
+ * (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
3180
+ * so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
3181
+ */
3182
+ function erlangArityAt(src, openIdx) {
3183
+ let depth = 1;
3184
+ let commas = 0;
3185
+ let sawArg = false;
3186
+ const limit = Math.min(src.length, openIdx + 4000);
3187
+ for (let i = openIdx + 1; i < limit; i++) {
3188
+ const ch = src[i];
3189
+ if (ch === '"' || ch === "'") {
3190
+ i++;
3191
+ while (i < limit && src[i] !== ch) {
3192
+ if (src[i] === '\\')
3193
+ i++;
3194
+ i++;
3195
+ }
3196
+ sawArg = true;
3197
+ continue;
3198
+ }
3199
+ if (ch === '$') {
3200
+ i++;
3201
+ if (src[i] === '\\')
3202
+ i++;
3203
+ sawArg = true;
3204
+ continue;
3205
+ }
3206
+ if (ch === '(' || ch === '[' || ch === '{') {
3207
+ depth++;
3208
+ sawArg = true;
3209
+ continue;
3210
+ }
3211
+ if (ch === ')' || ch === ']' || ch === '}') {
3212
+ depth--;
3213
+ if (depth === 0)
3214
+ return sawArg ? commas + 1 : 0;
3215
+ continue;
3216
+ }
3217
+ if (ch === ',' && depth === 1) {
3218
+ commas++;
3219
+ continue;
3220
+ }
3221
+ if (!/\s/.test(ch))
3222
+ sawArg = true;
3223
+ }
3224
+ return -1;
3225
+ }
3226
+ /**
3227
+ * Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
3228
+ * DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
3229
+ * and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
3230
+ * config) — the connection happens by option-path unification inside the
3231
+ * module-system evaluator, so there is no static call/import edge to follow
3232
+ * and flow questions ("how does services.yabai.enable become a launchd
3233
+ * service?") go dark at the module boundary.
3234
+ *
3235
+ * This pass links each config-write binding to the option declaration whose
3236
+ * path is the longest static-segment prefix of the write path. Precision gates:
3237
+ * - only STATIC segments participate: plain identifiers, plus quoted segments
3238
+ * (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
3239
+ * only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
3240
+ * so dynamic paths never match beyond their static head;
3241
+ * - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
3242
+ * every package's `meta = { ... }` attrset to nixos's `options.meta`;
3243
+ * - a prefix declared in more than one file is ambiguous → no edge (a wrong
3244
+ * edge is worse than none);
3245
+ * - writes physically inside an options block are declaration internals
3246
+ * (types, defaults, examples), never config writes → excluded.
3247
+ * Both declaration spellings register: flat (`options.a.b = ...`) by name, and
3248
+ * nested (`options = { a.b = ...; }`) by line-span containment.
3249
+ */
3250
+ function nixLeadingPlainSegments(name) {
3251
+ const segs = [];
3252
+ let i = 0;
3253
+ const n = name.length;
3254
+ while (i < n) {
3255
+ if (name[i] === '"') {
3256
+ // Quoted segment — an opaque verbatim token (quotes kept, so it can
3257
+ // never collide with a plain identifier). `NSGlobalDomain."com.apple.
3258
+ // mouse.tapBehavior"` must match ITS OWN quoted declaration, not
3259
+ // whichever sibling registered the shared plain prefix first.
3260
+ let j = i + 1;
3261
+ while (j < n && name[j] !== '"') {
3262
+ if (name[j] === '\\')
3263
+ j++;
3264
+ j++;
3265
+ }
3266
+ if (j >= n)
3267
+ return segs; // unterminated — stop at the static head
3268
+ const tok = name.slice(i, j + 1);
3269
+ if (tok.includes('${'))
3270
+ return segs; // interpolated → dynamic → stop
3271
+ segs.push(tok);
3272
+ i = j + 1;
3273
+ if (i >= n)
3274
+ break;
3275
+ if (name[i] !== '.')
3276
+ return segs;
3277
+ i++;
3278
+ continue;
3279
+ }
3280
+ let j = i;
3281
+ while (j < n && name[j] !== '.') {
3282
+ if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
3283
+ return segs;
3284
+ j++;
3285
+ }
3286
+ const seg = name.slice(i, j);
3287
+ if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
3288
+ return segs;
3289
+ segs.push(seg);
3290
+ i = j + 1;
3291
+ }
3292
+ return segs;
3293
+ }
3294
+ async function nixOptionPathEdges(queries, onYield) {
3295
+ let scanned255 = 0;
3296
+ // One streaming pass over nix bindings (variables + the odd function-valued
3297
+ // option); memory stays O(bindings-kept), not O(all nodes) (#610).
3298
+ const byFile = new Map();
3299
+ let scanned = 0;
3300
+ for (const kind of ['variable', 'function']) {
3301
+ for (const node of queries.iterateNodesByKind(kind)) {
3302
+ if ((++scanned255 & 63) === 0)
3303
+ await onYield();
3304
+ if ((++scanned & 0x3fff) === 0 && onYield)
3305
+ await onYield();
3306
+ if (node.language !== 'nix')
3307
+ continue;
3308
+ const segs = nixLeadingPlainSegments(node.name);
3309
+ if (segs.length === 0)
3310
+ continue;
3311
+ const rec = {
3312
+ id: node.id,
3313
+ filePath: node.filePath,
3314
+ startLine: node.startLine,
3315
+ endLine: node.endLine,
3316
+ segs,
3317
+ };
3318
+ const arr = byFile.get(node.filePath);
3319
+ if (arr)
3320
+ arr.push(rec);
3321
+ else
3322
+ byFile.set(node.filePath, [rec]);
3323
+ }
3324
+ }
3325
+ // Per file: walk bindings outermost-first with a stack of active option
3326
+ // spans, composing nested declaration paths (`options = { services.foo = {
3327
+ // enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
3328
+ // An `options` binding nested inside another option span is a SUBMODULE's
3329
+ // own namespace (`attrsOf (submodule { options = ...; })`) — its internals
3330
+ // are not globally addressable, so the sentinel blocks registration below it
3331
+ // while still excluding the region from write candidates.
3332
+ const SUBMODULE = 'submodule';
3333
+ const decls = new Map();
3334
+ const writes = [];
3335
+ const register = (path, rec) => {
3336
+ if (path.length < 2 || path.includes(SUBMODULE))
3337
+ return;
3338
+ const key = path.join('.');
3339
+ const arr = decls.get(key);
3340
+ if (arr)
3341
+ arr.push(rec);
3342
+ else
3343
+ decls.set(key, [rec]);
3344
+ };
3345
+ for (const recs of byFile.values()) {
3346
+ recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
3347
+ const stack = [];
3348
+ for (const rec of recs) {
3349
+ while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
3350
+ stack.pop();
3351
+ // Strict containment at line granularity: a one-line nested binding is
3352
+ // indistinguishable from its container, so it stays unclassified (rare
3353
+ // in module code, where option blocks are multi-line).
3354
+ const enclosing = stack.length > 0 &&
3355
+ rec.startLine >= stack[stack.length - 1].start &&
3356
+ rec.endLine <= stack[stack.length - 1].end &&
3357
+ !(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
3358
+ ? stack[stack.length - 1]
3359
+ : null;
3360
+ if (rec.segs[0] === 'options') {
3361
+ const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
3362
+ const prefix = enclosing ? [SUBMODULE] : ownPath;
3363
+ register(prefix, rec);
3364
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix });
3365
+ continue;
3366
+ }
3367
+ if (enclosing) {
3368
+ const composed = [...enclosing.prefix, ...rec.segs];
3369
+ register(composed, rec);
3370
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
3371
+ continue;
3372
+ }
3373
+ if (rec.segs.length >= 2) {
3374
+ writes.push(rec);
3375
+ }
3376
+ }
3377
+ }
3378
+ if (decls.size === 0 || writes.length === 0)
3379
+ return [];
3380
+ const edges = [];
3381
+ for (const w of writes) {
3382
+ // `config.services.x = ...` spells the same write with an explicit prefix.
3383
+ const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
3384
+ if (segs.length < 2)
3385
+ continue;
3386
+ // Longest prefix wins; an ambiguous longest match does NOT fall back to a
3387
+ // shorter one (that would link `services.nginx.virtualHosts.x` to
3388
+ // `options.services.nginx` when virtualHosts is the contested path).
3389
+ for (let len = Math.min(segs.length, 6); len >= 2; len--) {
3390
+ const candidates = decls.get(segs.slice(0, len).join('.'));
3391
+ if (!candidates || candidates.length === 0)
3392
+ continue;
3393
+ const files = new Set(candidates.map((c) => c.filePath));
3394
+ if (files.size === 1) {
3395
+ const target = candidates[0];
3396
+ if (target.id !== w.id) {
3397
+ edges.push({
3398
+ source: w.id,
3399
+ target: target.id,
3400
+ kind: 'references',
3401
+ line: w.startLine,
3402
+ provenance: 'heuristic',
3403
+ metadata: {
3404
+ synthesizedBy: 'nix-option-path',
3405
+ optionPath: segs.slice(0, len).join('.'),
3406
+ registeredAt: `${target.filePath}:${target.startLine}`,
3407
+ },
3408
+ });
3409
+ }
3410
+ }
3411
+ break; // longest hit decides, matched or ambiguous
3412
+ }
3413
+ }
3414
+ return edges;
3415
+ }
3416
+ async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
3417
+ let scannedFiles = 0;
3418
+ let scanned255 = 0;
3419
+ // Cheap language gate: no Erlang modules → no cost beyond one streamed
3420
+ // kind scan (never a materialized array of every namespace — #1212).
3421
+ const erlangModules = [];
3422
+ for (const n of queries.iterateNodesByKind('namespace')) {
3423
+ if ((++scanned255 & 63) === 0)
3424
+ await onYield();
3425
+ if (n.language === 'erlang')
3426
+ erlangModules.push(n);
3427
+ }
3428
+ if (erlangModules.length === 0)
3429
+ return [];
3430
+ // Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
3431
+ // its (name, arity) callback set, and the global `name/arity` → declaring
3432
+ // behaviours map that drives the ambiguity gate.
3433
+ const moduleByFile = new Map();
3434
+ for (const ns of erlangModules) {
3435
+ if (!moduleByFile.has(ns.filePath))
3436
+ moduleByFile.set(ns.filePath, ns);
3437
+ }
3438
+ const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
3439
+ const callbackNames = new Set();
3440
+ for (const file of ctx.getAllFiles()) {
3441
+ if ((++scannedFiles & 15) === 0)
3442
+ await onYield();
3443
+ if (!ERLANG_EXT.test(file))
3444
+ continue;
3445
+ const behaviour = moduleByFile.get(file);
3446
+ if (!behaviour)
3447
+ continue; // a .hrl or module-less file can't be a behaviour
3448
+ const content = ctx.readFile(file);
3449
+ if (!content || !content.includes('-callback'))
3450
+ continue;
3451
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3452
+ ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
3453
+ let m;
3454
+ while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
3455
+ const name = m[2].replace(/^'|'$/g, '');
3456
+ const arity = erlangArityAt(safe, m.index + m[0].length - 1);
3457
+ if (arity < 0)
3458
+ continue;
3459
+ const key = `${name}/${arity}`;
3460
+ const arr = declaringBehaviours.get(key);
3461
+ if (arr) {
3462
+ if (!arr.some((b) => b.id === behaviour.id))
3463
+ arr.push(behaviour);
3464
+ }
3465
+ else {
3466
+ declaringBehaviours.set(key, [behaviour]);
3467
+ }
3468
+ callbackNames.add(name);
3469
+ }
3470
+ }
3471
+ if (declaringBehaviours.size === 0)
3472
+ return [];
3473
+ // Implementer target lookup, lazy per (behaviour, fn): implementers come
3474
+ // from the `implements` edges extraction resolved, and the target is the
3475
+ // implementer module's own exported `fn` function node.
3476
+ const targetCache = new Map();
3477
+ const targetsOf = (behaviour, fn) => {
3478
+ const cacheKey = `${behaviour.id}#${fn}`;
3479
+ let targets = targetCache.get(cacheKey);
3480
+ if (targets)
3481
+ return targets;
3482
+ targets = [];
3483
+ for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
3484
+ const impl = queries.getNodeById(e.source);
3485
+ if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
3486
+ continue;
3487
+ const fnNode = ctx
3488
+ .getNodesInFile(impl.filePath)
3489
+ .find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
3490
+ if (fnNode)
3491
+ targets.push(fnNode);
3492
+ }
3493
+ targetCache.set(cacheKey, targets);
3494
+ return targets;
3495
+ };
3496
+ // Pass 2 — dispatch sites. Only files containing a var-module call shape are
3497
+ // scanned in full.
3498
+ const edges = [];
3499
+ const seen = new Set();
3500
+ for (const file of ctx.getAllFiles()) {
3501
+ if ((++scannedFiles & 15) === 0)
3502
+ await onYield();
3503
+ if (!ERLANG_EXT.test(file))
3504
+ continue;
3505
+ const content = ctx.readFile(file);
3506
+ if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
3507
+ continue;
3508
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3509
+ const nodesInFile = ctx.getNodesInFile(file);
3510
+ ERLANG_DISPATCH_RE.lastIndex = 0;
3511
+ let m;
3512
+ while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
3513
+ const fn = m[3];
3514
+ if (!callbackNames.has(fn))
3515
+ continue;
3516
+ const openIdx = m.index + m[0].length - 1;
3517
+ const arity = erlangArityAt(safe, openIdx);
3518
+ if (arity < 0)
3519
+ continue;
3520
+ const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
3521
+ if (!behaviours || behaviours.length !== 1)
3522
+ continue; // unknown or ambiguous
3523
+ const behaviour = behaviours[0];
3524
+ const targets = targetsOf(behaviour, fn);
3525
+ if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
3526
+ continue;
3527
+ const line = safe.slice(0, m.index).split('\n').length;
3528
+ const disp = enclosingFn(nodesInFile, line);
3529
+ if (!disp)
3530
+ continue;
3531
+ for (const target of targets) {
3532
+ if (target.id === disp.id)
3533
+ continue;
3534
+ const key = `${disp.id}>${target.id}`;
3535
+ if (seen.has(key))
3536
+ continue;
3537
+ seen.add(key);
3538
+ edges.push({
3539
+ source: disp.id,
3540
+ target: target.id,
3541
+ kind: 'calls',
3542
+ line,
3543
+ provenance: 'heuristic',
3544
+ metadata: {
3545
+ synthesizedBy: 'erlang-behaviour',
3546
+ via: `${behaviour.name}:${fn}/${arity}`,
3547
+ registeredAt: `${file}:${line}`,
3548
+ },
3549
+ });
3550
+ }
3551
+ }
3552
+ }
3553
+ return edges;
3554
+ }
2676
3555
  // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2677
3556
  // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2678
3557
  // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
@@ -2719,7 +3598,8 @@ function phpArrayBody(src, openIdx) {
2719
3598
  }
2720
3599
  return null;
2721
3600
  }
2722
- function laravelEventEdges(ctx) {
3601
+ async function laravelEventEdges(ctx, onYield) {
3602
+ let scannedFiles = 0;
2723
3603
  // event short name → its listener `handle` methods (deduped by node id).
2724
3604
  const listeners = new Map();
2725
3605
  const add = (event, handle) => {
@@ -2736,6 +3616,8 @@ function laravelEventEdges(ctx) {
2736
3616
  && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
2737
3617
  // Pass 1 — build the event→handle map from both registration mechanisms.
2738
3618
  for (const file of ctx.getAllFiles()) {
3619
+ if ((++scannedFiles & 15) === 0)
3620
+ await onYield();
2739
3621
  if (!LARAVEL_PHP_EXT.test(file))
2740
3622
  continue;
2741
3623
  const content = ctx.readFile(file);
@@ -2781,6 +3663,8 @@ function laravelEventEdges(ctx) {
2781
3663
  const edges = [];
2782
3664
  const seen = new Set();
2783
3665
  for (const file of ctx.getAllFiles()) {
3666
+ if ((++scannedFiles & 15) === 0)
3667
+ await onYield();
2784
3668
  if (!LARAVEL_PHP_EXT.test(file))
2785
3669
  continue;
2786
3670
  const content = ctx.readFile(file);
@@ -2831,53 +3715,169 @@ function laravelEventEdges(ctx) {
2831
3715
  * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
2832
3716
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
2833
3717
  */
2834
- function synthesizeCallbackEdges(queries, ctx) {
3718
+ async function synthesizeCallbackEdges(queries, ctx) {
3719
+ // Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
3720
+ // running synchronously on the indexer's main thread. Their AGGREGATE can run
3721
+ // for well over a minute on a large repo — long enough for the #850 liveness
3722
+ // watchdog to SIGKILL the process mid-index (#1091), since its heartbeat lives
3723
+ // on this same thread. Yield between passes so the heartbeat can fire; a pass
3724
+ // that itself hangs (a real wedge) never reaches the next yield, so the
3725
+ // watchdog still catches that. See ./cooperative-yield.
3726
+ const yieldToLoop = (0, cooperative_yield_1.createYielder)();
3727
+ // Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
3728
+ // (=1: passes over 250ms; =all: every pass). This is the diagnostic that
3729
+ // located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
3730
+ const markT = { t: Date.now() };
3731
+ const __mark = (label) => {
3732
+ const now = Date.now();
3733
+ const dt = now - markT.t;
3734
+ markT.t = now;
3735
+ if (process.env.CODEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.CODEGRAPH_SYNTH_TIMINGS === 'all')) {
3736
+ console.error(`[synth-timing] ${label}: ${dt}ms`);
3737
+ }
3738
+ };
3739
+ // Language gating: one indexed DISTINCT over the files table lets a pass
3740
+ // whose own filters reference a specific language/extension be skipped
3741
+ // outright when the project has no such files — its result is provably
3742
+ // empty, so skipping is behavior-identical and the cost drops to zero
3743
+ // (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
3744
+ // Passes without an explicit language filter always run.
3745
+ const langs = queries.getDistinctFileLanguages();
3746
+ const has = (...ls) => ls.some((l) => langs.has(l));
3747
+ const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
3748
+ const NONE = [];
2835
3749
  // Cross-file Go method→type `contains` edges must be synthesized AND persisted
2836
3750
  // FIRST: a method declared in a different file from its receiver type is
2837
3751
  // otherwise orphaned from the struct, and goImplementsEdges (next) derives a
2838
3752
  // struct's method set from its `contains` edges — so without this it would
2839
3753
  // under-count the interfaces a cross-file struct satisfies. (#583)
2840
- const goMethodContains = goCrossFileMethodContainsEdges(queries);
2841
- if (goMethodContains.length > 0)
2842
- queries.insertEdges(goMethodContains);
3754
+ const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
3755
+ for (let i = 0; i < goMethodContains.length; i += 2000) {
3756
+ queries.insertEdges(goMethodContains.slice(i, i + 2000));
3757
+ await yieldToLoop();
3758
+ }
3759
+ await yieldToLoop();
3760
+ __mark('goMethodContains');
2843
3761
  // Go implicit `implements` edges must be synthesized AND persisted next: the
2844
3762
  // interface-dispatch bridge below reads `implements` edges from the DB, and
2845
3763
  // Go has none statically. (Other languages already have static implements
2846
3764
  // edges from extraction, so they don't need this pre-pass.)
2847
- const goImpl = goImplementsEdges(queries);
2848
- if (goImpl.length > 0)
2849
- queries.insertEdges(goImpl);
2850
- const fieldEdges = fieldChannelEdges(queries, ctx);
2851
- const closureCollEdges = closureCollectionEdges(queries, ctx);
2852
- const emitterEdges = eventEmitterEdges(ctx);
2853
- const renderEdges = reactRenderEdges(queries, ctx);
2854
- const jsxEdges = reactJsxChildEdges(ctx);
2855
- const vueEdges = vueTemplateEdges(ctx);
2856
- const svelteKitEdges = svelteKitLoadEdges(ctx);
2857
- const pascalEdges = pascalFormEdges(ctx);
2858
- const flutterEdges = flutterBuildEdges(queries, ctx);
2859
- const cppEdges = cppOverrideEdges(queries);
2860
- const ifaceEdges = interfaceOverrideEdges(queries);
2861
- const kotlinExpectActual = kotlinExpectActualEdges(queries);
2862
- const goGrpcEdges = goGrpcStubImplEdges(queries);
2863
- const rnEventEdgesList = rnEventEdges(ctx);
2864
- const fabricNativeEdges = fabricNativeImplEdges(ctx);
2865
- const expoXPlatEdges = expoCrossPlatformEdges(queries);
2866
- const rnXPlatEdges = rnCrossPlatformEdges(queries);
2867
- const mybatisEdges = mybatisJavaXmlEdges(queries);
2868
- const ginEdges = ginMiddlewareChainEdges(queries, ctx);
2869
- const thunkEdges = reduxThunkEdges(queries, ctx);
2870
- const registryEdges = objectRegistryEdges(ctx);
2871
- const rtkEdges = rtkQueryEdges(queries, ctx);
2872
- const piniaEdges = piniaStoreEdges(ctx);
2873
- const vuexEdges = vuexDispatchEdges(ctx);
2874
- const celeryEdges = celeryDispatchEdges(ctx);
2875
- const springEdges = springEventEdges(ctx);
2876
- const mediatrEdges = mediatrDispatchEdges(ctx);
2877
- const sidekiqEdges = sidekiqDispatchEdges(ctx);
2878
- const laravelEdges = laravelEventEdges(ctx);
2879
- const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
2880
- const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
3765
+ const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
3766
+ for (let i = 0; i < goImpl.length; i += 2000) {
3767
+ queries.insertEdges(goImpl.slice(i, i + 2000));
3768
+ await yieldToLoop();
3769
+ }
3770
+ await yieldToLoop();
3771
+ __mark('goImplements');
3772
+ const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
3773
+ await yieldToLoop();
3774
+ __mark('fieldEdges');
3775
+ const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
3776
+ await yieldToLoop();
3777
+ __mark('closureCollEdges');
3778
+ const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
3779
+ await yieldToLoop();
3780
+ __mark('emitterEdges');
3781
+ const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
3782
+ await yieldToLoop();
3783
+ __mark('renderEdges');
3784
+ const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
3785
+ await yieldToLoop();
3786
+ __mark('jsxEdges');
3787
+ const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
3788
+ await yieldToLoop();
3789
+ __mark('vueEdges');
3790
+ const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
3791
+ await yieldToLoop();
3792
+ __mark('svelteKitEdges');
3793
+ const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
3794
+ await yieldToLoop();
3795
+ __mark('pascalEdges');
3796
+ const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
3797
+ await yieldToLoop();
3798
+ __mark('flutterEdges');
3799
+ const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
3800
+ await yieldToLoop();
3801
+ __mark('arkuiStateEdges');
3802
+ const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
3803
+ await yieldToLoop();
3804
+ __mark('arkuiEmitter');
3805
+ const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
3806
+ await yieldToLoop();
3807
+ __mark('arkuiRoutes');
3808
+ const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
3809
+ await yieldToLoop();
3810
+ __mark('cppEdges');
3811
+ const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
3812
+ ? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
3813
+ await yieldToLoop();
3814
+ __mark('ifaceEdges');
3815
+ const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
3816
+ await yieldToLoop();
3817
+ __mark('kotlinExpectActual');
3818
+ const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
3819
+ await yieldToLoop();
3820
+ __mark('goGrpcEdges');
3821
+ const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
3822
+ await yieldToLoop();
3823
+ __mark('rnEventEdgesList');
3824
+ const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
3825
+ await yieldToLoop();
3826
+ __mark('fabricNativeEdges');
3827
+ const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
3828
+ await yieldToLoop();
3829
+ __mark('expoXPlatEdges');
3830
+ const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
3831
+ await yieldToLoop();
3832
+ __mark('rnXPlatEdges');
3833
+ const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE;
3834
+ await yieldToLoop();
3835
+ __mark('mybatisEdges');
3836
+ const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE;
3837
+ await yieldToLoop();
3838
+ __mark('ginEdges');
3839
+ const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE;
3840
+ await yieldToLoop();
3841
+ __mark('thunkEdges');
3842
+ const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
3843
+ await yieldToLoop();
3844
+ __mark('registryEdges');
3845
+ const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
3846
+ await yieldToLoop();
3847
+ __mark('rtkEdges');
3848
+ const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
3849
+ await yieldToLoop();
3850
+ __mark('piniaEdges');
3851
+ const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
3852
+ await yieldToLoop();
3853
+ __mark('vuexEdges');
3854
+ const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
3855
+ await yieldToLoop();
3856
+ __mark('celeryEdges');
3857
+ const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
3858
+ await yieldToLoop();
3859
+ __mark('springEdges');
3860
+ const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
3861
+ await yieldToLoop();
3862
+ __mark('mediatrEdges');
3863
+ const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
3864
+ await yieldToLoop();
3865
+ __mark('sidekiqEdges');
3866
+ const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
3867
+ await yieldToLoop();
3868
+ __mark('erlangBehaviourEdges');
3869
+ const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
3870
+ await yieldToLoop();
3871
+ __mark('laravelEdges');
3872
+ const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop) : NONE;
3873
+ await yieldToLoop();
3874
+ __mark('cFnPtrEdges');
3875
+ const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
3876
+ await yieldToLoop();
3877
+ __mark('goframeEdges');
3878
+ const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE;
3879
+ await yieldToLoop();
3880
+ __mark('nixOptionEdges');
2881
3881
  const merged = [];
2882
3882
  const seen = new Set();
2883
3883
  for (const e of [
@@ -2890,6 +3890,9 @@ function synthesizeCallbackEdges(queries, ctx) {
2890
3890
  ...svelteKitEdges,
2891
3891
  ...pascalEdges,
2892
3892
  ...flutterEdges,
3893
+ ...arkuiStateEdges,
3894
+ ...arkuiEmitter,
3895
+ ...arkuiRoutes,
2893
3896
  ...cppEdges,
2894
3897
  ...ifaceEdges,
2895
3898
  ...kotlinExpectActual,
@@ -2909,9 +3912,11 @@ function synthesizeCallbackEdges(queries, ctx) {
2909
3912
  ...springEdges,
2910
3913
  ...mediatrEdges,
2911
3914
  ...sidekiqEdges,
3915
+ ...erlangBehaviourEdges,
2912
3916
  ...laravelEdges,
2913
3917
  ...cFnPtrEdges,
2914
3918
  ...goframeEdges,
3919
+ ...nixOptionEdges,
2915
3920
  ]) {
2916
3921
  const key = `${e.source}>${e.target}`;
2917
3922
  if (seen.has(key))
@@ -2919,8 +3924,15 @@ function synthesizeCallbackEdges(queries, ctx) {
2919
3924
  seen.add(key);
2920
3925
  merged.push(e);
2921
3926
  }
2922
- if (merged.length > 0)
2923
- queries.insertEdges(merged);
3927
+ __mark('dedupe-merge');
3928
+ // Chunked insert with yields: on the Linux kernel the merged synthesized
3929
+ // edge set is ~275k rows, and one transaction for all of them was a 20s
3930
+ // unyielded main-thread span (#1212 follow-up) — the last one in the tail.
3931
+ for (let i = 0; i < merged.length; i += 2000) {
3932
+ queries.insertEdges(merged.slice(i, i + 2000));
3933
+ await yieldToLoop();
3934
+ }
3935
+ __mark('insertMergedEdges');
2924
3936
  return merged.length + goImpl.length + goMethodContains.length;
2925
3937
  }
2926
3938
  //# sourceMappingURL=callback-synthesizer.js.map