@colbymchenry/codegraph-win32-x64 1.2.0 → 1.3.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 (231) hide show
  1. package/lib/dist/bin/codegraph.d.ts +1 -1
  2. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  3. package/lib/dist/bin/codegraph.js +232 -61
  4. package/lib/dist/bin/codegraph.js.map +1 -1
  5. package/lib/dist/bin/command-supervision.d.ts.map +1 -1
  6. package/lib/dist/bin/command-supervision.js +4 -1
  7. package/lib/dist/bin/command-supervision.js.map +1 -1
  8. package/lib/dist/db/index.d.ts +17 -5
  9. package/lib/dist/db/index.d.ts.map +1 -1
  10. package/lib/dist/db/index.js +68 -12
  11. package/lib/dist/db/index.js.map +1 -1
  12. package/lib/dist/db/migrations.d.ts +1 -1
  13. package/lib/dist/db/migrations.d.ts.map +1 -1
  14. package/lib/dist/db/migrations.js +19 -1
  15. package/lib/dist/db/migrations.js.map +1 -1
  16. package/lib/dist/db/queries.d.ts +76 -1
  17. package/lib/dist/db/queries.d.ts.map +1 -1
  18. package/lib/dist/db/queries.js +180 -0
  19. package/lib/dist/db/queries.js.map +1 -1
  20. package/lib/dist/db/schema.sql +19 -0
  21. package/lib/dist/directory.d.ts +9 -5
  22. package/lib/dist/directory.d.ts.map +1 -1
  23. package/lib/dist/directory.js +210 -19
  24. package/lib/dist/directory.js.map +1 -1
  25. package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
  26. package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
  27. package/lib/dist/extraction/cfml-extractor.js +494 -0
  28. package/lib/dist/extraction/cfml-extractor.js.map +1 -0
  29. package/lib/dist/extraction/grammars.d.ts +9 -0
  30. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  31. package/lib/dist/extraction/grammars.js +115 -3
  32. package/lib/dist/extraction/grammars.js.map +1 -1
  33. package/lib/dist/extraction/index.d.ts +46 -1
  34. package/lib/dist/extraction/index.d.ts.map +1 -1
  35. package/lib/dist/extraction/index.js +338 -34
  36. package/lib/dist/extraction/index.js.map +1 -1
  37. package/lib/dist/extraction/languages/arkts.d.ts +3 -0
  38. package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
  39. package/lib/dist/extraction/languages/arkts.js +127 -0
  40. package/lib/dist/extraction/languages/arkts.js.map +1 -0
  41. package/lib/dist/extraction/languages/c-cpp.d.ts +42 -0
  42. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  43. package/lib/dist/extraction/languages/c-cpp.js +369 -4
  44. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  45. package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
  46. package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
  47. package/lib/dist/extraction/languages/cfquery.js +28 -0
  48. package/lib/dist/extraction/languages/cfquery.js.map +1 -0
  49. package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
  50. package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
  51. package/lib/dist/extraction/languages/cfscript.js +73 -0
  52. package/lib/dist/extraction/languages/cfscript.js.map +1 -0
  53. package/lib/dist/extraction/languages/cobol.d.ts +33 -0
  54. package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
  55. package/lib/dist/extraction/languages/cobol.js +499 -0
  56. package/lib/dist/extraction/languages/cobol.js.map +1 -0
  57. package/lib/dist/extraction/languages/erlang.d.ts +3 -0
  58. package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
  59. package/lib/dist/extraction/languages/erlang.js +350 -0
  60. package/lib/dist/extraction/languages/erlang.js.map +1 -0
  61. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  62. package/lib/dist/extraction/languages/index.js +18 -0
  63. package/lib/dist/extraction/languages/index.js.map +1 -1
  64. package/lib/dist/extraction/languages/nix.d.ts +3 -0
  65. package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
  66. package/lib/dist/extraction/languages/nix.js +294 -0
  67. package/lib/dist/extraction/languages/nix.js.map +1 -0
  68. package/lib/dist/extraction/languages/solidity.d.ts +3 -0
  69. package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
  70. package/lib/dist/extraction/languages/solidity.js +293 -0
  71. package/lib/dist/extraction/languages/solidity.js.map +1 -0
  72. package/lib/dist/extraction/languages/terraform.d.ts +3 -0
  73. package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
  74. package/lib/dist/extraction/languages/terraform.js +641 -0
  75. package/lib/dist/extraction/languages/terraform.js.map +1 -0
  76. package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
  77. package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
  78. package/lib/dist/extraction/languages/vbnet.js +141 -0
  79. package/lib/dist/extraction/languages/vbnet.js.map +1 -0
  80. package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
  81. package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  82. package/lib/dist/extraction/mybatis-extractor.js +140 -38
  83. package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
  84. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  85. package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
  86. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  87. package/lib/dist/extraction/tree-sitter-types.d.ts +3 -1
  88. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  89. package/lib/dist/extraction/tree-sitter.d.ts +38 -0
  90. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  91. package/lib/dist/extraction/tree-sitter.js +753 -10
  92. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  93. package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  94. package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  95. package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  96. package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  97. package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  98. package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  99. package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  100. package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  101. package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  102. package/lib/dist/index.d.ts +63 -1
  103. package/lib/dist/index.d.ts.map +1 -1
  104. package/lib/dist/index.js +327 -5
  105. package/lib/dist/index.js.map +1 -1
  106. package/lib/dist/installer/index.d.ts.map +1 -1
  107. package/lib/dist/installer/index.js +3 -1
  108. package/lib/dist/installer/index.js.map +1 -1
  109. package/lib/dist/mcp/daemon.d.ts +25 -3
  110. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  111. package/lib/dist/mcp/daemon.js +62 -8
  112. package/lib/dist/mcp/daemon.js.map +1 -1
  113. package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  114. package/lib/dist/mcp/dynamic-boundaries.js +2 -1
  115. package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
  116. package/lib/dist/mcp/early-ppid.d.ts +26 -0
  117. package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
  118. package/lib/dist/mcp/early-ppid.js +29 -0
  119. package/lib/dist/mcp/early-ppid.js.map +1 -0
  120. package/lib/dist/mcp/index.d.ts.map +1 -1
  121. package/lib/dist/mcp/index.js +21 -4
  122. package/lib/dist/mcp/index.js.map +1 -1
  123. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  124. package/lib/dist/mcp/proxy.js +29 -3
  125. package/lib/dist/mcp/proxy.js.map +1 -1
  126. package/lib/dist/mcp/query-pool.d.ts +14 -0
  127. package/lib/dist/mcp/query-pool.d.ts.map +1 -1
  128. package/lib/dist/mcp/query-pool.js +18 -0
  129. package/lib/dist/mcp/query-pool.js.map +1 -1
  130. package/lib/dist/mcp/session.d.ts.map +1 -1
  131. package/lib/dist/mcp/session.js +6 -0
  132. package/lib/dist/mcp/session.js.map +1 -1
  133. package/lib/dist/mcp/startup-handshake.d.ts +44 -0
  134. package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
  135. package/lib/dist/mcp/startup-handshake.js +73 -0
  136. package/lib/dist/mcp/startup-handshake.js.map +1 -0
  137. package/lib/dist/mcp/tools.d.ts +22 -0
  138. package/lib/dist/mcp/tools.d.ts.map +1 -1
  139. package/lib/dist/mcp/tools.js +88 -18
  140. package/lib/dist/mcp/tools.js.map +1 -1
  141. package/lib/dist/mcp/transport.d.ts.map +1 -1
  142. package/lib/dist/mcp/transport.js +18 -0
  143. package/lib/dist/mcp/transport.js.map +1 -1
  144. package/lib/dist/project-config.d.ts +38 -0
  145. package/lib/dist/project-config.d.ts.map +1 -1
  146. package/lib/dist/project-config.js +101 -2
  147. package/lib/dist/project-config.js.map +1 -1
  148. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  149. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  150. package/lib/dist/resolution/c-fnptr-synthesizer.js +170 -146
  151. package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  152. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  153. package/lib/dist/resolution/callback-synthesizer.js +982 -81
  154. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  155. package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
  156. package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
  157. package/lib/dist/resolution/frameworks/cics.js +90 -0
  158. package/lib/dist/resolution/frameworks/cics.js.map +1 -0
  159. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  160. package/lib/dist/resolution/frameworks/index.js +6 -0
  161. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  162. package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
  163. package/lib/dist/resolution/frameworks/java.js +14 -6
  164. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  165. package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
  166. package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  167. package/lib/dist/resolution/frameworks/terraform.js +277 -0
  168. package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
  169. package/lib/dist/resolution/goframe-synthesizer.d.ts +2 -1
  170. package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  171. package/lib/dist/resolution/goframe-synthesizer.js +8 -3
  172. package/lib/dist/resolution/goframe-synthesizer.js.map +1 -1
  173. package/lib/dist/resolution/import-resolver.d.ts +7 -0
  174. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  175. package/lib/dist/resolution/import-resolver.js +129 -4
  176. package/lib/dist/resolution/import-resolver.js.map +1 -1
  177. package/lib/dist/resolution/index.d.ts +49 -6
  178. package/lib/dist/resolution/index.d.ts.map +1 -1
  179. package/lib/dist/resolution/index.js +342 -65
  180. package/lib/dist/resolution/index.js.map +1 -1
  181. package/lib/dist/resolution/name-matcher.d.ts +0 -3
  182. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  183. package/lib/dist/resolution/name-matcher.js +225 -33
  184. package/lib/dist/resolution/name-matcher.js.map +1 -1
  185. package/lib/dist/resolution/strip-comments.d.ts +1 -1
  186. package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
  187. package/lib/dist/resolution/strip-comments.js +49 -0
  188. package/lib/dist/resolution/strip-comments.js.map +1 -1
  189. package/lib/dist/resolution/types.d.ts +29 -0
  190. package/lib/dist/resolution/types.d.ts.map +1 -1
  191. package/lib/dist/resolution/workspace-packages.d.ts +10 -0
  192. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
  193. package/lib/dist/resolution/workspace-packages.js +142 -4
  194. package/lib/dist/resolution/workspace-packages.js.map +1 -1
  195. package/lib/dist/search/identifier-segments.d.ts +60 -0
  196. package/lib/dist/search/identifier-segments.d.ts.map +1 -0
  197. package/lib/dist/search/identifier-segments.js +176 -0
  198. package/lib/dist/search/identifier-segments.js.map +1 -0
  199. package/lib/dist/sync/git-hooks.d.ts.map +1 -1
  200. package/lib/dist/sync/git-hooks.js +2 -0
  201. package/lib/dist/sync/git-hooks.js.map +1 -1
  202. package/lib/dist/sync/watcher.d.ts +10 -5
  203. package/lib/dist/sync/watcher.d.ts.map +1 -1
  204. package/lib/dist/sync/watcher.js +51 -14
  205. package/lib/dist/sync/watcher.js.map +1 -1
  206. package/lib/dist/sync/worktree.d.ts.map +1 -1
  207. package/lib/dist/sync/worktree.js +5 -0
  208. package/lib/dist/sync/worktree.js.map +1 -1
  209. package/lib/dist/types.d.ts +19 -1
  210. package/lib/dist/types.d.ts.map +1 -1
  211. package/lib/dist/types.js +10 -0
  212. package/lib/dist/types.js.map +1 -1
  213. package/lib/node_modules/.package-lock.json +1 -1
  214. package/lib/package.json +1 -1
  215. package/package.json +1 -1
  216. package/lib/dist/reasoning/config.d.ts +0 -45
  217. package/lib/dist/reasoning/config.d.ts.map +0 -1
  218. package/lib/dist/reasoning/config.js +0 -171
  219. package/lib/dist/reasoning/config.js.map +0 -1
  220. package/lib/dist/reasoning/credentials.d.ts +0 -5
  221. package/lib/dist/reasoning/credentials.d.ts.map +0 -1
  222. package/lib/dist/reasoning/credentials.js +0 -83
  223. package/lib/dist/reasoning/credentials.js.map +0 -1
  224. package/lib/dist/reasoning/login.d.ts +0 -21
  225. package/lib/dist/reasoning/login.d.ts.map +0 -1
  226. package/lib/dist/reasoning/login.js +0 -85
  227. package/lib/dist/reasoning/login.js.map +0 -1
  228. package/lib/dist/reasoning/reasoner.d.ts +0 -43
  229. package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
  230. package/lib/dist/reasoning/reasoner.js +0 -308
  231. package/lib/dist/reasoning/reasoner.js.map +0 -1
@@ -276,10 +276,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
276
276
  }
277
277
  /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
278
278
  async function eventEmitterEdges(ctx, onYield) {
279
+ let scannedFiles = 0;
279
280
  const emitsByEvent = new Map(); // event → dispatcher node ids
280
281
  const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
281
282
  let scanned = 0;
282
283
  for (const file of ctx.getAllFiles()) {
284
+ if ((++scannedFiles & 15) === 0)
285
+ await onYield();
283
286
  if ((++scanned & 255) === 0)
284
287
  await onYield(); // #1091: yield mid-scan on huge graphs
285
288
  const content = ctx.readFile(file);
@@ -353,10 +356,13 @@ async function eventEmitterEdges(ctx, onYield) {
353
356
  * `this.setState`). Over-approximation (all setState methods reach render) is
354
357
  * accepted — it's reachability-correct, like the callback channels.
355
358
  */
356
- function reactRenderEdges(queries, ctx) {
359
+ async function reactRenderEdges(queries, ctx, onYield) {
360
+ let scanned255 = 0;
357
361
  const edges = [];
358
362
  const seen = new Set();
359
- for (const cls of queries.getNodesByKind('class')) {
363
+ for (const cls of queries.iterateNodesByKind('class')) {
364
+ if ((++scanned255 & 63) === 0)
365
+ await onYield();
360
366
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
361
367
  .map((e) => queries.getNodeById(e.target))
362
368
  .filter((n) => !!n && n.kind === 'method');
@@ -396,10 +402,13 @@ function reactRenderEdges(queries, ctx) {
396
402
  * body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
397
403
  * Flutter State classes. Over-approximation accepted (reachability-correct).
398
404
  */
399
- function flutterBuildEdges(queries, ctx) {
405
+ async function flutterBuildEdges(queries, ctx, onYield) {
406
+ let scanned255 = 0;
400
407
  const edges = [];
401
408
  const seen = new Set();
402
- for (const cls of queries.getNodesByKind('class')) {
409
+ for (const cls of queries.iterateNodesByKind('class')) {
410
+ if ((++scanned255 & 63) === 0)
411
+ await onYield();
403
412
  const children = queries.getOutgoingEdges(cls.id, ['contains'])
404
413
  .map((e) => queries.getNodeById(e.target))
405
414
  .filter((n) => !!n && n.kind === 'method');
@@ -430,6 +439,279 @@ function flutterBuildEdges(queries, ctx) {
430
439
  }
431
440
  return edges;
432
441
  }
442
+ /**
443
+ * Reactive ArkUI property decorators: assigning a property carrying one of
444
+ * these re-runs the owning struct's `build()`. Covers both state models —
445
+ * V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
446
+ * (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
447
+ * the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
448
+ * data classes, not struct properties).
449
+ */
450
+ const ARKUI_REACTIVE_DECORATORS = new Set([
451
+ 'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
452
+ 'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
453
+ 'Local', 'Provider', 'Consumer',
454
+ ]);
455
+ /** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
456
+ const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
457
+ /**
458
+ * Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
459
+ * flutter-build). Assigning a reactive-decorated property (`@State count`,
460
+ * `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
461
+ * hop is framework-internal — no static edge — so "onClick → markAllDone →
462
+ * this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
463
+ * for each arkts struct with a `build()` method and at least one reactive
464
+ * property, link every sibling method whose body ASSIGNS (or array-mutates)
465
+ * one of those properties → `build`. Assignment-gated on the struct's OWN
466
+ * reactive property names — a method that merely reads state, or a struct
467
+ * with no reactive properties, gets nothing (this is the precision line the
468
+ * all-sibling-methods design would erase).
469
+ */
470
+ async function arkuiStateBuildEdges(queries, ctx, onYield) {
471
+ let scanned255 = 0;
472
+ const edges = [];
473
+ const seen = new Set();
474
+ for (const struct of queries.iterateNodesByKind('struct')) {
475
+ if ((++scanned255 & 63) === 0)
476
+ await onYield();
477
+ if (struct.language !== 'arkts')
478
+ continue;
479
+ const children = queries.getOutgoingEdges(struct.id, ['contains'])
480
+ .map((e) => queries.getNodeById(e.target))
481
+ .filter((n) => !!n);
482
+ const build = children.find((n) => n.kind === 'method' && n.name === 'build');
483
+ if (!build)
484
+ continue;
485
+ const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
486
+ if (reactiveProps.length === 0)
487
+ continue;
488
+ const propAlternation = reactiveProps
489
+ .map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
490
+ .join('|');
491
+ // `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
492
+ // `=(?!=)` keeps `this.done == x` comparisons out.
493
+ const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
494
+ let added = 0;
495
+ for (const m of children) {
496
+ if (added >= MAX_CALLBACKS_PER_CHANNEL)
497
+ break;
498
+ if (m.kind !== 'method' || m.id === build.id)
499
+ continue;
500
+ const content = ctx.readFile(m.filePath);
501
+ const src = content && sliceLines(content, m.startLine, m.endLine);
502
+ if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
503
+ continue;
504
+ const key = `${m.id}>${build.id}`;
505
+ if (seen.has(key))
506
+ continue;
507
+ seen.add(key);
508
+ edges.push({
509
+ source: m.id, target: build.id, kind: 'calls', line: m.startLine,
510
+ provenance: 'heuristic',
511
+ metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
512
+ });
513
+ added++;
514
+ }
515
+ }
516
+ return edges;
517
+ }
518
+ /** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
519
+ const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
520
+ /** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
521
+ const ARKUI_EMITTER_FANOUT_CAP = 8;
522
+ /**
523
+ * Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
524
+ * bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
525
+ * framework-internal, so an order flow riding it (OrangeShopping's
526
+ * add-to-cart) dead-ends at the emit. Link emit-site enclosing
527
+ * function/method → on/once-site enclosing function/method when both
528
+ * reference the SAME statically-recoverable event key.
529
+ *
530
+ * Key recovery, per call site (comment-stripped enclosing-file source): the
531
+ * first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
532
+ * a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
533
+ * level. Precision scoping learned from the samples monorepo (thousands of
534
+ * unrelated samples, most using eventId 1): NUMERIC keys pair within the same
535
+ * FILE only; NAMED keys pair within the same workspace module directory (or
536
+ * the whole project when it declares no modules — the single-app case), both
537
+ * behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
538
+ * handling — their bodies' calls already attribute to the registering method,
539
+ * so targeting that method keeps the chain connected.
540
+ */
541
+ async function arkuiEmitterEdges(ctx, onYield) {
542
+ let scannedFiles = 0;
543
+ // bucket key -> emit sites / handler sites
544
+ const emits = new Map();
545
+ const handlers = new Map();
546
+ const moduleDirs = (() => {
547
+ const ws = ctx.getWorkspacePackages?.();
548
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
549
+ })();
550
+ const moduleScopeOf = (file) => {
551
+ for (const dir of moduleDirs) {
552
+ if (file === dir || file.startsWith(dir + '/'))
553
+ return dir;
554
+ }
555
+ return '';
556
+ };
557
+ for (const file of ctx.getAllFiles()) {
558
+ if ((++scannedFiles & 15) === 0)
559
+ await onYield();
560
+ if (!file.endsWith('.ets'))
561
+ continue;
562
+ const content = ctx.readFile(file);
563
+ if (!content || !content.includes('emitter.'))
564
+ continue;
565
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
566
+ const nodes = ctx.getNodesInFile(file)
567
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
568
+ ARKUI_EMITTER_CALL_RE.lastIndex = 0;
569
+ let m;
570
+ while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
571
+ const verb = m[1];
572
+ const arg = m[2].trim();
573
+ const line = safe.slice(0, m.index).split('\n').length;
574
+ const encl = nodes
575
+ .filter((n) => n.startLine <= line && n.endLine >= line)
576
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
577
+ if (!encl)
578
+ continue;
579
+ // Recover the event key from the first argument.
580
+ let key = null;
581
+ const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
582
+ const token = idLit ?? arg;
583
+ if (token !== undefined) {
584
+ if (/^\d+$/.test(token)) {
585
+ key = `num:${file}:${token}`; // numeric: same-file only
586
+ }
587
+ else if (token.includes('.')) {
588
+ key = `name:${moduleScopeOf(file)}:${token}`;
589
+ }
590
+ else {
591
+ // Local variable — chase its same-file declaration one level:
592
+ // `let x = new EventsId(K)` / `const x = K`.
593
+ const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
594
+ const decl = safe.match(declRe);
595
+ const inner = (decl?.[1] ?? decl?.[2])?.trim();
596
+ if (inner && /^\d+$/.test(inner))
597
+ key = `num:${file}:${inner}`;
598
+ else if (inner && /^[\w$.]+$/.test(inner))
599
+ key = `name:${moduleScopeOf(file)}:${inner}`;
600
+ }
601
+ }
602
+ if (!key)
603
+ continue;
604
+ const site = { nodeId: encl.id, file, line };
605
+ if (verb === 'emit') {
606
+ (emits.get(key) ?? emits.set(key, []).get(key)).push(site);
607
+ }
608
+ else {
609
+ (handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
610
+ }
611
+ }
612
+ }
613
+ const edges = [];
614
+ const seen = new Set();
615
+ for (const [key, emitSites] of emits) {
616
+ const handlerSites = handlers.get(key);
617
+ if (!handlerSites)
618
+ continue;
619
+ if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
620
+ continue;
621
+ const eventLabel = key.slice(key.lastIndexOf(':') + 1);
622
+ for (const e of emitSites)
623
+ for (const h of handlerSites) {
624
+ if (e.nodeId === h.nodeId)
625
+ continue;
626
+ const dedupe = `${e.nodeId}>${h.nodeId}`;
627
+ if (seen.has(dedupe))
628
+ continue;
629
+ seen.add(dedupe);
630
+ edges.push({
631
+ source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
632
+ provenance: 'heuristic',
633
+ metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
634
+ });
635
+ }
636
+ }
637
+ return edges;
638
+ }
639
+ /** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
640
+ const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
641
+ /**
642
+ * Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
643
+ * 'pages/Detail' })` reaches the `@Entry struct` of
644
+ * `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
645
+ * static edge — so "tap → openDetail → ???" ends at the router call. Bridge
646
+ * literal urls to the page struct: the url resolves against the standard
647
+ * `src/main/ets/` layout (what main_pages.json entries name); candidates
648
+ * prefer the caller's own workspace module (routes are module-scoped), and
649
+ * anything still ambiguous is dropped rather than guessed. Only `@Entry`
650
+ * structs qualify as targets — the decorator is what makes a file a page.
651
+ */
652
+ async function arkuiRouterEdges(ctx, onYield) {
653
+ const edges = [];
654
+ const seen = new Set();
655
+ const allFiles = ctx.getAllFiles();
656
+ const moduleDirs = (() => {
657
+ const ws = ctx.getWorkspacePackages?.();
658
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
659
+ })();
660
+ const moduleScopeOf = (file) => {
661
+ for (const dir of moduleDirs) {
662
+ if (file === dir || file.startsWith(dir + '/'))
663
+ return dir;
664
+ }
665
+ return '';
666
+ };
667
+ let scannedFiles = 0;
668
+ for (const file of allFiles) {
669
+ if ((++scannedFiles & 15) === 0)
670
+ await onYield();
671
+ if (!file.endsWith('.ets'))
672
+ continue;
673
+ const content = ctx.readFile(file);
674
+ if (!content || !content.includes('router.'))
675
+ continue;
676
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
677
+ const nodes = ctx.getNodesInFile(file)
678
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
679
+ ARKUI_ROUTER_RE.lastIndex = 0;
680
+ let m;
681
+ while ((m = ARKUI_ROUTER_RE.exec(safe))) {
682
+ const url = m[1];
683
+ const line = safe.slice(0, m.index).split('\n').length;
684
+ const encl = nodes
685
+ .filter((n) => n.startLine <= line && n.endLine >= line)
686
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
687
+ if (!encl)
688
+ continue;
689
+ const suffix = `/src/main/ets/${url}.ets`;
690
+ let candidates = allFiles.filter((f) => f.endsWith(suffix));
691
+ if (candidates.length > 1) {
692
+ const scope = moduleScopeOf(file);
693
+ const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
694
+ if (sameModule.length > 0)
695
+ candidates = sameModule;
696
+ }
697
+ if (candidates.length !== 1)
698
+ continue; // ambiguous or unresolved — never guess
699
+ const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
700
+ if (!page)
701
+ continue;
702
+ const key = `${encl.id}>${page.id}`;
703
+ if (seen.has(key))
704
+ continue;
705
+ seen.add(key);
706
+ edges.push({
707
+ source: encl.id, target: page.id, kind: 'calls', line,
708
+ provenance: 'heuristic',
709
+ metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
710
+ });
711
+ }
712
+ }
713
+ return edges;
714
+ }
433
715
  /**
434
716
  * Phase 4c: C++ virtual override. A call through a base/interface pointer
435
717
  * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
@@ -440,14 +722,17 @@ function flutterBuildEdges(queries, ctx) {
440
722
  * implementation(s). Over-approximation accepted (reachability-correct); capped
441
723
  * per class and gated to C++ to avoid touching other languages' dispatch.
442
724
  */
443
- function cppOverrideEdges(queries) {
725
+ async function cppOverrideEdges(queries, onYield) {
726
+ let scanned255 = 0;
444
727
  const edges = [];
445
728
  const seen = new Set();
446
729
  const methodsOf = (classId) => queries
447
730
  .getOutgoingEdges(classId, ['contains'])
448
731
  .map((e) => queries.getNodeById(e.target))
449
732
  .filter((n) => !!n && n.kind === 'method');
450
- for (const cls of queries.getNodesByKind('class')) {
733
+ for (const cls of queries.iterateNodesByKind('class')) {
734
+ if ((++scanned255 & 63) === 0)
735
+ await onYield();
451
736
  const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
452
737
  if (subMethods.length === 0)
453
738
  continue;
@@ -501,6 +786,7 @@ function cppOverrideEdges(queries) {
501
786
  // or an `object` (Scala) so the loop also iterates those kinds.
502
787
  const IFACE_OVERRIDE_LANGS = new Set([
503
788
  'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
789
+ 'arkts',
504
790
  ]);
505
791
  /**
506
792
  * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
@@ -514,7 +800,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
514
800
  * with the other dispatch synthesizers; capped per interface. Empty interfaces
515
801
  * (`any`) are skipped so they don't match every struct.
516
802
  */
517
- function goImplementsEdges(queries) {
803
+ async function goImplementsEdges(queries, onYield) {
804
+ let scanned255 = 0;
518
805
  const edges = [];
519
806
  const seen = new Set();
520
807
  const methodNameSet = (id) => new Set(queries
@@ -522,11 +809,24 @@ function goImplementsEdges(queries) {
522
809
  .map((e) => queries.getNodeById(e.target))
523
810
  .filter((n) => !!n && n.kind === 'method')
524
811
  .map((n) => n.name));
525
- const goStructs = queries.getNodesByKind('struct').filter((s) => s.language === 'go');
812
+ // Materializes GO structs only (the pass is language-gated by the caller),
813
+ // never the whole struct kind — that array is O(nodes) on struct-heavy
814
+ // repos like the Linux kernel (#1212).
815
+ const goStructs = [];
816
+ for (const s of queries.iterateNodesByKind('struct')) {
817
+ if ((++scanned255 & 63) === 0)
818
+ await onYield();
819
+ if (s.language === 'go')
820
+ goStructs.push(s);
821
+ }
526
822
  const structMethods = new Map();
527
823
  for (const s of goStructs)
528
824
  structMethods.set(s.id, methodNameSet(s.id));
529
- for (const iface of queries.getNodesByKind('interface')) {
825
+ for (const iface of queries.iterateNodesByKind('interface')) {
826
+ if ((++scanned255 & 63) === 0)
827
+ await onYield();
828
+ if ((++scanned255 & 63) === 0)
829
+ await onYield();
530
830
  if (iface.language !== 'go')
531
831
  continue;
532
832
  const want = methodNameSet(iface.id);
@@ -586,7 +886,8 @@ function goImplementsEdges(queries) {
586
886
  * matching the same-file edges extraction already emits). Skips methods that
587
887
  * already have a type parent (the same-file case). (#583, cross-file half)
588
888
  */
589
- function goCrossFileMethodContainsEdges(queries) {
889
+ async function goCrossFileMethodContainsEdges(queries, onYield) {
890
+ let scanned255 = 0;
590
891
  const edges = [];
591
892
  const seen = new Set();
592
893
  const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
@@ -594,7 +895,11 @@ function goCrossFileMethodContainsEdges(queries) {
594
895
  const i = p.replace(/\\/g, '/').lastIndexOf('/');
595
896
  return i >= 0 ? p.slice(0, i) : '';
596
897
  };
597
- for (const method of queries.getNodesByKind('method')) {
898
+ for (const method of queries.iterateNodesByKind('method')) {
899
+ if ((++scanned255 & 63) === 0)
900
+ await onYield();
901
+ if ((++scanned255 & 63) === 0)
902
+ await onYield();
598
903
  if (method.language !== 'go')
599
904
  continue;
600
905
  // The receiver type is encoded in the method's qualifiedName as `Recv::name`
@@ -664,13 +969,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
664
969
  function kmpKindsCompatible(a, b) {
665
970
  return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
666
971
  }
667
- function kotlinExpectActualEdges(queries) {
972
+ async function kotlinExpectActualEdges(queries, onYield) {
973
+ let scanned255 = 0;
668
974
  const edges = [];
669
975
  const seen = new Set();
670
- const actuals = queries
671
- .getAllNodes()
672
- .filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
673
- for (const act of actuals) {
976
+ // SQL-side language+decorator pre-filter, streamed. The old
977
+ // `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
978
+ // just to find kotlin `actual` declarations — on a 2M-node graph that alone
979
+ // exceeded Node's default heap and killed the index (#1212). The LIKE
980
+ // pre-filter can over-match (substring), so the exact decorator check stays.
981
+ for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
982
+ if ((++scanned255 & 63) === 0)
983
+ await onYield();
984
+ if (!act.decorators?.includes('actual'))
985
+ continue;
674
986
  let added = 0;
675
987
  for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
676
988
  if (added >= MAX_CALLBACKS_PER_CHANNEL)
@@ -704,7 +1016,8 @@ function kotlinExpectActualEdges(queries) {
704
1016
  }
705
1017
  return edges;
706
1018
  }
707
- function interfaceOverrideEdges(queries) {
1019
+ async function interfaceOverrideEdges(queries, onYield) {
1020
+ let scanned255 = 0;
708
1021
  const edges = [];
709
1022
  const seen = new Set();
710
1023
  const methodsOf = (classId) => queries
@@ -716,7 +1029,9 @@ function interfaceOverrideEdges(queries) {
716
1029
  // types that conform to protocols. Iterate both.
717
1030
  const concreteKinds = ['class', 'struct'];
718
1031
  for (const kind of concreteKinds) {
719
- for (const cls of queries.getNodesByKind(kind)) {
1032
+ for (const cls of queries.iterateNodesByKind(kind)) {
1033
+ if ((++scanned255 & 63) === 0)
1034
+ await onYield();
720
1035
  const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
721
1036
  if (implMethods.length === 0)
722
1037
  continue;
@@ -790,7 +1105,8 @@ function interfaceOverrideEdges(queries) {
790
1105
  * Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
791
1106
  * stub's source line is the wiring site shown in the trace trail.
792
1107
  */
793
- function goGrpcStubImplEdges(queries) {
1108
+ async function goGrpcStubImplEdges(queries, onYield) {
1109
+ let scanned255 = 0;
794
1110
  const edges = [];
795
1111
  const seen = new Set();
796
1112
  const STUB_RE = /^Unimplemented.*Server$/;
@@ -802,7 +1118,9 @@ function goGrpcStubImplEdges(queries) {
802
1118
  const methodNamesByStruct = new Map();
803
1119
  const methodNodesByStruct = new Map();
804
1120
  const goStructs = [];
805
- for (const s of queries.getNodesByKind('struct')) {
1121
+ for (const s of queries.iterateNodesByKind('struct')) {
1122
+ if ((++scanned255 & 63) === 0)
1123
+ await onYield();
806
1124
  if (s.language !== 'go')
807
1125
  continue;
808
1126
  goStructs.push(s);
@@ -886,11 +1204,14 @@ function goGrpcStubImplEdges(queries) {
886
1204
  * (or nothing) and are dropped.
887
1205
  */
888
1206
  async function reactJsxChildEdges(ctx, onYield) {
1207
+ let scannedFiles = 0;
889
1208
  const edges = [];
890
1209
  const seen = new Set();
891
1210
  const PARENT_KINDS = new Set(['method', 'function', 'component']);
892
1211
  let scanned = 0;
893
1212
  for (const file of ctx.getAllFiles()) {
1213
+ if ((++scannedFiles & 15) === 0)
1214
+ await onYield();
894
1215
  if ((++scanned & 255) === 0)
895
1216
  await onYield(); // #1091: yield mid-scan on huge graphs
896
1217
  const content = ctx.readFile(file);
@@ -940,7 +1261,9 @@ async function reactJsxChildEdges(ctx, onYield) {
940
1261
  * component, handler→function/method) keeps precision; inline arrows / `$emit`
941
1262
  * skipped.
942
1263
  */
943
- function vueTemplateEdges(ctx) {
1264
+ async function vueTemplateEdges(ctx, onYield) {
1265
+ let scannedFiles = 0;
1266
+ let scanned255 = 0;
944
1267
  const edges = [];
945
1268
  const seen = new Set();
946
1269
  const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
@@ -954,12 +1277,16 @@ function vueTemplateEdges(ctx) {
954
1277
  // misses it (flat components match by basename and don't need this). Map each
955
1278
  // nested component's Nuxt name → node so those template usages resolve.
956
1279
  const nuxtComponents = new Map();
957
- for (const c of ctx.getNodesByKind('component')) {
1280
+ for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1281
+ if ((++scanned255 & 63) === 0)
1282
+ await onYield();
958
1283
  const nn = nuxtComponentName(c.filePath);
959
1284
  if (nn && !nuxtComponents.has(nn))
960
1285
  nuxtComponents.set(nn, c);
961
1286
  }
962
1287
  for (const file of ctx.getAllFiles()) {
1288
+ if ((++scannedFiles & 15) === 0)
1289
+ await onYield();
963
1290
  if (!file.endsWith('.vue'))
964
1291
  continue;
965
1292
  const content = ctx.readFile(file);
@@ -1093,13 +1420,16 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
1093
1420
  // statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
1094
1421
  // is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
1095
1422
  const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
1096
- function rnEventEdges(ctx) {
1423
+ async function rnEventEdges(ctx, onYield) {
1424
+ let scannedFiles = 0;
1097
1425
  // Native dispatchers (source = the native method whose body sends the
1098
1426
  // event) and JS handlers (target = the function/method registered as
1099
1427
  // the listener) keyed by event name.
1100
1428
  const nativeDispatchersByEvent = new Map();
1101
1429
  const jsHandlersByEvent = new Map();
1102
1430
  for (const file of ctx.getAllFiles()) {
1431
+ if ((++scannedFiles & 15) === 0)
1432
+ await onYield();
1103
1433
  const content = ctx.readFile(file);
1104
1434
  if (!content)
1105
1435
  continue;
@@ -1287,11 +1617,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
1287
1617
  * caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
1288
1618
  * `<file>::<module>.<method>` by the framework extractor.
1289
1619
  */
1290
- function expoCrossPlatformEdges(queries) {
1620
+ async function expoCrossPlatformEdges(queries, onYield) {
1621
+ let scanned255 = 0;
1291
1622
  const edges = [];
1292
1623
  const seen = new Set();
1293
1624
  const byKey = new Map();
1294
- for (const m of queries.getNodesByKind('method')) {
1625
+ for (const m of queries.iterateNodesByKind('method')) {
1626
+ if ((++scanned255 & 63) === 0)
1627
+ await onYield();
1295
1628
  if (!m.id.startsWith('expo-module:'))
1296
1629
  continue;
1297
1630
  const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
@@ -1340,7 +1673,8 @@ function expoCrossPlatformEdges(queries) {
1340
1673
  * `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
1341
1674
  * lines up with the bare Android method name.
1342
1675
  */
1343
- function rnCrossPlatformEdges(queries) {
1676
+ async function rnCrossPlatformEdges(queries, onYield) {
1677
+ let scanned255 = 0;
1344
1678
  const edges = [];
1345
1679
  const seen = new Set();
1346
1680
  const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
@@ -1362,6 +1696,8 @@ function rnCrossPlatformEdges(queries) {
1362
1696
  // below only runs for genuine cross-platform candidates.
1363
1697
  const byName = new Map();
1364
1698
  for (const m of queries.iterateNodesByKind('method')) {
1699
+ if ((++scanned255 & 63) === 0)
1700
+ await onYield();
1365
1701
  if (!NATIVE.has(m.language))
1366
1702
  continue;
1367
1703
  const key = norm(m.name);
@@ -1412,17 +1748,27 @@ function rnCrossPlatformEdges(queries) {
1412
1748
  }
1413
1749
  return edges;
1414
1750
  }
1415
- function fabricNativeImplEdges(ctx) {
1751
+ async function fabricNativeImplEdges(ctx, onYield) {
1752
+ let scanned255 = 0;
1416
1753
  const edges = [];
1417
1754
  const seen = new Set();
1418
1755
  // The Fabric extractor IDs are prefixed `fabric-component:` so we can
1419
- // filter to just those without iterating all `component` nodes.
1420
- const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
1756
+ // filter to just those while streaming never materializing the whole
1757
+ // `component` kind (#1212).
1758
+ const components = [];
1759
+ for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
1760
+ if ((++scanned255 & 63) === 0)
1761
+ await onYield();
1762
+ if (n.id.startsWith('fabric-component:'))
1763
+ components.push(n);
1764
+ }
1421
1765
  if (components.length === 0)
1422
1766
  return edges;
1423
1767
  // Pre-index native classes by name for O(1) lookup.
1424
1768
  const nativeClassesByName = new Map();
1425
- for (const n of ctx.getNodesByKind('class')) {
1769
+ for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
1770
+ if ((++scanned255 & 63) === 0)
1771
+ await onYield();
1426
1772
  if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
1427
1773
  continue;
1428
1774
  const arr = nativeClassesByName.get(n.name);
@@ -1475,12 +1821,15 @@ function fabricNativeImplEdges(ctx) {
1475
1821
  * same simple name) are dropped. We need-not bridge by package because Java
1476
1822
  * mapper interfaces are typically uniquely named within a project.
1477
1823
  */
1478
- function mybatisJavaXmlEdges(queries) {
1824
+ async function mybatisJavaXmlEdges(queries, onYield) {
1825
+ let scanned255 = 0;
1479
1826
  const edges = [];
1480
1827
  const seen = new Set();
1481
1828
  // Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
1482
1829
  const javaIndex = new Map();
1483
1830
  for (const m of queries.iterateNodesByKind('method')) {
1831
+ if ((++scanned255 & 63) === 0)
1832
+ await onYield();
1484
1833
  if (m.language !== 'java' && m.language !== 'kotlin')
1485
1834
  continue;
1486
1835
  const parts = m.qualifiedName.split('::');
@@ -1496,6 +1845,8 @@ function mybatisJavaXmlEdges(queries) {
1496
1845
  javaIndex.set(key, [m]);
1497
1846
  }
1498
1847
  for (const xml of queries.iterateNodesByKind('method')) {
1848
+ if ((++scanned255 & 63) === 0)
1849
+ await onYield();
1499
1850
  if (xml.language !== 'xml')
1500
1851
  continue;
1501
1852
  // Qualified name: `<namespace>::<id>`. Extract the simple class name.
@@ -1609,10 +1960,14 @@ function goHandlerIdent(expr) {
1609
1960
  const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
1610
1961
  return m ? m[1] : null;
1611
1962
  }
1612
- function ginMiddlewareChainEdges(queries, ctx) {
1963
+ async function ginMiddlewareChainEdges(queries, ctx, onYield) {
1964
+ let scanned255 = 0;
1965
+ let scannedFiles = 0;
1613
1966
  // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
1614
1967
  const dispatchers = [];
1615
1968
  for (const n of queries.iterateNodesByKind('method')) {
1969
+ if ((++scanned255 & 63) === 0)
1970
+ await onYield();
1616
1971
  if (n.language !== 'go')
1617
1972
  continue;
1618
1973
  const content = ctx.readFile(n.filePath);
@@ -1627,6 +1982,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
1627
1982
  // closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
1628
1983
  const registered = new Map(); // name → registeredAt (file:line)
1629
1984
  for (const file of ctx.getAllFiles()) {
1985
+ if ((++scannedFiles & 15) === 0)
1986
+ await onYield();
1630
1987
  if (!file.endsWith('.go'))
1631
1988
  continue;
1632
1989
  const content = ctx.readFile(file);
@@ -1682,10 +2039,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
1682
2039
  * clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
1683
2040
  * definition isn't orphaned, and editing the form surfaces its code-behind unit.
1684
2041
  */
1685
- function pascalFormEdges(ctx) {
2042
+ async function pascalFormEdges(ctx, onYield) {
2043
+ let scannedFiles = 0;
1686
2044
  const edges = [];
1687
2045
  const allFiles = new Set(ctx.getAllFiles());
1688
2046
  for (const file of allFiles) {
2047
+ if ((++scannedFiles & 255) === 0)
2048
+ await onYield();
1689
2049
  if (!/\.(dfm|fmx)$/i.test(file))
1690
2050
  continue;
1691
2051
  const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
@@ -1721,12 +2081,15 @@ function pascalFormEdges(ctx) {
1721
2081
  * a loader's data shows the page it feeds) and the page's dependencies include
1722
2082
  * its loader.
1723
2083
  */
1724
- function svelteKitLoadEdges(ctx) {
2084
+ async function svelteKitLoadEdges(ctx, onYield) {
2085
+ let scannedFiles = 0;
1725
2086
  const edges = [];
1726
2087
  const allFiles = new Set(ctx.getAllFiles());
1727
2088
  const HOOKS = new Set(['load', 'actions']);
1728
2089
  const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
1729
2090
  for (const file of allFiles) {
2091
+ if ((++scannedFiles & 255) === 0)
2092
+ await onYield();
1730
2093
  const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
1731
2094
  if (!m)
1732
2095
  continue;
@@ -1775,10 +2138,13 @@ function svelteKitLoadEdges(ctx) {
1775
2138
  const THUNK_DECL_RE = /create(?:Async)?Thunk/;
1776
2139
  const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
1777
2140
  const THUNK_FANOUT_CAP = 24;
1778
- function reduxThunkEdges(queries, ctx) {
2141
+ async function reduxThunkEdges(queries, ctx, onYield) {
2142
+ let scanned255 = 0;
1779
2143
  const edges = [];
1780
2144
  const seen = new Set();
1781
2145
  for (const node of queries.iterateNodesByKind('constant')) {
2146
+ if ((++scanned255 & 63) === 0)
2147
+ await onYield();
1782
2148
  // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
1783
2149
  // avoids reading every constant's body on a large repo.
1784
2150
  if (!node.signature || !THUNK_DECL_RE.test(node.signature))
@@ -1910,10 +2276,13 @@ function resolveRegistryHandler(ctx, name, chained) {
1910
2276
  return cands.find((n) => n.kind === 'method') ?? null;
1911
2277
  }
1912
2278
  async function objectRegistryEdges(ctx, onYield) {
2279
+ let scannedFiles = 0;
1913
2280
  const edges = [];
1914
2281
  const seen = new Set();
1915
2282
  let scanned = 0;
1916
2283
  for (const file of ctx.getAllFiles()) {
2284
+ if ((++scannedFiles & 15) === 0)
2285
+ await onYield();
1917
2286
  if ((++scanned & 255) === 0)
1918
2287
  await onYield(); // #1091: yield mid-scan on huge graphs
1919
2288
  if (!REGISTRY_JS_EXT.test(file))
@@ -2025,10 +2394,13 @@ function rtkEndpointNameFromHook(hook) {
2025
2394
  return null;
2026
2395
  return mid.charAt(0).toLowerCase() + mid.slice(1);
2027
2396
  }
2028
- function rtkQueryEdges(queries, ctx) {
2397
+ async function rtkQueryEdges(queries, ctx, onYield) {
2398
+ let scanned255 = 0;
2029
2399
  const edges = [];
2030
2400
  const seen = new Set();
2031
2401
  for (const hook of queries.iterateNodesByKind('function')) {
2402
+ if ((++scanned255 & 63) === 0)
2403
+ await onYield();
2032
2404
  // Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
2033
2405
  if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
2034
2406
  continue;
@@ -2072,10 +2444,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
2072
2444
  const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
2073
2445
  const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
2074
2446
  const PINIA_FANOUT_CAP = 80;
2075
- function piniaStoreEdges(ctx) {
2447
+ async function piniaStoreEdges(ctx, onYield) {
2448
+ let scannedFiles = 0;
2076
2449
  // 1. Map each `const useXStore = defineStore(...)` factory → its store file.
2077
2450
  const factoryFile = new Map();
2078
2451
  for (const file of ctx.getAllFiles()) {
2452
+ if ((++scannedFiles & 15) === 0)
2453
+ await onYield();
2079
2454
  if (!PINIA_CONSUMER_EXT.test(file))
2080
2455
  continue;
2081
2456
  const content = ctx.readFile(file);
@@ -2091,6 +2466,8 @@ function piniaStoreEdges(ctx) {
2091
2466
  const edges = [];
2092
2467
  const seen = new Set();
2093
2468
  for (const file of ctx.getAllFiles()) {
2469
+ if ((++scannedFiles & 15) === 0)
2470
+ await onYield();
2094
2471
  if (!PINIA_CONSUMER_EXT.test(file))
2095
2472
  continue;
2096
2473
  const content = ctx.readFile(file);
@@ -2163,7 +2540,8 @@ const VUEX_FANOUT_CAP = 120;
2163
2540
  function pathHasSegment(filePath, seg) {
2164
2541
  return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
2165
2542
  }
2166
- function vuexDispatchEdges(ctx) {
2543
+ async function vuexDispatchEdges(ctx, onYield) {
2544
+ let scannedFiles = 0;
2167
2545
  const storeFileCache = new Map();
2168
2546
  const isStoreFile = (file) => {
2169
2547
  let v = storeFileCache.get(file);
@@ -2201,6 +2579,8 @@ function vuexDispatchEdges(ctx) {
2201
2579
  const edges = [];
2202
2580
  const seen = new Set();
2203
2581
  for (const file of ctx.getAllFiles()) {
2582
+ if ((++scannedFiles & 15) === 0)
2583
+ await onYield();
2204
2584
  if (!PINIA_CONSUMER_EXT.test(file))
2205
2585
  continue;
2206
2586
  const content = ctx.readFile(file);
@@ -2260,7 +2640,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
2260
2640
  const CELERY_PY_EXT = /\.py$/;
2261
2641
  const CELERY_FANOUT_CAP = 80;
2262
2642
  const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
2263
- function celeryDispatchEdges(ctx) {
2643
+ async function celeryDispatchEdges(ctx, onYield) {
2644
+ let scannedFiles = 0;
2264
2645
  // Memoize the decorator check per task-candidate node: it reads the file and scans a few
2265
2646
  // lines above the def. Only called on names that are actually `.delay`/`.apply_async`
2266
2647
  // receivers, so the candidate set stays small.
@@ -2304,6 +2685,8 @@ function celeryDispatchEdges(ctx) {
2304
2685
  const edges = [];
2305
2686
  const seen = new Set();
2306
2687
  for (const file of ctx.getAllFiles()) {
2688
+ if ((++scannedFiles & 15) === 0)
2689
+ await onYield();
2307
2690
  if (!CELERY_PY_EXT.test(file))
2308
2691
  continue;
2309
2692
  const content = ctx.readFile(file);
@@ -2382,15 +2765,24 @@ function springFirstParamType(sig) {
2382
2765
  const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
2383
2766
  return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
2384
2767
  }
2385
- function springEventEdges(ctx) {
2768
+ async function springEventEdges(ctx, onYield) {
2769
+ let scannedFiles = 0;
2386
2770
  // Pass 1 — event-type → listener methods, scanning only event-relevant files.
2771
+ // This is the ONLY full read sweep: publisher files are recorded here so
2772
+ // pass 2 re-reads just those instead of every .java file again (#1212 —
2773
+ // the double full-repo read was one of the tail's longest unyielded spans).
2387
2774
  const listeners = new Map();
2775
+ const publisherFiles = [];
2388
2776
  for (const file of ctx.getAllFiles()) {
2777
+ if ((++scannedFiles & 15) === 0)
2778
+ await onYield();
2389
2779
  if (!SPRING_JAVA_EXT.test(file))
2390
2780
  continue;
2391
2781
  const content = ctx.readFile(file);
2392
2782
  if (!content)
2393
2783
  continue;
2784
+ if (content.includes('.publishEvent('))
2785
+ publisherFiles.push(file);
2394
2786
  const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
2395
2787
  const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
2396
2788
  if (!hasAnno && !hasAppListener)
@@ -2430,12 +2822,13 @@ function springEventEdges(ctx) {
2430
2822
  }
2431
2823
  if (!listeners.size)
2432
2824
  return [];
2433
- // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
2825
+ // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
2826
+ // XEvent. Only the publisher files recorded in pass 1 are (re-)read.
2434
2827
  const edges = [];
2435
2828
  const seen = new Set();
2436
- for (const file of ctx.getAllFiles()) {
2437
- if (!SPRING_JAVA_EXT.test(file))
2438
- continue;
2829
+ for (const file of publisherFiles) {
2830
+ if ((++scannedFiles & 15) === 0)
2831
+ await onYield();
2439
2832
  const content = ctx.readFile(file);
2440
2833
  if (!content || !content.includes('.publishEvent('))
2441
2834
  continue;
@@ -2520,10 +2913,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
2520
2913
  }
2521
2914
  return declType;
2522
2915
  }
2523
- function mediatrDispatchEdges(ctx) {
2916
+ async function mediatrDispatchEdges(ctx, onYield) {
2917
+ let scannedFiles = 0;
2524
2918
  // Pass 1 — request/notification type → the Handle method of each handler class.
2525
2919
  const handlers = new Map();
2526
2920
  for (const file of ctx.getAllFiles()) {
2921
+ if ((++scannedFiles & 15) === 0)
2922
+ await onYield();
2527
2923
  if (!MEDIATR_CS_EXT.test(file))
2528
2924
  continue;
2529
2925
  const content = ctx.readFile(file);
@@ -2557,6 +2953,8 @@ function mediatrDispatchEdges(ctx) {
2557
2953
  const edges = [];
2558
2954
  const seen = new Set();
2559
2955
  for (const file of ctx.getAllFiles()) {
2956
+ if ((++scannedFiles & 15) === 0)
2957
+ await onYield();
2560
2958
  if (!MEDIATR_CS_EXT.test(file))
2561
2959
  continue;
2562
2960
  const content = ctx.readFile(file);
@@ -2620,7 +3018,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
2620
3018
  const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
2621
3019
  const SIDEKIQ_RB_EXT = /\.rb$/;
2622
3020
  const SIDEKIQ_FANOUT_CAP = 80;
2623
- function sidekiqDispatchEdges(ctx) {
3021
+ async function sidekiqDispatchEdges(ctx, onYield) {
3022
+ let scannedFiles = 0;
2624
3023
  // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
2625
3024
  // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
2626
3025
  const performCache = new Map();
@@ -2657,6 +3056,8 @@ function sidekiqDispatchEdges(ctx) {
2657
3056
  const edges = [];
2658
3057
  const seen = new Set();
2659
3058
  for (const file of ctx.getAllFiles()) {
3059
+ if ((++scannedFiles & 15) === 0)
3060
+ await onYield();
2660
3061
  if (!SIDEKIQ_RB_EXT.test(file))
2661
3062
  continue;
2662
3063
  const content = ctx.readFile(file);
@@ -2692,6 +3093,414 @@ function sidekiqDispatchEdges(ctx) {
2692
3093
  }
2693
3094
  return edges;
2694
3095
  }
3096
+ // ── Erlang behaviour-callback dispatch ────────────────────────────────────────
3097
+ // An Erlang behaviour is a compile-checked callback contract: the behaviour
3098
+ // module declares `-callback init(...) -> ...`, implementers declare
3099
+ // `-behaviour(B)` and export the callbacks, and the framework side dispatches
3100
+ // through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
3101
+ // `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
3102
+ // deliberately leaves var-module calls silent (no static target), so the flow
3103
+ // breaks at exactly the hop agents ask about (request → handler init). Bridge:
3104
+ //
3105
+ // dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
3106
+ // declaring `fn` with the SITE's arity — provided exactly ONE in-repo
3107
+ // behaviour declares (fn, arity); a name+arity collision across behaviours
3108
+ // bails (silent beats wrong) — and the implementer defines and exports `fn`.
3109
+ //
3110
+ // Behaviours are discovered by scanning every Erlang file for `-callback`
3111
+ // declarations (not just `implements` targets), so a behaviour with zero
3112
+ // implementers still participates in the ambiguity gate. Fan-out control: a
3113
+ // mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
3114
+ // hundreds of edges per site that READ as complete coverage while being
3115
+ // arbitrary — above the cap the site is skipped entirely and the boundary
3116
+ // stays visibly dynamic (explore's boundary announcer covers it) instead of
3117
+ // silently truncated.
3118
+ const ERLANG_EXT = /\.(?:erl|hrl)$/;
3119
+ // `Var:fn(` — variable (capitalized) module, lowercase function, immediate
3120
+ // open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
3121
+ // (static remote call, already linked), and mid-word matches.
3122
+ const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
3123
+ const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
3124
+ const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
3125
+ /**
3126
+ * Argument count of the call/declaration whose `(` sits at `openIdx` —
3127
+ * top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
3128
+ * (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
3129
+ * so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
3130
+ */
3131
+ function erlangArityAt(src, openIdx) {
3132
+ let depth = 1;
3133
+ let commas = 0;
3134
+ let sawArg = false;
3135
+ const limit = Math.min(src.length, openIdx + 4000);
3136
+ for (let i = openIdx + 1; i < limit; i++) {
3137
+ const ch = src[i];
3138
+ if (ch === '"' || ch === "'") {
3139
+ i++;
3140
+ while (i < limit && src[i] !== ch) {
3141
+ if (src[i] === '\\')
3142
+ i++;
3143
+ i++;
3144
+ }
3145
+ sawArg = true;
3146
+ continue;
3147
+ }
3148
+ if (ch === '$') {
3149
+ i++;
3150
+ if (src[i] === '\\')
3151
+ i++;
3152
+ sawArg = true;
3153
+ continue;
3154
+ }
3155
+ if (ch === '(' || ch === '[' || ch === '{') {
3156
+ depth++;
3157
+ sawArg = true;
3158
+ continue;
3159
+ }
3160
+ if (ch === ')' || ch === ']' || ch === '}') {
3161
+ depth--;
3162
+ if (depth === 0)
3163
+ return sawArg ? commas + 1 : 0;
3164
+ continue;
3165
+ }
3166
+ if (ch === ',' && depth === 1) {
3167
+ commas++;
3168
+ continue;
3169
+ }
3170
+ if (!/\s/.test(ch))
3171
+ sawArg = true;
3172
+ }
3173
+ return -1;
3174
+ }
3175
+ /**
3176
+ * Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
3177
+ * DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
3178
+ * and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
3179
+ * config) — the connection happens by option-path unification inside the
3180
+ * module-system evaluator, so there is no static call/import edge to follow
3181
+ * and flow questions ("how does services.yabai.enable become a launchd
3182
+ * service?") go dark at the module boundary.
3183
+ *
3184
+ * This pass links each config-write binding to the option declaration whose
3185
+ * path is the longest static-segment prefix of the write path. Precision gates:
3186
+ * - only STATIC segments participate: plain identifiers, plus quoted segments
3187
+ * (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
3188
+ * only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
3189
+ * so dynamic paths never match beyond their static head;
3190
+ * - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
3191
+ * every package's `meta = { ... }` attrset to nixos's `options.meta`;
3192
+ * - a prefix declared in more than one file is ambiguous → no edge (a wrong
3193
+ * edge is worse than none);
3194
+ * - writes physically inside an options block are declaration internals
3195
+ * (types, defaults, examples), never config writes → excluded.
3196
+ * Both declaration spellings register: flat (`options.a.b = ...`) by name, and
3197
+ * nested (`options = { a.b = ...; }`) by line-span containment.
3198
+ */
3199
+ function nixLeadingPlainSegments(name) {
3200
+ const segs = [];
3201
+ let i = 0;
3202
+ const n = name.length;
3203
+ while (i < n) {
3204
+ if (name[i] === '"') {
3205
+ // Quoted segment — an opaque verbatim token (quotes kept, so it can
3206
+ // never collide with a plain identifier). `NSGlobalDomain."com.apple.
3207
+ // mouse.tapBehavior"` must match ITS OWN quoted declaration, not
3208
+ // whichever sibling registered the shared plain prefix first.
3209
+ let j = i + 1;
3210
+ while (j < n && name[j] !== '"') {
3211
+ if (name[j] === '\\')
3212
+ j++;
3213
+ j++;
3214
+ }
3215
+ if (j >= n)
3216
+ return segs; // unterminated — stop at the static head
3217
+ const tok = name.slice(i, j + 1);
3218
+ if (tok.includes('${'))
3219
+ return segs; // interpolated → dynamic → stop
3220
+ segs.push(tok);
3221
+ i = j + 1;
3222
+ if (i >= n)
3223
+ break;
3224
+ if (name[i] !== '.')
3225
+ return segs;
3226
+ i++;
3227
+ continue;
3228
+ }
3229
+ let j = i;
3230
+ while (j < n && name[j] !== '.') {
3231
+ if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
3232
+ return segs;
3233
+ j++;
3234
+ }
3235
+ const seg = name.slice(i, j);
3236
+ if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
3237
+ return segs;
3238
+ segs.push(seg);
3239
+ i = j + 1;
3240
+ }
3241
+ return segs;
3242
+ }
3243
+ async function nixOptionPathEdges(queries, onYield) {
3244
+ let scanned255 = 0;
3245
+ // One streaming pass over nix bindings (variables + the odd function-valued
3246
+ // option); memory stays O(bindings-kept), not O(all nodes) (#610).
3247
+ const byFile = new Map();
3248
+ let scanned = 0;
3249
+ for (const kind of ['variable', 'function']) {
3250
+ for (const node of queries.iterateNodesByKind(kind)) {
3251
+ if ((++scanned255 & 63) === 0)
3252
+ await onYield();
3253
+ if ((++scanned & 0x3fff) === 0 && onYield)
3254
+ await onYield();
3255
+ if (node.language !== 'nix')
3256
+ continue;
3257
+ const segs = nixLeadingPlainSegments(node.name);
3258
+ if (segs.length === 0)
3259
+ continue;
3260
+ const rec = {
3261
+ id: node.id,
3262
+ filePath: node.filePath,
3263
+ startLine: node.startLine,
3264
+ endLine: node.endLine,
3265
+ segs,
3266
+ };
3267
+ const arr = byFile.get(node.filePath);
3268
+ if (arr)
3269
+ arr.push(rec);
3270
+ else
3271
+ byFile.set(node.filePath, [rec]);
3272
+ }
3273
+ }
3274
+ // Per file: walk bindings outermost-first with a stack of active option
3275
+ // spans, composing nested declaration paths (`options = { services.foo = {
3276
+ // enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
3277
+ // An `options` binding nested inside another option span is a SUBMODULE's
3278
+ // own namespace (`attrsOf (submodule { options = ...; })`) — its internals
3279
+ // are not globally addressable, so the sentinel blocks registration below it
3280
+ // while still excluding the region from write candidates.
3281
+ const SUBMODULE = 'submodule';
3282
+ const decls = new Map();
3283
+ const writes = [];
3284
+ const register = (path, rec) => {
3285
+ if (path.length < 2 || path.includes(SUBMODULE))
3286
+ return;
3287
+ const key = path.join('.');
3288
+ const arr = decls.get(key);
3289
+ if (arr)
3290
+ arr.push(rec);
3291
+ else
3292
+ decls.set(key, [rec]);
3293
+ };
3294
+ for (const recs of byFile.values()) {
3295
+ recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
3296
+ const stack = [];
3297
+ for (const rec of recs) {
3298
+ while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
3299
+ stack.pop();
3300
+ // Strict containment at line granularity: a one-line nested binding is
3301
+ // indistinguishable from its container, so it stays unclassified (rare
3302
+ // in module code, where option blocks are multi-line).
3303
+ const enclosing = stack.length > 0 &&
3304
+ rec.startLine >= stack[stack.length - 1].start &&
3305
+ rec.endLine <= stack[stack.length - 1].end &&
3306
+ !(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
3307
+ ? stack[stack.length - 1]
3308
+ : null;
3309
+ if (rec.segs[0] === 'options') {
3310
+ const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
3311
+ const prefix = enclosing ? [SUBMODULE] : ownPath;
3312
+ register(prefix, rec);
3313
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix });
3314
+ continue;
3315
+ }
3316
+ if (enclosing) {
3317
+ const composed = [...enclosing.prefix, ...rec.segs];
3318
+ register(composed, rec);
3319
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
3320
+ continue;
3321
+ }
3322
+ if (rec.segs.length >= 2) {
3323
+ writes.push(rec);
3324
+ }
3325
+ }
3326
+ }
3327
+ if (decls.size === 0 || writes.length === 0)
3328
+ return [];
3329
+ const edges = [];
3330
+ for (const w of writes) {
3331
+ // `config.services.x = ...` spells the same write with an explicit prefix.
3332
+ const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
3333
+ if (segs.length < 2)
3334
+ continue;
3335
+ // Longest prefix wins; an ambiguous longest match does NOT fall back to a
3336
+ // shorter one (that would link `services.nginx.virtualHosts.x` to
3337
+ // `options.services.nginx` when virtualHosts is the contested path).
3338
+ for (let len = Math.min(segs.length, 6); len >= 2; len--) {
3339
+ const candidates = decls.get(segs.slice(0, len).join('.'));
3340
+ if (!candidates || candidates.length === 0)
3341
+ continue;
3342
+ const files = new Set(candidates.map((c) => c.filePath));
3343
+ if (files.size === 1) {
3344
+ const target = candidates[0];
3345
+ if (target.id !== w.id) {
3346
+ edges.push({
3347
+ source: w.id,
3348
+ target: target.id,
3349
+ kind: 'references',
3350
+ line: w.startLine,
3351
+ provenance: 'heuristic',
3352
+ metadata: {
3353
+ synthesizedBy: 'nix-option-path',
3354
+ optionPath: segs.slice(0, len).join('.'),
3355
+ registeredAt: `${target.filePath}:${target.startLine}`,
3356
+ },
3357
+ });
3358
+ }
3359
+ }
3360
+ break; // longest hit decides, matched or ambiguous
3361
+ }
3362
+ }
3363
+ return edges;
3364
+ }
3365
+ async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
3366
+ let scannedFiles = 0;
3367
+ let scanned255 = 0;
3368
+ // Cheap language gate: no Erlang modules → no cost beyond one streamed
3369
+ // kind scan (never a materialized array of every namespace — #1212).
3370
+ const erlangModules = [];
3371
+ for (const n of queries.iterateNodesByKind('namespace')) {
3372
+ if ((++scanned255 & 63) === 0)
3373
+ await onYield();
3374
+ if (n.language === 'erlang')
3375
+ erlangModules.push(n);
3376
+ }
3377
+ if (erlangModules.length === 0)
3378
+ return [];
3379
+ // Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
3380
+ // its (name, arity) callback set, and the global `name/arity` → declaring
3381
+ // behaviours map that drives the ambiguity gate.
3382
+ const moduleByFile = new Map();
3383
+ for (const ns of erlangModules) {
3384
+ if (!moduleByFile.has(ns.filePath))
3385
+ moduleByFile.set(ns.filePath, ns);
3386
+ }
3387
+ const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
3388
+ const callbackNames = new Set();
3389
+ for (const file of ctx.getAllFiles()) {
3390
+ if ((++scannedFiles & 15) === 0)
3391
+ await onYield();
3392
+ if (!ERLANG_EXT.test(file))
3393
+ continue;
3394
+ const behaviour = moduleByFile.get(file);
3395
+ if (!behaviour)
3396
+ continue; // a .hrl or module-less file can't be a behaviour
3397
+ const content = ctx.readFile(file);
3398
+ if (!content || !content.includes('-callback'))
3399
+ continue;
3400
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3401
+ ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
3402
+ let m;
3403
+ while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
3404
+ const name = m[2].replace(/^'|'$/g, '');
3405
+ const arity = erlangArityAt(safe, m.index + m[0].length - 1);
3406
+ if (arity < 0)
3407
+ continue;
3408
+ const key = `${name}/${arity}`;
3409
+ const arr = declaringBehaviours.get(key);
3410
+ if (arr) {
3411
+ if (!arr.some((b) => b.id === behaviour.id))
3412
+ arr.push(behaviour);
3413
+ }
3414
+ else {
3415
+ declaringBehaviours.set(key, [behaviour]);
3416
+ }
3417
+ callbackNames.add(name);
3418
+ }
3419
+ }
3420
+ if (declaringBehaviours.size === 0)
3421
+ return [];
3422
+ // Implementer target lookup, lazy per (behaviour, fn): implementers come
3423
+ // from the `implements` edges extraction resolved, and the target is the
3424
+ // implementer module's own exported `fn` function node.
3425
+ const targetCache = new Map();
3426
+ const targetsOf = (behaviour, fn) => {
3427
+ const cacheKey = `${behaviour.id}#${fn}`;
3428
+ let targets = targetCache.get(cacheKey);
3429
+ if (targets)
3430
+ return targets;
3431
+ targets = [];
3432
+ for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
3433
+ const impl = queries.getNodeById(e.source);
3434
+ if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
3435
+ continue;
3436
+ const fnNode = ctx
3437
+ .getNodesInFile(impl.filePath)
3438
+ .find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
3439
+ if (fnNode)
3440
+ targets.push(fnNode);
3441
+ }
3442
+ targetCache.set(cacheKey, targets);
3443
+ return targets;
3444
+ };
3445
+ // Pass 2 — dispatch sites. Only files containing a var-module call shape are
3446
+ // scanned in full.
3447
+ const edges = [];
3448
+ const seen = new Set();
3449
+ for (const file of ctx.getAllFiles()) {
3450
+ if ((++scannedFiles & 15) === 0)
3451
+ await onYield();
3452
+ if (!ERLANG_EXT.test(file))
3453
+ continue;
3454
+ const content = ctx.readFile(file);
3455
+ if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
3456
+ continue;
3457
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3458
+ const nodesInFile = ctx.getNodesInFile(file);
3459
+ ERLANG_DISPATCH_RE.lastIndex = 0;
3460
+ let m;
3461
+ while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
3462
+ const fn = m[3];
3463
+ if (!callbackNames.has(fn))
3464
+ continue;
3465
+ const openIdx = m.index + m[0].length - 1;
3466
+ const arity = erlangArityAt(safe, openIdx);
3467
+ if (arity < 0)
3468
+ continue;
3469
+ const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
3470
+ if (!behaviours || behaviours.length !== 1)
3471
+ continue; // unknown or ambiguous
3472
+ const behaviour = behaviours[0];
3473
+ const targets = targetsOf(behaviour, fn);
3474
+ if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
3475
+ continue;
3476
+ const line = safe.slice(0, m.index).split('\n').length;
3477
+ const disp = enclosingFn(nodesInFile, line);
3478
+ if (!disp)
3479
+ continue;
3480
+ for (const target of targets) {
3481
+ if (target.id === disp.id)
3482
+ continue;
3483
+ const key = `${disp.id}>${target.id}`;
3484
+ if (seen.has(key))
3485
+ continue;
3486
+ seen.add(key);
3487
+ edges.push({
3488
+ source: disp.id,
3489
+ target: target.id,
3490
+ kind: 'calls',
3491
+ line,
3492
+ provenance: 'heuristic',
3493
+ metadata: {
3494
+ synthesizedBy: 'erlang-behaviour',
3495
+ via: `${behaviour.name}:${fn}/${arity}`,
3496
+ registeredAt: `${file}:${line}`,
3497
+ },
3498
+ });
3499
+ }
3500
+ }
3501
+ }
3502
+ return edges;
3503
+ }
2695
3504
  // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2696
3505
  // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2697
3506
  // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
@@ -2738,7 +3547,8 @@ function phpArrayBody(src, openIdx) {
2738
3547
  }
2739
3548
  return null;
2740
3549
  }
2741
- function laravelEventEdges(ctx) {
3550
+ async function laravelEventEdges(ctx, onYield) {
3551
+ let scannedFiles = 0;
2742
3552
  // event short name → its listener `handle` methods (deduped by node id).
2743
3553
  const listeners = new Map();
2744
3554
  const add = (event, handle) => {
@@ -2755,6 +3565,8 @@ function laravelEventEdges(ctx) {
2755
3565
  && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
2756
3566
  // Pass 1 — build the event→handle map from both registration mechanisms.
2757
3567
  for (const file of ctx.getAllFiles()) {
3568
+ if ((++scannedFiles & 15) === 0)
3569
+ await onYield();
2758
3570
  if (!LARAVEL_PHP_EXT.test(file))
2759
3571
  continue;
2760
3572
  const content = ctx.readFile(file);
@@ -2800,6 +3612,8 @@ function laravelEventEdges(ctx) {
2800
3612
  const edges = [];
2801
3613
  const seen = new Set();
2802
3614
  for (const file of ctx.getAllFiles()) {
3615
+ if ((++scannedFiles & 15) === 0)
3616
+ await onYield();
2803
3617
  if (!LARAVEL_PHP_EXT.test(file))
2804
3618
  continue;
2805
3619
  const content = ctx.readFile(file);
@@ -2859,85 +3673,160 @@ async function synthesizeCallbackEdges(queries, ctx) {
2859
3673
  // that itself hangs (a real wedge) never reaches the next yield, so the
2860
3674
  // watchdog still catches that. See ./cooperative-yield.
2861
3675
  const yieldToLoop = (0, cooperative_yield_1.createYielder)();
3676
+ // Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
3677
+ // (=1: passes over 250ms; =all: every pass). This is the diagnostic that
3678
+ // located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
3679
+ const markT = { t: Date.now() };
3680
+ const __mark = (label) => {
3681
+ const now = Date.now();
3682
+ const dt = now - markT.t;
3683
+ markT.t = now;
3684
+ if (process.env.CODEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.CODEGRAPH_SYNTH_TIMINGS === 'all')) {
3685
+ console.error(`[synth-timing] ${label}: ${dt}ms`);
3686
+ }
3687
+ };
3688
+ // Language gating: one indexed DISTINCT over the files table lets a pass
3689
+ // whose own filters reference a specific language/extension be skipped
3690
+ // outright when the project has no such files — its result is provably
3691
+ // empty, so skipping is behavior-identical and the cost drops to zero
3692
+ // (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
3693
+ // Passes without an explicit language filter always run.
3694
+ const langs = queries.getDistinctFileLanguages();
3695
+ const has = (...ls) => ls.some((l) => langs.has(l));
3696
+ const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
3697
+ const NONE = [];
2862
3698
  // Cross-file Go method→type `contains` edges must be synthesized AND persisted
2863
3699
  // FIRST: a method declared in a different file from its receiver type is
2864
3700
  // otherwise orphaned from the struct, and goImplementsEdges (next) derives a
2865
3701
  // struct's method set from its `contains` edges — so without this it would
2866
3702
  // under-count the interfaces a cross-file struct satisfies. (#583)
2867
- const goMethodContains = goCrossFileMethodContainsEdges(queries);
2868
- if (goMethodContains.length > 0)
2869
- queries.insertEdges(goMethodContains);
3703
+ const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
3704
+ for (let i = 0; i < goMethodContains.length; i += 2000) {
3705
+ queries.insertEdges(goMethodContains.slice(i, i + 2000));
3706
+ await yieldToLoop();
3707
+ }
2870
3708
  await yieldToLoop();
3709
+ __mark('goMethodContains');
2871
3710
  // Go implicit `implements` edges must be synthesized AND persisted next: the
2872
3711
  // interface-dispatch bridge below reads `implements` edges from the DB, and
2873
3712
  // Go has none statically. (Other languages already have static implements
2874
3713
  // edges from extraction, so they don't need this pre-pass.)
2875
- const goImpl = goImplementsEdges(queries);
2876
- if (goImpl.length > 0)
2877
- queries.insertEdges(goImpl);
3714
+ const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
3715
+ for (let i = 0; i < goImpl.length; i += 2000) {
3716
+ queries.insertEdges(goImpl.slice(i, i + 2000));
3717
+ await yieldToLoop();
3718
+ }
2878
3719
  await yieldToLoop();
3720
+ __mark('goImplements');
2879
3721
  const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
2880
3722
  await yieldToLoop();
3723
+ __mark('fieldEdges');
2881
3724
  const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
2882
3725
  await yieldToLoop();
3726
+ __mark('closureCollEdges');
2883
3727
  const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
2884
3728
  await yieldToLoop();
2885
- const renderEdges = reactRenderEdges(queries, ctx);
3729
+ __mark('emitterEdges');
3730
+ const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
2886
3731
  await yieldToLoop();
3732
+ __mark('renderEdges');
2887
3733
  const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
2888
3734
  await yieldToLoop();
2889
- const vueEdges = vueTemplateEdges(ctx);
3735
+ __mark('jsxEdges');
3736
+ const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
3737
+ await yieldToLoop();
3738
+ __mark('vueEdges');
3739
+ const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
3740
+ await yieldToLoop();
3741
+ __mark('svelteKitEdges');
3742
+ const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
2890
3743
  await yieldToLoop();
2891
- const svelteKitEdges = svelteKitLoadEdges(ctx);
3744
+ __mark('pascalEdges');
3745
+ const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
2892
3746
  await yieldToLoop();
2893
- const pascalEdges = pascalFormEdges(ctx);
3747
+ __mark('flutterEdges');
3748
+ const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
2894
3749
  await yieldToLoop();
2895
- const flutterEdges = flutterBuildEdges(queries, ctx);
3750
+ __mark('arkuiStateEdges');
3751
+ const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
2896
3752
  await yieldToLoop();
2897
- const cppEdges = cppOverrideEdges(queries);
3753
+ __mark('arkuiEmitter');
3754
+ const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
2898
3755
  await yieldToLoop();
2899
- const ifaceEdges = interfaceOverrideEdges(queries);
3756
+ __mark('arkuiRoutes');
3757
+ const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
2900
3758
  await yieldToLoop();
2901
- const kotlinExpectActual = kotlinExpectActualEdges(queries);
3759
+ __mark('cppEdges');
3760
+ const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
3761
+ ? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
2902
3762
  await yieldToLoop();
2903
- const goGrpcEdges = goGrpcStubImplEdges(queries);
3763
+ __mark('ifaceEdges');
3764
+ const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
2904
3765
  await yieldToLoop();
2905
- const rnEventEdgesList = rnEventEdges(ctx);
3766
+ __mark('kotlinExpectActual');
3767
+ const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
2906
3768
  await yieldToLoop();
2907
- const fabricNativeEdges = fabricNativeImplEdges(ctx);
3769
+ __mark('goGrpcEdges');
3770
+ const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
2908
3771
  await yieldToLoop();
2909
- const expoXPlatEdges = expoCrossPlatformEdges(queries);
3772
+ __mark('rnEventEdgesList');
3773
+ const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
2910
3774
  await yieldToLoop();
2911
- const rnXPlatEdges = rnCrossPlatformEdges(queries);
3775
+ __mark('fabricNativeEdges');
3776
+ const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
2912
3777
  await yieldToLoop();
2913
- const mybatisEdges = mybatisJavaXmlEdges(queries);
3778
+ __mark('expoXPlatEdges');
3779
+ const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
2914
3780
  await yieldToLoop();
2915
- const ginEdges = ginMiddlewareChainEdges(queries, ctx);
3781
+ __mark('rnXPlatEdges');
3782
+ const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE;
2916
3783
  await yieldToLoop();
2917
- const thunkEdges = reduxThunkEdges(queries, ctx);
3784
+ __mark('mybatisEdges');
3785
+ const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE;
2918
3786
  await yieldToLoop();
3787
+ __mark('ginEdges');
3788
+ const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE;
3789
+ await yieldToLoop();
3790
+ __mark('thunkEdges');
2919
3791
  const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
2920
3792
  await yieldToLoop();
2921
- const rtkEdges = rtkQueryEdges(queries, ctx);
3793
+ __mark('registryEdges');
3794
+ const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
3795
+ await yieldToLoop();
3796
+ __mark('rtkEdges');
3797
+ const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
2922
3798
  await yieldToLoop();
2923
- const piniaEdges = piniaStoreEdges(ctx);
3799
+ __mark('piniaEdges');
3800
+ const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
2924
3801
  await yieldToLoop();
2925
- const vuexEdges = vuexDispatchEdges(ctx);
3802
+ __mark('vuexEdges');
3803
+ const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
2926
3804
  await yieldToLoop();
2927
- const celeryEdges = celeryDispatchEdges(ctx);
3805
+ __mark('celeryEdges');
3806
+ const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
2928
3807
  await yieldToLoop();
2929
- const springEdges = springEventEdges(ctx);
3808
+ __mark('springEdges');
3809
+ const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
2930
3810
  await yieldToLoop();
2931
- const mediatrEdges = mediatrDispatchEdges(ctx);
3811
+ __mark('mediatrEdges');
3812
+ const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
2932
3813
  await yieldToLoop();
2933
- const sidekiqEdges = sidekiqDispatchEdges(ctx);
3814
+ __mark('sidekiqEdges');
3815
+ const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
2934
3816
  await yieldToLoop();
2935
- const laravelEdges = laravelEventEdges(ctx);
3817
+ __mark('erlangBehaviourEdges');
3818
+ const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
2936
3819
  await yieldToLoop();
2937
- const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
3820
+ __mark('laravelEdges');
3821
+ const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop) : NONE;
2938
3822
  await yieldToLoop();
2939
- const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
3823
+ __mark('cFnPtrEdges');
3824
+ const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
2940
3825
  await yieldToLoop();
3826
+ __mark('goframeEdges');
3827
+ const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE;
3828
+ await yieldToLoop();
3829
+ __mark('nixOptionEdges');
2941
3830
  const merged = [];
2942
3831
  const seen = new Set();
2943
3832
  for (const e of [
@@ -2950,6 +3839,9 @@ async function synthesizeCallbackEdges(queries, ctx) {
2950
3839
  ...svelteKitEdges,
2951
3840
  ...pascalEdges,
2952
3841
  ...flutterEdges,
3842
+ ...arkuiStateEdges,
3843
+ ...arkuiEmitter,
3844
+ ...arkuiRoutes,
2953
3845
  ...cppEdges,
2954
3846
  ...ifaceEdges,
2955
3847
  ...kotlinExpectActual,
@@ -2969,9 +3861,11 @@ async function synthesizeCallbackEdges(queries, ctx) {
2969
3861
  ...springEdges,
2970
3862
  ...mediatrEdges,
2971
3863
  ...sidekiqEdges,
3864
+ ...erlangBehaviourEdges,
2972
3865
  ...laravelEdges,
2973
3866
  ...cFnPtrEdges,
2974
3867
  ...goframeEdges,
3868
+ ...nixOptionEdges,
2975
3869
  ]) {
2976
3870
  const key = `${e.source}>${e.target}`;
2977
3871
  if (seen.has(key))
@@ -2979,8 +3873,15 @@ async function synthesizeCallbackEdges(queries, ctx) {
2979
3873
  seen.add(key);
2980
3874
  merged.push(e);
2981
3875
  }
2982
- if (merged.length > 0)
2983
- queries.insertEdges(merged);
3876
+ __mark('dedupe-merge');
3877
+ // Chunked insert with yields: on the Linux kernel the merged synthesized
3878
+ // edge set is ~275k rows, and one transaction for all of them was a 20s
3879
+ // unyielded main-thread span (#1212 follow-up) — the last one in the tail.
3880
+ for (let i = 0; i < merged.length; i += 2000) {
3881
+ queries.insertEdges(merged.slice(i, i + 2000));
3882
+ await yieldToLoop();
3883
+ }
3884
+ __mark('insertMergedEdges');
2984
3885
  return merged.length + goImpl.length + goMethodContains.length;
2985
3886
  }
2986
3887
  //# sourceMappingURL=callback-synthesizer.js.map