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