homegraph 1.2.0 → 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 (284) hide show
  1. package/dist/bin/homegraph.js +3 -0
  2. package/dist/bin/homegraph.js.map +1 -1
  3. package/dist/context/index.d.ts.map +1 -1
  4. package/dist/context/index.js +85 -0
  5. package/dist/context/index.js.map +1 -1
  6. package/dist/db/index.d.ts +64 -5
  7. package/dist/db/index.d.ts.map +1 -1
  8. package/dist/db/index.js +178 -14
  9. package/dist/db/index.js.map +1 -1
  10. package/dist/db/migrations.d.ts +1 -1
  11. package/dist/db/migrations.d.ts.map +1 -1
  12. package/dist/db/migrations.js +19 -1
  13. package/dist/db/migrations.js.map +1 -1
  14. package/dist/db/queries.d.ts +117 -2
  15. package/dist/db/queries.d.ts.map +1 -1
  16. package/dist/db/queries.js +299 -6
  17. package/dist/db/queries.js.map +1 -1
  18. package/dist/db/schema.sql +32 -1
  19. package/dist/db/wal-valve.d.ts +106 -0
  20. package/dist/db/wal-valve.d.ts.map +1 -0
  21. package/dist/db/wal-valve.js +208 -0
  22. package/dist/db/wal-valve.js.map +1 -0
  23. package/dist/directory.d.ts +9 -5
  24. package/dist/directory.d.ts.map +1 -1
  25. package/dist/directory.js +210 -19
  26. package/dist/directory.js.map +1 -1
  27. package/dist/extraction/cfml-extractor.d.ts +107 -0
  28. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  29. package/dist/extraction/cfml-extractor.js +494 -0
  30. package/dist/extraction/cfml-extractor.js.map +1 -0
  31. package/dist/extraction/grammars.d.ts +25 -1
  32. package/dist/extraction/grammars.d.ts.map +1 -1
  33. package/dist/extraction/grammars.js +177 -24
  34. package/dist/extraction/grammars.js.map +1 -1
  35. package/dist/extraction/index.d.ts +43 -4
  36. package/dist/extraction/index.d.ts.map +1 -1
  37. package/dist/extraction/index.js +308 -5
  38. package/dist/extraction/index.js.map +1 -1
  39. package/dist/extraction/languages/c-cpp.d.ts +42 -0
  40. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  41. package/dist/extraction/languages/c-cpp.js +369 -4
  42. package/dist/extraction/languages/c-cpp.js.map +1 -1
  43. package/dist/extraction/languages/cfquery.d.ts +12 -0
  44. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  45. package/dist/extraction/languages/cfquery.js +28 -0
  46. package/dist/extraction/languages/cfquery.js.map +1 -0
  47. package/dist/extraction/languages/cfscript.d.ts +3 -0
  48. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  49. package/dist/extraction/languages/cfscript.js +73 -0
  50. package/dist/extraction/languages/cfscript.js.map +1 -0
  51. package/dist/extraction/languages/cobol.d.ts +33 -0
  52. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  53. package/dist/extraction/languages/cobol.js +499 -0
  54. package/dist/extraction/languages/cobol.js.map +1 -0
  55. package/dist/extraction/languages/erlang.d.ts +3 -0
  56. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  57. package/dist/extraction/languages/erlang.js +350 -0
  58. package/dist/extraction/languages/erlang.js.map +1 -0
  59. package/dist/extraction/languages/index.d.ts.map +1 -1
  60. package/dist/extraction/languages/index.js +16 -0
  61. package/dist/extraction/languages/index.js.map +1 -1
  62. package/dist/extraction/languages/nix.d.ts +3 -0
  63. package/dist/extraction/languages/nix.d.ts.map +1 -0
  64. package/dist/extraction/languages/nix.js +294 -0
  65. package/dist/extraction/languages/nix.js.map +1 -0
  66. package/dist/extraction/languages/solidity.d.ts +3 -0
  67. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  68. package/dist/extraction/languages/solidity.js +293 -0
  69. package/dist/extraction/languages/solidity.js.map +1 -0
  70. package/dist/extraction/languages/terraform.d.ts +3 -0
  71. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  72. package/dist/extraction/languages/terraform.js +641 -0
  73. package/dist/extraction/languages/terraform.js.map +1 -0
  74. package/dist/extraction/languages/vbnet.d.ts +11 -0
  75. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  76. package/dist/extraction/languages/vbnet.js +141 -0
  77. package/dist/extraction/languages/vbnet.js.map +1 -0
  78. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  79. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  80. package/dist/extraction/mybatis-extractor.js +140 -38
  81. package/dist/extraction/mybatis-extractor.js.map +1 -1
  82. package/dist/extraction/parse-pool.d.ts +27 -1
  83. package/dist/extraction/parse-pool.d.ts.map +1 -1
  84. package/dist/extraction/parse-pool.js +75 -7
  85. package/dist/extraction/parse-pool.js.map +1 -1
  86. package/dist/extraction/parse-worker.js +9 -2
  87. package/dist/extraction/parse-worker.js.map +1 -1
  88. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  89. package/dist/extraction/tree-sitter-helpers.js +1 -0
  90. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  91. package/dist/extraction/tree-sitter-types.d.ts +3 -1
  92. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  93. package/dist/extraction/tree-sitter.d.ts +38 -0
  94. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  95. package/dist/extraction/tree-sitter.js +754 -10
  96. package/dist/extraction/tree-sitter.js.map +1 -1
  97. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  98. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  99. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  100. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  101. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  102. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  103. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  104. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  105. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  106. package/dist/graph/traversal.d.ts.map +1 -1
  107. package/dist/graph/traversal.js +1 -1
  108. package/dist/graph/traversal.js.map +1 -1
  109. package/dist/index.d.ts +60 -2
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +348 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/installer/index.d.ts +42 -0
  114. package/dist/installer/index.d.ts.map +1 -1
  115. package/dist/installer/index.js +97 -2
  116. package/dist/installer/index.js.map +1 -1
  117. package/dist/mcp/daemon.d.ts +25 -3
  118. package/dist/mcp/daemon.d.ts.map +1 -1
  119. package/dist/mcp/daemon.js +62 -8
  120. package/dist/mcp/daemon.js.map +1 -1
  121. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  122. package/dist/mcp/dynamic-boundaries.js +2 -1
  123. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  124. package/dist/mcp/early-ppid.d.ts +26 -0
  125. package/dist/mcp/early-ppid.d.ts.map +1 -0
  126. package/dist/mcp/early-ppid.js +29 -0
  127. package/dist/mcp/early-ppid.js.map +1 -0
  128. package/dist/mcp/engine.d.ts +4 -5
  129. package/dist/mcp/engine.d.ts.map +1 -1
  130. package/dist/mcp/engine.js +27 -1
  131. package/dist/mcp/engine.js.map +1 -1
  132. package/dist/mcp/index.d.ts.map +1 -1
  133. package/dist/mcp/index.js +41 -6
  134. package/dist/mcp/index.js.map +1 -1
  135. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  136. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  137. package/dist/mcp/liveness-watchdog.js +73 -13
  138. package/dist/mcp/liveness-watchdog.js.map +1 -1
  139. package/dist/mcp/memory-budget.d.ts +30 -0
  140. package/dist/mcp/memory-budget.d.ts.map +1 -0
  141. package/dist/mcp/memory-budget.js +123 -0
  142. package/dist/mcp/memory-budget.js.map +1 -0
  143. package/dist/mcp/proxy.d.ts.map +1 -1
  144. package/dist/mcp/proxy.js +88 -4
  145. package/dist/mcp/proxy.js.map +1 -1
  146. package/dist/mcp/query-cache.d.ts +35 -4
  147. package/dist/mcp/query-cache.d.ts.map +1 -1
  148. package/dist/mcp/query-cache.js +89 -19
  149. package/dist/mcp/query-cache.js.map +1 -1
  150. package/dist/mcp/query-pool.d.ts +46 -12
  151. package/dist/mcp/query-pool.d.ts.map +1 -1
  152. package/dist/mcp/query-pool.js +100 -23
  153. package/dist/mcp/query-pool.js.map +1 -1
  154. package/dist/mcp/server-instructions.d.ts +4 -27
  155. package/dist/mcp/server-instructions.d.ts.map +1 -1
  156. package/dist/mcp/server-instructions.js +39 -84
  157. package/dist/mcp/server-instructions.js.map +1 -1
  158. package/dist/mcp/session.d.ts +14 -0
  159. package/dist/mcp/session.d.ts.map +1 -1
  160. package/dist/mcp/session.js +28 -1
  161. package/dist/mcp/session.js.map +1 -1
  162. package/dist/mcp/startup-handshake.d.ts +44 -0
  163. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  164. package/dist/mcp/startup-handshake.js +73 -0
  165. package/dist/mcp/startup-handshake.js.map +1 -0
  166. package/dist/mcp/tools.d.ts +104 -4
  167. package/dist/mcp/tools.d.ts.map +1 -1
  168. package/dist/mcp/tools.js +2390 -191
  169. package/dist/mcp/tools.js.map +1 -1
  170. package/dist/mcp/transport.d.ts.map +1 -1
  171. package/dist/mcp/transport.js +18 -0
  172. package/dist/mcp/transport.js.map +1 -1
  173. package/dist/project-config.d.ts +38 -0
  174. package/dist/project-config.d.ts.map +1 -1
  175. package/dist/project-config.js +101 -2
  176. package/dist/project-config.js.map +1 -1
  177. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  178. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  179. package/dist/resolution/c-fnptr-synthesizer.js +178 -146
  180. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  181. package/dist/resolution/callback-synthesizer.d.ts +9 -1
  182. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  183. package/dist/resolution/callback-synthesizer.js +1164 -176
  184. package/dist/resolution/callback-synthesizer.js.map +1 -1
  185. package/dist/resolution/frameworks/cics.d.ts +20 -0
  186. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  187. package/dist/resolution/frameworks/cics.js +90 -0
  188. package/dist/resolution/frameworks/cics.js.map +1 -0
  189. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  190. package/dist/resolution/frameworks/index.js +3 -0
  191. package/dist/resolution/frameworks/index.js.map +1 -1
  192. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  193. package/dist/resolution/frameworks/java.js +14 -6
  194. package/dist/resolution/frameworks/java.js.map +1 -1
  195. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  196. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  197. package/dist/resolution/frameworks/terraform.js +277 -0
  198. package/dist/resolution/frameworks/terraform.js.map +1 -0
  199. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  200. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  201. package/dist/resolution/goframe-synthesizer.js +8 -3
  202. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  203. package/dist/resolution/import-resolver.d.ts +7 -0
  204. package/dist/resolution/import-resolver.d.ts.map +1 -1
  205. package/dist/resolution/import-resolver.js +129 -4
  206. package/dist/resolution/import-resolver.js.map +1 -1
  207. package/dist/resolution/index.d.ts +60 -7
  208. package/dist/resolution/index.d.ts.map +1 -1
  209. package/dist/resolution/index.js +430 -80
  210. package/dist/resolution/index.js.map +1 -1
  211. package/dist/resolution/name-matcher.d.ts +0 -3
  212. package/dist/resolution/name-matcher.d.ts.map +1 -1
  213. package/dist/resolution/name-matcher.js +354 -33
  214. package/dist/resolution/name-matcher.js.map +1 -1
  215. package/dist/resolution/strip-comments.d.ts +1 -1
  216. package/dist/resolution/strip-comments.d.ts.map +1 -1
  217. package/dist/resolution/strip-comments.js +49 -0
  218. package/dist/resolution/strip-comments.js.map +1 -1
  219. package/dist/resolution/types.d.ts +29 -0
  220. package/dist/resolution/types.d.ts.map +1 -1
  221. package/dist/resolution/workspace-packages.d.ts +10 -0
  222. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  223. package/dist/resolution/workspace-packages.js +142 -4
  224. package/dist/resolution/workspace-packages.js.map +1 -1
  225. package/dist/search/identifier-segments.d.ts +60 -0
  226. package/dist/search/identifier-segments.d.ts.map +1 -0
  227. package/dist/search/identifier-segments.js +176 -0
  228. package/dist/search/identifier-segments.js.map +1 -0
  229. package/dist/search/query-utils.d.ts +201 -0
  230. package/dist/search/query-utils.d.ts.map +1 -1
  231. package/dist/search/query-utils.js +919 -2
  232. package/dist/search/query-utils.js.map +1 -1
  233. package/dist/sync/git-hooks.d.ts.map +1 -1
  234. package/dist/sync/git-hooks.js +2 -0
  235. package/dist/sync/git-hooks.js.map +1 -1
  236. package/dist/sync/watcher.d.ts +10 -5
  237. package/dist/sync/watcher.d.ts.map +1 -1
  238. package/dist/sync/watcher.js +51 -14
  239. package/dist/sync/watcher.js.map +1 -1
  240. package/dist/sync/worktree.d.ts.map +1 -1
  241. package/dist/sync/worktree.js +5 -0
  242. package/dist/sync/worktree.js.map +1 -1
  243. package/dist/types.d.ts +19 -1
  244. package/dist/types.d.ts.map +1 -1
  245. package/dist/types.js +10 -0
  246. package/dist/types.js.map +1 -1
  247. package/dist/upgrade/index.d.ts +32 -0
  248. package/dist/upgrade/index.d.ts.map +1 -1
  249. package/dist/upgrade/index.js +162 -14
  250. package/dist/upgrade/index.js.map +1 -1
  251. package/dist/upgrade/remove-binary.d.ts +87 -0
  252. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  253. package/dist/upgrade/remove-binary.js +289 -0
  254. package/dist/upgrade/remove-binary.js.map +1 -0
  255. package/dist/upgrade/update-check.d.ts +92 -0
  256. package/dist/upgrade/update-check.d.ts.map +1 -0
  257. package/dist/upgrade/update-check.js +258 -0
  258. package/dist/upgrade/update-check.js.map +1 -0
  259. package/package.json +2 -2
  260. package/scripts/agent-eval/run-all.sh +6 -0
  261. package/scripts/build-bundle.sh +5 -0
  262. package/scripts/npm-shim.js +8 -1
  263. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  264. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  265. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  266. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  267. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  268. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  269. package/scripts/exp_boundary_eval/data/agents.json +0 -109
  270. package/scripts/exp_boundary_eval/data/experiments.json +0 -140
  271. package/scripts/qa_eval/README.md +0 -407
  272. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  273. package/scripts/qa_eval/agent_runner.py +0 -526
  274. package/scripts/qa_eval/data/.gitignore +0 -4
  275. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  276. package/scripts/qa_eval/eval_metrics.py +0 -274
  277. package/scripts/qa_eval/external_agent.py +0 -976
  278. package/scripts/qa_eval/llm_config.py +0 -92
  279. package/scripts/qa_eval/memory_monitor.py +0 -132
  280. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  281. package/scripts/qa_eval/requirements.txt +0 -2
  282. package/scripts/qa_eval/run_pipeline.py +0 -804
  283. package/scripts/qa_eval/stats_efficiency.py +0 -279
  284. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -1,5 +1,6 @@
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");
@@ -41,6 +42,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
41
42
  const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
42
43
  const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
43
44
  const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
45
+ // The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
46
+ // trailing-closure syntax, so ONLY those languages can ever contribute a
47
+ // dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
48
+ // Swift dispatcher's field name) would be a wrong edge, not a missed one.
49
+ // Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
50
+ // in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
51
+ // where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
52
+ // 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
53
+ const CC_LANGUAGES = new Set(['swift', 'kotlin']);
44
54
  function kebabToPascal(s) {
45
55
  return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
46
56
  }
@@ -76,6 +86,35 @@ function sliceLines(content, startLine, endLine) {
76
86
  return null;
77
87
  return content.split('\n').slice(startLine - 1, endLine).join('\n');
78
88
  }
89
+ /**
90
+ * Per-match line resolver over `src`, 1-based at `baseLine`. The inline
91
+ * `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
92
+ * which goes quadratic on a match-dense source (a generated function full of
93
+ * `.push(` calls re-scanned tens of thousands of times was most of the #1235
94
+ * indexing wedge). Builds the newline index once — lazily, since most sources
95
+ * never produce a match — then answers each call with a binary search.
96
+ */
97
+ function makeLineAt(src, baseLine) {
98
+ let nl = null;
99
+ return (idx) => {
100
+ if (!nl) {
101
+ nl = [];
102
+ for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
103
+ nl.push(i);
104
+ }
105
+ // Count newlines strictly before idx.
106
+ let lo = 0;
107
+ let hi = nl.length;
108
+ while (lo < hi) {
109
+ const mid = (lo + hi) >> 1;
110
+ if (nl[mid] < idx)
111
+ lo = mid + 1;
112
+ else
113
+ hi = mid;
114
+ }
115
+ return baseLine + lo;
116
+ };
117
+ }
79
118
  function registrarField(src) {
80
119
  const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
81
120
  return m ? m[1] : null;
@@ -213,13 +252,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
213
252
  arr.push({ node, line: absLine });
214
253
  registrars.set(field, arr);
215
254
  };
216
- // Slices EVERY method/function's source (no cheap name-gate), so on a repo
217
- // with a huge file this is the heaviest synthesis pass — yield mid-scan so it
218
- // can't wedge the #850 watchdog on its own (#1091).
255
+ // Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
256
+ // on a repo with a huge file this is the heaviest synthesis pass — yield
257
+ // mid-scan (and mid-match-loop below: a single generated function dense with
258
+ // matches must not starve the watchdog either) so it can't wedge the #850
259
+ // watchdog on its own (#1091, #1235).
219
260
  let scanned = 0;
261
+ let matchTick = 0;
220
262
  for (const m of methodAndFunctionNodes(queries)) {
221
263
  if ((++scanned & 127) === 0)
222
264
  await onYield();
265
+ if (!CC_LANGUAGES.has(m.language))
266
+ continue;
223
267
  const content = ctx.readFile(m.filePath);
224
268
  const src = content && sliceLines(content, m.startLine, m.endLine);
225
269
  if (!src)
@@ -228,11 +272,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
228
272
  const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
229
273
  if (!hasForEach && !hasAppend)
230
274
  continue;
231
- const lineAt = (idx) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1;
275
+ const lineAt = makeLineAt(src, m.startLine ?? 1);
232
276
  if (hasForEach) {
233
277
  CC_DISPATCH_RE.lastIndex = 0;
234
278
  let d;
235
279
  while ((d = CC_DISPATCH_RE.exec(src))) {
280
+ if ((++matchTick & 255) === 0)
281
+ await onYield();
236
282
  const arr = dispatchers.get(d[1]) ?? [];
237
283
  if (!arr.some((n) => n.node.id === m.id))
238
284
  arr.push({ node: m, line: lineAt(d.index) });
@@ -242,12 +288,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
242
288
  if (hasAppend) {
243
289
  CC_APPEND_WRITE_RE.lastIndex = 0;
244
290
  let w;
245
- while ((w = CC_APPEND_WRITE_RE.exec(src)))
291
+ while ((w = CC_APPEND_WRITE_RE.exec(src))) {
292
+ if ((++matchTick & 255) === 0)
293
+ await onYield();
246
294
  addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
295
+ }
247
296
  CC_APPEND_DIRECT_RE.lastIndex = 0;
248
297
  let a;
249
- while ((a = CC_APPEND_DIRECT_RE.exec(src)))
298
+ while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
299
+ if ((++matchTick & 255) === 0)
300
+ await onYield();
250
301
  addReg(a[1], m, lineAt(a.index));
302
+ }
251
303
  }
252
304
  }
253
305
  const edges = [];
@@ -277,10 +329,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
277
329
  }
278
330
  /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
279
331
  async function eventEmitterEdges(ctx, onYield) {
332
+ let scannedFiles = 0;
280
333
  const emitsByEvent = new Map(); // event → dispatcher node ids
281
334
  const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
282
335
  let scanned = 0;
283
336
  for (const file of ctx.getAllFiles()) {
337
+ if ((++scannedFiles & 15) === 0)
338
+ await onYield();
284
339
  if ((++scanned & 255) === 0)
285
340
  await onYield(); // #1091: yield mid-scan on huge graphs
286
341
  const content = ctx.readFile(file);
@@ -291,7 +346,7 @@ async function eventEmitterEdges(ctx, onYield) {
291
346
  if (!hasEmit && !hasOn)
292
347
  continue;
293
348
  const nodesInFile = ctx.getNodesInFile(file);
294
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
349
+ const lineOf = makeLineAt(content, 1);
295
350
  if (hasEmit) {
296
351
  EMIT_RE.lastIndex = 0;
297
352
  let m;
@@ -354,10 +409,13 @@ async function eventEmitterEdges(ctx, onYield) {
354
409
  * `this.setState`). Over-approximation (all setState methods reach render) is
355
410
  * accepted — it's reachability-correct, like the callback channels.
356
411
  */
357
- function reactRenderEdges(queries, ctx) {
412
+ async function reactRenderEdges(queries, ctx, onYield) {
413
+ let scanned255 = 0;
358
414
  const edges = [];
359
415
  const seen = new Set();
360
- for (const cls of queries.getNodesByKind('class')) {
416
+ for (const cls of queries.iterateNodesByKind('class')) {
417
+ if ((++scanned255 & 63) === 0)
418
+ await onYield();
361
419
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
362
420
  .map((e) => queries.getNodeById(e.target))
363
421
  .filter((n) => !!n && n.kind === 'method');
@@ -397,10 +455,13 @@ function reactRenderEdges(queries, ctx) {
397
455
  * body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
398
456
  * Flutter State classes. Over-approximation accepted (reachability-correct).
399
457
  */
400
- function flutterBuildEdges(queries, ctx) {
458
+ async function flutterBuildEdges(queries, ctx, onYield) {
459
+ let scanned255 = 0;
401
460
  const edges = [];
402
461
  const seen = new Set();
403
- for (const cls of queries.getNodesByKind('class')) {
462
+ for (const cls of queries.iterateNodesByKind('class')) {
463
+ if ((++scanned255 & 63) === 0)
464
+ await onYield();
404
465
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
405
466
  .map((e) => queries.getNodeById(e.target))
406
467
  .filter((n) => !!n && n.kind === 'method');
@@ -431,6 +492,279 @@ function flutterBuildEdges(queries, ctx) {
431
492
  }
432
493
  return edges;
433
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
+ }
434
768
  /**
435
769
  * Phase 4c: C++ virtual override. A call through a base/interface pointer
436
770
  * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
@@ -441,14 +775,17 @@ function flutterBuildEdges(queries, ctx) {
441
775
  * implementation(s). Over-approximation accepted (reachability-correct); capped
442
776
  * per class and gated to C++ to avoid touching other languages' dispatch.
443
777
  */
444
- function cppOverrideEdges(queries) {
778
+ async function cppOverrideEdges(queries, onYield) {
779
+ let scanned255 = 0;
445
780
  const edges = [];
446
781
  const seen = new Set();
447
782
  const methodsOf = (classId) => queries
448
783
  .getOutgoingEdges(classId, ['contains'])
449
784
  .map((e) => queries.getNodeById(e.target))
450
785
  .filter((n) => !!n && n.kind === 'method');
451
- for (const cls of queries.getNodesByKind('class')) {
786
+ for (const cls of queries.iterateNodesByKind('class')) {
787
+ if ((++scanned255 & 63) === 0)
788
+ await onYield();
452
789
  const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
453
790
  if (subMethods.length === 0)
454
791
  continue;
@@ -502,6 +839,7 @@ function cppOverrideEdges(queries) {
502
839
  // or an `object` (Scala) so the loop also iterates those kinds.
503
840
  const IFACE_OVERRIDE_LANGS = new Set([
504
841
  'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
842
+ 'arkts',
505
843
  ]);
506
844
  /**
507
845
  * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
@@ -515,7 +853,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
515
853
  * with the other dispatch synthesizers; capped per interface. Empty interfaces
516
854
  * (`any`) are skipped so they don't match every struct.
517
855
  */
518
- function goImplementsEdges(queries) {
856
+ async function goImplementsEdges(queries, onYield) {
857
+ let scanned255 = 0;
519
858
  const edges = [];
520
859
  const seen = new Set();
521
860
  const methodNameSet = (id) => new Set(queries
@@ -523,11 +862,24 @@ function goImplementsEdges(queries) {
523
862
  .map((e) => queries.getNodeById(e.target))
524
863
  .filter((n) => !!n && n.kind === 'method')
525
864
  .map((n) => n.name));
526
- 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
+ }
527
875
  const structMethods = new Map();
528
876
  for (const s of goStructs)
529
877
  structMethods.set(s.id, methodNameSet(s.id));
530
- 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();
531
883
  if (iface.language !== 'go')
532
884
  continue;
533
885
  const want = methodNameSet(iface.id);
@@ -587,7 +939,8 @@ function goImplementsEdges(queries) {
587
939
  * matching the same-file edges extraction already emits). Skips methods that
588
940
  * already have a type parent (the same-file case). (#583, cross-file half)
589
941
  */
590
- function goCrossFileMethodContainsEdges(queries) {
942
+ async function goCrossFileMethodContainsEdges(queries, onYield) {
943
+ let scanned255 = 0;
591
944
  const edges = [];
592
945
  const seen = new Set();
593
946
  const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
@@ -595,7 +948,11 @@ function goCrossFileMethodContainsEdges(queries) {
595
948
  const i = p.replace(/\\/g, '/').lastIndexOf('/');
596
949
  return i >= 0 ? p.slice(0, i) : '';
597
950
  };
598
- 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();
599
956
  if (method.language !== 'go')
600
957
  continue;
601
958
  // The receiver type is encoded in the method's qualifiedName as `Recv::name`
@@ -665,13 +1022,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
665
1022
  function kmpKindsCompatible(a, b) {
666
1023
  return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
667
1024
  }
668
- function kotlinExpectActualEdges(queries) {
1025
+ async function kotlinExpectActualEdges(queries, onYield) {
1026
+ let scanned255 = 0;
669
1027
  const edges = [];
670
1028
  const seen = new Set();
671
- const actuals = queries
672
- .getAllNodes()
673
- .filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
674
- for (const act of actuals) {
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;
675
1039
  let added = 0;
676
1040
  for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
677
1041
  if (added >= MAX_CALLBACKS_PER_CHANNEL)
@@ -705,7 +1069,8 @@ function kotlinExpectActualEdges(queries) {
705
1069
  }
706
1070
  return edges;
707
1071
  }
708
- function interfaceOverrideEdges(queries) {
1072
+ async function interfaceOverrideEdges(queries, onYield) {
1073
+ let scanned255 = 0;
709
1074
  const edges = [];
710
1075
  const seen = new Set();
711
1076
  const methodsOf = (classId) => queries
@@ -717,7 +1082,9 @@ function interfaceOverrideEdges(queries) {
717
1082
  // types that conform to protocols. Iterate both.
718
1083
  const concreteKinds = ['class', 'struct'];
719
1084
  for (const kind of concreteKinds) {
720
- for (const cls of queries.getNodesByKind(kind)) {
1085
+ for (const cls of queries.iterateNodesByKind(kind)) {
1086
+ if ((++scanned255 & 63) === 0)
1087
+ await onYield();
721
1088
  const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
722
1089
  if (implMethods.length === 0)
723
1090
  continue;
@@ -791,7 +1158,8 @@ function interfaceOverrideEdges(queries) {
791
1158
  * Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
792
1159
  * stub's source line is the wiring site shown in the trace trail.
793
1160
  */
794
- function goGrpcStubImplEdges(queries) {
1161
+ async function goGrpcStubImplEdges(queries, onYield) {
1162
+ let scanned255 = 0;
795
1163
  const edges = [];
796
1164
  const seen = new Set();
797
1165
  const STUB_RE = /^Unimplemented.*Server$/;
@@ -803,7 +1171,9 @@ function goGrpcStubImplEdges(queries) {
803
1171
  const methodNamesByStruct = new Map();
804
1172
  const methodNodesByStruct = new Map();
805
1173
  const goStructs = [];
806
- for (const s of queries.getNodesByKind('struct')) {
1174
+ for (const s of queries.iterateNodesByKind('struct')) {
1175
+ if ((++scanned255 & 63) === 0)
1176
+ await onYield();
807
1177
  if (s.language !== 'go')
808
1178
  continue;
809
1179
  goStructs.push(s);
@@ -887,11 +1257,14 @@ function goGrpcStubImplEdges(queries) {
887
1257
  * (or nothing) and are dropped.
888
1258
  */
889
1259
  async function reactJsxChildEdges(ctx, onYield) {
1260
+ let scannedFiles = 0;
890
1261
  const edges = [];
891
1262
  const seen = new Set();
892
1263
  const PARENT_KINDS = new Set(['method', 'function', 'component']);
893
1264
  let scanned = 0;
894
1265
  for (const file of ctx.getAllFiles()) {
1266
+ if ((++scannedFiles & 15) === 0)
1267
+ await onYield();
895
1268
  if ((++scanned & 255) === 0)
896
1269
  await onYield(); // #1091: yield mid-scan on huge graphs
897
1270
  const content = ctx.readFile(file);
@@ -941,7 +1314,9 @@ async function reactJsxChildEdges(ctx, onYield) {
941
1314
  * component, handler→function/method) keeps precision; inline arrows / `$emit`
942
1315
  * skipped.
943
1316
  */
944
- function vueTemplateEdges(ctx) {
1317
+ async function vueTemplateEdges(ctx, onYield) {
1318
+ let scannedFiles = 0;
1319
+ let scanned255 = 0;
945
1320
  const edges = [];
946
1321
  const seen = new Set();
947
1322
  const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
@@ -955,12 +1330,16 @@ function vueTemplateEdges(ctx) {
955
1330
  // misses it (flat components match by basename and don't need this). Map each
956
1331
  // nested component's Nuxt name → node so those template usages resolve.
957
1332
  const nuxtComponents = new Map();
958
- 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();
959
1336
  const nn = nuxtComponentName(c.filePath);
960
1337
  if (nn && !nuxtComponents.has(nn))
961
1338
  nuxtComponents.set(nn, c);
962
1339
  }
963
1340
  for (const file of ctx.getAllFiles()) {
1341
+ if ((++scannedFiles & 15) === 0)
1342
+ await onYield();
964
1343
  if (!file.endsWith('.vue'))
965
1344
  continue;
966
1345
  const content = ctx.readFile(file);
@@ -1094,18 +1473,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
1094
1473
  // statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
1095
1474
  // is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
1096
1475
  const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
1097
- function rnEventEdges(ctx) {
1476
+ async function rnEventEdges(ctx, onYield) {
1477
+ let scannedFiles = 0;
1098
1478
  // Native dispatchers (source = the native method whose body sends the
1099
1479
  // event) and JS handlers (target = the function/method registered as
1100
1480
  // the listener) keyed by event name.
1101
1481
  const nativeDispatchersByEvent = new Map();
1102
1482
  const jsHandlersByEvent = new Map();
1103
1483
  for (const file of ctx.getAllFiles()) {
1484
+ if ((++scannedFiles & 15) === 0)
1485
+ await onYield();
1104
1486
  const content = ctx.readFile(file);
1105
1487
  if (!content)
1106
1488
  continue;
1107
1489
  const nodesInFile = ctx.getNodesInFile(file);
1108
- const lineOf = (idx) => content.slice(0, idx).split('\n').length;
1490
+ const lineOf = makeLineAt(content, 1);
1109
1491
  const addDispatcher = (event, line) => {
1110
1492
  const disp = enclosingFn(nodesInFile, line);
1111
1493
  if (!disp)
@@ -1288,11 +1670,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
1288
1670
  * caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
1289
1671
  * `<file>::<module>.<method>` by the framework extractor.
1290
1672
  */
1291
- function expoCrossPlatformEdges(queries) {
1673
+ async function expoCrossPlatformEdges(queries, onYield) {
1674
+ let scanned255 = 0;
1292
1675
  const edges = [];
1293
1676
  const seen = new Set();
1294
1677
  const byKey = new Map();
1295
- for (const m of queries.getNodesByKind('method')) {
1678
+ for (const m of queries.iterateNodesByKind('method')) {
1679
+ if ((++scanned255 & 63) === 0)
1680
+ await onYield();
1296
1681
  if (!m.id.startsWith('expo-module:'))
1297
1682
  continue;
1298
1683
  const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
@@ -1341,7 +1726,8 @@ function expoCrossPlatformEdges(queries) {
1341
1726
  * `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
1342
1727
  * lines up with the bare Android method name.
1343
1728
  */
1344
- function rnCrossPlatformEdges(queries) {
1729
+ async function rnCrossPlatformEdges(queries, onYield) {
1730
+ let scanned255 = 0;
1345
1731
  const edges = [];
1346
1732
  const seen = new Set();
1347
1733
  const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
@@ -1363,6 +1749,8 @@ function rnCrossPlatformEdges(queries) {
1363
1749
  // below only runs for genuine cross-platform candidates.
1364
1750
  const byName = new Map();
1365
1751
  for (const m of queries.iterateNodesByKind('method')) {
1752
+ if ((++scanned255 & 63) === 0)
1753
+ await onYield();
1366
1754
  if (!NATIVE.has(m.language))
1367
1755
  continue;
1368
1756
  const key = norm(m.name);
@@ -1413,17 +1801,27 @@ function rnCrossPlatformEdges(queries) {
1413
1801
  }
1414
1802
  return edges;
1415
1803
  }
1416
- function fabricNativeImplEdges(ctx) {
1804
+ async function fabricNativeImplEdges(ctx, onYield) {
1805
+ let scanned255 = 0;
1417
1806
  const edges = [];
1418
1807
  const seen = new Set();
1419
1808
  // The Fabric extractor IDs are prefixed `fabric-component:` so we can
1420
- // filter to just those without iterating all `component` nodes.
1421
- const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
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
+ }
1422
1818
  if (components.length === 0)
1423
1819
  return edges;
1424
1820
  // Pre-index native classes by name for O(1) lookup.
1425
1821
  const nativeClassesByName = new Map();
1426
- 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();
1427
1825
  if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
1428
1826
  continue;
1429
1827
  const arr = nativeClassesByName.get(n.name);
@@ -1476,12 +1874,15 @@ function fabricNativeImplEdges(ctx) {
1476
1874
  * same simple name) are dropped. We need-not bridge by package because Java
1477
1875
  * mapper interfaces are typically uniquely named within a project.
1478
1876
  */
1479
- function mybatisJavaXmlEdges(queries) {
1877
+ async function mybatisJavaXmlEdges(queries, onYield) {
1878
+ let scanned255 = 0;
1480
1879
  const edges = [];
1481
1880
  const seen = new Set();
1482
1881
  // Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
1483
1882
  const javaIndex = new Map();
1484
1883
  for (const m of queries.iterateNodesByKind('method')) {
1884
+ if ((++scanned255 & 63) === 0)
1885
+ await onYield();
1485
1886
  if (m.language !== 'java' && m.language !== 'kotlin')
1486
1887
  continue;
1487
1888
  const parts = m.qualifiedName.split('::');
@@ -1497,6 +1898,8 @@ function mybatisJavaXmlEdges(queries) {
1497
1898
  javaIndex.set(key, [m]);
1498
1899
  }
1499
1900
  for (const xml of queries.iterateNodesByKind('method')) {
1901
+ if ((++scanned255 & 63) === 0)
1902
+ await onYield();
1500
1903
  if (xml.language !== 'xml')
1501
1904
  continue;
1502
1905
  // Qualified name: `<namespace>::<id>`. Extract the simple class name.
@@ -1610,10 +2013,14 @@ function goHandlerIdent(expr) {
1610
2013
  const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
1611
2014
  return m ? m[1] : null;
1612
2015
  }
1613
- function ginMiddlewareChainEdges(queries, ctx) {
2016
+ async function ginMiddlewareChainEdges(queries, ctx, onYield) {
2017
+ let scanned255 = 0;
2018
+ let scannedFiles = 0;
1614
2019
  // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
1615
2020
  const dispatchers = [];
1616
2021
  for (const n of queries.iterateNodesByKind('method')) {
2022
+ if ((++scanned255 & 63) === 0)
2023
+ await onYield();
1617
2024
  if (n.language !== 'go')
1618
2025
  continue;
1619
2026
  const content = ctx.readFile(n.filePath);
@@ -1628,6 +2035,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
1628
2035
  // closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
1629
2036
  const registered = new Map(); // name → registeredAt (file:line)
1630
2037
  for (const file of ctx.getAllFiles()) {
2038
+ if ((++scannedFiles & 15) === 0)
2039
+ await onYield();
1631
2040
  if (!file.endsWith('.go'))
1632
2041
  continue;
1633
2042
  const content = ctx.readFile(file);
@@ -1683,10 +2092,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
1683
2092
  * clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
1684
2093
  * definition isn't orphaned, and editing the form surfaces its code-behind unit.
1685
2094
  */
1686
- function pascalFormEdges(ctx) {
2095
+ async function pascalFormEdges(ctx, onYield) {
2096
+ let scannedFiles = 0;
1687
2097
  const edges = [];
1688
2098
  const allFiles = new Set(ctx.getAllFiles());
1689
2099
  for (const file of allFiles) {
2100
+ if ((++scannedFiles & 255) === 0)
2101
+ await onYield();
1690
2102
  if (!/\.(dfm|fmx)$/i.test(file))
1691
2103
  continue;
1692
2104
  const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
@@ -1722,12 +2134,15 @@ function pascalFormEdges(ctx) {
1722
2134
  * a loader's data shows the page it feeds) and the page's dependencies include
1723
2135
  * its loader.
1724
2136
  */
1725
- function svelteKitLoadEdges(ctx) {
2137
+ async function svelteKitLoadEdges(ctx, onYield) {
2138
+ let scannedFiles = 0;
1726
2139
  const edges = [];
1727
2140
  const allFiles = new Set(ctx.getAllFiles());
1728
2141
  const HOOKS = new Set(['load', 'actions']);
1729
2142
  const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
1730
2143
  for (const file of allFiles) {
2144
+ if ((++scannedFiles & 255) === 0)
2145
+ await onYield();
1731
2146
  const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
1732
2147
  if (!m)
1733
2148
  continue;
@@ -1760,91 +2175,6 @@ function svelteKitLoadEdges(ctx) {
1760
2175
  }
1761
2176
  return edges;
1762
2177
  }
1763
- /** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
1764
- function arktsEntryEdges(queries, ctx) {
1765
- const edges = [];
1766
- const seen = new Set();
1767
- const add = (source, target, synthesizedBy, extra = {}) => {
1768
- const key = `${source}>${target}`;
1769
- if (seen.has(key))
1770
- return;
1771
- seen.add(key);
1772
- const targetNode = queries.getNodeById(target);
1773
- edges.push({
1774
- source,
1775
- target,
1776
- kind: 'calls',
1777
- provenance: 'heuristic',
1778
- metadata: {
1779
- synthesizedBy,
1780
- registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
1781
- ...extra,
1782
- },
1783
- });
1784
- };
1785
- for (const m of methodAndFunctionNodes(queries)) {
1786
- if (m.language !== 'arkts')
1787
- continue;
1788
- const content = ctx.readFile(m.filePath);
1789
- if (!content)
1790
- continue;
1791
- const lines = content.split('\n');
1792
- const start = Math.max((m.startLine ?? 1) - 1, 0);
1793
- const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
1794
- const body = lines.slice(start, end).join('\n');
1795
- const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
1796
- if (!loadMatch)
1797
- continue;
1798
- const page = loadMatch[1];
1799
- const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
1800
- if (pageComponent) {
1801
- const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
1802
- if (aboutToAppear) {
1803
- add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
1804
- }
1805
- }
1806
- }
1807
- for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
1808
- if (cls.language !== 'arkts')
1809
- continue;
1810
- const methods = queries
1811
- .getOutgoingEdges(cls.id, ['contains'])
1812
- .map((e) => queries.getNodeById(e.target))
1813
- .filter((n) => !!n && n.kind === 'method');
1814
- const onCreate = methods.find((n) => n.name === 'onCreate');
1815
- const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
1816
- if (onCreate && onWindowStageCreate) {
1817
- add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
1818
- }
1819
- }
1820
- for (const comp of [
1821
- ...queries.getNodesByKind('component'),
1822
- ...queries.getNodesByKind('class'),
1823
- ...queries.getNodesByKind('struct'),
1824
- ]) {
1825
- if (comp.language !== 'arkts')
1826
- continue;
1827
- const owner = comp.kind === 'component'
1828
- ? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
1829
- queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
1830
- comp
1831
- : comp;
1832
- const methods = queries
1833
- .getOutgoingEdges(owner.id, ['contains'])
1834
- .map((e) => queries.getNodeById(e.target))
1835
- .filter((n) => !!n && n.kind === 'method');
1836
- const build = methods.find((n) => n.name === 'build');
1837
- if (!build)
1838
- continue;
1839
- for (const life of ['aboutToAppear', 'onPageShow']) {
1840
- const lifeMethod = methods.find((n) => n.name === life);
1841
- if (lifeMethod) {
1842
- add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
1843
- }
1844
- }
1845
- }
1846
- return edges;
1847
- }
1848
2178
  /**
1849
2179
  * Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
1850
2180
  * (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
@@ -1861,10 +2191,13 @@ function arktsEntryEdges(queries, ctx) {
1861
2191
  const THUNK_DECL_RE = /create(?:Async)?Thunk/;
1862
2192
  const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
1863
2193
  const THUNK_FANOUT_CAP = 24;
1864
- function reduxThunkEdges(queries, ctx) {
2194
+ async function reduxThunkEdges(queries, ctx, onYield) {
2195
+ let scanned255 = 0;
1865
2196
  const edges = [];
1866
2197
  const seen = new Set();
1867
2198
  for (const node of queries.iterateNodesByKind('constant')) {
2199
+ if ((++scanned255 & 63) === 0)
2200
+ await onYield();
1868
2201
  // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
1869
2202
  // avoids reading every constant's body on a large repo.
1870
2203
  if (!node.signature || !THUNK_DECL_RE.test(node.signature))
@@ -1996,10 +2329,13 @@ function resolveRegistryHandler(ctx, name, chained) {
1996
2329
  return cands.find((n) => n.kind === 'method') ?? null;
1997
2330
  }
1998
2331
  async function objectRegistryEdges(ctx, onYield) {
2332
+ let scannedFiles = 0;
1999
2333
  const edges = [];
2000
2334
  const seen = new Set();
2001
2335
  let scanned = 0;
2002
2336
  for (const file of ctx.getAllFiles()) {
2337
+ if ((++scannedFiles & 15) === 0)
2338
+ await onYield();
2003
2339
  if ((++scanned & 255) === 0)
2004
2340
  await onYield(); // #1091: yield mid-scan on huge graphs
2005
2341
  if (!REGISTRY_JS_EXT.test(file))
@@ -2111,10 +2447,13 @@ function rtkEndpointNameFromHook(hook) {
2111
2447
  return null;
2112
2448
  return mid.charAt(0).toLowerCase() + mid.slice(1);
2113
2449
  }
2114
- function rtkQueryEdges(queries, ctx) {
2450
+ async function rtkQueryEdges(queries, ctx, onYield) {
2451
+ let scanned255 = 0;
2115
2452
  const edges = [];
2116
2453
  const seen = new Set();
2117
2454
  for (const hook of queries.iterateNodesByKind('function')) {
2455
+ if ((++scanned255 & 63) === 0)
2456
+ await onYield();
2118
2457
  // Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
2119
2458
  if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
2120
2459
  continue;
@@ -2158,10 +2497,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
2158
2497
  const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
2159
2498
  const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
2160
2499
  const PINIA_FANOUT_CAP = 80;
2161
- function piniaStoreEdges(ctx) {
2500
+ async function piniaStoreEdges(ctx, onYield) {
2501
+ let scannedFiles = 0;
2162
2502
  // 1. Map each `const useXStore = defineStore(...)` factory → its store file.
2163
2503
  const factoryFile = new Map();
2164
2504
  for (const file of ctx.getAllFiles()) {
2505
+ if ((++scannedFiles & 15) === 0)
2506
+ await onYield();
2165
2507
  if (!PINIA_CONSUMER_EXT.test(file))
2166
2508
  continue;
2167
2509
  const content = ctx.readFile(file);
@@ -2177,6 +2519,8 @@ function piniaStoreEdges(ctx) {
2177
2519
  const edges = [];
2178
2520
  const seen = new Set();
2179
2521
  for (const file of ctx.getAllFiles()) {
2522
+ if ((++scannedFiles & 15) === 0)
2523
+ await onYield();
2180
2524
  if (!PINIA_CONSUMER_EXT.test(file))
2181
2525
  continue;
2182
2526
  const content = ctx.readFile(file);
@@ -2249,7 +2593,8 @@ const VUEX_FANOUT_CAP = 120;
2249
2593
  function pathHasSegment(filePath, seg) {
2250
2594
  return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
2251
2595
  }
2252
- function vuexDispatchEdges(ctx) {
2596
+ async function vuexDispatchEdges(ctx, onYield) {
2597
+ let scannedFiles = 0;
2253
2598
  const storeFileCache = new Map();
2254
2599
  const isStoreFile = (file) => {
2255
2600
  let v = storeFileCache.get(file);
@@ -2287,6 +2632,8 @@ function vuexDispatchEdges(ctx) {
2287
2632
  const edges = [];
2288
2633
  const seen = new Set();
2289
2634
  for (const file of ctx.getAllFiles()) {
2635
+ if ((++scannedFiles & 15) === 0)
2636
+ await onYield();
2290
2637
  if (!PINIA_CONSUMER_EXT.test(file))
2291
2638
  continue;
2292
2639
  const content = ctx.readFile(file);
@@ -2346,7 +2693,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
2346
2693
  const CELERY_PY_EXT = /\.py$/;
2347
2694
  const CELERY_FANOUT_CAP = 80;
2348
2695
  const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
2349
- function celeryDispatchEdges(ctx) {
2696
+ async function celeryDispatchEdges(ctx, onYield) {
2697
+ let scannedFiles = 0;
2350
2698
  // Memoize the decorator check per task-candidate node: it reads the file and scans a few
2351
2699
  // lines above the def. Only called on names that are actually `.delay`/`.apply_async`
2352
2700
  // receivers, so the candidate set stays small.
@@ -2390,6 +2738,8 @@ function celeryDispatchEdges(ctx) {
2390
2738
  const edges = [];
2391
2739
  const seen = new Set();
2392
2740
  for (const file of ctx.getAllFiles()) {
2741
+ if ((++scannedFiles & 15) === 0)
2742
+ await onYield();
2393
2743
  if (!CELERY_PY_EXT.test(file))
2394
2744
  continue;
2395
2745
  const content = ctx.readFile(file);
@@ -2468,15 +2818,24 @@ function springFirstParamType(sig) {
2468
2818
  const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
2469
2819
  return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
2470
2820
  }
2471
- function springEventEdges(ctx) {
2821
+ async function springEventEdges(ctx, onYield) {
2822
+ let scannedFiles = 0;
2472
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).
2473
2827
  const listeners = new Map();
2828
+ const publisherFiles = [];
2474
2829
  for (const file of ctx.getAllFiles()) {
2830
+ if ((++scannedFiles & 15) === 0)
2831
+ await onYield();
2475
2832
  if (!SPRING_JAVA_EXT.test(file))
2476
2833
  continue;
2477
2834
  const content = ctx.readFile(file);
2478
2835
  if (!content)
2479
2836
  continue;
2837
+ if (content.includes('.publishEvent('))
2838
+ publisherFiles.push(file);
2480
2839
  const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
2481
2840
  const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
2482
2841
  if (!hasAnno && !hasAppListener)
@@ -2516,12 +2875,13 @@ function springEventEdges(ctx) {
2516
2875
  }
2517
2876
  if (!listeners.size)
2518
2877
  return [];
2519
- // 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.
2520
2880
  const edges = [];
2521
2881
  const seen = new Set();
2522
- for (const file of ctx.getAllFiles()) {
2523
- if (!SPRING_JAVA_EXT.test(file))
2524
- continue;
2882
+ for (const file of publisherFiles) {
2883
+ if ((++scannedFiles & 15) === 0)
2884
+ await onYield();
2525
2885
  const content = ctx.readFile(file);
2526
2886
  if (!content || !content.includes('.publishEvent('))
2527
2887
  continue;
@@ -2606,10 +2966,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
2606
2966
  }
2607
2967
  return declType;
2608
2968
  }
2609
- function mediatrDispatchEdges(ctx) {
2969
+ async function mediatrDispatchEdges(ctx, onYield) {
2970
+ let scannedFiles = 0;
2610
2971
  // Pass 1 — request/notification type → the Handle method of each handler class.
2611
2972
  const handlers = new Map();
2612
2973
  for (const file of ctx.getAllFiles()) {
2974
+ if ((++scannedFiles & 15) === 0)
2975
+ await onYield();
2613
2976
  if (!MEDIATR_CS_EXT.test(file))
2614
2977
  continue;
2615
2978
  const content = ctx.readFile(file);
@@ -2643,6 +3006,8 @@ function mediatrDispatchEdges(ctx) {
2643
3006
  const edges = [];
2644
3007
  const seen = new Set();
2645
3008
  for (const file of ctx.getAllFiles()) {
3009
+ if ((++scannedFiles & 15) === 0)
3010
+ await onYield();
2646
3011
  if (!MEDIATR_CS_EXT.test(file))
2647
3012
  continue;
2648
3013
  const content = ctx.readFile(file);
@@ -2706,7 +3071,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
2706
3071
  const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
2707
3072
  const SIDEKIQ_RB_EXT = /\.rb$/;
2708
3073
  const SIDEKIQ_FANOUT_CAP = 80;
2709
- function sidekiqDispatchEdges(ctx) {
3074
+ async function sidekiqDispatchEdges(ctx, onYield) {
3075
+ let scannedFiles = 0;
2710
3076
  // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
2711
3077
  // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
2712
3078
  const performCache = new Map();
@@ -2743,6 +3109,8 @@ function sidekiqDispatchEdges(ctx) {
2743
3109
  const edges = [];
2744
3110
  const seen = new Set();
2745
3111
  for (const file of ctx.getAllFiles()) {
3112
+ if ((++scannedFiles & 15) === 0)
3113
+ await onYield();
2746
3114
  if (!SIDEKIQ_RB_EXT.test(file))
2747
3115
  continue;
2748
3116
  const content = ctx.readFile(file);
@@ -2778,6 +3146,414 @@ function sidekiqDispatchEdges(ctx) {
2778
3146
  }
2779
3147
  return edges;
2780
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
+ }
2781
3557
  // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2782
3558
  // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2783
3559
  // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
@@ -2824,7 +3600,8 @@ function phpArrayBody(src, openIdx) {
2824
3600
  }
2825
3601
  return null;
2826
3602
  }
2827
- function laravelEventEdges(ctx) {
3603
+ async function laravelEventEdges(ctx, onYield) {
3604
+ let scannedFiles = 0;
2828
3605
  // event short name → its listener `handle` methods (deduped by node id).
2829
3606
  const listeners = new Map();
2830
3607
  const add = (event, handle) => {
@@ -2841,6 +3618,8 @@ function laravelEventEdges(ctx) {
2841
3618
  && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
2842
3619
  // Pass 1 — build the event→handle map from both registration mechanisms.
2843
3620
  for (const file of ctx.getAllFiles()) {
3621
+ if ((++scannedFiles & 15) === 0)
3622
+ await onYield();
2844
3623
  if (!LARAVEL_PHP_EXT.test(file))
2845
3624
  continue;
2846
3625
  const content = ctx.readFile(file);
@@ -2886,6 +3665,8 @@ function laravelEventEdges(ctx) {
2886
3665
  const edges = [];
2887
3666
  const seen = new Set();
2888
3667
  for (const file of ctx.getAllFiles()) {
3668
+ if ((++scannedFiles & 15) === 0)
3669
+ await onYield();
2889
3670
  if (!LARAVEL_PHP_EXT.test(file))
2890
3671
  continue;
2891
3672
  const content = ctx.readFile(file);
@@ -2925,6 +3706,91 @@ function laravelEventEdges(ctx) {
2925
3706
  }
2926
3707
  return edges;
2927
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
+ }
2928
3794
  /**
2929
3795
  * Synthesize dispatcher→callback edges (field observers + EventEmitters +
2930
3796
  * React re-render + JSX children + Vue templates + SvelteKit load + RN event
@@ -2936,7 +3802,15 @@ function laravelEventEdges(ctx) {
2936
3802
  * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
2937
3803
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
2938
3804
  */
2939
- async 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) {
2940
3814
  // Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
2941
3815
  // running synchronously on the indexer's main thread. Their AGGREGATE can run
2942
3816
  // for well over a minute on a large repo — long enough for the #850 liveness
@@ -2945,87 +3819,189 @@ async function synthesizeCallbackEdges(queries, ctx) {
2945
3819
  // that itself hangs (a real wedge) never reaches the next yield, so the
2946
3820
  // watchdog still catches that. See ./cooperative-yield.
2947
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 = [];
2948
3870
  // Cross-file Go method→type `contains` edges must be synthesized AND persisted
2949
3871
  // FIRST: a method declared in a different file from its receiver type is
2950
3872
  // otherwise orphaned from the struct, and goImplementsEdges (next) derives a
2951
3873
  // struct's method set from its `contains` edges — so without this it would
2952
3874
  // under-count the interfaces a cross-file struct satisfies. (#583)
2953
- const goMethodContains = goCrossFileMethodContainsEdges(queries);
2954
- if (goMethodContains.length > 0)
2955
- 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
+ }
2956
3880
  await yieldToLoop();
3881
+ __mark('goMethodContains');
2957
3882
  // Go implicit `implements` edges must be synthesized AND persisted next: the
2958
3883
  // interface-dispatch bridge below reads `implements` edges from the DB, and
2959
3884
  // Go has none statically. (Other languages already have static implements
2960
3885
  // edges from extraction, so they don't need this pre-pass.)
2961
- const goImpl = goImplementsEdges(queries);
2962
- if (goImpl.length > 0)
2963
- queries.insertEdges(goImpl);
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
+ }
2964
3891
  await yieldToLoop();
3892
+ __mark('goImplements');
2965
3893
  const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
2966
3894
  await yieldToLoop();
3895
+ __mark('fieldEdges');
2967
3896
  const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
2968
3897
  await yieldToLoop();
3898
+ __mark('closureCollEdges');
2969
3899
  const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
2970
3900
  await yieldToLoop();
2971
- const renderEdges = reactRenderEdges(queries, ctx);
3901
+ __mark('emitterEdges');
3902
+ const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
2972
3903
  await yieldToLoop();
3904
+ __mark('renderEdges');
2973
3905
  const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
2974
3906
  await yieldToLoop();
2975
- const vueEdges = vueTemplateEdges(ctx);
3907
+ __mark('jsxEdges');
3908
+ const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
2976
3909
  await yieldToLoop();
2977
- const svelteKitEdges = svelteKitLoadEdges(ctx);
3910
+ __mark('vueEdges');
3911
+ const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
2978
3912
  await yieldToLoop();
2979
- const pascalEdges = pascalFormEdges(ctx);
3913
+ __mark('svelteKitEdges');
3914
+ const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
2980
3915
  await yieldToLoop();
2981
- const flutterEdges = flutterBuildEdges(queries, ctx);
3916
+ __mark('pascalEdges');
3917
+ const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
2982
3918
  await yieldToLoop();
2983
- const cppEdges = cppOverrideEdges(queries);
3919
+ __mark('flutterEdges');
3920
+ const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
2984
3921
  await yieldToLoop();
2985
- const ifaceEdges = interfaceOverrideEdges(queries);
3922
+ __mark('arkuiStateEdges');
3923
+ const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
2986
3924
  await yieldToLoop();
2987
- const kotlinExpectActual = kotlinExpectActualEdges(queries);
3925
+ __mark('arkuiEmitter');
3926
+ const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
2988
3927
  await yieldToLoop();
2989
- const goGrpcEdges = goGrpcStubImplEdges(queries);
3928
+ __mark('arkuiRoutes');
3929
+ const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
2990
3930
  await yieldToLoop();
2991
- const rnEventEdgesList = rnEventEdges(ctx);
3931
+ __mark('cppEdges');
3932
+ const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
3933
+ ? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
2992
3934
  await yieldToLoop();
2993
- const fabricNativeEdges = fabricNativeImplEdges(ctx);
3935
+ __mark('ifaceEdges');
3936
+ const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
2994
3937
  await yieldToLoop();
2995
- const expoXPlatEdges = expoCrossPlatformEdges(queries);
3938
+ __mark('kotlinExpectActual');
3939
+ const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
2996
3940
  await yieldToLoop();
2997
- const rnXPlatEdges = rnCrossPlatformEdges(queries);
3941
+ __mark('goGrpcEdges');
3942
+ const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
2998
3943
  await yieldToLoop();
2999
- const mybatisEdges = mybatisJavaXmlEdges(queries);
3944
+ __mark('rnEventEdgesList');
3945
+ const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
3000
3946
  await yieldToLoop();
3001
- const ginEdges = ginMiddlewareChainEdges(queries, ctx);
3947
+ __mark('fabricNativeEdges');
3948
+ const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
3002
3949
  await yieldToLoop();
3003
- const thunkEdges = reduxThunkEdges(queries, ctx);
3950
+ __mark('expoXPlatEdges');
3951
+ const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
3004
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');
3005
3963
  const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
3006
3964
  await yieldToLoop();
3007
- const rtkEdges = rtkQueryEdges(queries, ctx);
3965
+ __mark('registryEdges');
3966
+ const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
3008
3967
  await yieldToLoop();
3009
- const piniaEdges = piniaStoreEdges(ctx);
3968
+ __mark('rtkEdges');
3969
+ const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
3010
3970
  await yieldToLoop();
3011
- const vuexEdges = vuexDispatchEdges(ctx);
3971
+ __mark('piniaEdges');
3972
+ const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
3012
3973
  await yieldToLoop();
3013
- const celeryEdges = celeryDispatchEdges(ctx);
3974
+ __mark('vuexEdges');
3975
+ const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
3014
3976
  await yieldToLoop();
3015
- const springEdges = springEventEdges(ctx);
3977
+ __mark('celeryEdges');
3978
+ const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
3016
3979
  await yieldToLoop();
3017
- const mediatrEdges = mediatrDispatchEdges(ctx);
3980
+ __mark('springEdges');
3981
+ const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
3018
3982
  await yieldToLoop();
3019
- const sidekiqEdges = sidekiqDispatchEdges(ctx);
3983
+ __mark('mediatrEdges');
3984
+ const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
3020
3985
  await yieldToLoop();
3021
- const laravelEdges = laravelEventEdges(ctx);
3986
+ __mark('sidekiqEdges');
3987
+ const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
3022
3988
  await yieldToLoop();
3023
- const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
3989
+ __mark('erlangBehaviourEdges');
3990
+ const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
3024
3991
  await yieldToLoop();
3025
- const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
3992
+ __mark('laravelEdges');
3993
+ const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop, subProgress) : NONE;
3026
3994
  await yieldToLoop();
3027
- const arktsStartupEdges = arktsEntryEdges(queries, ctx);
3995
+ __mark('cFnPtrEdges');
3996
+ const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
3028
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');
3029
4005
  const merged = [];
3030
4006
  const seen = new Set();
3031
4007
  for (const e of [
@@ -3038,6 +4014,9 @@ async function synthesizeCallbackEdges(queries, ctx) {
3038
4014
  ...svelteKitEdges,
3039
4015
  ...pascalEdges,
3040
4016
  ...flutterEdges,
4017
+ ...arkuiStateEdges,
4018
+ ...arkuiEmitter,
4019
+ ...arkuiRoutes,
3041
4020
  ...cppEdges,
3042
4021
  ...ifaceEdges,
3043
4022
  ...kotlinExpectActual,
@@ -3057,9 +4036,11 @@ async function synthesizeCallbackEdges(queries, ctx) {
3057
4036
  ...springEdges,
3058
4037
  ...mediatrEdges,
3059
4038
  ...sidekiqEdges,
4039
+ ...erlangBehaviourEdges,
3060
4040
  ...laravelEdges,
3061
4041
  ...cFnPtrEdges,
3062
4042
  ...goframeEdges,
4043
+ ...nixOptionEdges,
3063
4044
  ...arktsStartupEdges,
3064
4045
  ]) {
3065
4046
  const key = `${e.source}>${e.target}`;
@@ -3068,8 +4049,15 @@ async function synthesizeCallbackEdges(queries, ctx) {
3068
4049
  seen.add(key);
3069
4050
  merged.push(e);
3070
4051
  }
3071
- if (merged.length > 0)
3072
- 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');
3073
4061
  return merged.length + goImpl.length + goMethodContains.length;
3074
4062
  }
3075
4063
  //# sourceMappingURL=callback-synthesizer.js.map