@colbymchenry/codegraph-darwin-x64 1.0.0 → 1.1.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 (175) hide show
  1. package/lib/dist/bin/codegraph.js +258 -17
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/bin/fatal-handler.d.ts +20 -0
  4. package/lib/dist/bin/fatal-handler.d.ts.map +1 -0
  5. package/lib/dist/bin/fatal-handler.js +118 -0
  6. package/lib/dist/bin/fatal-handler.js.map +1 -0
  7. package/lib/dist/db/index.d.ts +22 -1
  8. package/lib/dist/db/index.d.ts.map +1 -1
  9. package/lib/dist/db/index.js +46 -1
  10. package/lib/dist/db/index.js.map +1 -1
  11. package/lib/dist/db/queries.d.ts +14 -0
  12. package/lib/dist/db/queries.d.ts.map +1 -1
  13. package/lib/dist/db/queries.js +25 -0
  14. package/lib/dist/db/queries.js.map +1 -1
  15. package/lib/dist/directory.d.ts +58 -0
  16. package/lib/dist/directory.d.ts.map +1 -1
  17. package/lib/dist/directory.js +165 -0
  18. package/lib/dist/directory.js.map +1 -1
  19. package/lib/dist/extraction/grammars.d.ts +11 -3
  20. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  21. package/lib/dist/extraction/grammars.js +14 -5
  22. package/lib/dist/extraction/grammars.js.map +1 -1
  23. package/lib/dist/extraction/index.d.ts.map +1 -1
  24. package/lib/dist/extraction/index.js +202 -32
  25. package/lib/dist/extraction/index.js.map +1 -1
  26. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  27. package/lib/dist/extraction/languages/c-cpp.js +47 -2
  28. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  29. package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
  30. package/lib/dist/extraction/languages/csharp.js +20 -0
  31. package/lib/dist/extraction/languages/csharp.js.map +1 -1
  32. package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
  33. package/lib/dist/extraction/languages/dart.js +22 -0
  34. package/lib/dist/extraction/languages/dart.js.map +1 -1
  35. package/lib/dist/extraction/languages/java.d.ts.map +1 -1
  36. package/lib/dist/extraction/languages/java.js +213 -9
  37. package/lib/dist/extraction/languages/java.js.map +1 -1
  38. package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
  39. package/lib/dist/extraction/languages/kotlin.js +51 -0
  40. package/lib/dist/extraction/languages/kotlin.js.map +1 -1
  41. package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
  42. package/lib/dist/extraction/languages/scala.js +19 -9
  43. package/lib/dist/extraction/languages/scala.js.map +1 -1
  44. package/lib/dist/extraction/parse-worker.js +4 -1
  45. package/lib/dist/extraction/parse-worker.js.map +1 -1
  46. package/lib/dist/extraction/tree-sitter-types.d.ts +13 -0
  47. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  48. package/lib/dist/extraction/tree-sitter.d.ts +119 -0
  49. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  50. package/lib/dist/extraction/tree-sitter.js +890 -11
  51. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  52. package/lib/dist/index.d.ts +33 -0
  53. package/lib/dist/index.d.ts.map +1 -1
  54. package/lib/dist/index.js +68 -7
  55. package/lib/dist/index.js.map +1 -1
  56. package/lib/dist/installer/index.d.ts.map +1 -1
  57. package/lib/dist/installer/index.js +33 -56
  58. package/lib/dist/installer/index.js.map +1 -1
  59. package/lib/dist/installer/instructions-template.d.ts +3 -3
  60. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  61. package/lib/dist/installer/instructions-template.js +4 -4
  62. package/lib/dist/installer/targets/claude.d.ts +18 -12
  63. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  64. package/lib/dist/installer/targets/claude.js +78 -6
  65. package/lib/dist/installer/targets/claude.js.map +1 -1
  66. package/lib/dist/installer/targets/shared.d.ts +12 -2
  67. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  68. package/lib/dist/installer/targets/shared.js +13 -12
  69. package/lib/dist/installer/targets/shared.js.map +1 -1
  70. package/lib/dist/installer/targets/types.d.ts +7 -0
  71. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  72. package/lib/dist/mcp/daemon-manager.d.ts +42 -0
  73. package/lib/dist/mcp/daemon-manager.d.ts.map +1 -0
  74. package/lib/dist/mcp/daemon-manager.js +129 -0
  75. package/lib/dist/mcp/daemon-manager.js.map +1 -0
  76. package/lib/dist/mcp/daemon-registry.d.ts +47 -0
  77. package/lib/dist/mcp/daemon-registry.d.ts.map +1 -0
  78. package/lib/dist/mcp/daemon-registry.js +229 -0
  79. package/lib/dist/mcp/daemon-registry.js.map +1 -0
  80. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  81. package/lib/dist/mcp/daemon.js +5 -0
  82. package/lib/dist/mcp/daemon.js.map +1 -1
  83. package/lib/dist/mcp/engine.d.ts.map +1 -1
  84. package/lib/dist/mcp/engine.js +8 -0
  85. package/lib/dist/mcp/engine.js.map +1 -1
  86. package/lib/dist/mcp/index.d.ts +1 -0
  87. package/lib/dist/mcp/index.d.ts.map +1 -1
  88. package/lib/dist/mcp/index.js +13 -0
  89. package/lib/dist/mcp/index.js.map +1 -1
  90. package/lib/dist/mcp/liveness-watchdog.d.ts +18 -0
  91. package/lib/dist/mcp/liveness-watchdog.d.ts.map +1 -0
  92. package/lib/dist/mcp/liveness-watchdog.js +207 -0
  93. package/lib/dist/mcp/liveness-watchdog.js.map +1 -0
  94. package/lib/dist/mcp/server-instructions.d.ts +18 -14
  95. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  96. package/lib/dist/mcp/server-instructions.js +57 -52
  97. package/lib/dist/mcp/server-instructions.js.map +1 -1
  98. package/lib/dist/mcp/session.d.ts.map +1 -1
  99. package/lib/dist/mcp/session.js +23 -18
  100. package/lib/dist/mcp/session.js.map +1 -1
  101. package/lib/dist/mcp/tools.d.ts +51 -1
  102. package/lib/dist/mcp/tools.d.ts.map +1 -1
  103. package/lib/dist/mcp/tools.js +585 -151
  104. package/lib/dist/mcp/tools.js.map +1 -1
  105. package/lib/dist/project-config.d.ts +19 -0
  106. package/lib/dist/project-config.d.ts.map +1 -0
  107. package/lib/dist/project-config.js +180 -0
  108. package/lib/dist/project-config.js.map +1 -0
  109. package/lib/dist/reasoning/config.d.ts +45 -0
  110. package/lib/dist/reasoning/config.d.ts.map +1 -0
  111. package/lib/dist/reasoning/config.js +171 -0
  112. package/lib/dist/reasoning/config.js.map +1 -0
  113. package/lib/dist/reasoning/credentials.d.ts +5 -0
  114. package/lib/dist/reasoning/credentials.d.ts.map +1 -0
  115. package/lib/dist/reasoning/credentials.js +83 -0
  116. package/lib/dist/reasoning/credentials.js.map +1 -0
  117. package/lib/dist/reasoning/login.d.ts +21 -0
  118. package/lib/dist/reasoning/login.d.ts.map +1 -0
  119. package/lib/dist/reasoning/login.js +85 -0
  120. package/lib/dist/reasoning/login.js.map +1 -0
  121. package/lib/dist/reasoning/reasoner.d.ts +43 -0
  122. package/lib/dist/reasoning/reasoner.d.ts.map +1 -0
  123. package/lib/dist/reasoning/reasoner.js +308 -0
  124. package/lib/dist/reasoning/reasoner.js.map +1 -0
  125. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +33 -0
  126. package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -0
  127. package/lib/dist/resolution/c-fnptr-synthesizer.js +352 -0
  128. package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
  129. package/lib/dist/resolution/callback-synthesizer.d.ts +6 -1
  130. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  131. package/lib/dist/resolution/callback-synthesizer.js +1109 -1
  132. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  133. package/lib/dist/resolution/frameworks/goframe.d.ts +41 -0
  134. package/lib/dist/resolution/frameworks/goframe.d.ts.map +1 -0
  135. package/lib/dist/resolution/frameworks/goframe.js +112 -0
  136. package/lib/dist/resolution/frameworks/goframe.js.map +1 -0
  137. package/lib/dist/resolution/frameworks/index.d.ts +1 -0
  138. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  139. package/lib/dist/resolution/frameworks/index.js +5 -1
  140. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  141. package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
  142. package/lib/dist/resolution/frameworks/react.js +17 -60
  143. package/lib/dist/resolution/frameworks/react.js.map +1 -1
  144. package/lib/dist/resolution/goframe-synthesizer.d.ts +28 -0
  145. package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -0
  146. package/lib/dist/resolution/goframe-synthesizer.js +158 -0
  147. package/lib/dist/resolution/goframe-synthesizer.js.map +1 -0
  148. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  149. package/lib/dist/resolution/import-resolver.js +56 -0
  150. package/lib/dist/resolution/import-resolver.js.map +1 -1
  151. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  152. package/lib/dist/resolution/name-matcher.js +48 -8
  153. package/lib/dist/resolution/name-matcher.js.map +1 -1
  154. package/lib/dist/resolution/strip-comments.d.ts +1 -1
  155. package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
  156. package/lib/dist/resolution/strip-comments.js +2 -0
  157. package/lib/dist/resolution/strip-comments.js.map +1 -1
  158. package/lib/dist/sync/watcher.d.ts +68 -1
  159. package/lib/dist/sync/watcher.d.ts.map +1 -1
  160. package/lib/dist/sync/watcher.js +212 -14
  161. package/lib/dist/sync/watcher.js.map +1 -1
  162. package/lib/dist/telemetry/index.d.ts +0 -3
  163. package/lib/dist/telemetry/index.d.ts.map +1 -1
  164. package/lib/dist/telemetry/index.js +4 -7
  165. package/lib/dist/telemetry/index.js.map +1 -1
  166. package/lib/dist/upgrade/index.d.ts.map +1 -1
  167. package/lib/dist/upgrade/index.js +40 -4
  168. package/lib/dist/upgrade/index.js.map +1 -1
  169. package/lib/dist/utils.d.ts +14 -1
  170. package/lib/dist/utils.d.ts.map +1 -1
  171. package/lib/dist/utils.js +20 -2
  172. package/lib/dist/utils.js.map +1 -1
  173. package/lib/node_modules/.package-lock.json +1 -1
  174. package/lib/package.json +2 -2
  175. package/package.json +1 -1
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
4
4
  const generated_detection_1 = require("../extraction/generated-detection");
5
5
  const strip_comments_1 = require("./strip-comments");
6
+ const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
7
+ const goframe_synthesizer_1 = require("./goframe-synthesizer");
6
8
  const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
7
9
  const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
8
10
  const MAX_CALLBACKS_PER_CHANNEL = 40;
@@ -1741,10 +1743,1092 @@ function svelteKitLoadEdges(ctx) {
1741
1743
  }
1742
1744
  return edges;
1743
1745
  }
1746
+ /**
1747
+ * Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
1748
+ * (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
1749
+ * tree-sitter never extracts it as a function node: `X` is a `constant` whose body's calls are
1750
+ * ORPHANED. The `dispatch(nextThunk(...))` calls that drive a thunk chain forward therefore produce
1751
+ * no edges, so `callees(X)` is empty and a flow `dispatch(X(...)) → X → nextThunk` dead-ends at the
1752
+ * constant (validated on trezor-suite: the signXxxThunk constants had ZERO outgoing edges). Bridge
1753
+ * it: body-scan each thunk constant for `dispatch(Y(...))` and link `X → Y`, so the dispatch chain
1754
+ * connects. High-precision — the `dispatch(` keyword plus `Y` must resolve to a function/constant/
1755
+ * method node; capped; gated on thunk constants existing so it never runs on non-RTK repos.
1756
+ * Cross-file by design (a suite thunk dispatches a wallet-core thunk). Provenance `heuristic`,
1757
+ * `synthesizedBy:'redux-thunk'`; `registeredAt` is the dispatch site.
1758
+ */
1759
+ const THUNK_DECL_RE = /create(?:Async)?Thunk/;
1760
+ const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
1761
+ const THUNK_FANOUT_CAP = 24;
1762
+ function reduxThunkEdges(queries, ctx) {
1763
+ const edges = [];
1764
+ const seen = new Set();
1765
+ for (const node of queries.iterateNodesByKind('constant')) {
1766
+ // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
1767
+ // avoids reading every constant's body on a large repo.
1768
+ if (!node.signature || !THUNK_DECL_RE.test(node.signature))
1769
+ continue;
1770
+ const content = ctx.readFile(node.filePath);
1771
+ const src = content && sliceLines(content, node.startLine, node.endLine);
1772
+ if (!src)
1773
+ continue;
1774
+ // Thunks are TS/JS-family (same // and /* */ comment syntax); map to a CommentLang.
1775
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(src, node.language === 'javascript' || node.language === 'jsx' ? 'javascript' : 'typescript');
1776
+ THUNK_DISPATCH_RE.lastIndex = 0;
1777
+ let m;
1778
+ let added = 0;
1779
+ while ((m = THUNK_DISPATCH_RE.exec(safe)) && added < THUNK_FANOUT_CAP) {
1780
+ const name = m[1];
1781
+ if (name === node.name)
1782
+ continue; // self-dispatch (recursive thunk) — skip
1783
+ // Resolve the dispatched name, PREFERRING the thunk/action-creator over a same-named
1784
+ // service function. `dispatch(X(...))` dispatches a thunk or an action-creator (both
1785
+ // `constant`s) — never an unrelated helper that merely shares the name. On octo-call,
1786
+ // `leaveCall` is BOTH a `createAsyncThunk` const AND a service function, and the bare
1787
+ // `.find()` picked the function (wrong). Order: thunk const > other const > same-file
1788
+ // callable > first match. A single candidate (no collision) is unaffected.
1789
+ const cands = ctx
1790
+ .getNodesByName(name)
1791
+ .filter((n) => n.kind === 'constant' || n.kind === 'function' || n.kind === 'method');
1792
+ const target = cands.find((n) => !!n.signature && THUNK_DECL_RE.test(n.signature)) ??
1793
+ cands.find((n) => n.kind === 'constant') ??
1794
+ cands.find((n) => n.filePath === node.filePath) ??
1795
+ cands[0];
1796
+ if (!target || target.id === node.id)
1797
+ continue;
1798
+ const key = `${node.id}>${target.id}`;
1799
+ if (seen.has(key))
1800
+ continue;
1801
+ seen.add(key);
1802
+ const line = node.startLine + safe.slice(0, m.index).split('\n').length - 1;
1803
+ edges.push({
1804
+ source: node.id,
1805
+ target: target.id,
1806
+ kind: 'calls',
1807
+ line,
1808
+ provenance: 'heuristic',
1809
+ metadata: { synthesizedBy: 'redux-thunk', via: name, registeredAt: `${node.filePath}:${line}` },
1810
+ });
1811
+ added++;
1812
+ }
1813
+ }
1814
+ return edges;
1815
+ }
1816
+ // ── Object-literal registry dispatch ─────────────────────────────────────────
1817
+ // A command/handler registry maps string keys → handler class/function symbols in an
1818
+ // object literal, then dispatches by a RUNTIME key static parsing can't follow:
1819
+ // this.commands = { [Cmd.ADD]: AddObjectCommand, ... } // registration
1820
+ // new this.commands[command](args).execute() // dynamic dispatch
1821
+ // Bridge it like gin-middleware-chain: link each dispatching function → each registered
1822
+ // handler's callable entry (a class's execute/run/handle/… method — preferring the method
1823
+ // chained at the dispatch site — or the function value). Scoped to a registry + dispatch in
1824
+ // the SAME file (the cross-file barrel-namespace variant, e.g. trezor's getMethod, is
1825
+ // deferred). Gated on a real object literal with ≥2 entries that RESOLVE to callables (a
1826
+ // `{ width: 5 }` literal resolves to nothing → no edges); fan-out capped.
1827
+ const REGISTRY_ASSIGN_RE = /(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)|((?:this\.)?[A-Za-z_$][\w$]*))\s*=\s*\{/g;
1828
+ const REGISTRY_DISPATCH_RE = /(?:\bnew\s+)?((?:this\.)?[A-Za-z_$][\w$]*)\s*\[\s*([A-Za-z_$][\w$.]*)\s*\]\s*(?:\(|\.[A-Za-z_$])/g;
1829
+ const REGISTRY_MIN_ENTRIES = 2;
1830
+ const REGISTRY_FANOUT_CAP = 40;
1831
+ const REGISTRY_CLASS_ENTRY = new Set(['execute', 'run', 'handle', 'perform', 'process', 'call', 'apply', 'dispatch']);
1832
+ const REGISTRY_JS_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
1833
+ /** From the index of an opening `{`, return the brace-balanced body up to its matching `}`. */
1834
+ function braceBody(src, openIdx) {
1835
+ let depth = 0;
1836
+ for (let i = openIdx; i < src.length; i++) {
1837
+ if (src[i] === '{')
1838
+ depth++;
1839
+ else if (src[i] === '}' && --depth === 0)
1840
+ return src.slice(openIdx + 1, i);
1841
+ }
1842
+ return null;
1843
+ }
1844
+ /** Top-level `key: Identifier` entries of an object-literal body. DEPTH-AWARE: only depth-0
1845
+ * segments are considered, so method-shorthand bodies (`number(a,b){…}`), arrow values
1846
+ * (`x: () => …`), and nested objects (`x: { … }`) don't leak their inner `k: v` pairs as
1847
+ * bogus handlers. The per-segment anchor (`^… key: Ident …$`) keeps only pure identifier
1848
+ * values — a data value (`x: 5`), call, or arrow fails to match. */
1849
+ function registryEntryNames(body) {
1850
+ const segs = [];
1851
+ let depth = 0;
1852
+ let start = 0;
1853
+ for (let i = 0; i < body.length; i++) {
1854
+ const c = body[i];
1855
+ if (c === '{' || c === '(' || c === '[')
1856
+ depth++;
1857
+ else if (c === '}' || c === ')' || c === ']')
1858
+ depth--;
1859
+ else if (c === ',' && depth === 0) {
1860
+ segs.push(body.slice(start, i));
1861
+ start = i + 1;
1862
+ }
1863
+ }
1864
+ segs.push(body.slice(start));
1865
+ const names = [];
1866
+ for (const seg of segs) {
1867
+ const m = /^\s*(?:\[[^\]]+\]|['"]?[\w$]+['"]?)\s*:\s*([A-Za-z_$][\w$]*)\s*$/.exec(seg);
1868
+ if (m && m[1].length >= 3 && !names.includes(m[1]))
1869
+ names.push(m[1]);
1870
+ }
1871
+ return names;
1872
+ }
1873
+ /** Resolve a registered handler name to its callable entry: a function value, or a class's
1874
+ * `execute`-like method (preferring the method chained at the dispatch site), else the class. */
1875
+ function resolveRegistryHandler(ctx, name, chained) {
1876
+ const cands = ctx.getNodesByName(name);
1877
+ const fn = cands.find((n) => n.kind === 'function');
1878
+ if (fn)
1879
+ return fn;
1880
+ const cls = cands.find((n) => n.kind === 'class' || n.kind === 'struct');
1881
+ if (cls) {
1882
+ const methods = ctx
1883
+ .getNodesInFile(cls.filePath)
1884
+ .filter((n) => n.kind === 'method' && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine));
1885
+ const want = chained && REGISTRY_CLASS_ENTRY.has(chained) ? chained : null;
1886
+ const entry = (want && methods.find((m) => m.name === want)) ||
1887
+ methods.find((m) => REGISTRY_CLASS_ENTRY.has(m.name)) ||
1888
+ methods.find((m) => m.name === 'constructor');
1889
+ return entry ?? cls;
1890
+ }
1891
+ // Require a CALLABLE target — a registry dispatched as `reg[k](…)` invokes a function/
1892
+ // method, never a data `constant` (dropping it removes false positives like a `{ x: URL }`
1893
+ // entry resolving to the global URL constant).
1894
+ return cands.find((n) => n.kind === 'method') ?? null;
1895
+ }
1896
+ function objectRegistryEdges(ctx) {
1897
+ const edges = [];
1898
+ const seen = new Set();
1899
+ for (const file of ctx.getAllFiles()) {
1900
+ if (!REGISTRY_JS_EXT.test(file))
1901
+ continue;
1902
+ const content = ctx.readFile(file);
1903
+ // Cheap pre-filter: a computed member access BY NAME (`ident[ident`) — the dispatch shape.
1904
+ if (!content || !/[\w$]\s*\[\s*[A-Za-z_$]/.test(content))
1905
+ continue;
1906
+ // Skip minified/generated bundles (draco, three.min, base64…): their pervasive `h[x](...)`
1907
+ // calls + single-letter `{a:b}` literals are a false-positive minefield. Average line
1908
+ // length is the reliable tell — real source ~30–80, minified in the hundreds/thousands.
1909
+ const newlines = (content.match(/\n/g)?.length ?? 0) + 1;
1910
+ if (content.length / newlines > 200)
1911
+ continue;
1912
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
1913
+ // 1. Dispatch sites: `(new )?<ref>[<ident-key>]` followed by a call or a chained method.
1914
+ // A quoted-string key (`['save']`) does NOT match — that's a static access, not dispatch.
1915
+ REGISTRY_DISPATCH_RE.lastIndex = 0;
1916
+ const dispatches = [];
1917
+ let dm;
1918
+ while ((dm = REGISTRY_DISPATCH_RE.exec(safe))) {
1919
+ const win = safe.slice(dm.index, dm.index + 160);
1920
+ const cm = /\]\s*\([^)]*\)\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win) || /\]\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win);
1921
+ dispatches.push({ ref: dm[1], line: safe.slice(0, dm.index).split('\n').length, chained: cm ? cm[1] : null });
1922
+ }
1923
+ if (!dispatches.length)
1924
+ continue;
1925
+ // Normalize a leading `this.` so a class FIELD-INITIALIZER registry (`commands = {…}`)
1926
+ // matches a `this.commands[k]` dispatch, not just the constructor form `this.commands = {…}`.
1927
+ const norm = (r) => r.replace(/^this\./, '');
1928
+ const refs = new Set(dispatches.map((d) => norm(d.ref)));
1929
+ // 2. Registries: an object literal assigned to a dispatched ref, ≥2 entries resolving to callables.
1930
+ REGISTRY_ASSIGN_RE.lastIndex = 0;
1931
+ const registries = new Map();
1932
+ let am;
1933
+ while ((am = REGISTRY_ASSIGN_RE.exec(safe))) {
1934
+ const lhs = norm(am[1] ?? am[2]);
1935
+ if (!refs.has(lhs) || registries.has(lhs))
1936
+ continue;
1937
+ const body = braceBody(safe, am.index + am[0].length - 1);
1938
+ if (!body)
1939
+ continue;
1940
+ const names = registryEntryNames(body); // depth-0 `key: Identifier` entries only
1941
+ if (names.length >= REGISTRY_MIN_ENTRIES) {
1942
+ registries.set(lhs, { names, line: safe.slice(0, am.index).split('\n').length });
1943
+ }
1944
+ }
1945
+ if (!registries.size)
1946
+ continue;
1947
+ // 3. Link each dispatcher → each registered handler's callable entry.
1948
+ const nodesInFile = ctx.getNodesInFile(file);
1949
+ for (const d of dispatches) {
1950
+ const reg = registries.get(norm(d.ref));
1951
+ if (!reg)
1952
+ continue;
1953
+ const disp = enclosingFn(nodesInFile, d.line);
1954
+ if (!disp)
1955
+ continue;
1956
+ let added = 0;
1957
+ for (const name of reg.names) {
1958
+ if (added >= REGISTRY_FANOUT_CAP)
1959
+ break;
1960
+ const target = resolveRegistryHandler(ctx, name, d.chained);
1961
+ if (!target || target.id === disp.id)
1962
+ continue;
1963
+ const key = `${disp.id}>${target.id}`;
1964
+ if (seen.has(key))
1965
+ continue;
1966
+ seen.add(key);
1967
+ edges.push({
1968
+ source: disp.id,
1969
+ target: target.id,
1970
+ kind: 'calls',
1971
+ line: d.line,
1972
+ provenance: 'heuristic',
1973
+ metadata: { synthesizedBy: 'object-registry', via: name, registeredAt: `${file}:${reg.line}` },
1974
+ });
1975
+ added++;
1976
+ }
1977
+ }
1978
+ }
1979
+ return edges;
1980
+ }
1981
+ // ── RTK Query generated-hook → endpoint ──────────────────────────────────────
1982
+ // RTK Query generates one `useGetXQuery`/`useUpdateYMutation` hook per endpoint
1983
+ // (`createApi({ endpoints: b => ({ getX: b.query(...) }) })`). Components call the
1984
+ // hook; the fetch logic lives in the endpoint's queryFn. The hook↔endpoint link is
1985
+ // pure NAMING CONVENTION (no static edge): strip `use` + the optional `Lazy`
1986
+ // variant + the `Query|Mutation` suffix, lowercase the head → the endpoint key.
1987
+ // Both are extracted as function nodes (the hook from its `export const {…}=api`
1988
+ // binding, carrying a sentinel signature; the endpoint from the createApi object),
1989
+ // so bridging hook→endpoint connects `component → useGetXQuery → getX → queryFn`.
1990
+ // Gated on the extraction sentinel so it only ever fires on genuinely-generated
1991
+ // hooks (never a hand-written `useFooQuery`), and on a SAME-FILE endpoint (RTK
1992
+ // colocates the hooks and their api in one module) — 0 on any non-RTK repo.
1993
+ const RTK_HOOK_DERIVE_RE = /^use([A-Z][A-Za-z0-9]*?)(?:Query|Mutation)$/;
1994
+ // MUST match the signature set in tree-sitter.ts `extractRtkHookBindings`.
1995
+ const RTK_GENERATED_HOOK_SIGNATURE = '= RTK Query generated hook';
1996
+ /** Derive the endpoint key from a generated-hook name (`useLazyGetRecordsQuery`
1997
+ * → `getRecords`), or null if it doesn't fit the convention. */
1998
+ function rtkEndpointNameFromHook(hook) {
1999
+ const m = RTK_HOOK_DERIVE_RE.exec(hook);
2000
+ if (!m)
2001
+ return null;
2002
+ let mid = m[1];
2003
+ if (mid.startsWith('Lazy'))
2004
+ mid = mid.slice(4); // useLazyGetX → getX (same endpoint)
2005
+ if (!mid)
2006
+ return null;
2007
+ return mid.charAt(0).toLowerCase() + mid.slice(1);
2008
+ }
2009
+ function rtkQueryEdges(queries, ctx) {
2010
+ const edges = [];
2011
+ const seen = new Set();
2012
+ for (const hook of queries.iterateNodesByKind('function')) {
2013
+ // Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
2014
+ if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
2015
+ continue;
2016
+ const endpointName = rtkEndpointNameFromHook(hook.name);
2017
+ if (!endpointName)
2018
+ continue;
2019
+ // The endpoint is a same-file function by the derived name (RTK colocates the
2020
+ // api definition and its generated-hook exports in one module).
2021
+ const target = ctx
2022
+ .getNodesByName(endpointName)
2023
+ .find((n) => n.kind === 'function' && n.filePath === hook.filePath);
2024
+ if (!target || target.id === hook.id)
2025
+ continue;
2026
+ const key = `${hook.id}>${target.id}`;
2027
+ if (seen.has(key))
2028
+ continue;
2029
+ seen.add(key);
2030
+ edges.push({
2031
+ source: hook.id,
2032
+ target: target.id,
2033
+ kind: 'calls',
2034
+ line: hook.startLine,
2035
+ provenance: 'heuristic',
2036
+ metadata: { synthesizedBy: 'rtk-query', via: endpointName, registeredAt: `${hook.filePath}:${hook.startLine}` },
2037
+ });
2038
+ }
2039
+ return edges;
2040
+ }
2041
+ // ── Pinia useStore().action() dispatch bridge ────────────────────────────────
2042
+ // A Pinia store factory `export const useXStore = defineStore(...)` exposes its
2043
+ // actions as methods on the store instance; a consumer does `const s = useXStore()`
2044
+ // then `s.action()`. The call is a method-on-instance with no static edge to the
2045
+ // action (which lives in the store's module). Bridge it: map each factory → its
2046
+ // file, bind `const <var> = useXStore()` per consumer file, and link the enclosing
2047
+ // function → the `<var>.method()` action node IN THE STORE'S FILE. The same-store-
2048
+ // file gate keeps it precise (a Pinia built-in like `$patch` or an unrelated
2049
+ // same-named method resolves to nothing). Covers both the options and setup store
2050
+ // forms uniformly (the action is a function node in the store file either way).
2051
+ const PINIA_CONSUMER_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs|vue)$/;
2052
+ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g;
2053
+ const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
2054
+ const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
2055
+ const PINIA_FANOUT_CAP = 80;
2056
+ function piniaStoreEdges(ctx) {
2057
+ // 1. Map each `const useXStore = defineStore(...)` factory → its store file.
2058
+ const factoryFile = new Map();
2059
+ for (const file of ctx.getAllFiles()) {
2060
+ if (!PINIA_CONSUMER_EXT.test(file))
2061
+ continue;
2062
+ const content = ctx.readFile(file);
2063
+ if (!content || !content.includes('defineStore'))
2064
+ continue;
2065
+ PINIA_FACTORY_RE.lastIndex = 0;
2066
+ let m;
2067
+ while ((m = PINIA_FACTORY_RE.exec(content)))
2068
+ factoryFile.set(m[1], file);
2069
+ }
2070
+ if (!factoryFile.size)
2071
+ return [];
2072
+ const edges = [];
2073
+ const seen = new Set();
2074
+ for (const file of ctx.getAllFiles()) {
2075
+ if (!PINIA_CONSUMER_EXT.test(file))
2076
+ continue;
2077
+ const content = ctx.readFile(file);
2078
+ if (!content || !content.includes('Store'))
2079
+ continue;
2080
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
2081
+ // 2. Bind store vars in this file: `const <var> = <known-factory>(...)`.
2082
+ const varStore = new Map();
2083
+ PINIA_BIND_RE.lastIndex = 0;
2084
+ let bm;
2085
+ while ((bm = PINIA_BIND_RE.exec(safe))) {
2086
+ const sf = factoryFile.get(bm[2]);
2087
+ if (sf)
2088
+ varStore.set(bm[1], sf);
2089
+ }
2090
+ if (!varStore.size)
2091
+ continue;
2092
+ // 3. Link `<var>.<method>(` → the action function node in the store's file.
2093
+ const nodesInFile = ctx.getNodesInFile(file);
2094
+ const fallbackDispatcher = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level setup
2095
+ PINIA_CALL_RE.lastIndex = 0;
2096
+ let cm;
2097
+ let added = 0;
2098
+ while ((cm = PINIA_CALL_RE.exec(safe)) && added < PINIA_FANOUT_CAP) {
2099
+ const storeFile = varStore.get(cm[1]);
2100
+ if (!storeFile)
2101
+ continue;
2102
+ const method = cm[2];
2103
+ const line = safe.slice(0, cm.index).split('\n').length;
2104
+ const disp = enclosingFn(nodesInFile, line) ?? fallbackDispatcher;
2105
+ if (!disp)
2106
+ continue;
2107
+ const target = ctx
2108
+ .getNodesByName(method)
2109
+ .find((n) => n.kind === 'function' && n.filePath === storeFile);
2110
+ if (!target || target.id === disp.id)
2111
+ continue;
2112
+ const key = `${disp.id}>${target.id}`;
2113
+ if (seen.has(key))
2114
+ continue;
2115
+ seen.add(key);
2116
+ edges.push({
2117
+ source: disp.id,
2118
+ target: target.id,
2119
+ kind: 'calls',
2120
+ line,
2121
+ provenance: 'heuristic',
2122
+ metadata: { synthesizedBy: 'pinia-store', via: method, registeredAt: `${file}:${line}` },
2123
+ });
2124
+ added++;
2125
+ }
2126
+ }
2127
+ return edges;
2128
+ }
2129
+ // ── Vuex string-keyed dispatch / commit bridge ───────────────────────────────
2130
+ // Vuex dispatches actions/mutations by a runtime STRING key: `dispatch('user/login')`
2131
+ // / `commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')`. The action
2132
+ // & mutation definitions are object-literal methods in store module files (now
2133
+ // extracted as function nodes). Bridge the string key to its node: the LAST `/`
2134
+ // segment is the action/mutation name; the preceding segment is the namespace
2135
+ // (≈ the store module's file). Resolve the name to a function node IN A STORE FILE
2136
+ // (the store-file gate excludes a same-named `api/` helper — `getInfo`/`login`
2137
+ // commonly collide), disambiguated by the namespace appearing in the path (or, for
2138
+ // a root key, the same file — Vuex's local-module `commit('M')` inside an action).
2139
+ const VUEX_DISPATCH_RE = /\b(?:dispatch|commit)\s*\(\s*['"]([A-Za-z][\w/]*)['"]/g;
2140
+ const VUEX_STORE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
2141
+ const VUEX_FANOUT_CAP = 120;
2142
+ /** A path segment (dir or filename stem) equals `seg` — `…/modules/user.js` has
2143
+ * the segment `user` for namespace `user`. */
2144
+ function pathHasSegment(filePath, seg) {
2145
+ return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
2146
+ }
2147
+ function vuexDispatchEdges(ctx) {
2148
+ const storeFileCache = new Map();
2149
+ const isStoreFile = (file) => {
2150
+ let v = storeFileCache.get(file);
2151
+ if (v === undefined) {
2152
+ const c = ctx.readFile(file);
2153
+ const seen = new Set();
2154
+ if (c) {
2155
+ VUEX_STORE_SIGNAL.lastIndex = 0;
2156
+ let sm;
2157
+ while ((sm = VUEX_STORE_SIGNAL.exec(c))) {
2158
+ seen.add(sm[0]);
2159
+ if (seen.size >= 2)
2160
+ break;
2161
+ }
2162
+ }
2163
+ v = seen.size >= 2;
2164
+ storeFileCache.set(file, v);
2165
+ }
2166
+ return v;
2167
+ };
2168
+ const resolve = (key, dispatchFile) => {
2169
+ const segs = key.split('/');
2170
+ const action = segs[segs.length - 1];
2171
+ const cands = ctx.getNodesByName(action).filter((n) => n.kind === 'function' && isStoreFile(n.filePath));
2172
+ if (!cands.length)
2173
+ return null;
2174
+ if (segs.length > 1) {
2175
+ const mod = segs[segs.length - 2]; // immediate namespace ≈ the module file
2176
+ return cands.find((c) => pathHasSegment(c.filePath, mod)) ?? (cands.length === 1 ? cands[0] : null);
2177
+ }
2178
+ // Root key: a local `commit('M')` inside an action targets the same module file;
2179
+ // otherwise accept only an unambiguous single store-wide match.
2180
+ return cands.find((c) => c.filePath === dispatchFile) ?? (cands.length === 1 ? cands[0] : null);
2181
+ };
2182
+ const edges = [];
2183
+ const seen = new Set();
2184
+ for (const file of ctx.getAllFiles()) {
2185
+ if (!PINIA_CONSUMER_EXT.test(file))
2186
+ continue;
2187
+ const content = ctx.readFile(file);
2188
+ if (!content || (!content.includes('dispatch(') && !content.includes('commit(')))
2189
+ continue;
2190
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
2191
+ const nodesInFile = ctx.getNodesInFile(file);
2192
+ const fallback = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level
2193
+ VUEX_DISPATCH_RE.lastIndex = 0;
2194
+ let m;
2195
+ let added = 0;
2196
+ while ((m = VUEX_DISPATCH_RE.exec(safe)) && added < VUEX_FANOUT_CAP) {
2197
+ const key = m[1];
2198
+ const line = safe.slice(0, m.index).split('\n').length;
2199
+ const disp = enclosingFn(nodesInFile, line) ?? fallback;
2200
+ if (!disp)
2201
+ continue;
2202
+ const target = resolve(key, file);
2203
+ if (!target || target.id === disp.id)
2204
+ continue;
2205
+ const edgeKey = `${disp.id}>${target.id}`;
2206
+ if (seen.has(edgeKey))
2207
+ continue;
2208
+ seen.add(edgeKey);
2209
+ edges.push({
2210
+ source: disp.id,
2211
+ target: target.id,
2212
+ kind: 'calls',
2213
+ line,
2214
+ provenance: 'heuristic',
2215
+ metadata: { synthesizedBy: 'vuex-dispatch', via: key, registeredAt: `${file}:${line}` },
2216
+ });
2217
+ added++;
2218
+ }
2219
+ }
2220
+ return edges;
2221
+ }
2222
+ // ── Celery task dispatch (Python) ─────────────────────────────────────────────
2223
+ // Celery decouples a task's call site from its body through async dispatch:
2224
+ // # tasks.py
2225
+ // @shared_task # also @app.task / @celery_app.task / @<app>.task / @task
2226
+ // def process(account_ids): ...
2227
+ // # views.py — a DIFFERENT module
2228
+ // process.apply_async(kwargs={...}) # or process.delay(...) — dynamic, no static edge
2229
+ // Bridge it: link the enclosing function/method at each `.delay(`/`.apply_async(` site → the
2230
+ // task function body. Precision rests on the DECORATOR gate — the dispatched name must resolve
2231
+ // to a Python function carrying a celery task decorator (read from the source lines above its
2232
+ // `def`, since the def's own startLine excludes the decorator). A `.delay()` on a non-task
2233
+ // object resolves to no task node → no edge, so a Celery-free repo yields 0. Same-file /
2234
+ // unique-candidate disambiguation like vuex. (Canvas forms — `group(t).delay()`, `t.s()`/`.si()`
2235
+ // — have no single identifier before `.delay`/`.apply_async`, so they're skipped, not mis-bridged.)
2236
+ const CELERY_DISPATCH_RE = /\b([A-Za-z_]\w*)\s*\.\s*(?:delay|apply_async)\s*\(/g;
2237
+ // A task decorator: bare `@shared_task`/`@task` or attribute `@app.task`/`@celery_app.task`,
2238
+ // each optionally called with args. `\b`-bounded and `@`-anchored so `@mytask`, or a symbol
2239
+ // merely named `task`, can't match. No `/g`, so `.test()` is stateless across reuse.
2240
+ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)\b/;
2241
+ const CELERY_PY_EXT = /\.py$/;
2242
+ const CELERY_FANOUT_CAP = 80;
2243
+ const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
2244
+ function celeryDispatchEdges(ctx) {
2245
+ // Memoize the decorator check per task-candidate node: it reads the file and scans a few
2246
+ // lines above the def. Only called on names that are actually `.delay`/`.apply_async`
2247
+ // receivers, so the candidate set stays small.
2248
+ const taskCache = new Map();
2249
+ const isCeleryTask = (node) => {
2250
+ let v = taskCache.get(node.id);
2251
+ if (v !== undefined)
2252
+ return v;
2253
+ v = false;
2254
+ if (node.kind === 'function' && CELERY_PY_EXT.test(node.filePath)) {
2255
+ const content = ctx.readFile(node.filePath);
2256
+ if (content) {
2257
+ const lines = content.split('\n');
2258
+ // startLine is the `def` line (decorators sit ABOVE it). Walk upward, stopping at the
2259
+ // previous declaration so a non-task def can never inherit the prior def's decorator.
2260
+ const stop = Math.max(0, node.startLine - 1 - CELERY_DECORATOR_LOOKBACK);
2261
+ for (let i = node.startLine - 2; i >= stop; i--) {
2262
+ const t = (lines[i] ?? '').trim();
2263
+ if (/^(?:async\s+def|def|class)\b/.test(t))
2264
+ break; // previous decl → stop
2265
+ if (CELERY_TASK_DECORATOR_RE.test(t)) {
2266
+ v = true;
2267
+ break;
2268
+ }
2269
+ }
2270
+ }
2271
+ }
2272
+ taskCache.set(node.id, v);
2273
+ return v;
2274
+ };
2275
+ const resolve = (name, dispatchFile) => {
2276
+ const cands = ctx.getNodesByName(name).filter((n) => n.kind === 'function' && isCeleryTask(n));
2277
+ if (!cands.length)
2278
+ return null;
2279
+ if (cands.length === 1)
2280
+ return cands[0];
2281
+ // Cross-module name collision: prefer a task defined in the dispatching file, else bail
2282
+ // (ambiguous — precision over recall, like vuex's root-key resolution).
2283
+ return cands.find((c) => c.filePath === dispatchFile) ?? null;
2284
+ };
2285
+ const edges = [];
2286
+ const seen = new Set();
2287
+ for (const file of ctx.getAllFiles()) {
2288
+ if (!CELERY_PY_EXT.test(file))
2289
+ continue;
2290
+ const content = ctx.readFile(file);
2291
+ if (!content || (!content.includes('.delay(') && !content.includes('.apply_async(')))
2292
+ continue;
2293
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'python');
2294
+ const nodesInFile = ctx.getNodesInFile(file);
2295
+ CELERY_DISPATCH_RE.lastIndex = 0;
2296
+ let m;
2297
+ let added = 0;
2298
+ while ((m = CELERY_DISPATCH_RE.exec(safe)) && added < CELERY_FANOUT_CAP) {
2299
+ const name = m[1];
2300
+ const line = safe.slice(0, m.index).split('\n').length;
2301
+ const disp = enclosingFn(nodesInFile, line);
2302
+ if (!disp)
2303
+ continue; // module-level dispatch — no source symbol to attribute
2304
+ const target = resolve(name, file);
2305
+ if (!target || target.id === disp.id)
2306
+ continue;
2307
+ const key = `${disp.id}>${target.id}`;
2308
+ if (seen.has(key))
2309
+ continue;
2310
+ seen.add(key);
2311
+ edges.push({
2312
+ source: disp.id,
2313
+ target: target.id,
2314
+ kind: 'calls',
2315
+ line,
2316
+ provenance: 'heuristic',
2317
+ metadata: { synthesizedBy: 'celery-dispatch', via: name, registeredAt: `${file}:${line}` },
2318
+ });
2319
+ added++;
2320
+ }
2321
+ }
2322
+ return edges;
2323
+ }
2324
+ // ── Spring application events (Java) ──────────────────────────────────────────
2325
+ // Spring decouples an event PUBLISHER from its LISTENER(s) through the application
2326
+ // event bus, linked by the EVENT TYPE (not a name):
2327
+ // // SomeService.java
2328
+ // eventPublisher.publishEvent(new PasswordChangedEvent(this, username)); // publish
2329
+ // // RememberMeTokenRevoker.java — a DIFFERENT file
2330
+ // @EventListener(PasswordChangedEvent.class) // listen
2331
+ // public void onPasswordChanged(PasswordChangedEvent event) { ... }
2332
+ // Bridge it: link the enclosing method at each `publishEvent(new XEvent(...))` site →
2333
+ // every listener method of XEvent. Listeners are `@EventListener` / `@TransactionalEventListener`
2334
+ // methods (event type = the first param type, or the `@EventListener(X.class)` value form) and
2335
+ // the older `class … implements ApplicationListener<X> { void onApplicationEvent(X e) }`. Keyed
2336
+ // by exact type name, usually cross-file. A repo with no `@EventListener`/`publishEvent` yields 0.
2337
+ // (Java method nodes INCLUDE their leading annotations in the range — startLine is the first
2338
+ // `@…` line — so the annotation block is scanned DOWNWARD from startLine, bounded to consecutive
2339
+ // `@`-lines so it can't bleed into an adjacent method.)
2340
+ const SPRING_PUBLISH_RE = /\.publishEvent\s*\(\s*new\s+([A-Z][A-Za-z0-9_]*)/g;
2341
+ const SPRING_LISTENER_ANNO_RE = /@(?:EventListener|TransactionalEventListener)\b/;
2342
+ const SPRING_ANNO_TYPE_RE = /@(?:EventListener|TransactionalEventListener)\s*\(\s*([A-Z][A-Za-z0-9_]*)\.class/;
2343
+ const SPRING_APP_LISTENER_RE = /\bApplicationListener\s*</;
2344
+ const SPRING_JAVA_EXT = /\.java$/;
2345
+ const SPRING_FANOUT_CAP = 80;
2346
+ /** The first parameter's type from a Java method `signature` (`"void (XEvent e)"` → `XEvent`).
2347
+ * Skips a leading `final`/`@Anno`, strips generics, and requires a PascalCase class name (event
2348
+ * types are classes) — so a no-arg or primitive-param method yields null. */
2349
+ function springFirstParamType(sig) {
2350
+ if (!sig)
2351
+ return null;
2352
+ const open = sig.indexOf('(');
2353
+ if (open < 0)
2354
+ return null;
2355
+ const close = sig.indexOf(')', open);
2356
+ const inner = sig.slice(open + 1, close < 0 ? sig.length : close).trim();
2357
+ if (!inner)
2358
+ return null;
2359
+ const first = inner.split(',')[0].trim();
2360
+ const toks = first.split(/\s+/).filter((t) => t && t !== 'final' && !t.startsWith('@'));
2361
+ if (toks.length < 2)
2362
+ return null; // need `Type name`
2363
+ const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
2364
+ return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
2365
+ }
2366
+ function springEventEdges(ctx) {
2367
+ // Pass 1 — event-type → listener methods, scanning only event-relevant files.
2368
+ const listeners = new Map();
2369
+ for (const file of ctx.getAllFiles()) {
2370
+ if (!SPRING_JAVA_EXT.test(file))
2371
+ continue;
2372
+ const content = ctx.readFile(file);
2373
+ if (!content)
2374
+ continue;
2375
+ const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
2376
+ const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
2377
+ if (!hasAnno && !hasAppListener)
2378
+ continue;
2379
+ const lines = content.split('\n');
2380
+ for (const node of ctx.getNodesInFile(file)) {
2381
+ if (node.kind !== 'method')
2382
+ continue;
2383
+ // Collect this method's own leading annotation block (consecutive `@`-lines from startLine).
2384
+ const annoLines = [];
2385
+ for (let i = node.startLine - 1; i < lines.length && i < node.startLine + 7; i++) {
2386
+ const t = (lines[i] ?? '').trim();
2387
+ if (!t.startsWith('@'))
2388
+ break; // reached the declaration → stop (no bleed into next method)
2389
+ annoLines.push(t);
2390
+ }
2391
+ const head = annoLines.join('\n');
2392
+ const annotated = hasAnno && SPRING_LISTENER_ANNO_RE.test(head);
2393
+ const isAppListener = hasAppListener && node.name === 'onApplicationEvent';
2394
+ if (!annotated && !isAppListener)
2395
+ continue;
2396
+ let type = springFirstParamType(node.signature);
2397
+ if (!type && annotated) {
2398
+ const m = SPRING_ANNO_TYPE_RE.exec(head);
2399
+ if (m)
2400
+ type = m[1];
2401
+ }
2402
+ if (!type)
2403
+ continue;
2404
+ let arr = listeners.get(type);
2405
+ if (!arr) {
2406
+ arr = [];
2407
+ listeners.set(type, arr);
2408
+ }
2409
+ arr.push(node);
2410
+ }
2411
+ }
2412
+ if (!listeners.size)
2413
+ return [];
2414
+ // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
2415
+ const edges = [];
2416
+ const seen = new Set();
2417
+ for (const file of ctx.getAllFiles()) {
2418
+ if (!SPRING_JAVA_EXT.test(file))
2419
+ continue;
2420
+ const content = ctx.readFile(file);
2421
+ if (!content || !content.includes('.publishEvent('))
2422
+ continue;
2423
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'java');
2424
+ const nodesInFile = ctx.getNodesInFile(file);
2425
+ SPRING_PUBLISH_RE.lastIndex = 0;
2426
+ let m;
2427
+ let added = 0;
2428
+ while ((m = SPRING_PUBLISH_RE.exec(safe)) && added < SPRING_FANOUT_CAP) {
2429
+ const targets = listeners.get(m[1]);
2430
+ if (!targets || !targets.length)
2431
+ continue;
2432
+ const line = safe.slice(0, m.index).split('\n').length;
2433
+ const disp = enclosingFn(nodesInFile, line);
2434
+ if (!disp)
2435
+ continue;
2436
+ for (const target of targets) {
2437
+ if (target.id === disp.id)
2438
+ continue;
2439
+ const key = `${disp.id}>${target.id}`;
2440
+ if (seen.has(key))
2441
+ continue;
2442
+ seen.add(key);
2443
+ edges.push({
2444
+ source: disp.id,
2445
+ target: target.id,
2446
+ kind: 'calls',
2447
+ line,
2448
+ provenance: 'heuristic',
2449
+ metadata: { synthesizedBy: 'spring-event', via: m[1], registeredAt: `${file}:${line}` },
2450
+ });
2451
+ added++;
2452
+ }
2453
+ }
2454
+ }
2455
+ return edges;
2456
+ }
2457
+ // ── MediatR request/notification dispatch (C#/.NET) ───────────────────────────
2458
+ // MediatR decouples a Send/Publish call site from its Handle method through a mediator,
2459
+ // linked by the request/notification TYPE (the IRequestHandler<T,…> generic):
2460
+ // // CancelOrderCommandHandler.cs — the handler
2461
+ // public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool> {
2462
+ // public async Task<bool> Handle(CancelOrderCommand request, CancellationToken ct) { … }
2463
+ // // some controller — the dispatch (usually a DIFFERENT file)
2464
+ // var command = new CancelOrderCommand(orderId); await _mediator.Send(command);
2465
+ // Bridge it: link the enclosing method at each mediator `.Send(x)`/`.Publish(x)` site → the
2466
+ // `Handle` method of the handler for x's type. The sent type is resolved from the argument —
2467
+ // inline `new X(…)`, a local `var v = new X(…)`, or a parameter/local declared `X v` — bounded
2468
+ // to the enclosing method. Precision rests on TWO gates: the receiver must be mediator-ish
2469
+ // (`mediator`/`sender`/`publisher`, so MAUI `MessagingCenter.Send` is ignored) AND the resolved
2470
+ // type must be a known handler request type (so a same-named non-request DTO is never bridged).
2471
+ // C# has no `signature` on method nodes, so the handler's request type is read from the class
2472
+ // base-list source (`: IRequestHandler<X,…>`), not a param signature.
2473
+ const MEDIATR_HANDLER_BASE_RE = /(?:IRequestHandler|INotificationHandler)\s*<\s*([A-Za-z_]\w*)/;
2474
+ const MEDIATR_DISPATCH_RE = /([A-Za-z_][\w.]*)\s*\.\s*(?:Send|Publish)\s*\(\s*(new\s+[A-Z]\w*|[A-Za-z_]\w*)/g;
2475
+ const MEDIATR_RECEIVER_RE = /(?:mediator|sender|publisher)/i;
2476
+ const MEDIATR_CS_EXT = /\.cs$/;
2477
+ const MEDIATR_FANOUT_CAP = 80;
2478
+ const MEDIATR_HANDLER_DECL_LOOKAHEAD = 4; // lines from a class startLine to find a wrapped base list
2479
+ /** The type sent at a MediatR `.Send(arg)`/`.Publish(arg)` site: an inline `new X(…)`, else
2480
+ * `arg` as an identifier resolved within the enclosing method — a `… arg = new X(…)` assignment
2481
+ * (wins), or a parameter/local declared `X arg`. Returns null when the type can't be seen. */
2482
+ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
2483
+ const inl = /^new\s+([A-Z]\w*)/.exec(arg);
2484
+ if (inl)
2485
+ return inl[1];
2486
+ if (!/^[A-Za-z_]\w*$/.test(arg))
2487
+ return null;
2488
+ const assignRe = new RegExp(`\\b${arg}\\b\\s*=\\s*new\\s+([A-Z]\\w*)`);
2489
+ const declRe = new RegExp(`\\b([A-Z]\\w*)\\b\\s+${arg}\\b`);
2490
+ let declType = null;
2491
+ for (let i = Math.max(0, methodStart - 1); i < dispatchLine && i < lines.length; i++) {
2492
+ const ln = lines[i] ?? '';
2493
+ const a = assignRe.exec(ln);
2494
+ if (a)
2495
+ return a[1]; // an explicit `arg = new X` is the most specific — take it
2496
+ if (!declType) {
2497
+ const d = declRe.exec(ln);
2498
+ if (d)
2499
+ declType = d[1]; // a `X arg` declaration — remember, but keep scanning for an assignment
2500
+ }
2501
+ }
2502
+ return declType;
2503
+ }
2504
+ function mediatrDispatchEdges(ctx) {
2505
+ // Pass 1 — request/notification type → the Handle method of each handler class.
2506
+ const handlers = new Map();
2507
+ for (const file of ctx.getAllFiles()) {
2508
+ if (!MEDIATR_CS_EXT.test(file))
2509
+ continue;
2510
+ const content = ctx.readFile(file);
2511
+ if (!content || (!content.includes('IRequestHandler<') && !content.includes('INotificationHandler<')))
2512
+ continue;
2513
+ const lines = content.split('\n');
2514
+ const nodesInFile = ctx.getNodesInFile(file);
2515
+ for (const cls of nodesInFile) {
2516
+ if (cls.kind !== 'class')
2517
+ continue;
2518
+ const decl = lines.slice(cls.startLine - 1, cls.startLine - 1 + MEDIATR_HANDLER_DECL_LOOKAHEAD).join('\n');
2519
+ const m = MEDIATR_HANDLER_BASE_RE.exec(decl);
2520
+ if (!m)
2521
+ continue;
2522
+ const type = m[1];
2523
+ const end = cls.endLine ?? cls.startLine;
2524
+ const handle = nodesInFile.find((n) => n.kind === 'method' && n.name === 'Handle' && n.startLine >= cls.startLine && n.startLine <= end);
2525
+ if (!handle)
2526
+ continue;
2527
+ let arr = handlers.get(type);
2528
+ if (!arr) {
2529
+ arr = [];
2530
+ handlers.set(type, arr);
2531
+ }
2532
+ arr.push(handle);
2533
+ }
2534
+ }
2535
+ if (!handlers.size)
2536
+ return [];
2537
+ // Pass 2 — link each mediator-ish .Send(x)/.Publish(x) → the handler of x's type.
2538
+ const edges = [];
2539
+ const seen = new Set();
2540
+ for (const file of ctx.getAllFiles()) {
2541
+ if (!MEDIATR_CS_EXT.test(file))
2542
+ continue;
2543
+ const content = ctx.readFile(file);
2544
+ if (!content || (!content.includes('.Send(') && !content.includes('.Publish(')))
2545
+ continue;
2546
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'csharp');
2547
+ const safeLines = safe.split('\n');
2548
+ const nodesInFile = ctx.getNodesInFile(file);
2549
+ MEDIATR_DISPATCH_RE.lastIndex = 0;
2550
+ let m;
2551
+ let added = 0;
2552
+ while ((m = MEDIATR_DISPATCH_RE.exec(safe)) && added < MEDIATR_FANOUT_CAP) {
2553
+ if (!MEDIATR_RECEIVER_RE.test(m[1]))
2554
+ continue; // not a mediator (MessagingCenter, HttpClient, …)
2555
+ const line = safe.slice(0, m.index).split('\n').length;
2556
+ const disp = enclosingFn(nodesInFile, line);
2557
+ if (!disp)
2558
+ continue;
2559
+ const type = resolveMediatrArgType(m[2], safeLines, disp.startLine, line);
2560
+ if (!type)
2561
+ continue;
2562
+ const targets = handlers.get(type);
2563
+ if (!targets)
2564
+ continue;
2565
+ for (const target of targets) {
2566
+ if (target.id === disp.id)
2567
+ continue;
2568
+ const key = `${disp.id}>${target.id}`;
2569
+ if (seen.has(key))
2570
+ continue;
2571
+ seen.add(key);
2572
+ edges.push({
2573
+ source: disp.id,
2574
+ target: target.id,
2575
+ kind: 'calls',
2576
+ line,
2577
+ provenance: 'heuristic',
2578
+ metadata: { synthesizedBy: 'mediatr-dispatch', via: type, registeredAt: `${file}:${line}` },
2579
+ });
2580
+ added++;
2581
+ }
2582
+ }
2583
+ }
2584
+ return edges;
2585
+ }
2586
+ // ── Sidekiq job dispatch (Ruby) ───────────────────────────────────────────────
2587
+ // Sidekiq decouples a job's enqueue site from the worker's `perform`, linked by the WORKER
2588
+ // CLASS NAME:
2589
+ // # app/workers/destroy_user_worker.rb
2590
+ // class DestroyUserWorker
2591
+ // include Sidekiq::Worker # or Sidekiq::Job (the modern alias)
2592
+ // def perform(user_id) … end
2593
+ // # app/services/… — a DIFFERENT file
2594
+ // DestroyUserWorker.perform_async(user.id) # also .perform_in(t, …) / .perform_at(t, …)
2595
+ // Bridge it: link the enclosing method at each `Worker.perform_async/_in/_at(…)` site → that
2596
+ // worker's instance `perform`. Name-keyed (like Celery): the receiver class must be a Sidekiq
2597
+ // worker — gated by reading `include Sidekiq::Job|Worker` from the class body, since that mixin
2598
+ // is an external gem module that forms no resolvable edge. ActiveJob's `perform_later`/`_now` is
2599
+ // a different shape and deliberately not matched, so an ActiveJob-only app yields 0.
2600
+ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\s*perform_(?:async|in|at)\b/g;
2601
+ const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
2602
+ const SIDEKIQ_RB_EXT = /\.rb$/;
2603
+ const SIDEKIQ_FANOUT_CAP = 80;
2604
+ function sidekiqDispatchEdges(ctx) {
2605
+ // class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
2606
+ // memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
2607
+ const performCache = new Map();
2608
+ const performOf = (cls) => {
2609
+ let v = performCache.get(cls.id);
2610
+ if (v !== undefined)
2611
+ return v;
2612
+ v = null;
2613
+ const content = ctx.readFile(cls.filePath);
2614
+ if (content) {
2615
+ const end = cls.endLine ?? cls.startLine;
2616
+ const body = content.split('\n').slice(cls.startLine - 1, end).join('\n');
2617
+ if (SIDEKIQ_WORKER_RE.test(body)) {
2618
+ v = ctx.getNodesInFile(cls.filePath).find((n) => n.kind === 'method' && n.name === 'perform' && n.startLine >= cls.startLine && n.startLine <= end) ?? null;
2619
+ }
2620
+ }
2621
+ performCache.set(cls.id, v);
2622
+ return v;
2623
+ };
2624
+ // Resolve a (possibly namespaced) worker reference to its `perform`. A namespaced ref is
2625
+ // matched by EXACT qualified name first, so same-named workers in different namespaces
2626
+ // (forem has four `SendEmailNotificationWorker`s) resolve to the right one; an unqualified
2627
+ // ref falls back to the simple name and links only when a single worker bears it — an
2628
+ // ambiguous collision bails (precision over recall).
2629
+ const resolve = (ref) => {
2630
+ if (ref.includes('::')) {
2631
+ const q = ctx.getNodesByQualifiedName(ref).find((n) => n.kind === 'class' && performOf(n));
2632
+ if (q)
2633
+ return performOf(q);
2634
+ }
2635
+ const workers = ctx.getNodesByName(ref.split('::').pop()).filter((n) => n.kind === 'class' && performOf(n));
2636
+ return workers.length === 1 ? performOf(workers[0]) : null;
2637
+ };
2638
+ const edges = [];
2639
+ const seen = new Set();
2640
+ for (const file of ctx.getAllFiles()) {
2641
+ if (!SIDEKIQ_RB_EXT.test(file))
2642
+ continue;
2643
+ const content = ctx.readFile(file);
2644
+ if (!content || !/\.perform_(?:async|in|at)\b/.test(content))
2645
+ continue;
2646
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'ruby');
2647
+ const nodesInFile = ctx.getNodesInFile(file);
2648
+ SIDEKIQ_DISPATCH_RE.lastIndex = 0;
2649
+ let m;
2650
+ let added = 0;
2651
+ while ((m = SIDEKIQ_DISPATCH_RE.exec(safe)) && added < SIDEKIQ_FANOUT_CAP) {
2652
+ const line = safe.slice(0, m.index).split('\n').length;
2653
+ const disp = enclosingFn(nodesInFile, line);
2654
+ if (!disp)
2655
+ continue;
2656
+ const target = resolve(m[1]);
2657
+ if (!target || target.id === disp.id)
2658
+ continue;
2659
+ const key = `${disp.id}>${target.id}`;
2660
+ if (seen.has(key))
2661
+ continue;
2662
+ seen.add(key);
2663
+ edges.push({
2664
+ source: disp.id,
2665
+ target: target.id,
2666
+ kind: 'calls',
2667
+ line,
2668
+ provenance: 'heuristic',
2669
+ metadata: { synthesizedBy: 'sidekiq-dispatch', via: m[1], registeredAt: `${file}:${line}` },
2670
+ });
2671
+ added++;
2672
+ }
2673
+ }
2674
+ return edges;
2675
+ }
2676
+ // ── Laravel events (PHP) ──────────────────────────────────────────────────────
2677
+ // Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
2678
+ // // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
2679
+ // class UpdateLastfmNowPlaying { public function handle(PlaybackStarted $event) { … } }
2680
+ // // a controller / service — a DIFFERENT file
2681
+ // event(new PlaybackStarted($song, $user));
2682
+ // Bridge it: link the enclosing method at each `event(new XEvent(...))` site → every listener's
2683
+ // `handle` for XEvent. Listeners come from TWO registration mechanisms (both real, both needed):
2684
+ // (A) auto-discovery — a `handle(EventType $e)` typed first param (also splits a union A|B);
2685
+ // (B) the `protected $listen = [ XEvent::class => [Listener::class, …] ]` map in an
2686
+ // EventServiceProvider, which also covers a listener whose `handle()` is UNTYPED.
2687
+ // Only `event(new X)` is matched — queued JOBS dispatch via `::dispatch()` and their `handle()`
2688
+ // takes an injected service, never an event type, so jobs are excluded by construction.
2689
+ const LARAVEL_DISPATCH_RE = /\bevent\s*\(\s*new\s+\\?([A-Za-z_][\w\\]*)/g;
2690
+ const LARAVEL_PHP_EXT = /\.php$/;
2691
+ const LARAVEL_FANOUT_CAP = 200;
2692
+ // A `$listen` entry: `Event::class => [Listener::class, …]`, key/values as `::class` or strings.
2693
+ const LISTEN_ENTRY_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")\s*=>\s*\[([^\]]*)\]/g;
2694
+ const LISTEN_CLASS_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")/g;
2695
+ /** Short class name from a PHP reference: `\App\Events\Foo` / `App\Events::Foo` → `Foo`. */
2696
+ function phpSimpleName(s) {
2697
+ return s.replace(/^\\/, '').split('\\').pop().split('::').pop().trim();
2698
+ }
2699
+ /** The first-parameter class type(s) of a `handle(...)` declaration — union-split, short-named,
2700
+ * primitives dropped. `handle(A|B $e)` → [A, B]; `handle(string $x)` / `handle()` → []. */
2701
+ function laravelHandleEventTypes(decl) {
2702
+ const m = /function\s+handle\s*\(\s*(?:\.\.\.\s*)?(\??[A-Za-z_\\][\w\\|]*)\s+&?\s*(?:\.\.\.\s*)?\$/.exec(decl);
2703
+ if (!m)
2704
+ return [];
2705
+ return m[1]
2706
+ .replace(/^\?/, '')
2707
+ .split('|')
2708
+ .map((t) => phpSimpleName(t))
2709
+ .filter((t) => /^[A-Z]\w*$/.test(t));
2710
+ }
2711
+ /** From an opening `[`, the bracket-balanced body up to its matching `]`. */
2712
+ function phpArrayBody(src, openIdx) {
2713
+ let depth = 0;
2714
+ for (let i = openIdx; i < src.length; i++) {
2715
+ if (src[i] === '[')
2716
+ depth++;
2717
+ else if (src[i] === ']' && --depth === 0)
2718
+ return src.slice(openIdx + 1, i);
2719
+ }
2720
+ return null;
2721
+ }
2722
+ function laravelEventEdges(ctx) {
2723
+ // event short name → its listener `handle` methods (deduped by node id).
2724
+ const listeners = new Map();
2725
+ const add = (event, handle) => {
2726
+ let m = listeners.get(event);
2727
+ if (!m) {
2728
+ m = new Map();
2729
+ listeners.set(event, m);
2730
+ }
2731
+ m.set(handle.id, handle);
2732
+ };
2733
+ const handleOf = (cls) => ctx
2734
+ .getNodesInFile(cls.filePath)
2735
+ .find((n) => n.kind === 'method' && n.name === 'handle'
2736
+ && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
2737
+ // Pass 1 — build the event→handle map from both registration mechanisms.
2738
+ for (const file of ctx.getAllFiles()) {
2739
+ if (!LARAVEL_PHP_EXT.test(file))
2740
+ continue;
2741
+ const content = ctx.readFile(file);
2742
+ if (!content)
2743
+ continue;
2744
+ // (A) typed listener handles — node-driven, so a commented-out method can't leak in.
2745
+ if (content.includes('function handle')) {
2746
+ const lines = content.split('\n');
2747
+ for (const node of ctx.getNodesInFile(file)) {
2748
+ if (node.kind !== 'method' || node.name !== 'handle')
2749
+ continue;
2750
+ const decl = lines.slice(node.startLine - 1, node.startLine + 2).join('\n');
2751
+ for (const ev of laravelHandleEventTypes(decl))
2752
+ add(ev, node);
2753
+ }
2754
+ }
2755
+ // (B) the EventServiceProvider `$listen` map — parsed from comment-stripped source so a
2756
+ // fully-commented map (firefly's, on auto-discovery) contributes nothing.
2757
+ if (content.includes('$listen')) {
2758
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
2759
+ const decl = safe.search(/\$listen\s*=\s*\[/);
2760
+ const body = decl >= 0 ? phpArrayBody(safe, safe.indexOf('[', decl)) : null;
2761
+ if (body) {
2762
+ LISTEN_ENTRY_RE.lastIndex = 0;
2763
+ let em;
2764
+ while ((em = LISTEN_ENTRY_RE.exec(body))) {
2765
+ const event = phpSimpleName(em[1] ?? em[2] ?? em[3] ?? '');
2766
+ LISTEN_CLASS_RE.lastIndex = 0;
2767
+ let lm;
2768
+ while ((lm = LISTEN_CLASS_RE.exec(em[4]))) {
2769
+ const ln = phpSimpleName(lm[1] ?? lm[2] ?? lm[3] ?? '');
2770
+ const cls = ctx.getNodesByName(ln).find((n) => n.kind === 'class' && handleOf(n));
2771
+ if (cls)
2772
+ add(event, handleOf(cls));
2773
+ }
2774
+ }
2775
+ }
2776
+ }
2777
+ }
2778
+ if (!listeners.size)
2779
+ return [];
2780
+ // Pass 2 — link each event(new X(...)) site → every listener of X.
2781
+ const edges = [];
2782
+ const seen = new Set();
2783
+ for (const file of ctx.getAllFiles()) {
2784
+ if (!LARAVEL_PHP_EXT.test(file))
2785
+ continue;
2786
+ const content = ctx.readFile(file);
2787
+ if (!content || !content.includes('event('))
2788
+ continue;
2789
+ const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
2790
+ const nodesInFile = ctx.getNodesInFile(file);
2791
+ LARAVEL_DISPATCH_RE.lastIndex = 0;
2792
+ let m;
2793
+ let added = 0;
2794
+ while ((m = LARAVEL_DISPATCH_RE.exec(safe)) && added < LARAVEL_FANOUT_CAP) {
2795
+ const targets = listeners.get(phpSimpleName(m[1]));
2796
+ if (!targets)
2797
+ continue;
2798
+ const line = safe.slice(0, m.index).split('\n').length;
2799
+ const disp = enclosingFn(nodesInFile, line);
2800
+ if (!disp)
2801
+ continue;
2802
+ for (const target of targets.values()) {
2803
+ if (target.id === disp.id)
2804
+ continue;
2805
+ const key = `${disp.id}>${target.id}`;
2806
+ if (seen.has(key))
2807
+ continue;
2808
+ seen.add(key);
2809
+ edges.push({
2810
+ source: disp.id,
2811
+ target: target.id,
2812
+ kind: 'calls',
2813
+ line,
2814
+ provenance: 'heuristic',
2815
+ metadata: { synthesizedBy: 'laravel-event', via: phpSimpleName(m[1]), registeredAt: `${file}:${line}` },
2816
+ });
2817
+ added++;
2818
+ }
2819
+ }
2820
+ }
2821
+ return edges;
2822
+ }
1744
2823
  /**
1745
2824
  * Synthesize dispatcher→callback edges (field observers + EventEmitters +
1746
2825
  * React re-render + JSX children + Vue templates + SvelteKit load + RN event
1747
- * channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain).
2826
+ * channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
2827
+ * Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
2828
+ * generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch +
2829
+ * Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener +
2830
+ * MediatR Send/Publish → IRequestHandler/INotificationHandler +
2831
+ * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
1748
2832
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
1749
2833
  */
1750
2834
  function synthesizeCallbackEdges(queries, ctx) {
@@ -1782,6 +2866,18 @@ function synthesizeCallbackEdges(queries, ctx) {
1782
2866
  const rnXPlatEdges = rnCrossPlatformEdges(queries);
1783
2867
  const mybatisEdges = mybatisJavaXmlEdges(queries);
1784
2868
  const ginEdges = ginMiddlewareChainEdges(queries, ctx);
2869
+ const thunkEdges = reduxThunkEdges(queries, ctx);
2870
+ const registryEdges = objectRegistryEdges(ctx);
2871
+ const rtkEdges = rtkQueryEdges(queries, ctx);
2872
+ const piniaEdges = piniaStoreEdges(ctx);
2873
+ const vuexEdges = vuexDispatchEdges(ctx);
2874
+ const celeryEdges = celeryDispatchEdges(ctx);
2875
+ const springEdges = springEventEdges(ctx);
2876
+ const mediatrEdges = mediatrDispatchEdges(ctx);
2877
+ const sidekiqEdges = sidekiqDispatchEdges(ctx);
2878
+ const laravelEdges = laravelEventEdges(ctx);
2879
+ const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
2880
+ const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
1785
2881
  const merged = [];
1786
2882
  const seen = new Set();
1787
2883
  for (const e of [
@@ -1804,6 +2900,18 @@ function synthesizeCallbackEdges(queries, ctx) {
1804
2900
  ...rnXPlatEdges,
1805
2901
  ...mybatisEdges,
1806
2902
  ...ginEdges,
2903
+ ...thunkEdges,
2904
+ ...registryEdges,
2905
+ ...rtkEdges,
2906
+ ...piniaEdges,
2907
+ ...vuexEdges,
2908
+ ...celeryEdges,
2909
+ ...springEdges,
2910
+ ...mediatrEdges,
2911
+ ...sidekiqEdges,
2912
+ ...laravelEdges,
2913
+ ...cFnPtrEdges,
2914
+ ...goframeEdges,
1807
2915
  ]) {
1808
2916
  const key = `${e.source}>${e.target}`;
1809
2917
  if (seen.has(key))