@colbymchenry/codegraph-darwin-arm64 1.1.6 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/bin/codegraph +5 -0
  2. package/lib/dist/bin/codegraph.d.ts +1 -1
  3. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  4. package/lib/dist/bin/codegraph.js +232 -61
  5. package/lib/dist/bin/codegraph.js.map +1 -1
  6. package/lib/dist/bin/command-supervision.d.ts.map +1 -1
  7. package/lib/dist/bin/command-supervision.js +14 -1
  8. package/lib/dist/bin/command-supervision.js.map +1 -1
  9. package/lib/dist/db/migrations.d.ts +1 -1
  10. package/lib/dist/db/migrations.d.ts.map +1 -1
  11. package/lib/dist/db/migrations.js +19 -1
  12. package/lib/dist/db/migrations.js.map +1 -1
  13. package/lib/dist/db/queries.d.ts +51 -0
  14. package/lib/dist/db/queries.d.ts.map +1 -1
  15. package/lib/dist/db/queries.js +141 -0
  16. package/lib/dist/db/queries.js.map +1 -1
  17. package/lib/dist/db/schema.sql +19 -0
  18. package/lib/dist/directory.d.ts +9 -5
  19. package/lib/dist/directory.d.ts.map +1 -1
  20. package/lib/dist/directory.js +210 -19
  21. package/lib/dist/directory.js.map +1 -1
  22. package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
  23. package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
  24. package/lib/dist/extraction/cfml-extractor.js +494 -0
  25. package/lib/dist/extraction/cfml-extractor.js.map +1 -0
  26. package/lib/dist/extraction/grammars.d.ts +9 -0
  27. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  28. package/lib/dist/extraction/grammars.js +115 -3
  29. package/lib/dist/extraction/grammars.js.map +1 -1
  30. package/lib/dist/extraction/index.d.ts +46 -1
  31. package/lib/dist/extraction/index.d.ts.map +1 -1
  32. package/lib/dist/extraction/index.js +254 -3
  33. package/lib/dist/extraction/index.js.map +1 -1
  34. package/lib/dist/extraction/languages/arkts.d.ts +3 -0
  35. package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
  36. package/lib/dist/extraction/languages/arkts.js +127 -0
  37. package/lib/dist/extraction/languages/arkts.js.map +1 -0
  38. package/lib/dist/extraction/languages/c-cpp.d.ts +59 -0
  39. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  40. package/lib/dist/extraction/languages/c-cpp.js +499 -3
  41. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  42. package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
  43. package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
  44. package/lib/dist/extraction/languages/cfquery.js +28 -0
  45. package/lib/dist/extraction/languages/cfquery.js.map +1 -0
  46. package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
  47. package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
  48. package/lib/dist/extraction/languages/cfscript.js +73 -0
  49. package/lib/dist/extraction/languages/cfscript.js.map +1 -0
  50. package/lib/dist/extraction/languages/cobol.d.ts +33 -0
  51. package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
  52. package/lib/dist/extraction/languages/cobol.js +499 -0
  53. package/lib/dist/extraction/languages/cobol.js.map +1 -0
  54. package/lib/dist/extraction/languages/erlang.d.ts +3 -0
  55. package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
  56. package/lib/dist/extraction/languages/erlang.js +350 -0
  57. package/lib/dist/extraction/languages/erlang.js.map +1 -0
  58. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  59. package/lib/dist/extraction/languages/index.js +18 -0
  60. package/lib/dist/extraction/languages/index.js.map +1 -1
  61. package/lib/dist/extraction/languages/nix.d.ts +3 -0
  62. package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
  63. package/lib/dist/extraction/languages/nix.js +294 -0
  64. package/lib/dist/extraction/languages/nix.js.map +1 -0
  65. package/lib/dist/extraction/languages/solidity.d.ts +3 -0
  66. package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
  67. package/lib/dist/extraction/languages/solidity.js +293 -0
  68. package/lib/dist/extraction/languages/solidity.js.map +1 -0
  69. package/lib/dist/extraction/languages/terraform.d.ts +3 -0
  70. package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
  71. package/lib/dist/extraction/languages/terraform.js +641 -0
  72. package/lib/dist/extraction/languages/terraform.js.map +1 -0
  73. package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
  74. package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
  75. package/lib/dist/extraction/languages/vbnet.js +141 -0
  76. package/lib/dist/extraction/languages/vbnet.js.map +1 -0
  77. package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
  78. package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  79. package/lib/dist/extraction/mybatis-extractor.js +140 -38
  80. package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
  81. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  82. package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
  83. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  84. package/lib/dist/extraction/tree-sitter-types.d.ts +20 -1
  85. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  86. package/lib/dist/extraction/tree-sitter.d.ts +38 -0
  87. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  88. package/lib/dist/extraction/tree-sitter.js +845 -16
  89. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  90. package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  91. package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  92. package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  93. package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  94. package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  95. package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  96. package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  97. package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  98. package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  99. package/lib/dist/graph/traversal.d.ts.map +1 -1
  100. package/lib/dist/graph/traversal.js +76 -17
  101. package/lib/dist/graph/traversal.js.map +1 -1
  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 +310 -6
  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 +39 -7
  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 +21 -3
  125. package/lib/dist/mcp/proxy.js.map +1 -1
  126. package/lib/dist/mcp/startup-handshake.d.ts +44 -0
  127. package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
  128. package/lib/dist/mcp/startup-handshake.js +73 -0
  129. package/lib/dist/mcp/startup-handshake.js.map +1 -0
  130. package/lib/dist/mcp/tools.d.ts +22 -0
  131. package/lib/dist/mcp/tools.d.ts.map +1 -1
  132. package/lib/dist/mcp/tools.js +74 -9
  133. package/lib/dist/mcp/tools.js.map +1 -1
  134. package/lib/dist/project-config.d.ts +38 -0
  135. package/lib/dist/project-config.d.ts.map +1 -1
  136. package/lib/dist/project-config.js +101 -2
  137. package/lib/dist/project-config.js.map +1 -1
  138. package/lib/dist/resolution/callback-synthesizer.d.ts +1 -1
  139. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  140. package/lib/dist/resolution/callback-synthesizer.js +743 -11
  141. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  142. package/lib/dist/resolution/cooperative-yield.d.ts +32 -0
  143. package/lib/dist/resolution/cooperative-yield.d.ts.map +1 -0
  144. package/lib/dist/resolution/cooperative-yield.js +42 -0
  145. package/lib/dist/resolution/cooperative-yield.js.map +1 -0
  146. package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
  147. package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
  148. package/lib/dist/resolution/frameworks/cics.js +90 -0
  149. package/lib/dist/resolution/frameworks/cics.js.map +1 -0
  150. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  151. package/lib/dist/resolution/frameworks/index.js +6 -0
  152. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  153. package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
  154. package/lib/dist/resolution/frameworks/java.js +14 -6
  155. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  156. package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
  157. package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  158. package/lib/dist/resolution/frameworks/terraform.js +277 -0
  159. package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
  160. package/lib/dist/resolution/import-resolver.d.ts +7 -0
  161. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  162. package/lib/dist/resolution/import-resolver.js +129 -4
  163. package/lib/dist/resolution/import-resolver.js.map +1 -1
  164. package/lib/dist/resolution/index.d.ts +44 -2
  165. package/lib/dist/resolution/index.d.ts.map +1 -1
  166. package/lib/dist/resolution/index.js +335 -30
  167. package/lib/dist/resolution/index.js.map +1 -1
  168. package/lib/dist/resolution/name-matcher.d.ts +22 -3
  169. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  170. package/lib/dist/resolution/name-matcher.js +484 -39
  171. package/lib/dist/resolution/name-matcher.js.map +1 -1
  172. package/lib/dist/resolution/strip-comments.d.ts +1 -1
  173. package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
  174. package/lib/dist/resolution/strip-comments.js +49 -0
  175. package/lib/dist/resolution/strip-comments.js.map +1 -1
  176. package/lib/dist/resolution/types.d.ts +20 -0
  177. package/lib/dist/resolution/types.d.ts.map +1 -1
  178. package/lib/dist/resolution/workspace-packages.d.ts +10 -0
  179. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
  180. package/lib/dist/resolution/workspace-packages.js +142 -4
  181. package/lib/dist/resolution/workspace-packages.js.map +1 -1
  182. package/lib/dist/search/identifier-segments.d.ts +60 -0
  183. package/lib/dist/search/identifier-segments.d.ts.map +1 -0
  184. package/lib/dist/search/identifier-segments.js +176 -0
  185. package/lib/dist/search/identifier-segments.js.map +1 -0
  186. package/lib/dist/sync/git-hooks.d.ts.map +1 -1
  187. package/lib/dist/sync/git-hooks.js +2 -0
  188. package/lib/dist/sync/git-hooks.js.map +1 -1
  189. package/lib/dist/sync/watcher.d.ts +10 -5
  190. package/lib/dist/sync/watcher.d.ts.map +1 -1
  191. package/lib/dist/sync/watcher.js +51 -14
  192. package/lib/dist/sync/watcher.js.map +1 -1
  193. package/lib/dist/sync/worktree.d.ts.map +1 -1
  194. package/lib/dist/sync/worktree.js +5 -0
  195. package/lib/dist/sync/worktree.js.map +1 -1
  196. package/lib/dist/types.d.ts +19 -1
  197. package/lib/dist/types.d.ts.map +1 -1
  198. package/lib/dist/types.js +10 -0
  199. package/lib/dist/types.js.map +1 -1
  200. package/lib/dist/upgrade/index.js +1 -1
  201. package/lib/dist/upgrade/index.js.map +1 -1
  202. package/lib/node_modules/.package-lock.json +1 -1
  203. package/lib/package.json +1 -1
  204. package/package.json +1 -1
  205. package/lib/dist/reasoning/config.d.ts +0 -45
  206. package/lib/dist/reasoning/config.d.ts.map +0 -1
  207. package/lib/dist/reasoning/config.js +0 -171
  208. package/lib/dist/reasoning/config.js.map +0 -1
  209. package/lib/dist/reasoning/credentials.d.ts +0 -5
  210. package/lib/dist/reasoning/credentials.d.ts.map +0 -1
  211. package/lib/dist/reasoning/credentials.js +0 -83
  212. package/lib/dist/reasoning/credentials.js.map +0 -1
  213. package/lib/dist/reasoning/login.d.ts +0 -21
  214. package/lib/dist/reasoning/login.d.ts.map +0 -1
  215. package/lib/dist/reasoning/login.js +0 -85
  216. package/lib/dist/reasoning/login.js.map +0 -1
  217. package/lib/dist/reasoning/reasoner.d.ts +0 -43
  218. package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
  219. package/lib/dist/reasoning/reasoner.js +0 -308
  220. package/lib/dist/reasoning/reasoner.js.map +0 -1
@@ -5,6 +5,7 @@ const generated_detection_1 = require("../extraction/generated-detection");
5
5
  const strip_comments_1 = require("./strip-comments");
6
6
  const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
7
7
  const goframe_synthesizer_1 = require("./goframe-synthesizer");
8
+ const cooperative_yield_1 = require("./cooperative-yield");
8
9
  const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
9
10
  const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
10
11
  const MAX_CALLBACKS_PER_CHANNEL = 40;
@@ -113,10 +114,13 @@ function* methodAndFunctionNodes(queries) {
113
114
  yield* queries.iterateNodesByKind('function');
114
115
  }
115
116
  /** Phase 1: field-backed observer channels (registrar/dispatcher share a store). */
116
- function fieldChannelEdges(queries, ctx) {
117
+ async function fieldChannelEdges(queries, ctx, onYield) {
117
118
  const registrars = [];
118
119
  const dispatchers = [];
120
+ let scanned = 0;
119
121
  for (const m of methodAndFunctionNodes(queries)) {
122
+ if ((++scanned & 255) === 0)
123
+ await onYield(); // #1091: yield mid-scan on huge graphs
120
124
  const isReg = REGISTRAR_NAME.test(m.name);
121
125
  const isDisp = DISPATCHER_NAME.test(m.name);
122
126
  if (!isReg && !isDisp)
@@ -197,7 +201,7 @@ function fieldChannelEdges(queries, ctx) {
197
201
  * subclass `DataRequest.validate`), bounded by a fan-out cap so a generic field
198
202
  * name shared across unrelated classes can't fan out into noise.
199
203
  */
200
- function closureCollectionEdges(queries, ctx) {
204
+ async function closureCollectionEdges(queries, ctx, onYield) {
201
205
  const dispatchers = new Map(); // field → dispatcher methods + forEach line
202
206
  const registrars = new Map(); // field → registrar methods + append line
203
207
  const addReg = (field, node, absLine) => {
@@ -208,7 +212,13 @@ function closureCollectionEdges(queries, ctx) {
208
212
  arr.push({ node, line: absLine });
209
213
  registrars.set(field, arr);
210
214
  };
215
+ // Slices EVERY method/function's source (no cheap name-gate), so on a repo
216
+ // with a huge file this is the heaviest synthesis pass — yield mid-scan so it
217
+ // can't wedge the #850 watchdog on its own (#1091).
218
+ let scanned = 0;
211
219
  for (const m of methodAndFunctionNodes(queries)) {
220
+ if ((++scanned & 127) === 0)
221
+ await onYield();
212
222
  const content = ctx.readFile(m.filePath);
213
223
  const src = content && sliceLines(content, m.startLine, m.endLine);
214
224
  if (!src)
@@ -265,10 +275,13 @@ function closureCollectionEdges(queries, ctx) {
265
275
  return edges;
266
276
  }
267
277
  /** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
268
- function eventEmitterEdges(ctx) {
278
+ async function eventEmitterEdges(ctx, onYield) {
269
279
  const emitsByEvent = new Map(); // event → dispatcher node ids
270
280
  const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
281
+ let scanned = 0;
271
282
  for (const file of ctx.getAllFiles()) {
283
+ if ((++scanned & 255) === 0)
284
+ await onYield(); // #1091: yield mid-scan on huge graphs
272
285
  const content = ctx.readFile(file);
273
286
  if (!content)
274
287
  continue;
@@ -417,6 +430,270 @@ function flutterBuildEdges(queries, ctx) {
417
430
  }
418
431
  return edges;
419
432
  }
433
+ /**
434
+ * Reactive ArkUI property decorators: assigning a property carrying one of
435
+ * these re-runs the owning struct's `build()`. Covers both state models —
436
+ * V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
437
+ * (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
438
+ * the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
439
+ * data classes, not struct properties).
440
+ */
441
+ const ARKUI_REACTIVE_DECORATORS = new Set([
442
+ 'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
443
+ 'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
444
+ 'Local', 'Provider', 'Consumer',
445
+ ]);
446
+ /** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
447
+ const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
448
+ /**
449
+ * Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
450
+ * flutter-build). Assigning a reactive-decorated property (`@State count`,
451
+ * `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
452
+ * hop is framework-internal — no static edge — so "onClick → markAllDone →
453
+ * this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
454
+ * for each arkts struct with a `build()` method and at least one reactive
455
+ * property, link every sibling method whose body ASSIGNS (or array-mutates)
456
+ * one of those properties → `build`. Assignment-gated on the struct's OWN
457
+ * reactive property names — a method that merely reads state, or a struct
458
+ * with no reactive properties, gets nothing (this is the precision line the
459
+ * all-sibling-methods design would erase).
460
+ */
461
+ function arkuiStateBuildEdges(queries, ctx) {
462
+ const edges = [];
463
+ const seen = new Set();
464
+ for (const struct of queries.getNodesByKind('struct')) {
465
+ if (struct.language !== 'arkts')
466
+ continue;
467
+ const children = queries.getOutgoingEdges(struct.id, ['contains'])
468
+ .map((e) => queries.getNodeById(e.target))
469
+ .filter((n) => !!n);
470
+ const build = children.find((n) => n.kind === 'method' && n.name === 'build');
471
+ if (!build)
472
+ continue;
473
+ const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
474
+ if (reactiveProps.length === 0)
475
+ continue;
476
+ const propAlternation = reactiveProps
477
+ .map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
478
+ .join('|');
479
+ // `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
480
+ // `=(?!=)` keeps `this.done == x` comparisons out.
481
+ const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
482
+ let added = 0;
483
+ for (const m of children) {
484
+ if (added >= MAX_CALLBACKS_PER_CHANNEL)
485
+ break;
486
+ if (m.kind !== 'method' || m.id === build.id)
487
+ continue;
488
+ const content = ctx.readFile(m.filePath);
489
+ const src = content && sliceLines(content, m.startLine, m.endLine);
490
+ if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
491
+ continue;
492
+ const key = `${m.id}>${build.id}`;
493
+ if (seen.has(key))
494
+ continue;
495
+ seen.add(key);
496
+ edges.push({
497
+ source: m.id, target: build.id, kind: 'calls', line: m.startLine,
498
+ provenance: 'heuristic',
499
+ metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
500
+ });
501
+ added++;
502
+ }
503
+ }
504
+ return edges;
505
+ }
506
+ /** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
507
+ const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
508
+ /** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
509
+ const ARKUI_EMITTER_FANOUT_CAP = 8;
510
+ /**
511
+ * Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
512
+ * bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
513
+ * framework-internal, so an order flow riding it (OrangeShopping's
514
+ * add-to-cart) dead-ends at the emit. Link emit-site enclosing
515
+ * function/method → on/once-site enclosing function/method when both
516
+ * reference the SAME statically-recoverable event key.
517
+ *
518
+ * Key recovery, per call site (comment-stripped enclosing-file source): the
519
+ * first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
520
+ * a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
521
+ * level. Precision scoping learned from the samples monorepo (thousands of
522
+ * unrelated samples, most using eventId 1): NUMERIC keys pair within the same
523
+ * FILE only; NAMED keys pair within the same workspace module directory (or
524
+ * the whole project when it declares no modules — the single-app case), both
525
+ * behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
526
+ * handling — their bodies' calls already attribute to the registering method,
527
+ * so targeting that method keeps the chain connected.
528
+ */
529
+ function arkuiEmitterEdges(ctx) {
530
+ // bucket key -> emit sites / handler sites
531
+ const emits = new Map();
532
+ const handlers = new Map();
533
+ const moduleDirs = (() => {
534
+ const ws = ctx.getWorkspacePackages?.();
535
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
536
+ })();
537
+ const moduleScopeOf = (file) => {
538
+ for (const dir of moduleDirs) {
539
+ if (file === dir || file.startsWith(dir + '/'))
540
+ return dir;
541
+ }
542
+ return '';
543
+ };
544
+ for (const file of ctx.getAllFiles()) {
545
+ if (!file.endsWith('.ets'))
546
+ continue;
547
+ const content = ctx.readFile(file);
548
+ if (!content || !content.includes('emitter.'))
549
+ continue;
550
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
551
+ const nodes = ctx.getNodesInFile(file)
552
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
553
+ ARKUI_EMITTER_CALL_RE.lastIndex = 0;
554
+ let m;
555
+ while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
556
+ const verb = m[1];
557
+ const arg = m[2].trim();
558
+ const line = safe.slice(0, m.index).split('\n').length;
559
+ const encl = nodes
560
+ .filter((n) => n.startLine <= line && n.endLine >= line)
561
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
562
+ if (!encl)
563
+ continue;
564
+ // Recover the event key from the first argument.
565
+ let key = null;
566
+ const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
567
+ const token = idLit ?? arg;
568
+ if (token !== undefined) {
569
+ if (/^\d+$/.test(token)) {
570
+ key = `num:${file}:${token}`; // numeric: same-file only
571
+ }
572
+ else if (token.includes('.')) {
573
+ key = `name:${moduleScopeOf(file)}:${token}`;
574
+ }
575
+ else {
576
+ // Local variable — chase its same-file declaration one level:
577
+ // `let x = new EventsId(K)` / `const x = K`.
578
+ const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
579
+ const decl = safe.match(declRe);
580
+ const inner = (decl?.[1] ?? decl?.[2])?.trim();
581
+ if (inner && /^\d+$/.test(inner))
582
+ key = `num:${file}:${inner}`;
583
+ else if (inner && /^[\w$.]+$/.test(inner))
584
+ key = `name:${moduleScopeOf(file)}:${inner}`;
585
+ }
586
+ }
587
+ if (!key)
588
+ continue;
589
+ const site = { nodeId: encl.id, file, line };
590
+ if (verb === 'emit') {
591
+ (emits.get(key) ?? emits.set(key, []).get(key)).push(site);
592
+ }
593
+ else {
594
+ (handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
595
+ }
596
+ }
597
+ }
598
+ const edges = [];
599
+ const seen = new Set();
600
+ for (const [key, emitSites] of emits) {
601
+ const handlerSites = handlers.get(key);
602
+ if (!handlerSites)
603
+ continue;
604
+ if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
605
+ continue;
606
+ const eventLabel = key.slice(key.lastIndexOf(':') + 1);
607
+ for (const e of emitSites)
608
+ for (const h of handlerSites) {
609
+ if (e.nodeId === h.nodeId)
610
+ continue;
611
+ const dedupe = `${e.nodeId}>${h.nodeId}`;
612
+ if (seen.has(dedupe))
613
+ continue;
614
+ seen.add(dedupe);
615
+ edges.push({
616
+ source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
617
+ provenance: 'heuristic',
618
+ metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
619
+ });
620
+ }
621
+ }
622
+ return edges;
623
+ }
624
+ /** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
625
+ const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
626
+ /**
627
+ * Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
628
+ * 'pages/Detail' })` reaches the `@Entry struct` of
629
+ * `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
630
+ * static edge — so "tap → openDetail → ???" ends at the router call. Bridge
631
+ * literal urls to the page struct: the url resolves against the standard
632
+ * `src/main/ets/` layout (what main_pages.json entries name); candidates
633
+ * prefer the caller's own workspace module (routes are module-scoped), and
634
+ * anything still ambiguous is dropped rather than guessed. Only `@Entry`
635
+ * structs qualify as targets — the decorator is what makes a file a page.
636
+ */
637
+ function arkuiRouterEdges(ctx) {
638
+ const edges = [];
639
+ const seen = new Set();
640
+ const allFiles = ctx.getAllFiles();
641
+ const moduleDirs = (() => {
642
+ const ws = ctx.getWorkspacePackages?.();
643
+ return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
644
+ })();
645
+ const moduleScopeOf = (file) => {
646
+ for (const dir of moduleDirs) {
647
+ if (file === dir || file.startsWith(dir + '/'))
648
+ return dir;
649
+ }
650
+ return '';
651
+ };
652
+ for (const file of allFiles) {
653
+ if (!file.endsWith('.ets'))
654
+ continue;
655
+ const content = ctx.readFile(file);
656
+ if (!content || !content.includes('router.'))
657
+ continue;
658
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
659
+ const nodes = ctx.getNodesInFile(file)
660
+ .filter((n) => n.kind === 'method' || n.kind === 'function');
661
+ ARKUI_ROUTER_RE.lastIndex = 0;
662
+ let m;
663
+ while ((m = ARKUI_ROUTER_RE.exec(safe))) {
664
+ const url = m[1];
665
+ const line = safe.slice(0, m.index).split('\n').length;
666
+ const encl = nodes
667
+ .filter((n) => n.startLine <= line && n.endLine >= line)
668
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
669
+ if (!encl)
670
+ continue;
671
+ const suffix = `/src/main/ets/${url}.ets`;
672
+ let candidates = allFiles.filter((f) => f.endsWith(suffix));
673
+ if (candidates.length > 1) {
674
+ const scope = moduleScopeOf(file);
675
+ const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
676
+ if (sameModule.length > 0)
677
+ candidates = sameModule;
678
+ }
679
+ if (candidates.length !== 1)
680
+ continue; // ambiguous or unresolved — never guess
681
+ const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
682
+ if (!page)
683
+ continue;
684
+ const key = `${encl.id}>${page.id}`;
685
+ if (seen.has(key))
686
+ continue;
687
+ seen.add(key);
688
+ edges.push({
689
+ source: encl.id, target: page.id, kind: 'calls', line,
690
+ provenance: 'heuristic',
691
+ metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
692
+ });
693
+ }
694
+ }
695
+ return edges;
696
+ }
420
697
  /**
421
698
  * Phase 4c: C++ virtual override. A call through a base/interface pointer
422
699
  * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
@@ -488,6 +765,7 @@ function cppOverrideEdges(queries) {
488
765
  // or an `object` (Scala) so the loop also iterates those kinds.
489
766
  const IFACE_OVERRIDE_LANGS = new Set([
490
767
  'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
768
+ 'arkts',
491
769
  ]);
492
770
  /**
493
771
  * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
@@ -872,11 +1150,14 @@ function goGrpcStubImplEdges(queries) {
872
1150
  * component/function/class node — TS generics like `Array<Foo>` resolve to a type
873
1151
  * (or nothing) and are dropped.
874
1152
  */
875
- function reactJsxChildEdges(ctx) {
1153
+ async function reactJsxChildEdges(ctx, onYield) {
876
1154
  const edges = [];
877
1155
  const seen = new Set();
878
1156
  const PARENT_KINDS = new Set(['method', 'function', 'component']);
1157
+ let scanned = 0;
879
1158
  for (const file of ctx.getAllFiles()) {
1159
+ if ((++scanned & 255) === 0)
1160
+ await onYield(); // #1091: yield mid-scan on huge graphs
880
1161
  const content = ctx.readFile(file);
881
1162
  if (!content || (!content.includes('</') && !content.includes('/>')))
882
1163
  continue; // JSX-file gate
@@ -1893,10 +2174,13 @@ function resolveRegistryHandler(ctx, name, chained) {
1893
2174
  // entry resolving to the global URL constant).
1894
2175
  return cands.find((n) => n.kind === 'method') ?? null;
1895
2176
  }
1896
- function objectRegistryEdges(ctx) {
2177
+ async function objectRegistryEdges(ctx, onYield) {
1897
2178
  const edges = [];
1898
2179
  const seen = new Set();
2180
+ let scanned = 0;
1899
2181
  for (const file of ctx.getAllFiles()) {
2182
+ if ((++scanned & 255) === 0)
2183
+ await onYield(); // #1091: yield mid-scan on huge graphs
1900
2184
  if (!REGISTRY_JS_EXT.test(file))
1901
2185
  continue;
1902
2186
  const content = ctx.readFile(file);
@@ -2673,6 +2957,398 @@ function sidekiqDispatchEdges(ctx) {
2673
2957
  }
2674
2958
  return edges;
2675
2959
  }
2960
+ // ── Erlang behaviour-callback dispatch ────────────────────────────────────────
2961
+ // An Erlang behaviour is a compile-checked callback contract: the behaviour
2962
+ // module declares `-callback init(...) -> ...`, implementers declare
2963
+ // `-behaviour(B)` and export the callbacks, and the framework side dispatches
2964
+ // through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
2965
+ // `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
2966
+ // deliberately leaves var-module calls silent (no static target), so the flow
2967
+ // breaks at exactly the hop agents ask about (request → handler init). Bridge:
2968
+ //
2969
+ // dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
2970
+ // declaring `fn` with the SITE's arity — provided exactly ONE in-repo
2971
+ // behaviour declares (fn, arity); a name+arity collision across behaviours
2972
+ // bails (silent beats wrong) — and the implementer defines and exports `fn`.
2973
+ //
2974
+ // Behaviours are discovered by scanning every Erlang file for `-callback`
2975
+ // declarations (not just `implements` targets), so a behaviour with zero
2976
+ // implementers still participates in the ambiguity gate. Fan-out control: a
2977
+ // mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
2978
+ // hundreds of edges per site that READ as complete coverage while being
2979
+ // arbitrary — above the cap the site is skipped entirely and the boundary
2980
+ // stays visibly dynamic (explore's boundary announcer covers it) instead of
2981
+ // silently truncated.
2982
+ const ERLANG_EXT = /\.(?:erl|hrl)$/;
2983
+ // `Var:fn(` — variable (capitalized) module, lowercase function, immediate
2984
+ // open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
2985
+ // (static remote call, already linked), and mid-word matches.
2986
+ const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
2987
+ const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
2988
+ const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
2989
+ /**
2990
+ * Argument count of the call/declaration whose `(` sits at `openIdx` —
2991
+ * top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
2992
+ * (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
2993
+ * so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
2994
+ */
2995
+ function erlangArityAt(src, openIdx) {
2996
+ let depth = 1;
2997
+ let commas = 0;
2998
+ let sawArg = false;
2999
+ const limit = Math.min(src.length, openIdx + 4000);
3000
+ for (let i = openIdx + 1; i < limit; i++) {
3001
+ const ch = src[i];
3002
+ if (ch === '"' || ch === "'") {
3003
+ i++;
3004
+ while (i < limit && src[i] !== ch) {
3005
+ if (src[i] === '\\')
3006
+ i++;
3007
+ i++;
3008
+ }
3009
+ sawArg = true;
3010
+ continue;
3011
+ }
3012
+ if (ch === '$') {
3013
+ i++;
3014
+ if (src[i] === '\\')
3015
+ i++;
3016
+ sawArg = true;
3017
+ continue;
3018
+ }
3019
+ if (ch === '(' || ch === '[' || ch === '{') {
3020
+ depth++;
3021
+ sawArg = true;
3022
+ continue;
3023
+ }
3024
+ if (ch === ')' || ch === ']' || ch === '}') {
3025
+ depth--;
3026
+ if (depth === 0)
3027
+ return sawArg ? commas + 1 : 0;
3028
+ continue;
3029
+ }
3030
+ if (ch === ',' && depth === 1) {
3031
+ commas++;
3032
+ continue;
3033
+ }
3034
+ if (!/\s/.test(ch))
3035
+ sawArg = true;
3036
+ }
3037
+ return -1;
3038
+ }
3039
+ /**
3040
+ * Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
3041
+ * DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
3042
+ * and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
3043
+ * config) — the connection happens by option-path unification inside the
3044
+ * module-system evaluator, so there is no static call/import edge to follow
3045
+ * and flow questions ("how does services.yabai.enable become a launchd
3046
+ * service?") go dark at the module boundary.
3047
+ *
3048
+ * This pass links each config-write binding to the option declaration whose
3049
+ * path is the longest static-segment prefix of the write path. Precision gates:
3050
+ * - only STATIC segments participate: plain identifiers, plus quoted segments
3051
+ * (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
3052
+ * only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
3053
+ * so dynamic paths never match beyond their static head;
3054
+ * - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
3055
+ * every package's `meta = { ... }` attrset to nixos's `options.meta`;
3056
+ * - a prefix declared in more than one file is ambiguous → no edge (a wrong
3057
+ * edge is worse than none);
3058
+ * - writes physically inside an options block are declaration internals
3059
+ * (types, defaults, examples), never config writes → excluded.
3060
+ * Both declaration spellings register: flat (`options.a.b = ...`) by name, and
3061
+ * nested (`options = { a.b = ...; }`) by line-span containment.
3062
+ */
3063
+ function nixLeadingPlainSegments(name) {
3064
+ const segs = [];
3065
+ let i = 0;
3066
+ const n = name.length;
3067
+ while (i < n) {
3068
+ if (name[i] === '"') {
3069
+ // Quoted segment — an opaque verbatim token (quotes kept, so it can
3070
+ // never collide with a plain identifier). `NSGlobalDomain."com.apple.
3071
+ // mouse.tapBehavior"` must match ITS OWN quoted declaration, not
3072
+ // whichever sibling registered the shared plain prefix first.
3073
+ let j = i + 1;
3074
+ while (j < n && name[j] !== '"') {
3075
+ if (name[j] === '\\')
3076
+ j++;
3077
+ j++;
3078
+ }
3079
+ if (j >= n)
3080
+ return segs; // unterminated — stop at the static head
3081
+ const tok = name.slice(i, j + 1);
3082
+ if (tok.includes('${'))
3083
+ return segs; // interpolated → dynamic → stop
3084
+ segs.push(tok);
3085
+ i = j + 1;
3086
+ if (i >= n)
3087
+ break;
3088
+ if (name[i] !== '.')
3089
+ return segs;
3090
+ i++;
3091
+ continue;
3092
+ }
3093
+ let j = i;
3094
+ while (j < n && name[j] !== '.') {
3095
+ if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
3096
+ return segs;
3097
+ j++;
3098
+ }
3099
+ const seg = name.slice(i, j);
3100
+ if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
3101
+ return segs;
3102
+ segs.push(seg);
3103
+ i = j + 1;
3104
+ }
3105
+ return segs;
3106
+ }
3107
+ async function nixOptionPathEdges(queries, onYield) {
3108
+ // One streaming pass over nix bindings (variables + the odd function-valued
3109
+ // option); memory stays O(bindings-kept), not O(all nodes) (#610).
3110
+ const byFile = new Map();
3111
+ let scanned = 0;
3112
+ for (const kind of ['variable', 'function']) {
3113
+ for (const node of queries.iterateNodesByKind(kind)) {
3114
+ if ((++scanned & 0x3fff) === 0 && onYield)
3115
+ await onYield();
3116
+ if (node.language !== 'nix')
3117
+ continue;
3118
+ const segs = nixLeadingPlainSegments(node.name);
3119
+ if (segs.length === 0)
3120
+ continue;
3121
+ const rec = {
3122
+ id: node.id,
3123
+ filePath: node.filePath,
3124
+ startLine: node.startLine,
3125
+ endLine: node.endLine,
3126
+ segs,
3127
+ };
3128
+ const arr = byFile.get(node.filePath);
3129
+ if (arr)
3130
+ arr.push(rec);
3131
+ else
3132
+ byFile.set(node.filePath, [rec]);
3133
+ }
3134
+ }
3135
+ // Per file: walk bindings outermost-first with a stack of active option
3136
+ // spans, composing nested declaration paths (`options = { services.foo = {
3137
+ // enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
3138
+ // An `options` binding nested inside another option span is a SUBMODULE's
3139
+ // own namespace (`attrsOf (submodule { options = ...; })`) — its internals
3140
+ // are not globally addressable, so the sentinel blocks registration below it
3141
+ // while still excluding the region from write candidates.
3142
+ const SUBMODULE = 'submodule';
3143
+ const decls = new Map();
3144
+ const writes = [];
3145
+ const register = (path, rec) => {
3146
+ if (path.length < 2 || path.includes(SUBMODULE))
3147
+ return;
3148
+ const key = path.join('.');
3149
+ const arr = decls.get(key);
3150
+ if (arr)
3151
+ arr.push(rec);
3152
+ else
3153
+ decls.set(key, [rec]);
3154
+ };
3155
+ for (const recs of byFile.values()) {
3156
+ recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
3157
+ const stack = [];
3158
+ for (const rec of recs) {
3159
+ while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
3160
+ stack.pop();
3161
+ // Strict containment at line granularity: a one-line nested binding is
3162
+ // indistinguishable from its container, so it stays unclassified (rare
3163
+ // in module code, where option blocks are multi-line).
3164
+ const enclosing = stack.length > 0 &&
3165
+ rec.startLine >= stack[stack.length - 1].start &&
3166
+ rec.endLine <= stack[stack.length - 1].end &&
3167
+ !(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
3168
+ ? stack[stack.length - 1]
3169
+ : null;
3170
+ if (rec.segs[0] === 'options') {
3171
+ const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
3172
+ const prefix = enclosing ? [SUBMODULE] : ownPath;
3173
+ register(prefix, rec);
3174
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix });
3175
+ continue;
3176
+ }
3177
+ if (enclosing) {
3178
+ const composed = [...enclosing.prefix, ...rec.segs];
3179
+ register(composed, rec);
3180
+ stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
3181
+ continue;
3182
+ }
3183
+ if (rec.segs.length >= 2) {
3184
+ writes.push(rec);
3185
+ }
3186
+ }
3187
+ }
3188
+ if (decls.size === 0 || writes.length === 0)
3189
+ return [];
3190
+ const edges = [];
3191
+ for (const w of writes) {
3192
+ // `config.services.x = ...` spells the same write with an explicit prefix.
3193
+ const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
3194
+ if (segs.length < 2)
3195
+ continue;
3196
+ // Longest prefix wins; an ambiguous longest match does NOT fall back to a
3197
+ // shorter one (that would link `services.nginx.virtualHosts.x` to
3198
+ // `options.services.nginx` when virtualHosts is the contested path).
3199
+ for (let len = Math.min(segs.length, 6); len >= 2; len--) {
3200
+ const candidates = decls.get(segs.slice(0, len).join('.'));
3201
+ if (!candidates || candidates.length === 0)
3202
+ continue;
3203
+ const files = new Set(candidates.map((c) => c.filePath));
3204
+ if (files.size === 1) {
3205
+ const target = candidates[0];
3206
+ if (target.id !== w.id) {
3207
+ edges.push({
3208
+ source: w.id,
3209
+ target: target.id,
3210
+ kind: 'references',
3211
+ line: w.startLine,
3212
+ provenance: 'heuristic',
3213
+ metadata: {
3214
+ synthesizedBy: 'nix-option-path',
3215
+ optionPath: segs.slice(0, len).join('.'),
3216
+ registeredAt: `${target.filePath}:${target.startLine}`,
3217
+ },
3218
+ });
3219
+ }
3220
+ }
3221
+ break; // longest hit decides, matched or ambiguous
3222
+ }
3223
+ }
3224
+ return edges;
3225
+ }
3226
+ function erlangBehaviourDispatchEdges(queries, ctx) {
3227
+ // Cheap language gate: no Erlang modules → no cost beyond one kind query.
3228
+ const erlangModules = queries.getNodesByKind('namespace').filter((n) => n.language === 'erlang');
3229
+ if (erlangModules.length === 0)
3230
+ return [];
3231
+ // Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
3232
+ // its (name, arity) callback set, and the global `name/arity` → declaring
3233
+ // behaviours map that drives the ambiguity gate.
3234
+ const moduleByFile = new Map();
3235
+ for (const ns of erlangModules) {
3236
+ if (!moduleByFile.has(ns.filePath))
3237
+ moduleByFile.set(ns.filePath, ns);
3238
+ }
3239
+ const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
3240
+ const callbackNames = new Set();
3241
+ for (const file of ctx.getAllFiles()) {
3242
+ if (!ERLANG_EXT.test(file))
3243
+ continue;
3244
+ const behaviour = moduleByFile.get(file);
3245
+ if (!behaviour)
3246
+ continue; // a .hrl or module-less file can't be a behaviour
3247
+ const content = ctx.readFile(file);
3248
+ if (!content || !content.includes('-callback'))
3249
+ continue;
3250
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3251
+ ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
3252
+ let m;
3253
+ while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
3254
+ const name = m[2].replace(/^'|'$/g, '');
3255
+ const arity = erlangArityAt(safe, m.index + m[0].length - 1);
3256
+ if (arity < 0)
3257
+ continue;
3258
+ const key = `${name}/${arity}`;
3259
+ const arr = declaringBehaviours.get(key);
3260
+ if (arr) {
3261
+ if (!arr.some((b) => b.id === behaviour.id))
3262
+ arr.push(behaviour);
3263
+ }
3264
+ else {
3265
+ declaringBehaviours.set(key, [behaviour]);
3266
+ }
3267
+ callbackNames.add(name);
3268
+ }
3269
+ }
3270
+ if (declaringBehaviours.size === 0)
3271
+ return [];
3272
+ // Implementer target lookup, lazy per (behaviour, fn): implementers come
3273
+ // from the `implements` edges extraction resolved, and the target is the
3274
+ // implementer module's own exported `fn` function node.
3275
+ const targetCache = new Map();
3276
+ const targetsOf = (behaviour, fn) => {
3277
+ const cacheKey = `${behaviour.id}#${fn}`;
3278
+ let targets = targetCache.get(cacheKey);
3279
+ if (targets)
3280
+ return targets;
3281
+ targets = [];
3282
+ for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
3283
+ const impl = queries.getNodeById(e.source);
3284
+ if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
3285
+ continue;
3286
+ const fnNode = ctx
3287
+ .getNodesInFile(impl.filePath)
3288
+ .find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
3289
+ if (fnNode)
3290
+ targets.push(fnNode);
3291
+ }
3292
+ targetCache.set(cacheKey, targets);
3293
+ return targets;
3294
+ };
3295
+ // Pass 2 — dispatch sites. Only files containing a var-module call shape are
3296
+ // scanned in full.
3297
+ const edges = [];
3298
+ const seen = new Set();
3299
+ for (const file of ctx.getAllFiles()) {
3300
+ if (!ERLANG_EXT.test(file))
3301
+ continue;
3302
+ const content = ctx.readFile(file);
3303
+ if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
3304
+ continue;
3305
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
3306
+ const nodesInFile = ctx.getNodesInFile(file);
3307
+ ERLANG_DISPATCH_RE.lastIndex = 0;
3308
+ let m;
3309
+ while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
3310
+ const fn = m[3];
3311
+ if (!callbackNames.has(fn))
3312
+ continue;
3313
+ const openIdx = m.index + m[0].length - 1;
3314
+ const arity = erlangArityAt(safe, openIdx);
3315
+ if (arity < 0)
3316
+ continue;
3317
+ const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
3318
+ if (!behaviours || behaviours.length !== 1)
3319
+ continue; // unknown or ambiguous
3320
+ const behaviour = behaviours[0];
3321
+ const targets = targetsOf(behaviour, fn);
3322
+ if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
3323
+ continue;
3324
+ const line = safe.slice(0, m.index).split('\n').length;
3325
+ const disp = enclosingFn(nodesInFile, line);
3326
+ if (!disp)
3327
+ continue;
3328
+ for (const target of targets) {
3329
+ if (target.id === disp.id)
3330
+ continue;
3331
+ const key = `${disp.id}>${target.id}`;
3332
+ if (seen.has(key))
3333
+ continue;
3334
+ seen.add(key);
3335
+ edges.push({
3336
+ source: disp.id,
3337
+ target: target.id,
3338
+ kind: 'calls',
3339
+ line,
3340
+ provenance: 'heuristic',
3341
+ metadata: {
3342
+ synthesizedBy: 'erlang-behaviour',
3343
+ via: `${behaviour.name}:${fn}/${arity}`,
3344
+ registeredAt: `${file}:${line}`,
3345
+ },
3346
+ });
3347
+ }
3348
+ }
3349
+ }
3350
+ return edges;
3351
+ }
2676
3352
  // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2677
3353
  // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2678
3354
  // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
@@ -2831,7 +3507,15 @@ function laravelEventEdges(ctx) {
2831
3507
  * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
2832
3508
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
2833
3509
  */
2834
- function synthesizeCallbackEdges(queries, ctx) {
3510
+ async function synthesizeCallbackEdges(queries, ctx) {
3511
+ // Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
3512
+ // running synchronously on the indexer's main thread. Their AGGREGATE can run
3513
+ // for well over a minute on a large repo — long enough for the #850 liveness
3514
+ // watchdog to SIGKILL the process mid-index (#1091), since its heartbeat lives
3515
+ // on this same thread. Yield between passes so the heartbeat can fire; a pass
3516
+ // that itself hangs (a real wedge) never reaches the next yield, so the
3517
+ // watchdog still catches that. See ./cooperative-yield.
3518
+ const yieldToLoop = (0, cooperative_yield_1.createYielder)();
2835
3519
  // Cross-file Go method→type `contains` edges must be synthesized AND persisted
2836
3520
  // FIRST: a method declared in a different file from its receiver type is
2837
3521
  // otherwise orphaned from the struct, and goImplementsEdges (next) derives a
@@ -2840,6 +3524,7 @@ function synthesizeCallbackEdges(queries, ctx) {
2840
3524
  const goMethodContains = goCrossFileMethodContainsEdges(queries);
2841
3525
  if (goMethodContains.length > 0)
2842
3526
  queries.insertEdges(goMethodContains);
3527
+ await yieldToLoop();
2843
3528
  // Go implicit `implements` edges must be synthesized AND persisted next: the
2844
3529
  // interface-dispatch bridge below reads `implements` edges from the DB, and
2845
3530
  // Go has none statically. (Other languages already have static implements
@@ -2847,37 +3532,79 @@ function synthesizeCallbackEdges(queries, ctx) {
2847
3532
  const goImpl = goImplementsEdges(queries);
2848
3533
  if (goImpl.length > 0)
2849
3534
  queries.insertEdges(goImpl);
2850
- const fieldEdges = fieldChannelEdges(queries, ctx);
2851
- const closureCollEdges = closureCollectionEdges(queries, ctx);
2852
- const emitterEdges = eventEmitterEdges(ctx);
3535
+ await yieldToLoop();
3536
+ const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
3537
+ await yieldToLoop();
3538
+ const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
3539
+ await yieldToLoop();
3540
+ const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
3541
+ await yieldToLoop();
2853
3542
  const renderEdges = reactRenderEdges(queries, ctx);
2854
- const jsxEdges = reactJsxChildEdges(ctx);
3543
+ await yieldToLoop();
3544
+ const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
3545
+ await yieldToLoop();
2855
3546
  const vueEdges = vueTemplateEdges(ctx);
3547
+ await yieldToLoop();
2856
3548
  const svelteKitEdges = svelteKitLoadEdges(ctx);
3549
+ await yieldToLoop();
2857
3550
  const pascalEdges = pascalFormEdges(ctx);
3551
+ await yieldToLoop();
2858
3552
  const flutterEdges = flutterBuildEdges(queries, ctx);
3553
+ await yieldToLoop();
3554
+ const arkuiStateEdges = arkuiStateBuildEdges(queries, ctx);
3555
+ await yieldToLoop();
3556
+ const arkuiEmitter = arkuiEmitterEdges(ctx);
3557
+ await yieldToLoop();
3558
+ const arkuiRoutes = arkuiRouterEdges(ctx);
3559
+ await yieldToLoop();
2859
3560
  const cppEdges = cppOverrideEdges(queries);
3561
+ await yieldToLoop();
2860
3562
  const ifaceEdges = interfaceOverrideEdges(queries);
3563
+ await yieldToLoop();
2861
3564
  const kotlinExpectActual = kotlinExpectActualEdges(queries);
3565
+ await yieldToLoop();
2862
3566
  const goGrpcEdges = goGrpcStubImplEdges(queries);
3567
+ await yieldToLoop();
2863
3568
  const rnEventEdgesList = rnEventEdges(ctx);
3569
+ await yieldToLoop();
2864
3570
  const fabricNativeEdges = fabricNativeImplEdges(ctx);
3571
+ await yieldToLoop();
2865
3572
  const expoXPlatEdges = expoCrossPlatformEdges(queries);
3573
+ await yieldToLoop();
2866
3574
  const rnXPlatEdges = rnCrossPlatformEdges(queries);
3575
+ await yieldToLoop();
2867
3576
  const mybatisEdges = mybatisJavaXmlEdges(queries);
3577
+ await yieldToLoop();
2868
3578
  const ginEdges = ginMiddlewareChainEdges(queries, ctx);
3579
+ await yieldToLoop();
2869
3580
  const thunkEdges = reduxThunkEdges(queries, ctx);
2870
- const registryEdges = objectRegistryEdges(ctx);
3581
+ await yieldToLoop();
3582
+ const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
3583
+ await yieldToLoop();
2871
3584
  const rtkEdges = rtkQueryEdges(queries, ctx);
3585
+ await yieldToLoop();
2872
3586
  const piniaEdges = piniaStoreEdges(ctx);
3587
+ await yieldToLoop();
2873
3588
  const vuexEdges = vuexDispatchEdges(ctx);
3589
+ await yieldToLoop();
2874
3590
  const celeryEdges = celeryDispatchEdges(ctx);
3591
+ await yieldToLoop();
2875
3592
  const springEdges = springEventEdges(ctx);
3593
+ await yieldToLoop();
2876
3594
  const mediatrEdges = mediatrDispatchEdges(ctx);
3595
+ await yieldToLoop();
2877
3596
  const sidekiqEdges = sidekiqDispatchEdges(ctx);
3597
+ await yieldToLoop();
3598
+ const erlangBehaviourEdges = erlangBehaviourDispatchEdges(queries, ctx);
3599
+ await yieldToLoop();
2878
3600
  const laravelEdges = laravelEventEdges(ctx);
3601
+ await yieldToLoop();
2879
3602
  const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
3603
+ await yieldToLoop();
2880
3604
  const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
3605
+ await yieldToLoop();
3606
+ const nixOptionEdges = await nixOptionPathEdges(queries, yieldToLoop);
3607
+ await yieldToLoop();
2881
3608
  const merged = [];
2882
3609
  const seen = new Set();
2883
3610
  for (const e of [
@@ -2890,6 +3617,9 @@ function synthesizeCallbackEdges(queries, ctx) {
2890
3617
  ...svelteKitEdges,
2891
3618
  ...pascalEdges,
2892
3619
  ...flutterEdges,
3620
+ ...arkuiStateEdges,
3621
+ ...arkuiEmitter,
3622
+ ...arkuiRoutes,
2893
3623
  ...cppEdges,
2894
3624
  ...ifaceEdges,
2895
3625
  ...kotlinExpectActual,
@@ -2909,9 +3639,11 @@ function synthesizeCallbackEdges(queries, ctx) {
2909
3639
  ...springEdges,
2910
3640
  ...mediatrEdges,
2911
3641
  ...sidekiqEdges,
3642
+ ...erlangBehaviourEdges,
2912
3643
  ...laravelEdges,
2913
3644
  ...cFnPtrEdges,
2914
3645
  ...goframeEdges,
3646
+ ...nixOptionEdges,
2915
3647
  ]) {
2916
3648
  const key = `${e.source}>${e.target}`;
2917
3649
  if (seen.has(key))