homegraph 0.9.10 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +243 -193
  3. package/dist/bin/fatal-handler.d.ts +20 -0
  4. package/dist/bin/fatal-handler.d.ts.map +1 -0
  5. package/dist/bin/fatal-handler.js +118 -0
  6. package/dist/bin/fatal-handler.js.map +1 -0
  7. package/dist/bin/homegraph.d.ts +6 -6
  8. package/dist/bin/homegraph.js +532 -109
  9. package/dist/bin/homegraph.js.map +1 -1
  10. package/dist/bin/node-version-check.d.ts +4 -4
  11. package/dist/bin/node-version-check.js +12 -12
  12. package/dist/bin/uninstall.d.ts +1 -1
  13. package/dist/bin/uninstall.js +1 -1
  14. package/dist/context/index.d.ts.map +1 -1
  15. package/dist/context/index.js +8 -1
  16. package/dist/context/index.js.map +1 -1
  17. package/dist/db/index.d.ts +22 -1
  18. package/dist/db/index.d.ts.map +1 -1
  19. package/dist/db/index.js +48 -2
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/db/migrations.d.ts +1 -1
  22. package/dist/db/migrations.d.ts.map +1 -1
  23. package/dist/db/migrations.js +26 -17
  24. package/dist/db/migrations.js.map +1 -1
  25. package/dist/db/queries.d.ts +21 -1
  26. package/dist/db/queries.d.ts.map +1 -1
  27. package/dist/db/queries.js +180 -137
  28. package/dist/db/queries.js.map +1 -1
  29. package/dist/db/schema.sql +152 -151
  30. package/dist/db/sqlite-adapter.d.ts +1 -1
  31. package/dist/db/sqlite-adapter.js +3 -3
  32. package/dist/directory.d.ts +87 -6
  33. package/dist/directory.d.ts.map +1 -1
  34. package/dist/directory.js +319 -57
  35. package/dist/directory.js.map +1 -1
  36. package/dist/errors.d.ts +15 -15
  37. package/dist/errors.js +20 -20
  38. package/dist/extraction/astro-extractor.d.ts +79 -0
  39. package/dist/extraction/astro-extractor.d.ts.map +1 -0
  40. package/dist/extraction/astro-extractor.js +320 -0
  41. package/dist/extraction/astro-extractor.js.map +1 -0
  42. package/dist/extraction/context.d.ts +1 -1
  43. package/dist/extraction/context.js +1 -1
  44. package/dist/extraction/extraction-version.d.ts +2 -2
  45. package/dist/extraction/extraction-version.d.ts.map +1 -1
  46. package/dist/extraction/extraction-version.js +2 -2
  47. package/dist/extraction/extraction-version.js.map +1 -1
  48. package/dist/extraction/function-ref.d.ts +118 -0
  49. package/dist/extraction/function-ref.d.ts.map +1 -0
  50. package/dist/extraction/function-ref.js +727 -0
  51. package/dist/extraction/function-ref.js.map +1 -0
  52. package/dist/extraction/generated-detection.d.ts.map +1 -1
  53. package/dist/extraction/generated-detection.js +3 -0
  54. package/dist/extraction/generated-detection.js.map +1 -1
  55. package/dist/extraction/grammars.d.ts +15 -5
  56. package/dist/extraction/grammars.d.ts.map +1 -1
  57. package/dist/extraction/grammars.js +44 -10
  58. package/dist/extraction/grammars.js.map +1 -1
  59. package/dist/extraction/index.d.ts +37 -0
  60. package/dist/extraction/index.d.ts.map +1 -1
  61. package/dist/extraction/index.js +562 -70
  62. package/dist/extraction/index.js.map +1 -1
  63. package/dist/extraction/languages/arkts-viewtree.d.ts +22 -0
  64. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -0
  65. package/dist/extraction/languages/arkts-viewtree.js +133 -0
  66. package/dist/extraction/languages/arkts-viewtree.js.map +1 -0
  67. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  68. package/dist/extraction/languages/arkts.js +211 -20
  69. package/dist/extraction/languages/arkts.js.map +1 -1
  70. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  71. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  72. package/dist/extraction/languages/c-cpp.js +104 -2
  73. package/dist/extraction/languages/c-cpp.js.map +1 -1
  74. package/dist/extraction/languages/csharp.d.ts.map +1 -1
  75. package/dist/extraction/languages/csharp.js +58 -3
  76. package/dist/extraction/languages/csharp.js.map +1 -1
  77. package/dist/extraction/languages/dart.d.ts.map +1 -1
  78. package/dist/extraction/languages/dart.js +183 -1
  79. package/dist/extraction/languages/dart.js.map +1 -1
  80. package/dist/extraction/languages/go.d.ts.map +1 -1
  81. package/dist/extraction/languages/go.js +37 -0
  82. package/dist/extraction/languages/go.js.map +1 -1
  83. package/dist/extraction/languages/index.d.ts.map +1 -1
  84. package/dist/extraction/languages/index.js +2 -0
  85. package/dist/extraction/languages/index.js.map +1 -1
  86. package/dist/extraction/languages/java.d.ts.map +1 -1
  87. package/dist/extraction/languages/java.js +241 -0
  88. package/dist/extraction/languages/java.js.map +1 -1
  89. package/dist/extraction/languages/javascript.d.ts.map +1 -1
  90. package/dist/extraction/languages/javascript.js +16 -0
  91. package/dist/extraction/languages/javascript.js.map +1 -1
  92. package/dist/extraction/languages/kotlin.d.ts.map +1 -1
  93. package/dist/extraction/languages/kotlin.js +94 -0
  94. package/dist/extraction/languages/kotlin.js.map +1 -1
  95. package/dist/extraction/languages/objc.d.ts.map +1 -1
  96. package/dist/extraction/languages/objc.js +42 -0
  97. package/dist/extraction/languages/objc.js.map +1 -1
  98. package/dist/extraction/languages/pascal.d.ts.map +1 -1
  99. package/dist/extraction/languages/pascal.js +11 -0
  100. package/dist/extraction/languages/pascal.js.map +1 -1
  101. package/dist/extraction/languages/php.d.ts.map +1 -1
  102. package/dist/extraction/languages/php.js +77 -1
  103. package/dist/extraction/languages/php.js.map +1 -1
  104. package/dist/extraction/languages/r.d.ts +3 -0
  105. package/dist/extraction/languages/r.d.ts.map +1 -0
  106. package/dist/extraction/languages/r.js +314 -0
  107. package/dist/extraction/languages/r.js.map +1 -0
  108. package/dist/extraction/languages/rust.d.ts.map +1 -1
  109. package/dist/extraction/languages/rust.js +29 -0
  110. package/dist/extraction/languages/rust.js.map +1 -1
  111. package/dist/extraction/languages/scala.d.ts.map +1 -1
  112. package/dist/extraction/languages/scala.js +44 -9
  113. package/dist/extraction/languages/scala.js.map +1 -1
  114. package/dist/extraction/languages/swift.d.ts.map +1 -1
  115. package/dist/extraction/languages/swift.js +61 -0
  116. package/dist/extraction/languages/swift.js.map +1 -1
  117. package/dist/extraction/languages/typescript.d.ts +13 -0
  118. package/dist/extraction/languages/typescript.d.ts.map +1 -1
  119. package/dist/extraction/languages/typescript.js +38 -0
  120. package/dist/extraction/languages/typescript.js.map +1 -1
  121. package/dist/extraction/liquid-extractor.d.ts.map +1 -1
  122. package/dist/extraction/liquid-extractor.js +4 -1
  123. package/dist/extraction/liquid-extractor.js.map +1 -1
  124. package/dist/extraction/parse-worker.js +4 -1
  125. package/dist/extraction/parse-worker.js.map +1 -1
  126. package/dist/extraction/svelte-extractor.d.ts.map +1 -1
  127. package/dist/extraction/svelte-extractor.js +6 -3
  128. package/dist/extraction/svelte-extractor.js.map +1 -1
  129. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  130. package/dist/extraction/tree-sitter-helpers.js +59 -10
  131. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  132. package/dist/extraction/tree-sitter-types.d.ts +29 -0
  133. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  134. package/dist/extraction/tree-sitter.d.ts +192 -0
  135. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  136. package/dist/extraction/tree-sitter.js +1553 -45
  137. package/dist/extraction/tree-sitter.js.map +1 -1
  138. package/dist/extraction/vue-extractor.d.ts.map +1 -1
  139. package/dist/extraction/vue-extractor.js +6 -3
  140. package/dist/extraction/vue-extractor.js.map +1 -1
  141. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  142. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  143. package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
  144. package/dist/graph/traversal.d.ts.map +1 -1
  145. package/dist/graph/traversal.js +11 -2
  146. package/dist/graph/traversal.js.map +1 -1
  147. package/dist/index.d.ts +64 -23
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +137 -41
  150. package/dist/index.js.map +1 -1
  151. package/dist/installer/config-writer.d.ts +1 -1
  152. package/dist/installer/config-writer.js +1 -1
  153. package/dist/installer/index.d.ts +4 -4
  154. package/dist/installer/index.d.ts.map +1 -1
  155. package/dist/installer/index.js +100 -73
  156. package/dist/installer/index.js.map +1 -1
  157. package/dist/installer/instructions-template.d.ts +34 -11
  158. package/dist/installer/instructions-template.d.ts.map +1 -1
  159. package/dist/installer/instructions-template.js +44 -12
  160. package/dist/installer/instructions-template.js.map +1 -1
  161. package/dist/installer/targets/antigravity.d.ts +6 -6
  162. package/dist/installer/targets/antigravity.js +14 -14
  163. package/dist/installer/targets/claude.d.ts +19 -13
  164. package/dist/installer/targets/claude.d.ts.map +1 -1
  165. package/dist/installer/targets/claude.js +97 -29
  166. package/dist/installer/targets/claude.js.map +1 -1
  167. package/dist/installer/targets/codebuddy.d.ts +17 -0
  168. package/dist/installer/targets/codebuddy.d.ts.map +1 -0
  169. package/dist/installer/targets/codebuddy.js +150 -0
  170. package/dist/installer/targets/codebuddy.js.map +1 -0
  171. package/dist/installer/targets/codex.js +5 -7
  172. package/dist/installer/targets/codex.js.map +1 -1
  173. package/dist/installer/targets/cursor.d.ts +1 -1
  174. package/dist/installer/targets/cursor.js +7 -7
  175. package/dist/installer/targets/deveco.d.ts +23 -0
  176. package/dist/installer/targets/deveco.d.ts.map +1 -0
  177. package/dist/installer/targets/deveco.js +217 -0
  178. package/dist/installer/targets/deveco.js.map +1 -0
  179. package/dist/installer/targets/gemini.js +5 -7
  180. package/dist/installer/targets/gemini.js.map +1 -1
  181. package/dist/installer/targets/hermes.js +15 -15
  182. package/dist/installer/targets/kiro.d.ts +1 -1
  183. package/dist/installer/targets/kiro.js +4 -4
  184. package/dist/installer/targets/opencode.d.ts +9 -1
  185. package/dist/installer/targets/opencode.d.ts.map +1 -1
  186. package/dist/installer/targets/opencode.js +92 -41
  187. package/dist/installer/targets/opencode.js.map +1 -1
  188. package/dist/installer/targets/registry.d.ts.map +1 -1
  189. package/dist/installer/targets/registry.js +4 -0
  190. package/dist/installer/targets/registry.js.map +1 -1
  191. package/dist/installer/targets/shared.d.ts +28 -4
  192. package/dist/installer/targets/shared.d.ts.map +1 -1
  193. package/dist/installer/targets/shared.js +32 -15
  194. package/dist/installer/targets/shared.js.map +1 -1
  195. package/dist/installer/targets/types.d.ts +9 -2
  196. package/dist/installer/targets/types.d.ts.map +1 -1
  197. package/dist/mcp/daemon-manager.d.ts +42 -0
  198. package/dist/mcp/daemon-manager.d.ts.map +1 -0
  199. package/dist/mcp/daemon-manager.js +129 -0
  200. package/dist/mcp/daemon-manager.js.map +1 -0
  201. package/dist/mcp/daemon-paths.d.ts +1 -1
  202. package/dist/mcp/daemon-paths.js +3 -3
  203. package/dist/mcp/daemon-registry.d.ts +47 -0
  204. package/dist/mcp/daemon-registry.d.ts.map +1 -0
  205. package/dist/mcp/daemon-registry.js +229 -0
  206. package/dist/mcp/daemon-registry.js.map +1 -0
  207. package/dist/mcp/daemon.d.ts.map +1 -1
  208. package/dist/mcp/daemon.js +27 -6
  209. package/dist/mcp/daemon.js.map +1 -1
  210. package/dist/mcp/dynamic-boundaries.d.ts +41 -0
  211. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
  212. package/dist/mcp/dynamic-boundaries.js +359 -0
  213. package/dist/mcp/dynamic-boundaries.js.map +1 -0
  214. package/dist/mcp/engine.d.ts +4 -4
  215. package/dist/mcp/engine.d.ts.map +1 -1
  216. package/dist/mcp/engine.js +32 -24
  217. package/dist/mcp/engine.js.map +1 -1
  218. package/dist/mcp/index.d.ts +8 -7
  219. package/dist/mcp/index.d.ts.map +1 -1
  220. package/dist/mcp/index.js +39 -18
  221. package/dist/mcp/index.js.map +1 -1
  222. package/dist/mcp/liveness-watchdog.d.ts +18 -0
  223. package/dist/mcp/liveness-watchdog.d.ts.map +1 -0
  224. package/dist/mcp/liveness-watchdog.js +207 -0
  225. package/dist/mcp/liveness-watchdog.js.map +1 -0
  226. package/dist/mcp/proxy.d.ts +6 -0
  227. package/dist/mcp/proxy.d.ts.map +1 -1
  228. package/dist/mcp/proxy.js +75 -18
  229. package/dist/mcp/proxy.js.map +1 -1
  230. package/dist/mcp/server-instructions.d.ts +22 -7
  231. package/dist/mcp/server-instructions.d.ts.map +1 -1
  232. package/dist/mcp/server-instructions.js +91 -59
  233. package/dist/mcp/server-instructions.js.map +1 -1
  234. package/dist/mcp/session.d.ts +3 -1
  235. package/dist/mcp/session.d.ts.map +1 -1
  236. package/dist/mcp/session.js +43 -7
  237. package/dist/mcp/session.js.map +1 -1
  238. package/dist/mcp/stdin-teardown.d.ts +27 -0
  239. package/dist/mcp/stdin-teardown.d.ts.map +1 -0
  240. package/dist/mcp/stdin-teardown.js +49 -0
  241. package/dist/mcp/stdin-teardown.js.map +1 -0
  242. package/dist/mcp/tools.d.ts +137 -16
  243. package/dist/mcp/tools.d.ts.map +1 -1
  244. package/dist/mcp/tools.js +1271 -219
  245. package/dist/mcp/tools.js.map +1 -1
  246. package/dist/mcp/transport.d.ts +1 -1
  247. package/dist/mcp/transport.d.ts.map +1 -1
  248. package/dist/mcp/transport.js +20 -4
  249. package/dist/mcp/transport.js.map +1 -1
  250. package/dist/mcp/version.d.ts +1 -1
  251. package/dist/mcp/version.js +2 -2
  252. package/dist/project-config.d.ts +36 -0
  253. package/dist/project-config.d.ts.map +1 -0
  254. package/dist/project-config.js +235 -0
  255. package/dist/project-config.js.map +1 -0
  256. package/dist/reasoning/config.d.ts +45 -0
  257. package/dist/reasoning/config.d.ts.map +1 -0
  258. package/dist/reasoning/config.js +171 -0
  259. package/dist/reasoning/config.js.map +1 -0
  260. package/dist/reasoning/credentials.d.ts +5 -0
  261. package/dist/reasoning/credentials.d.ts.map +1 -0
  262. package/dist/reasoning/credentials.js +83 -0
  263. package/dist/reasoning/credentials.js.map +1 -0
  264. package/dist/reasoning/login.d.ts +21 -0
  265. package/dist/reasoning/login.d.ts.map +1 -0
  266. package/dist/reasoning/login.js +85 -0
  267. package/dist/reasoning/login.js.map +1 -0
  268. package/dist/reasoning/reasoner.d.ts +43 -0
  269. package/dist/reasoning/reasoner.d.ts.map +1 -0
  270. package/dist/reasoning/reasoner.js +308 -0
  271. package/dist/reasoning/reasoner.js.map +1 -0
  272. package/dist/resolution/c-fnptr-synthesizer.d.ts +33 -0
  273. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -0
  274. package/dist/resolution/c-fnptr-synthesizer.js +352 -0
  275. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
  276. package/dist/resolution/callback-synthesizer.d.ts +6 -1
  277. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  278. package/dist/resolution/callback-synthesizer.js +1197 -1
  279. package/dist/resolution/callback-synthesizer.js.map +1 -1
  280. package/dist/resolution/frameworks/arkts-entry.d.ts +12 -0
  281. package/dist/resolution/frameworks/arkts-entry.d.ts.map +1 -0
  282. package/dist/resolution/frameworks/arkts-entry.js +214 -0
  283. package/dist/resolution/frameworks/arkts-entry.js.map +1 -0
  284. package/dist/resolution/frameworks/arkts-napi.d.ts +3 -0
  285. package/dist/resolution/frameworks/arkts-napi.d.ts.map +1 -0
  286. package/dist/resolution/frameworks/arkts-napi.js +166 -0
  287. package/dist/resolution/frameworks/arkts-napi.js.map +1 -0
  288. package/dist/resolution/frameworks/astro.d.ts +9 -0
  289. package/dist/resolution/frameworks/astro.d.ts.map +1 -0
  290. package/dist/resolution/frameworks/astro.js +169 -0
  291. package/dist/resolution/frameworks/astro.js.map +1 -0
  292. package/dist/resolution/frameworks/goframe.d.ts +41 -0
  293. package/dist/resolution/frameworks/goframe.d.ts.map +1 -0
  294. package/dist/resolution/frameworks/goframe.js +112 -0
  295. package/dist/resolution/frameworks/goframe.js.map +1 -0
  296. package/dist/resolution/frameworks/index.d.ts +3 -0
  297. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  298. package/dist/resolution/frameworks/index.js +17 -1
  299. package/dist/resolution/frameworks/index.js.map +1 -1
  300. package/dist/resolution/frameworks/java.js +6 -1
  301. package/dist/resolution/frameworks/java.js.map +1 -1
  302. package/dist/resolution/frameworks/react.d.ts.map +1 -1
  303. package/dist/resolution/frameworks/react.js +32 -63
  304. package/dist/resolution/frameworks/react.js.map +1 -1
  305. package/dist/resolution/frameworks/svelte.js +6 -2
  306. package/dist/resolution/frameworks/svelte.js.map +1 -1
  307. package/dist/resolution/frameworks/vue.js +24 -27
  308. package/dist/resolution/frameworks/vue.js.map +1 -1
  309. package/dist/resolution/goframe-synthesizer.d.ts +28 -0
  310. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -0
  311. package/dist/resolution/goframe-synthesizer.js +158 -0
  312. package/dist/resolution/goframe-synthesizer.js.map +1 -0
  313. package/dist/resolution/import-resolver.d.ts +10 -0
  314. package/dist/resolution/import-resolver.d.ts.map +1 -1
  315. package/dist/resolution/import-resolver.js +125 -2
  316. package/dist/resolution/import-resolver.js.map +1 -1
  317. package/dist/resolution/index.d.ts +37 -0
  318. package/dist/resolution/index.d.ts.map +1 -1
  319. package/dist/resolution/index.js +306 -3
  320. package/dist/resolution/index.js.map +1 -1
  321. package/dist/resolution/name-matcher.d.ts +41 -0
  322. package/dist/resolution/name-matcher.d.ts.map +1 -1
  323. package/dist/resolution/name-matcher.js +519 -14
  324. package/dist/resolution/name-matcher.js.map +1 -1
  325. package/dist/resolution/strip-comments.d.ts +1 -1
  326. package/dist/resolution/strip-comments.d.ts.map +1 -1
  327. package/dist/resolution/strip-comments.js +2 -0
  328. package/dist/resolution/strip-comments.js.map +1 -1
  329. package/dist/resolution/types.d.ts +20 -3
  330. package/dist/resolution/types.d.ts.map +1 -1
  331. package/dist/search/query-utils.d.ts +17 -1
  332. package/dist/search/query-utils.d.ts.map +1 -1
  333. package/dist/search/query-utils.js +79 -10
  334. package/dist/search/query-utils.js.map +1 -1
  335. package/dist/sync/git-hooks.d.ts +4 -4
  336. package/dist/sync/git-hooks.js +5 -5
  337. package/dist/sync/watcher.d.ts +69 -2
  338. package/dist/sync/watcher.d.ts.map +1 -1
  339. package/dist/sync/watcher.js +226 -21
  340. package/dist/sync/watcher.js.map +1 -1
  341. package/dist/sync/worktree.d.ts +3 -3
  342. package/dist/sync/worktree.js +5 -5
  343. package/dist/telemetry/index.d.ts +143 -0
  344. package/dist/telemetry/index.d.ts.map +1 -0
  345. package/dist/telemetry/index.js +541 -0
  346. package/dist/telemetry/index.js.map +1 -0
  347. package/dist/types.d.ts +18 -3
  348. package/dist/types.d.ts.map +1 -1
  349. package/dist/types.js +3 -1
  350. package/dist/types.js.map +1 -1
  351. package/dist/upgrade/index.d.ts.map +1 -1
  352. package/dist/upgrade/index.js +46 -10
  353. package/dist/upgrade/index.js.map +1 -1
  354. package/dist/utils.d.ts +46 -27
  355. package/dist/utils.d.ts.map +1 -1
  356. package/dist/utils.js +87 -53
  357. package/dist/utils.js.map +1 -1
  358. package/package.json +57 -57
  359. package/scripts/add-lang/bench.sh +60 -60
  360. package/scripts/add-lang/check-grammar.mjs +75 -75
  361. package/scripts/add-lang/dump-ast.mjs +103 -103
  362. package/scripts/add-lang/verify-extraction.mjs +70 -70
  363. package/scripts/agent-eval/ab-adoption.sh +91 -0
  364. package/scripts/agent-eval/ab-hook.sh +86 -0
  365. package/scripts/agent-eval/ab-impl.sh +78 -0
  366. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -0
  367. package/scripts/agent-eval/ab-sufficiency.sh +78 -0
  368. package/scripts/agent-eval/arms-F.sh +21 -21
  369. package/scripts/agent-eval/arms-matrix.sh +37 -37
  370. package/scripts/agent-eval/audit.sh +68 -68
  371. package/scripts/agent-eval/bench-readme.sh +28 -28
  372. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  373. package/scripts/agent-eval/block-read-hook.sh +19 -19
  374. package/scripts/agent-eval/hook-settings.json +15 -15
  375. package/scripts/agent-eval/itrun.sh +120 -120
  376. package/scripts/agent-eval/offload-eval-3arm.sh +72 -0
  377. package/scripts/agent-eval/offload-eval-cost.mjs +133 -0
  378. package/scripts/agent-eval/offload-eval-effort.mjs +108 -0
  379. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -0
  380. package/scripts/agent-eval/offload-eval-frontload.sh +47 -0
  381. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -0
  382. package/scripts/agent-eval/offload-eval-hook.mjs +84 -0
  383. package/scripts/agent-eval/offload-eval-judge.mjs +103 -0
  384. package/scripts/agent-eval/offload-eval-matrix.sh +20 -0
  385. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -0
  386. package/scripts/agent-eval/offload-eval-refs1.sh +50 -0
  387. package/scripts/agent-eval/offload-eval-setup.sh +24 -0
  388. package/scripts/agent-eval/offload-eval-styles.sh +72 -0
  389. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -0
  390. package/scripts/agent-eval/offload-eval.md +76 -0
  391. package/scripts/agent-eval/parse-arms.mjs +116 -116
  392. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  393. package/scripts/agent-eval/parse-run.mjs +45 -45
  394. package/scripts/agent-eval/parse-session.mjs +93 -93
  395. package/scripts/agent-eval/probe-context.mjs +21 -21
  396. package/scripts/agent-eval/probe-explore.mjs +40 -40
  397. package/scripts/agent-eval/probe-node.mjs +20 -20
  398. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  399. package/scripts/agent-eval/probe-trace.mjs +20 -20
  400. package/scripts/agent-eval/redirect-read-hook.sh +38 -0
  401. package/scripts/agent-eval/run-agent.sh +34 -34
  402. package/scripts/agent-eval/run-all.sh +69 -67
  403. package/scripts/agent-eval/run-arms.sh +56 -56
  404. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  405. package/scripts/build-bundle.sh +118 -118
  406. package/scripts/extract-release-notes.mjs +130 -130
  407. package/scripts/local-install.sh +41 -41
  408. package/scripts/npm-sdk.js +75 -75
  409. package/scripts/npm-shim.js +246 -246
  410. package/scripts/pack-npm.sh +119 -119
  411. package/scripts/prepare-release.mjs +270 -270
  412. package/scripts/qa_eval/README.md +400 -0
  413. package/scripts/qa_eval/agent_runner.py +526 -0
  414. package/scripts/qa_eval/data/.gitignore +4 -0
  415. package/scripts/qa_eval/data/test-set.jsonl +22 -0
  416. package/scripts/qa_eval/eval_metrics.py +233 -0
  417. package/scripts/qa_eval/external_agent.py +577 -0
  418. package/scripts/qa_eval/llm_config.py +92 -0
  419. package/scripts/qa_eval/memory_monitor.py +132 -0
  420. package/scripts/qa_eval/my_answer_accuracy.py +187 -0
  421. package/scripts/qa_eval/requirements.txt +2 -0
  422. package/scripts/qa_eval/run_pipeline.py +702 -0
  423. package/scripts/qa_eval/stats_efficiency.py +279 -0
  424. package/scripts/qa_eval/stats_scores.py +207 -0
  425. package/dist/bin/codegraph.d.ts +0 -26
  426. package/dist/bin/codegraph.d.ts.map +0 -1
  427. package/dist/bin/codegraph.js +0 -1572
  428. package/dist/bin/codegraph.js.map +0 -1
  429. package/dist/extraction/arkanalyzer-extractor.d.ts +0 -6
  430. package/dist/extraction/arkanalyzer-extractor.d.ts.map +0 -1
  431. package/dist/extraction/arkanalyzer-extractor.js +0 -11
  432. package/dist/extraction/arkanalyzer-extractor.js.map +0 -1
  433. package/dist/extraction/arkanalyzer-project.d.ts +0 -20
  434. package/dist/extraction/arkanalyzer-project.d.ts.map +0 -1
  435. package/dist/extraction/arkanalyzer-project.js +0 -596
  436. package/dist/extraction/arkanalyzer-project.js.map +0 -1
@@ -43,10 +43,13 @@ exports.extractFromSource = extractFromSource;
43
43
  const path = __importStar(require("path"));
44
44
  const grammars_1 = require("./grammars");
45
45
  const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
46
+ const function_ref_1 = require("./function-ref");
47
+ const generated_detection_1 = require("./generated-detection");
46
48
  const languages_1 = require("./languages");
47
49
  const liquid_extractor_1 = require("./liquid-extractor");
48
50
  const razor_extractor_1 = require("./razor-extractor");
49
51
  const svelte_extractor_1 = require("./svelte-extractor");
52
+ const astro_extractor_1 = require("./astro-extractor");
50
53
  const dfm_extractor_1 = require("./dfm-extractor");
51
54
  const vue_extractor_1 = require("./vue-extractor");
52
55
  const mybatis_extractor_1 = require("./mybatis-extractor");
@@ -55,6 +58,25 @@ const frameworks_1 = require("../resolution/frameworks");
55
58
  // Re-export for backward compatibility
56
59
  var tree_sitter_helpers_2 = require("./tree-sitter-helpers");
57
60
  Object.defineProperty(exports, "generateNodeId", { enumerable: true, get: function () { return tree_sitter_helpers_2.generateNodeId; } });
61
+ /**
62
+ * RTK Query generated-hook naming convention: `use` + PascalCase endpoint (with
63
+ * an optional `Lazy` variant prefix) + `Query`/`Mutation`. Matches the hook
64
+ * bindings to extract from an `export const {...} = api` destructuring. Kept in
65
+ * sync with the same convention in `callback-synthesizer.ts` (the synth side).
66
+ */
67
+ const RTK_HOOK_NAME_RE = /^use[A-Z][A-Za-z0-9]*(?:Query|Mutation)$/;
68
+ /** React HOC callees whose result is itself a component — a PascalCase const
69
+ * initialized with one of these is a component, not a constant (#841). */
70
+ const REACT_COMPONENT_HOCS = new Set(['forwardRef', 'memo', 'React.forwardRef', 'React.memo']);
71
+ /** Vue store collections whose object-literal members are the symbols an agent
72
+ * looks for. Extracted as function nodes so `actions`/`mutations`/`getters` are
73
+ * findable + readable (the foundation under any later dispatch-bridge synth). */
74
+ const VUE_STORE_COLLECTION_NAMES = new Set(['actions', 'mutations', 'getters']);
75
+ /** Store-definition callees whose config object carries those collections. */
76
+ const VUE_STORE_FACTORY_CALLEES = new Set(['defineStore', 'createStore']);
77
+ /** Distinct signals that a file is a Vuex/Pinia store (≥2 ⇒ treat a bare
78
+ * `const actions = {…}` as a store collection — see looksLikeVueStoreFile). */
79
+ const VUE_STORE_FILE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
58
80
  /**
59
81
  * Extract the name from a node based on language
60
82
  */
@@ -162,6 +184,77 @@ function scalaBaseTypeName(node, source) {
162
184
  }
163
185
  }
164
186
  }
187
+ /**
188
+ * Resolve the declared identifier inside a C declarator. A `declaration`'s
189
+ * `declarator` field nests the name through `init_declarator` (with value),
190
+ * `pointer_declarator`/`array_declarator`/`parenthesized_declarator`
191
+ * wrappers (each via their own `declarator` field) down to an `identifier`.
192
+ * A `function_declarator` means the declaration is a function prototype (or a
193
+ * function-pointer var) — return null so it isn't extracted as a variable.
194
+ */
195
+ function cDeclaratorIdentifier(node) {
196
+ let cur = node;
197
+ let guard = 0;
198
+ while (cur && guard++ < 12) {
199
+ switch (cur.type) {
200
+ case 'identifier':
201
+ return cur;
202
+ case 'function_declarator':
203
+ return null;
204
+ case 'init_declarator':
205
+ case 'pointer_declarator':
206
+ case 'array_declarator':
207
+ case 'parenthesized_declarator':
208
+ cur = (0, tree_sitter_helpers_1.getChildByField)(cur, 'declarator');
209
+ break;
210
+ default:
211
+ return null;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+ /** First `simple_identifier` in `node`'s subtree (breadth-ish, first-found).
217
+ * Swift's property name nests as `property_declaration → <name> pattern →
218
+ * bound_identifier → simple_identifier`; this resolves it (and the bound name of
219
+ * a Kotlin/Swift property declarator for the shadow prune). For a tuple pattern
220
+ * (`let (a, b)`) it returns the first — acceptable, those are rare for consts. */
221
+ function firstSimpleIdentifier(node) {
222
+ const stack = node ? [node] : [];
223
+ let guard = 0;
224
+ while (stack.length > 0 && guard++ < 40) {
225
+ const n = stack.shift();
226
+ if (n.type === 'simple_identifier')
227
+ return n;
228
+ for (let i = 0; i < n.namedChildCount; i++) {
229
+ const c = n.namedChild(i);
230
+ if (c)
231
+ stack.push(c);
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+ /** Swift property facts: the bound name, whether it's a `let`, and whether it's
237
+ * a *computed* property (a getter block, no stored value — never a constant). */
238
+ function swiftPropertyInfo(node, source) {
239
+ const pattern = (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ??
240
+ node.namedChildren.find((c) => c.type === 'value_binding_pattern' || c.type === 'pattern') ??
241
+ null;
242
+ const binding = node.namedChildren.find((c) => c.type === 'value_binding_pattern');
243
+ const isLet = binding != null && (0, tree_sitter_helpers_1.getNodeText)(binding, source).trimStart().startsWith('let');
244
+ const isComputed = node.namedChildren.some((c) => c.type === 'computed_property' || c.type === 'protocol_property_requirements');
245
+ return { nameNode: firstSimpleIdentifier(pattern), isLet, isComputed };
246
+ }
247
+ /** True when `node` is (transitively) inside a C function body — i.e. a local,
248
+ * not a file/namespace-scope declaration. Walks the parent chain to the root. */
249
+ function hasFunctionAncestor(node) {
250
+ let p = node.parent;
251
+ while (p) {
252
+ if (p.type === 'function_definition')
253
+ return true;
254
+ p = p.parent;
255
+ }
256
+ return false;
257
+ }
165
258
  /**
166
259
  * PHP type-position wrapper node kinds (a type-hint is `named_type`,
167
260
  * `?Foo` is `optional_type`, `A|B` is `union_type`, `A&B` is
@@ -228,15 +321,32 @@ class TreeSitterExtractor {
228
321
  nodes = [];
229
322
  edges = [];
230
323
  unresolvedReferences = [];
324
+ // Value-reference edges (default ON; set HOMEGRAPH_VALUE_REFS=0 to disable; see flushValueRefs).
325
+ // Same-file reads of file-scope const/var symbols → `references` edges so impact analysis catches
326
+ // value consumers ("change this constant/table, affect its readers").
327
+ static VALUE_REF_LANGS = new Set(['typescript', 'javascript', 'tsx', 'go', 'python', 'rust', 'ruby', 'c', 'java', 'csharp', 'php', 'scala', 'kotlin', 'swift', 'dart', 'pascal']);
328
+ static MAX_VALUE_REF_NODES = 20_000;
329
+ valueRefsEnabled = process.env.HOMEGRAPH_VALUE_REFS !== '0';
330
+ fileScopeValues = new Map();
331
+ fileScopeValueCounts = new Map(); // file-scope nodes per name (conditional-def detection)
332
+ valueRefScopes = [];
231
333
  errors = [];
232
334
  extractor = null;
233
335
  nodeStack = []; // Stack of parent node IDs
234
336
  methodIndex = null; // lookup key → node ID for Pascal defProc lookup
337
+ // Function-as-value capture (#756): per-language spec + candidates collected
338
+ // during the walk, gated & flushed into unresolvedReferences at end-of-file
339
+ // (see flushFnRefCandidates).
340
+ fnRefSpec;
341
+ fnRefCandidates = [];
342
+ // Memoized "is this a Vue store file" verdict (per-extractor = per-file).
343
+ vueStoreFile = null;
235
344
  constructor(filePath, source, language) {
236
345
  this.filePath = filePath;
237
346
  this.source = source;
238
347
  this.language = language || (0, grammars_1.detectLanguage)(filePath, source);
239
348
  this.extractor = languages_1.EXTRACTORS[this.language] || null;
349
+ this.fnRefSpec = function_ref_1.FN_REF_SPECS[this.language];
240
350
  }
241
351
  /**
242
352
  * Parse and extract from the source code
@@ -315,6 +425,10 @@ class TreeSitterExtractor {
315
425
  if (packageNodeId)
316
426
  this.nodeStack.push(packageNodeId);
317
427
  this.visitNode(this.tree.rootNode);
428
+ // Gate + flush function-as-value candidates (#756) while the file's
429
+ // nodes and import refs are complete and the file node is still pushed.
430
+ this.flushFnRefCandidates();
431
+ this.flushValueRefs();
318
432
  if (packageNodeId)
319
433
  this.nodeStack.pop();
320
434
  this.nodeStack.pop();
@@ -352,6 +466,364 @@ class TreeSitterExtractor {
352
466
  durationMs: Date.now() - startTime,
353
467
  };
354
468
  }
469
+ /**
470
+ * Function-as-value capture (#756): if this node is one of the language's
471
+ * value-position containers (call arguments, assignment RHS, struct/object
472
+ * initializer, array/table literal), collect candidate function names from
473
+ * it. Candidates are gated & flushed at end-of-file (flushFnRefCandidates).
474
+ */
475
+ maybeCaptureFnRefs(node, nodeType) {
476
+ const spec = this.fnRefSpec;
477
+ if (!spec)
478
+ return;
479
+ const rule = spec.dispatch.get(nodeType);
480
+ if (!rule || this.nodeStack.length === 0)
481
+ return;
482
+ const fromNodeId = this.nodeStack[this.nodeStack.length - 1];
483
+ if (!fromNodeId)
484
+ return;
485
+ for (const cand of (0, function_ref_1.captureFnRefCandidates)(node, rule, spec, this.source)) {
486
+ this.fnRefCandidates.push({ ...cand, fromNodeId });
487
+ }
488
+ }
489
+ /**
490
+ * Candidates-only scan of a subtree the main walkers won't traverse
491
+ * (top-level variable initializers). No extraction side effects. Halts at
492
+ * nested function definitions: their bodies are walked — and their
493
+ * candidates attributed — by extractFunction's own body walk.
494
+ */
495
+ scanFnRefSubtree(node, depth) {
496
+ if (!this.fnRefSpec || depth > 12)
497
+ return;
498
+ const nodeType = node.type;
499
+ if (depth > 0 && (this.extractor?.functionTypes.includes(nodeType) ||
500
+ nodeType === 'arrow_function' ||
501
+ nodeType === 'function_expression' ||
502
+ nodeType === 'lambda_literal' ||
503
+ nodeType === 'lambda_expression')) {
504
+ return;
505
+ }
506
+ this.maybeCaptureFnRefs(node, nodeType);
507
+ for (let i = 0; i < node.namedChildCount; i++) {
508
+ const child = node.namedChild(i);
509
+ if (child)
510
+ this.scanFnRefSubtree(child, depth + 1);
511
+ }
512
+ }
513
+ /**
514
+ * Gate captured function-as-value candidates and push survivors as
515
+ * `function_ref` unresolved references.
516
+ *
517
+ * The gate bounds volume and protects precision: a candidate survives only
518
+ * if its name matches a function/method DEFINED IN THIS FILE or a name this
519
+ * file imports/references. Everything else (locals, params, fields passed
520
+ * as arguments) is dropped before it ever reaches the database. Resolution
521
+ * then matches survivors against function/method nodes only
522
+ * (matchFunctionRef) and emits `references` edges — which callers/impact
523
+ * already traverse.
524
+ *
525
+ * Known v1 limit, deliberate: a C/C++ callback registered in a DIFFERENT
526
+ * translation unit than its definition (extern, no symbol imports to match)
527
+ * is not captured. Same-file registration — the dominant C pattern (static
528
+ * callback + same-file ops struct) — is.
529
+ */
530
+ flushFnRefCandidates() {
531
+ if (this.fnRefCandidates.length === 0)
532
+ return;
533
+ const candidates = this.fnRefCandidates;
534
+ this.fnRefCandidates = [];
535
+ // Generated/minified files (vendored jquery.min.js and friends): their
536
+ // function-as-value edges are noise — single-letter minified symbols
537
+ // resolve everywhere. Same policy as the callback synthesizer.
538
+ if ((0, generated_detection_1.isGeneratedFile)(this.filePath))
539
+ return;
540
+ const definedHere = new Set();
541
+ for (const n of this.nodes) {
542
+ if (n.kind === 'function' || n.kind === 'method')
543
+ definedHere.add(n.name);
544
+ }
545
+ // Import-binding names only (all binding emitters push kind 'imports').
546
+ // Deliberately NOT 'references': those carry type-annotation and
547
+ // interface-member names, which let local variables that share a type
548
+ // member's name slip through the gate (excalidraw A/B finding). A dotted
549
+ // import (JVM `import com.example.OtherClass`) also contributes its LAST
550
+ // segment — the simple name Java/Kotlin code uses in `OtherClass::method`
551
+ // references.
552
+ const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
553
+ // JVM imports are dotted (`com.example.OtherClass`); PHP `use` imports
554
+ // are backslashed (`App\Services\Mailer`). Both contribute their last
555
+ // segment — the simple name code uses to reference them.
556
+ const QUALIFIED_IMPORT = /^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$/;
557
+ const importedNames = new Set();
558
+ for (const r of this.unresolvedReferences) {
559
+ if (r.referenceKind !== 'imports')
560
+ continue;
561
+ if (SIMPLE_NAME.test(r.referenceName)) {
562
+ importedNames.add(r.referenceName);
563
+ }
564
+ else {
565
+ const qualified = r.referenceName.match(QUALIFIED_IMPORT);
566
+ if (qualified)
567
+ importedNames.add(qualified[1]);
568
+ }
569
+ }
570
+ const ungated = this.fnRefSpec?.ungatedModes;
571
+ const addressOfOnly = this.fnRefSpec?.addressOfOnly === true;
572
+ const seen = new Set();
573
+ for (const c of candidates) {
574
+ const atFileScope = c.fromNodeId.startsWith('file:');
575
+ // C++ (addressOfOnly): a BARE identifier qualifies only inside a
576
+ // file-scope initializer table. Everywhere else — args, assignments,
577
+ // local braced-init lists like `{begin, size}` — only explicit `&`
578
+ // forms count (fmt A/B finding: generic names `begin`/`out`/`size`
579
+ // collide with locals and members).
580
+ if (addressOfOnly &&
581
+ !c.explicitRef &&
582
+ !(atFileScope && (c.mode === 'value' || c.mode === 'list'))) {
583
+ continue;
584
+ }
585
+ // Gate policy by candidate shape:
586
+ // - `this.<member>`: ALWAYS flush — the member may be inherited from a
587
+ // class in another file (definedHere can't see it), volume is
588
+ // naturally bounded by real `this.X` expressions, and resolution is
589
+ // strictly class-scoped (own members or the validated supertype
590
+ // pass), so nothing fuzzy can leak.
591
+ // - `Scope::member` (C++ member-pointers, Java/Kotlin type-qualified
592
+ // method refs, PHP `'Cls::m'`): ALWAYS flush — the explicit-ref
593
+ // syntax is self-selecting, the referenced type often needs NO
594
+ // import (Java/Kotlin same-package, Kotlin companions), and
595
+ // resolution is scope-suffix-anchored + unique-or-drop, so a
596
+ // same-named member on another class can't match.
597
+ // - C-family file-scope initializers skip the gate entirely
598
+ // (constant-expression context — see FnRefSpec.ungatedModes).
599
+ // - everything else: name ∈ same-file functions/methods ∪ imports.
600
+ if (!c.name.startsWith('this.') && !c.name.includes('::')) {
601
+ const skipGate = (ungated?.has(c.mode) === true && atFileScope) ||
602
+ c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
603
+ if (!skipGate && !definedHere.has(c.name) && !importedNames.has(c.name)) {
604
+ continue;
605
+ }
606
+ }
607
+ const key = `${c.fromNodeId}|${c.name}`;
608
+ if (seen.has(key))
609
+ continue;
610
+ seen.add(key);
611
+ this.unresolvedReferences.push({
612
+ fromNodeId: c.fromNodeId,
613
+ referenceName: c.name,
614
+ referenceKind: 'function_ref',
615
+ line: c.line,
616
+ column: c.column,
617
+ });
618
+ }
619
+ }
620
+ /**
621
+ * Record value-reference bookkeeping as nodes are created: file-scope const/var symbols with
622
+ * distinctive names become reference targets; function/method/const/var symbols become reader
623
+ * scopes whose bodies flushValueRefs scans.
624
+ */
625
+ captureValueRefScope(kind, name, id, node) {
626
+ // Pascal targets `constant` only: its extractor emits function PARAMETERS
627
+ // (`Dest: TBufferWriter`) and class fields (`declField`) as `variable` at the
628
+ // enclosing scope, which would otherwise become noisy targets (a param name
629
+ // shared across many procs collapses to one file-wide target). Genuine
630
+ // Pascal shared values are `const` (`constant`), so restrict to that. (Unit
631
+ // `var` globals are the rare cost; the parameter/field noise dominates.)
632
+ const targetKindOk = this.language === 'pascal' ? kind === 'constant' : kind === 'constant' || kind === 'variable';
633
+ if (targetKindOk && name.length >= 3 && /[A-Z_]/.test(name)) {
634
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
635
+ // file-scope OR class/module/struct/enum-scope constants are targets.
636
+ // Class/module scope matters for languages (Ruby) that keep nearly all
637
+ // constants inside a class or module; struct/enum scope matters for Swift,
638
+ // which namespaces shared constants in `struct`/`enum` (`enum Constants {
639
+ // static let X }`). Readers are same-file methods of that type.
640
+ if (parentId &&
641
+ (parentId.startsWith('file:') || parentId.startsWith('class:') ||
642
+ parentId.startsWith('module:') || parentId.startsWith('struct:') ||
643
+ parentId.startsWith('enum:'))) {
644
+ this.fileScopeValues.set(name, id);
645
+ // How many target nodes carry this name. A conditional def
646
+ // (`try: X = a; except: X = b`) makes >1 — distinct from a local shadow,
647
+ // which adds a binding the prune must catch (see flushValueRefs).
648
+ this.fileScopeValueCounts.set(name, (this.fileScopeValueCounts.get(name) ?? 0) + 1);
649
+ }
650
+ }
651
+ if (kind === 'function' || kind === 'method' || kind === 'constant' || kind === 'variable') {
652
+ this.valueRefScopes.push({ id, node, name });
653
+ }
654
+ }
655
+ /**
656
+ * Emit same-file `references` edges from a symbol to the file-scope const/var it reads (TS/JS).
657
+ * The engine doesn't edge const→consumer, so impact analysis misses "change this table, affect
658
+ * its readers" (the ReScript-PR false positive). Same-file only (resolution is unambiguous),
659
+ * distinctive target names only (dodges the local-shadowing precision trap documented on
660
+ * function_ref), deduped per (reader, target). Default on (HOMEGRAPH_VALUE_REFS=0 disables) +
661
+ * additive. Shadowed targets are pruned — see below.
662
+ */
663
+ flushValueRefs() {
664
+ const scopes = this.valueRefScopes;
665
+ const targets = this.fileScopeValues;
666
+ const fileScopeCounts = this.fileScopeValueCounts;
667
+ this.valueRefScopes = [];
668
+ this.fileScopeValues = new Map();
669
+ this.fileScopeValueCounts = new Map();
670
+ if (!this.valueRefsEnabled || !TreeSitterExtractor.VALUE_REF_LANGS.has(this.language))
671
+ return;
672
+ if (targets.size === 0 || scopes.length === 0 || (0, generated_detection_1.isGeneratedFile)(this.filePath))
673
+ return;
674
+ // Prune SHADOWED targets. A target re-bound in an INNER scope (a
675
+ // bundled/Emscripten `const Module` re-declared as a nested `var Module`; a
676
+ // Go package `const Timeout` shadowed by a local `Timeout := …`; a Python
677
+ // module `CONFIG` shadowed by a local `CONFIG = …`) resolves to the inner
678
+ // binding for nested readers, so a file-scope edge is a false positive.
679
+ // Inner re-bindings aren't graph nodes, so detect them at the syntax level:
680
+ // count every declarator of the name across the tree and compare against how
681
+ // many FILE-SCOPE nodes carry it. A real shadow makes (declarators >
682
+ // file-scope nodes) — the excess is the local binding. A conditional
683
+ // module-level def (`try: X = a; except: X = b`) makes them EQUAL (both
684
+ // declarators are file-scope nodes), so it's correctly kept. Complements the
685
+ // path-based isGeneratedFile() check, which can't catch content-minified
686
+ // bundles.
687
+ //
688
+ // Declarator node types are per-grammar; a file only contains its own
689
+ // language's nodes, so matching all of them in one switch is safe.
690
+ if (this.tree) {
691
+ const declCounts = new Map();
692
+ const bump = (nameNode) => {
693
+ // `simple_identifier` is Kotlin's name node (a property declarator's name).
694
+ if (nameNode && (nameNode.type === 'identifier' || nameNode.type === 'simple_identifier')) {
695
+ const nm = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
696
+ if (targets.has(nm))
697
+ declCounts.set(nm, (declCounts.get(nm) ?? 0) + 1);
698
+ }
699
+ };
700
+ const dstack = [this.tree.rootNode];
701
+ let dvisited = 0;
702
+ while (dstack.length > 0 && dvisited < TreeSitterExtractor.MAX_VALUE_REF_NODES) {
703
+ const n = dstack.pop();
704
+ dvisited++;
705
+ switch (n.type) {
706
+ case 'variable_declarator': // TS/JS/tsx
707
+ case 'const_spec': // Go `const X = …`
708
+ case 'var_spec': // Go `var X = …`
709
+ bump(n.namedChild(0));
710
+ break;
711
+ case 'const_item': // Rust `const X: T = …`
712
+ case 'static_item': // Rust `static X: T = …`
713
+ bump((0, tree_sitter_helpers_1.getChildByField)(n, 'name'));
714
+ break;
715
+ case 'let_declaration': // Rust `let x = …` (locals — the shadow source)
716
+ case 'short_var_declaration': // Go `x, Y := …`
717
+ case 'assignment': { // Python `X = …` / `X: T = …` / `A, B = …`
718
+ const left = (0, tree_sitter_helpers_1.getChildByField)(n, 'left') ?? (0, tree_sitter_helpers_1.getChildByField)(n, 'pattern') ?? n.namedChild(0);
719
+ if (left?.type === 'identifier')
720
+ bump(left);
721
+ else if (left)
722
+ for (const c of left.namedChildren)
723
+ bump(c);
724
+ break;
725
+ }
726
+ case 'init_declarator': // C `T X = …` (file-scope const AND the local that shadows it)
727
+ bump(cDeclaratorIdentifier(n));
728
+ break;
729
+ case 'val_definition': // Scala `val X = …` (object/top-level const AND a method-local that shadows it)
730
+ case 'var_definition': { // Scala `var X = …`
731
+ const pat = (0, tree_sitter_helpers_1.getChildByField)(n, 'pattern');
732
+ if (pat?.type === 'identifier')
733
+ bump(pat);
734
+ break;
735
+ }
736
+ case 'static_final_declaration': // Dart top-level/`static` `const`/`final` (the target itself)
737
+ case 'initialized_identifier': // Dart instance field / `var`
738
+ case 'initialized_variable_definition': { // Dart a method-local `const`/`final`/`var` that shadows a const
739
+ const id = n.namedChildren.find((c) => c.type === 'identifier');
740
+ if (id)
741
+ bump(id);
742
+ break;
743
+ }
744
+ case 'declConst': // Pascal unit/class `const` (the target itself) AND a function-local `const` that shadows it
745
+ case 'declVar': { // Pascal a function-local `var` that shadows a const
746
+ bump((0, tree_sitter_helpers_1.getChildByField)(n, 'name'));
747
+ break;
748
+ }
749
+ case 'property_declaration': { // Kotlin / Swift `val`/`let X = …` (object/static const AND a method-local that shadows it)
750
+ // Kotlin: variable_declaration → simple_identifier; Swift: a `pattern`
751
+ // (`<name>` field) → simple_identifier. Resolve either shape.
752
+ const vd = n.namedChildren.find((c) => c.type === 'variable_declaration');
753
+ const id = vd
754
+ ? vd.namedChildren.find((c) => c.type === 'simple_identifier')
755
+ : firstSimpleIdentifier((0, tree_sitter_helpers_1.getChildByField)(n, 'name') ??
756
+ n.namedChildren.find((c) => c.type === 'value_binding_pattern' || c.type === 'pattern') ??
757
+ null);
758
+ if (id)
759
+ bump(id);
760
+ break;
761
+ }
762
+ }
763
+ for (let i = 0; i < n.namedChildCount; i++) {
764
+ const c = n.namedChild(i);
765
+ if (c)
766
+ dstack.push(c);
767
+ }
768
+ }
769
+ for (const [nm, c] of declCounts)
770
+ if (c > (fileScopeCounts.get(nm) ?? 1))
771
+ targets.delete(nm);
772
+ if (targets.size === 0)
773
+ return;
774
+ }
775
+ for (const scope of scopes) {
776
+ const seen = new Set();
777
+ const stack = [scope.node];
778
+ // Dart and Pascal attach a function/method BODY as a *next sibling* of the
779
+ // signature node that is stored as the reader scope (Dart `method_signature`
780
+ // ← `function_body`; Pascal `declProc` ← `block`, both under a `defProc`),
781
+ // not as a child — so the scope subtree is just the signature and the reads
782
+ // live in the sibling. Pull it in. (A body as a next sibling of the scope
783
+ // node is unique to Dart/Pascal among the value-ref languages — every other
784
+ // grammar nests the body inside the function node — so this is inert
785
+ // elsewhere.)
786
+ const sib = scope.node.nextNamedSibling;
787
+ if (sib && (sib.type === 'function_body' || sib.type === 'block'))
788
+ stack.push(sib);
789
+ let visited = 0;
790
+ while (stack.length > 0 && visited < TreeSitterExtractor.MAX_VALUE_REF_NODES) {
791
+ const n = stack.pop();
792
+ visited++;
793
+ // `constant` covers Ruby, where both a constant's definition and its
794
+ // references are `constant`-typed nodes, not `identifier`. `name` covers
795
+ // PHP, where a constant reference — bare `MAX_ITEMS` or the const half of
796
+ // `self::MAX_ITEMS` / `Foo::MAX_ITEMS` — is a `name` node (a `$var` local
797
+ // is a `variable_name`, a different namespace, so it can never shadow a
798
+ // bare constant — no prune wiring needed). `simple_identifier` covers
799
+ // Kotlin, whose every name reference (a const read included) is that
800
+ // node type. Safe across languages: a file only holds its own grammar's
801
+ // nodes; `name` is PHP-only and `simple_identifier` is Kotlin-only here.
802
+ if (n.type === 'identifier' || n.type === 'constant' ||
803
+ n.type === 'name' || n.type === 'simple_identifier') {
804
+ const refName = (0, tree_sitter_helpers_1.getNodeText)(n, this.source);
805
+ const targetId = targets.get(refName);
806
+ // Skip self and same-name targets: a symbol referencing a file-scope
807
+ // sibling of its own name (the two halves of a conditional `try: X=…;
808
+ // except: X=…`) is never a meaningful value read.
809
+ if (targetId && targetId !== scope.id && refName !== scope.name && !seen.has(targetId)) {
810
+ seen.add(targetId);
811
+ this.edges.push({
812
+ source: scope.id,
813
+ target: targetId,
814
+ kind: 'references',
815
+ metadata: { valueRef: true },
816
+ });
817
+ }
818
+ }
819
+ for (let i = 0; i < n.namedChildCount; i++) {
820
+ const c = n.namedChild(i);
821
+ if (c)
822
+ stack.push(c);
823
+ }
824
+ }
825
+ }
826
+ }
355
827
  /**
356
828
  * Visit a node and extract information
357
829
  */
@@ -364,8 +836,14 @@ class TreeSitterExtractor {
364
836
  if (this.extractor.visitNode) {
365
837
  const ctx = this.makeExtractorContext();
366
838
  const handled = this.extractor.visitNode(node, ctx);
367
- if (handled)
839
+ if (handled) {
840
+ // The hook consumed this subtree, so the walkers below never descend
841
+ // into it — scan it for function-as-value candidates (#756). Scala's
842
+ // hook handles val/var definitions (`val table = Seq(targetCb)`), for
843
+ // example. The scan is capture-only and halts at nested functions.
844
+ this.scanFnRefSubtree(node, 0);
368
845
  return;
846
+ }
369
847
  }
370
848
  // Pascal-specific AST handling
371
849
  if (this.language === 'pascal') {
@@ -373,6 +851,10 @@ class TreeSitterExtractor {
373
851
  if (skipChildren)
374
852
  return;
375
853
  }
854
+ // Function-as-value capture (#756) — independent of the dispatch ladder
855
+ // below (the captured container types have no other handler there), so it
856
+ // can never shadow or be shadowed by an extraction branch.
857
+ this.maybeCaptureFnRefs(node, nodeType);
376
858
  // Check for function declarations
377
859
  // For Python/Ruby, function_definition inside a class should be treated as method
378
860
  if (this.extractor.functionTypes.includes(nodeType)) {
@@ -414,8 +896,31 @@ class TreeSitterExtractor {
414
896
  }
415
897
  // Check for method declarations (only if not already handled by functionTypes)
416
898
  else if (this.extractor.methodTypes.includes(nodeType)) {
417
- this.extractMethod(node);
418
- skipChildren = true; // extractMethod visits children via visitFunctionBody
899
+ // TS/JS class fields parse as a methodTypes node; only function-valued
900
+ // fields are methods — a plain field (`public fonts: Fonts;`) is a
901
+ // property (#808). classifyMethodNode is absent for other languages.
902
+ if (this.extractor.classifyMethodNode?.(node) === 'property') {
903
+ const propNode = this.extractProperty(node);
904
+ // Walk the initializer so its calls/instantiations attribute to the
905
+ // property (`history = createHistory()` → history calls
906
+ // createHistory). The old field-as-method path never walked these
907
+ // (resolveBody only resolves function bodies), so this is additive.
908
+ const valueNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
909
+ if (propNode && valueNode) {
910
+ this.nodeStack.push(propNode.id);
911
+ this.visitFunctionBody(valueNode, '');
912
+ this.nodeStack.pop();
913
+ }
914
+ // A field initializer can also register callbacks
915
+ // (`static handlers = { click: onClick }`) — scan it for
916
+ // function-as-value candidates (capture-only, halts at functions).
917
+ this.scanFnRefSubtree(node, 0);
918
+ skipChildren = true;
919
+ }
920
+ else {
921
+ this.extractMethod(node);
922
+ skipChildren = true; // extractMethod visits children via visitFunctionBody
923
+ }
419
924
  }
420
925
  // Check for interface/protocol/trait declarations
421
926
  else if (this.extractor.interfaceTypes.includes(nodeType)) {
@@ -441,17 +946,38 @@ class TreeSitterExtractor {
441
946
  // Check for class properties (e.g. C# property_declaration)
442
947
  else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
443
948
  this.extractProperty(node);
949
+ // Property initializers aren't walked — scan for function-as-value
950
+ // candidates (#756): Scala `val table = Seq(targetCb)` in an object,
951
+ // Kotlin `val cb = ::handler` class properties.
952
+ this.scanFnRefSubtree(node, 0);
444
953
  skipChildren = true;
445
954
  }
446
955
  // Check for class fields (e.g. Java field_declaration, C# field_declaration)
447
956
  else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
448
957
  this.extractField(node);
958
+ // Field initializers aren't walked — scan for function-as-value
959
+ // candidates (#756): Java `List<IntConsumer> table = List.of(Main::cb)`,
960
+ // C# `List<Action<int>> table = new() { TargetCb }`.
961
+ this.scanFnRefSubtree(node, 0);
449
962
  skipChildren = true;
450
963
  }
451
964
  // Check for variable declarations (const, let, var, etc.)
452
- // Only extract top-level variables (not inside functions/methods)
453
- else if (this.extractor.variableTypes.includes(nodeType) && !this.isInsideClassLikeNode()) {
965
+ // Only extract top-level variables (not inside functions/methods) — plus
966
+ // class/module-scope CONSTANTS, which Ruby (and other const-in-class
967
+ // languages) keep almost exclusively inside a class/module. A Ruby `CONST =
968
+ // …` has a `constant`-typed LHS; other languages don't put one here, so this
969
+ // is effectively Ruby-only and doesn't disturb their class-internal locals.
970
+ else if (this.extractor.variableTypes.includes(nodeType) &&
971
+ (!this.isInsideClassLikeNode() || this.isClassScopeConstantAssignment(node))) {
454
972
  this.extractVariable(node);
973
+ // extractVariable doesn't walk every initializer shape (object literals
974
+ // are deliberately skipped; Python/Ruby don't walk at all), so scan the
975
+ // declaration subtree for function-as-value candidates — `const routes =
976
+ // { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts
977
+ // at nested function definitions (their bodies are walked — and
978
+ // attributed — separately) and flush-time dedup absorbs any overlap with
979
+ // initializers extractVariable DOES walk.
980
+ this.scanFnRefSubtree(node, 0);
455
981
  skipChildren = true; // extractVariable handles children
456
982
  }
457
983
  // Swift stored properties inside a type. Swift instance properties aren't
@@ -464,6 +990,20 @@ class TreeSitterExtractor {
464
990
  nodeType === 'property_declaration' &&
465
991
  this.isInsideClassLikeNode()) {
466
992
  const ownerId = this.nodeStack[this.nodeStack.length - 1];
993
+ // A `static let`/`static var` member is a SHARED constant of the type
994
+ // (Swift's `static`-namespacing idiom, esp. in `enum`/`struct`) — extract
995
+ // it as `constant`/`variable` so value-reference edges can target it. An
996
+ // instance stored property stays a `field` (per-instance; Swift instance
997
+ // properties otherwise aren't own nodes — that's unchanged). A *computed*
998
+ // property (getter, no stored value) is never a constant — skip the node.
999
+ const { nameNode, isLet, isComputed } = swiftPropertyInfo(node, this.source);
1000
+ if (nameNode && !isComputed) {
1001
+ const isStatic = this.extractor.isStatic?.(node) ?? false;
1002
+ this.createNode(isStatic ? (isLet ? 'constant' : 'variable') : 'field', (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source), node, {
1003
+ visibility: this.extractor.getVisibility?.(node),
1004
+ isStatic,
1005
+ });
1006
+ }
467
1007
  if (ownerId) {
468
1008
  this.extractDecoratorsFor(node, ownerId);
469
1009
  this.extractVariableTypeAnnotation(node, ownerId);
@@ -521,6 +1061,22 @@ class TreeSitterExtractor {
521
1061
  if (parentId)
522
1062
  this.emitReExportRefs(node, parentId);
523
1063
  }
1064
+ // Vuex MODULE default export — `export default { namespaced, actions: {…},
1065
+ // mutations: {…} }` (the canonical Vuex module shape). Object-literal methods
1066
+ // aren't otherwise extracted, so scan the config's actions/mutations/getters
1067
+ // collections and extract their methods as nodes. Store-file gated (the
1068
+ // ≥2-signal heuristic) so a plain default-exported object is untouched; skip
1069
+ // the subtree afterward (the collection methods are now handled).
1070
+ else if (nodeType === 'export_statement' &&
1071
+ (this.language === 'typescript' || this.language === 'tsx' ||
1072
+ this.language === 'javascript' || this.language === 'jsx') &&
1073
+ this.looksLikeVueStoreFile()) {
1074
+ const exported = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
1075
+ if (exported && (exported.type === 'object' || exported.type === 'object_expression')) {
1076
+ this.extractStoreCollectionMethods(exported);
1077
+ skipChildren = true;
1078
+ }
1079
+ }
524
1080
  // Check for function calls
525
1081
  else if (this.extractor.callTypes.includes(nodeType)) {
526
1082
  this.extractCall(node);
@@ -632,6 +1188,8 @@ class TreeSitterExtractor {
632
1188
  });
633
1189
  }
634
1190
  }
1191
+ if (this.valueRefsEnabled)
1192
+ this.captureValueRefScope(kind, name, id, node);
635
1193
  return newNode;
636
1194
  }
637
1195
  /**
@@ -727,6 +1285,18 @@ class TreeSitterExtractor {
727
1285
  parentNode.kind === 'enum' ||
728
1286
  parentNode.kind === 'module');
729
1287
  }
1288
+ /**
1289
+ * Ruby `CONST = …` assignment whose LHS is a `constant` node — a class/module
1290
+ * (or top-level) constant worth extracting as a symbol even inside a class.
1291
+ * Other languages don't give an assignment a `constant`-typed LHS, so this
1292
+ * gate is effectively Ruby-only.
1293
+ */
1294
+ isClassScopeConstantAssignment(node) {
1295
+ if (node.type !== 'assignment')
1296
+ return false;
1297
+ const left = (0, tree_sitter_helpers_1.getChildByField)(node, 'left') ?? node.namedChild(0);
1298
+ return left?.type === 'constant';
1299
+ }
730
1300
  /**
731
1301
  * Extract a function
732
1302
  */
@@ -788,6 +1358,7 @@ class TreeSitterExtractor {
788
1358
  const isExported = this.extractor.isExported?.(node, this.source);
789
1359
  const isAsync = this.extractor.isAsync?.(node);
790
1360
  const isStatic = this.extractor.isStatic?.(node);
1361
+ const returnType = this.extractor.getReturnType?.(node, this.source);
791
1362
  const funcNode = this.createNode('function', name, node, {
792
1363
  docstring,
793
1364
  signature,
@@ -795,6 +1366,7 @@ class TreeSitterExtractor {
795
1366
  isExported,
796
1367
  isAsync,
797
1368
  isStatic,
1369
+ returnType,
798
1370
  });
799
1371
  if (!funcNode)
800
1372
  return;
@@ -813,6 +1385,70 @@ class TreeSitterExtractor {
813
1385
  }
814
1386
  this.nodeStack.pop();
815
1387
  }
1388
+ /**
1389
+ * Detect a React component declared via an HOC wrapper whose result is itself a
1390
+ * component: `forwardRef(...)`, `memo(...)`, `React.forwardRef/memo(...)`, and
1391
+ * styled-components / emotion `styled.tag\`…\`` / `styled(Base)\`…\``. These
1392
+ * initializers are a call / tagged-template (not a bare arrow), so the const is
1393
+ * otherwise classified `constant` — and a constant is skipped by both the
1394
+ * JSX-render edge synthesizer and component resolution, so `<Button/>` usages
1395
+ * get no edge and callers/impact silently return empty (#841).
1396
+ *
1397
+ * Returns `{ inner }` — the inline render function to extract as the component
1398
+ * body, or `null` when the wrapper has no inline function (`memo(Imported)`,
1399
+ * `styled.button\`…\``) and only a bodyless component node is minted — or
1400
+ * `undefined` when this initializer is not a recognized component wrapper.
1401
+ */
1402
+ reactComponentHoc(valueNode) {
1403
+ if (valueNode.type !== 'call_expression')
1404
+ return undefined;
1405
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(valueNode, 'function');
1406
+ if (!callee)
1407
+ return undefined;
1408
+ const calleeText = (0, tree_sitter_helpers_1.getNodeText)(callee, this.source);
1409
+ // styled-components / emotion: `styled.button\`…\`` / `styled(Base)\`…\``.
1410
+ // tree-sitter models these tagged templates as a call_expression whose callee
1411
+ // is the `styled.x` / `styled(Base)` tag (\b avoids matching `styledFoo`).
1412
+ // No inline render fn — the argument is the CSS template.
1413
+ if (/^styled\b/.test(calleeText))
1414
+ return { inner: null };
1415
+ // React HOCs: `forwardRef`/`memo`/`React.forwardRef`/`React.memo`.
1416
+ if (!REACT_COMPONENT_HOCS.has(calleeText))
1417
+ return undefined;
1418
+ // The first arrow / function-expression argument is the render fn (if inline;
1419
+ // `memo(Imported)` passes a bare identifier and has none).
1420
+ const args = (0, tree_sitter_helpers_1.getChildByField)(valueNode, 'arguments');
1421
+ let inner = null;
1422
+ if (args) {
1423
+ for (let i = 0; i < args.namedChildCount; i++) {
1424
+ const a = args.namedChild(i);
1425
+ if (a && (a.type === 'arrow_function' || a.type === 'function_expression')) {
1426
+ inner = a;
1427
+ break;
1428
+ }
1429
+ }
1430
+ }
1431
+ return { inner };
1432
+ }
1433
+ /**
1434
+ * Emit a `component` node for an HOC-wrapped React component declaration (see
1435
+ * reactComponentHoc). Named by the declarator (`Button`) and located at it so
1436
+ * the node range spans the body. When the wrapper has an inline render
1437
+ * function, its body is walked so the component's callees (hooks, helpers) are
1438
+ * captured under the component node — matching how a plain
1439
+ * `const Foo = () => …` arrow component already behaves.
1440
+ */
1441
+ extractReactComponentNode(name, declarator, innerFn, extra) {
1442
+ const compNode = this.createNode('component', name, declarator, extra);
1443
+ if (!compNode || !innerFn || !this.extractor)
1444
+ return;
1445
+ this.nodeStack.push(compNode.id);
1446
+ const body = this.extractor.resolveBody?.(innerFn, this.extractor.bodyField)
1447
+ ?? (0, tree_sitter_helpers_1.getChildByField)(innerFn, this.extractor.bodyField);
1448
+ if (body)
1449
+ this.visitFunctionBody(body, compNode.id);
1450
+ this.nodeStack.pop();
1451
+ }
816
1452
  /**
817
1453
  * Extract a class
818
1454
  */
@@ -849,6 +1485,12 @@ class TreeSitterExtractor {
849
1485
  this.visitNode(child);
850
1486
  }
851
1487
  }
1488
+ // Synthesize compile-time-generated members (Lombok accessors, #912). Runs
1489
+ // after the body so the hook can dedup against hand-written members, and
1490
+ // while the class is still on the stack so containment/QNs attach.
1491
+ if (this.extractor.synthesizeMembers) {
1492
+ this.extractor.synthesizeMembers(node, this.makeExtractorContext());
1493
+ }
852
1494
  this.nodeStack.pop();
853
1495
  }
854
1496
  /**
@@ -894,12 +1536,14 @@ class TreeSitterExtractor {
894
1536
  const visibility = this.extractor.getVisibility?.(node);
895
1537
  const isAsync = this.extractor.isAsync?.(node);
896
1538
  const isStatic = this.extractor.isStatic?.(node);
1539
+ const returnType = this.extractor.getReturnType?.(node, this.source);
897
1540
  const extraProps = {
898
1541
  docstring,
899
1542
  signature,
900
1543
  visibility,
901
1544
  isAsync,
902
1545
  isStatic,
1546
+ returnType,
903
1547
  };
904
1548
  if (receiverType) {
905
1549
  extraProps.qualifiedName = `${receiverType}::${name}`;
@@ -973,8 +1617,10 @@ class TreeSitterExtractor {
973
1617
  if (!this.extractor)
974
1618
  return;
975
1619
  // Skip forward declarations and type references (no body = not a definition)
1620
+ // — EXCEPT C# positional records (`record struct M(decimal Amount);`),
1621
+ // complete definitions with no body block. (#831)
976
1622
  const body = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
977
- if (!body)
1623
+ if (!body && node.type !== 'record_declaration')
978
1624
  return;
979
1625
  const name = extractName(node, this.source, this.extractor);
980
1626
  const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
@@ -992,15 +1638,18 @@ class TreeSitterExtractor {
992
1638
  // C# primary-constructor parameter dependencies (`struct P(int x)`, and
993
1639
  // `record struct M(decimal Amount)` which the grammar nests here).
994
1640
  this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
995
- // Push to stack for field extraction
996
- this.nodeStack.push(structNode.id);
997
- for (let i = 0; i < body.namedChildCount; i++) {
998
- const child = body.namedChild(i);
999
- if (child) {
1000
- this.visitNode(child);
1641
+ // Push to stack for field extraction (bodiless positional records have
1642
+ // no members to visit)
1643
+ if (body) {
1644
+ this.nodeStack.push(structNode.id);
1645
+ for (let i = 0; i < body.namedChildCount; i++) {
1646
+ const child = body.namedChild(i);
1647
+ if (child) {
1648
+ this.visitNode(child);
1649
+ }
1001
1650
  }
1651
+ this.nodeStack.pop();
1002
1652
  }
1003
- this.nodeStack.pop();
1004
1653
  }
1005
1654
  /**
1006
1655
  * Extract an enum
@@ -1073,22 +1722,35 @@ class TreeSitterExtractor {
1073
1722
  */
1074
1723
  extractProperty(node) {
1075
1724
  if (!this.extractor)
1076
- return;
1725
+ return null;
1077
1726
  const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
1078
1727
  const visibility = this.extractor.getVisibility?.(node);
1079
1728
  const isStatic = this.extractor.isStatic?.(node) ?? false;
1080
1729
  const hookName = this.extractor.extractPropertyName?.(node, this.source);
1730
+ // JS `field_definition` names its key the `property` field (TS uses
1731
+ // `name`) — try both before the generic identifier scan (#808).
1081
1732
  const nameNode = hookName
1082
1733
  ? null
1083
- : (0, tree_sitter_helpers_1.getChildByField)(node, 'name') || node.namedChildren.find(c => c.type === 'identifier');
1734
+ : (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
1735
+ (0, tree_sitter_helpers_1.getChildByField)(node, 'property') ||
1736
+ node.namedChildren.find(c => c.type === 'identifier');
1084
1737
  const name = hookName ?? (nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : null);
1085
1738
  if (!name)
1086
- return;
1087
- // Get property type from the type child (first named child that isn't modifier or identifier)
1088
- const typeNode = node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
1089
- && c.type !== 'identifier' && c.type !== 'accessor_list'
1090
- && c.type !== 'accessors' && c.type !== 'equals_value_clause');
1091
- const typeText = typeNode ? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source) : undefined;
1739
+ return null;
1740
+ // Get property type. TS/JS field definitions carry an explicit `type`
1741
+ // field (a `type_annotation`); their other named children are the name
1742
+ // and the initializer VALUE, which the generic finder below would
1743
+ // wrongly pick — so fields use the type field only (#808). Other
1744
+ // languages (C# property_declaration) keep the generic scan.
1745
+ const isTsJsField = node.type === 'public_field_definition' || node.type === 'field_definition';
1746
+ const typeNode = isTsJsField
1747
+ ? (0, tree_sitter_helpers_1.getChildByField)(node, 'type')
1748
+ : node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
1749
+ && c.type !== 'identifier' && c.type !== 'accessor_list'
1750
+ && c.type !== 'accessors' && c.type !== 'equals_value_clause');
1751
+ const typeText = typeNode
1752
+ ? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source).replace(/^:\s*/, '')
1753
+ : undefined;
1092
1754
  const signature = typeText ? `${typeText} ${name}` : name;
1093
1755
  const propNode = this.createNode('property', name, node, {
1094
1756
  docstring,
@@ -1105,6 +1767,7 @@ class TreeSitterExtractor {
1105
1767
  // `type_annotation` children; the C# branch walks the `type` field.
1106
1768
  this.extractTypeAnnotations(node, propNode.id);
1107
1769
  }
1770
+ return propNode;
1108
1771
  }
1109
1772
  /**
1110
1773
  * Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
@@ -1116,6 +1779,15 @@ class TreeSitterExtractor {
1116
1779
  const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
1117
1780
  const visibility = this.extractor.getVisibility?.(node);
1118
1781
  const isStatic = this.extractor.isStatic?.(node) ?? false;
1782
+ // A class field that is actually a CONSTANT (Java `static final`, C# `const`
1783
+ // / `static readonly`) is extracted as `constant` kind, not `field`, so
1784
+ // value-reference edges treat it as a target (the gate accepts
1785
+ // constant/variable, not field). Scoped to languages whose `isConst`
1786
+ // predicate is field-shaped — other languages' fields stay `field`.
1787
+ const fieldKind = (this.language === 'java' || this.language === 'csharp') &&
1788
+ (this.extractor.isConst?.(node) ?? false)
1789
+ ? 'constant'
1790
+ : 'field';
1119
1791
  // Java field_declaration: "private final String name = value;" → variable_declarator(s) are direct children
1120
1792
  // C# field_declaration: wraps in variable_declaration → variable_declarator(s)
1121
1793
  let declarators = node.namedChildren.filter(c => c.type === 'variable_declarator');
@@ -1168,7 +1840,7 @@ class TreeSitterExtractor {
1168
1840
  continue;
1169
1841
  const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
1170
1842
  const signature = typeText ? `${typeText} ${name}` : name;
1171
- const fieldNode = this.createNode('field', name, decl, {
1843
+ const fieldNode = this.createNode(fieldKind, name, decl, {
1172
1844
  docstring,
1173
1845
  signature,
1174
1846
  visibility,
@@ -1193,7 +1865,7 @@ class TreeSitterExtractor {
1193
1865
  || node.namedChildren.find(c => c.type === 'identifier');
1194
1866
  if (nameNode) {
1195
1867
  const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
1196
- this.createNode('field', name, node, {
1868
+ this.createNode(fieldKind, name, node, {
1197
1869
  docstring,
1198
1870
  visibility,
1199
1871
  isStatic,
@@ -1309,6 +1981,317 @@ class TreeSitterExtractor {
1309
1981
  }
1310
1982
  return null;
1311
1983
  }
1984
+ /**
1985
+ * RTK Query: from a `createApi({ ..., endpoints: build => ({...}) })` or a
1986
+ * `baseApi.injectEndpoints({ endpoints: build => ({...}) })` call initializer,
1987
+ * return the object literal of endpoint definitions (the object the `endpoints`
1988
+ * arrow returns). Returns null for any other call — the common case — so this
1989
+ * stays cheap and silent. Keyed on the RTK entry-point names (`createApi` /
1990
+ * `injectEndpoints`) like the framework extractors key on their library APIs.
1991
+ */
1992
+ findRtkEndpointsObject(callNode) {
1993
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function');
1994
+ if (!callee)
1995
+ return null;
1996
+ const calleeName = callee.type === 'identifier'
1997
+ ? (0, tree_sitter_helpers_1.getNodeText)(callee, this.source)
1998
+ : callee.type === 'member_expression'
1999
+ ? (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source)
2000
+ : '';
2001
+ if (calleeName !== 'createApi' && calleeName !== 'injectEndpoints')
2002
+ return null;
2003
+ const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
2004
+ if (!args)
2005
+ return null;
2006
+ for (let i = 0; i < args.namedChildCount; i++) {
2007
+ const arg = args.namedChild(i);
2008
+ if (arg?.type !== 'object' && arg?.type !== 'object_expression')
2009
+ continue;
2010
+ for (let j = 0; j < arg.namedChildCount; j++) {
2011
+ const member = arg.namedChild(j);
2012
+ // Two equally-common spellings: `endpoints: build => ({...})` (pair with an
2013
+ // arrow value) and `endpoints(build) { return {...} }` (method shorthand).
2014
+ if (member?.type === 'pair') {
2015
+ const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
2016
+ if (!key || (0, tree_sitter_helpers_1.getNodeText)(key, this.source) !== 'endpoints')
2017
+ continue;
2018
+ const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2019
+ if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
2020
+ return this.functionReturnedObject(value);
2021
+ }
2022
+ }
2023
+ else if (member?.type === 'method_definition') {
2024
+ const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'name');
2025
+ if (!key || (0, tree_sitter_helpers_1.getNodeText)(key, this.source) !== 'endpoints')
2026
+ continue;
2027
+ return this.functionReturnedObject(member);
2028
+ }
2029
+ }
2030
+ }
2031
+ return null;
2032
+ }
2033
+ /**
2034
+ * Extract each RTK Query endpoint (`getX: build.query({...})` / `build.mutation`)
2035
+ * as a function node named by the endpoint key, spanning its primary handler
2036
+ * (the `queryFn`/`query` arrow) so the fetch logic's calls attribute to the
2037
+ * endpoint. Without this an endpoint exists only as an object-literal property —
2038
+ * never a node — so the generated `useXQuery` hook can't be bridged to it.
2039
+ */
2040
+ extractRtkEndpoints(obj) {
2041
+ for (let i = 0; i < obj.namedChildCount; i++) {
2042
+ const member = obj.namedChild(i);
2043
+ if (member?.type !== 'pair')
2044
+ continue;
2045
+ const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
2046
+ const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2047
+ if (!key || value?.type !== 'call_expression')
2048
+ continue;
2049
+ // The value must be a builder dispatch `<builder>.query|mutation(...)`.
2050
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(value, 'function');
2051
+ if (callee?.type !== 'member_expression')
2052
+ continue;
2053
+ const method = (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source);
2054
+ if (method !== 'query' && method !== 'mutation' && method !== 'infiniteQuery')
2055
+ continue;
2056
+ const handler = this.rtkEndpointHandler(value);
2057
+ if (handler) {
2058
+ this.extractFunction(handler, this.objectKeyName(key));
2059
+ }
2060
+ else {
2061
+ // Factory / config-only handler (`queryFn: makeQueryFn(url)`): no function
2062
+ // literal to name. Mint a bare endpoint node spanning the builder call so
2063
+ // the generated hook still bridges to it, and walk the call so its handler
2064
+ // factory (and any inline transform) is captured as an outgoing edge.
2065
+ const epNode = this.createNode('function', this.objectKeyName(key), value, {
2066
+ signature: (0, tree_sitter_helpers_1.getNodeText)(value, this.source).slice(0, 80),
2067
+ });
2068
+ if (epNode) {
2069
+ this.nodeStack.push(epNode.id);
2070
+ this.visitFunctionBody(value, epNode.id);
2071
+ this.nodeStack.pop();
2072
+ }
2073
+ }
2074
+ }
2075
+ }
2076
+ /**
2077
+ * The primary handler arrow of a `build.query({ queryFn|query: (…) => … })`
2078
+ * endpoint — prefers `queryFn`, then `query`, else the first function-valued
2079
+ * property. Returns null when the endpoint is config-only (no handler arrow).
2080
+ */
2081
+ rtkEndpointHandler(callNode) {
2082
+ const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
2083
+ if (!args)
2084
+ return null;
2085
+ for (let i = 0; i < args.namedChildCount; i++) {
2086
+ const arg = args.namedChild(i);
2087
+ if (arg?.type !== 'object' && arg?.type !== 'object_expression')
2088
+ continue;
2089
+ let queryFn = null;
2090
+ let query = null;
2091
+ let firstFn = null;
2092
+ for (let j = 0; j < arg.namedChildCount; j++) {
2093
+ const member = arg.namedChild(j);
2094
+ // The handler may be `queryFn: () => …` / `query: () => …` (pair) or the
2095
+ // method-shorthand `query(arg) { … }` / `queryFn(arg) { … }`.
2096
+ let fn = null;
2097
+ let kn = '';
2098
+ if (member?.type === 'pair') {
2099
+ const v = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2100
+ if (v?.type === 'arrow_function' || v?.type === 'function_expression') {
2101
+ fn = v;
2102
+ const k = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
2103
+ kn = k ? (0, tree_sitter_helpers_1.getNodeText)(k, this.source) : '';
2104
+ }
2105
+ }
2106
+ else if (member?.type === 'method_definition') {
2107
+ fn = member;
2108
+ const k = (0, tree_sitter_helpers_1.getChildByField)(member, 'name');
2109
+ kn = k ? (0, tree_sitter_helpers_1.getNodeText)(k, this.source) : '';
2110
+ }
2111
+ if (!fn)
2112
+ continue;
2113
+ if (kn === 'queryFn')
2114
+ queryFn = fn;
2115
+ else if (kn === 'query')
2116
+ query = fn;
2117
+ if (!firstFn)
2118
+ firstFn = fn;
2119
+ }
2120
+ if (queryFn)
2121
+ return queryFn;
2122
+ if (query)
2123
+ return query;
2124
+ if (firstFn)
2125
+ return firstFn;
2126
+ }
2127
+ return null;
2128
+ }
2129
+ /**
2130
+ * RTK Query generated-hook bindings. `export const { useGetXQuery,
2131
+ * useUpdateYMutation } = someApi` destructures the hooks RTK generates per
2132
+ * endpoint off a createApi result. They are real exported symbols that
2133
+ * components import, but destructured bindings aren't otherwise extracted —
2134
+ * mint a function node per binding matching the RTK hook convention so the hook
2135
+ * resolves and the synthesizer can bridge it to its endpoint. Gated tight by the
2136
+ * caller (object-pattern off a bare identifier) + the name convention here, so
2137
+ * ordinary destructures stay unextracted.
2138
+ */
2139
+ extractRtkHookBindings(pattern, isExported) {
2140
+ for (let i = 0; i < pattern.namedChildCount; i++) {
2141
+ const binding = pattern.namedChild(i);
2142
+ if (binding?.type !== 'shorthand_property_identifier_pattern')
2143
+ continue;
2144
+ const name = (0, tree_sitter_helpers_1.getNodeText)(binding, this.source);
2145
+ if (!RTK_HOOK_NAME_RE.test(name))
2146
+ continue;
2147
+ this.createNode('function', name, binding, {
2148
+ isExported,
2149
+ signature: '= RTK Query generated hook',
2150
+ });
2151
+ }
2152
+ }
2153
+ /** Cheap per-file heuristic: the file carries ≥2 distinct Vue-store signals
2154
+ * (defineStore/createStore/Vuex, or the actions/mutations/getters/namespaced
2155
+ * vocabulary). Gates the non-exported `const actions = {…}` Vuex-module form so
2156
+ * a stray `const actions` in unrelated code is never mistaken for a store. */
2157
+ looksLikeVueStoreFile() {
2158
+ if (this.vueStoreFile !== null)
2159
+ return this.vueStoreFile;
2160
+ const seen = new Set();
2161
+ VUE_STORE_FILE_SIGNAL.lastIndex = 0;
2162
+ let m;
2163
+ while ((m = VUE_STORE_FILE_SIGNAL.exec(this.source))) {
2164
+ seen.add(m[0]);
2165
+ if (seen.size >= 2)
2166
+ break;
2167
+ }
2168
+ this.vueStoreFile = seen.size >= 2;
2169
+ return this.vueStoreFile;
2170
+ }
2171
+ /** True if an object literal has ≥1 inline function member (`key: () => …` /
2172
+ * `method(){}`) — distinguishes an inline action map (zustand/SvelteKit form
2173
+ * actions) from a Pinia SETUP store's all-shorthand `return { foo, bar }`
2174
+ * (whose functions are body-local consts, walked normally instead). */
2175
+ objectHasInlineFunctions(obj) {
2176
+ for (let i = 0; i < obj.namedChildCount; i++) {
2177
+ const member = obj.namedChild(i);
2178
+ if (member?.type === 'method_definition')
2179
+ return true;
2180
+ if (member?.type === 'pair') {
2181
+ const v = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2182
+ if (v?.type === 'arrow_function' || v?.type === 'function_expression')
2183
+ return true;
2184
+ }
2185
+ }
2186
+ return false;
2187
+ }
2188
+ /** Vue store action/mutation/getter collections defined INLINE in a store call:
2189
+ * `defineStore({ actions: {…}, getters: {…} })` (Pinia options form),
2190
+ * `defineStore('id', { actions: {…} })`, `createStore({ mutations: {…} })`,
2191
+ * `new Vuex.Store({ actions: {…} })`. Returns the object literals under those
2192
+ * keys so their methods become nodes. Gated on the store-factory callee. */
2193
+ findVueStoreCollectionObjects(callNode) {
2194
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function') ?? (0, tree_sitter_helpers_1.getChildByField)(callNode, 'constructor');
2195
+ if (!callee)
2196
+ return [];
2197
+ const calleeName = callee.type === 'identifier'
2198
+ ? (0, tree_sitter_helpers_1.getNodeText)(callee, this.source)
2199
+ : callee.type === 'member_expression'
2200
+ ? (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source)
2201
+ : '';
2202
+ if (!VUE_STORE_FACTORY_CALLEES.has(calleeName) && calleeName !== 'Store')
2203
+ return [];
2204
+ const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
2205
+ if (!args)
2206
+ return [];
2207
+ const objects = [];
2208
+ for (let i = 0; i < args.namedChildCount; i++) {
2209
+ const arg = args.namedChild(i);
2210
+ if (arg?.type !== 'object' && arg?.type !== 'object_expression')
2211
+ continue;
2212
+ for (let j = 0; j < arg.namedChildCount; j++) {
2213
+ const member = arg.namedChild(j);
2214
+ if (member?.type !== 'pair')
2215
+ continue;
2216
+ const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
2217
+ if (!key || !VUE_STORE_COLLECTION_NAMES.has((0, tree_sitter_helpers_1.getNodeText)(key, this.source)))
2218
+ continue;
2219
+ const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2220
+ if (value && (value.type === 'object' || value.type === 'object_expression')) {
2221
+ objects.push(value);
2222
+ }
2223
+ }
2224
+ }
2225
+ return objects;
2226
+ }
2227
+ /** Extract the methods of a store-config object's `actions`/`mutations`/`getters`
2228
+ * properties. Used for the canonical Vuex MODULE shape `export default {
2229
+ * namespaced, actions: {…}, mutations: {…} }` — object-literal methods aren't
2230
+ * otherwise extracted, so the actions/mutations would never be nodes. */
2231
+ extractStoreCollectionMethods(configObj) {
2232
+ for (let j = 0; j < configObj.namedChildCount; j++) {
2233
+ const member = configObj.namedChild(j);
2234
+ if (member?.type !== 'pair')
2235
+ continue;
2236
+ const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
2237
+ if (!key || !VUE_STORE_COLLECTION_NAMES.has((0, tree_sitter_helpers_1.getNodeText)(key, this.source)))
2238
+ continue;
2239
+ const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
2240
+ if (value && (value.type === 'object' || value.type === 'object_expression')) {
2241
+ this.extractObjectLiteralFunctions(value);
2242
+ }
2243
+ }
2244
+ }
2245
+ /** The SETUP function of a Pinia setup store (`defineStore('id', () => {…})`)
2246
+ * — an arrow/function arg with a block body. Returns null for the options form
2247
+ * (`defineStore({…})`) and for any non-defineStore call. The setup body's local
2248
+ * function consts are the store's actions; the generic body walk doesn't reach
2249
+ * them (nested functions are separate scopes), so they're extracted explicitly. */
2250
+ findPiniaSetupFn(callNode) {
2251
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function');
2252
+ if (!callee || callee.type !== 'identifier' || (0, tree_sitter_helpers_1.getNodeText)(callee, this.source) !== 'defineStore')
2253
+ return null;
2254
+ const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
2255
+ if (!args)
2256
+ return null;
2257
+ for (let i = 0; i < args.namedChildCount; i++) {
2258
+ const arg = args.namedChild(i);
2259
+ if (arg?.type !== 'arrow_function' && arg?.type !== 'function_expression')
2260
+ continue;
2261
+ const body = (0, tree_sitter_helpers_1.getChildByField)(arg, 'body');
2262
+ if (body?.type === 'statement_block')
2263
+ return arg; // block body ⇒ setup form
2264
+ }
2265
+ return null;
2266
+ }
2267
+ /** Extract a Pinia setup store's actions: the body-local `const foo = () => …`
2268
+ * / `function foo(){}` declarations, named by the binding. (State refs and other
2269
+ * consts are left to the normal value-extraction; only the functions matter as
2270
+ * the store's callable surface.) */
2271
+ extractPiniaSetupBody(setupFn) {
2272
+ const body = (0, tree_sitter_helpers_1.getChildByField)(setupFn, 'body');
2273
+ if (!body || body.type !== 'statement_block')
2274
+ return;
2275
+ for (let i = 0; i < body.namedChildCount; i++) {
2276
+ const stmt = body.namedChild(i);
2277
+ if (!stmt)
2278
+ continue;
2279
+ if (stmt.type === 'function_declaration') {
2280
+ this.extractFunction(stmt);
2281
+ }
2282
+ else if (this.extractor.variableTypes.includes(stmt.type)) {
2283
+ for (let j = 0; j < stmt.namedChildCount; j++) {
2284
+ const decl = stmt.namedChild(j);
2285
+ if (decl?.type !== 'variable_declarator')
2286
+ continue;
2287
+ const v = (0, tree_sitter_helpers_1.getChildByField)(decl, 'value');
2288
+ if (v?.type === 'arrow_function' || v?.type === 'function_expression') {
2289
+ this.extractFunction(v); // name resolved from the parent declarator
2290
+ }
2291
+ }
2292
+ }
2293
+ }
2294
+ }
1312
2295
  /**
1313
2296
  * Extract a variable declaration (const, let, var, etc.)
1314
2297
  *
@@ -1338,8 +2321,15 @@ class TreeSitterExtractor {
1338
2321
  const valueNode = (0, tree_sitter_helpers_1.getChildByField)(child, 'value');
1339
2322
  if (nameNode) {
1340
2323
  // Skip destructured patterns (e.g., `let { x, y } = $props()` in Svelte)
1341
- // These produce ugly multi-line names like "{ class: className }"
2324
+ // These produce ugly multi-line names like "{ class: className }".
2325
+ // EXCEPT `export const { useGetXQuery } = someApi` — the RTK Query
2326
+ // generated hooks: real exported symbols destructured off a createApi
2327
+ // result. Mint a node per binding matching the hook convention (gated
2328
+ // on a bare-identifier RHS so ordinary destructures stay skipped).
1342
2329
  if (nameNode.type === 'object_pattern' || nameNode.type === 'array_pattern') {
2330
+ if (nameNode.type === 'object_pattern' && valueNode?.type === 'identifier') {
2331
+ this.extractRtkHookBindings(nameNode, isExported);
2332
+ }
1343
2333
  continue;
1344
2334
  }
1345
2335
  const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
@@ -1351,6 +2341,25 @@ class TreeSitterExtractor {
1351
2341
  // Capture first 100 chars of initializer for context (stored in signature for searchability)
1352
2342
  const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
1353
2343
  const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
2344
+ // React HOC-wrapped components (`forwardRef`/`memo`/`styled`) — see
2345
+ // reactComponentHoc. The initializer is a call / tagged-template (not
2346
+ // a bare arrow), so without this the const is a plain `constant`,
2347
+ // which the JSX-render synthesizer and component resolution both skip
2348
+ // → `<Button/>` usages get no edge and callers/impact return empty
2349
+ // (the whole shadcn/ui design-system pattern, #841). PascalCase-gated
2350
+ // to the component naming convention so a memoization util
2351
+ // (`const cache = memo(fn)`) stays a constant.
2352
+ if (valueNode && /^[A-Z]/.test(name)) {
2353
+ const hoc = this.reactComponentHoc(valueNode);
2354
+ if (hoc) {
2355
+ this.extractReactComponentNode(name, child, hoc.inner, {
2356
+ docstring,
2357
+ signature: initSignature,
2358
+ isExported,
2359
+ });
2360
+ continue;
2361
+ }
2362
+ }
1354
2363
  const varNode = this.createNode(kind, name, child, {
1355
2364
  docstring,
1356
2365
  signature: initSignature,
@@ -1381,21 +2390,65 @@ class TreeSitterExtractor {
1381
2390
  : valueNode?.type === 'call_expression'
1382
2391
  ? this.findInitializerReturnedObject(valueNode)
1383
2392
  : null;
1384
- const extractObjectMethods = isExported && !!objectOfFns;
2393
+ // Only treat as an inline object-of-functions when the object actually
2394
+ // HAS inline functions. A Pinia SETUP store `defineStore('id', () => {
2395
+ // const foo = …; return { foo } })` returns an ALL-SHORTHAND object
2396
+ // whose functions are body-local consts — it must fall through to a
2397
+ // normal body walk (extracting those consts), not be skipped here.
2398
+ const hasInlineFns = !!objectOfFns && this.objectHasInlineFunctions(objectOfFns);
2399
+ const extractObjectMethods = isExported && !!objectOfFns && hasInlineFns;
2400
+ // RTK Query: `createApi`/`injectEndpoints` define endpoints as
2401
+ // object-literal properties whose values are `build.query/mutation(...)`
2402
+ // calls — nested under an `endpoints` arrow, so neither the
2403
+ // object-of-functions path above nor the normal walk extracts them.
2404
+ // Extract each endpoint as a function node (named by its key), and skip
2405
+ // walking the createApi call body (its handler arrows are extracted
2406
+ // individually below, exactly like the store-factory case).
2407
+ const rtkEndpoints = valueNode?.type === 'call_expression' ? this.findRtkEndpointsObject(valueNode) : null;
2408
+ // Pinia SETUP store: `defineStore('id', () => { const foo = …; return {…} })`.
2409
+ // Its actions are body-local consts the generic walk can't reach.
2410
+ const piniaSetup = valueNode?.type === 'call_expression' ? this.findPiniaSetupFn(valueNode) : null;
2411
+ // Vue store collections — make `actions`/`mutations`/`getters` findable
2412
+ // function nodes (the foundation under any later dispatch-bridge synth).
2413
+ // Two positions: INLINE in a store call (`defineStore({ actions: {…} })`
2414
+ // / `createStore` / `new Vuex.Store`), and the non-exported Vuex-MODULE
2415
+ // form (`const actions = {…}` at a store file's top level, wired via a
2416
+ // `export default { actions }`). The Pinia SETUP form is handled by the
2417
+ // body walk above (its actions are local consts).
2418
+ const storeCollections = [];
2419
+ if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
2420
+ storeCollections.push(...this.findVueStoreCollectionObjects(valueNode));
2421
+ }
2422
+ if (objectOfFns && !extractObjectMethods &&
2423
+ VUE_STORE_COLLECTION_NAMES.has(name) && this.looksLikeVueStoreFile()) {
2424
+ storeCollections.push(objectOfFns);
2425
+ }
1385
2426
  // Visit the initializer body for calls — EXCEPT object literals (their
1386
2427
  // function-valued properties are extracted below) and the store-factory
1387
- // call whose returned object we extract method-by-method below (walking
1388
- // the whole call would re-visit those method arrows and mis-attribute
1389
- // their inner calls to the file/module scope).
2428
+ // / createApi / store-collection call whose nested objects we extract
2429
+ // method-by-method below (walking the whole call would re-visit those
2430
+ // method arrows and mis-attribute their inner calls to the file scope).
1390
2431
  if (valueNode &&
1391
2432
  valueNode.type !== 'object' &&
1392
2433
  valueNode.type !== 'object_expression' &&
1393
- !(extractObjectMethods && valueNode.type === 'call_expression')) {
2434
+ !(extractObjectMethods && valueNode.type === 'call_expression') &&
2435
+ !rtkEndpoints &&
2436
+ !piniaSetup &&
2437
+ storeCollections.length === 0) {
1394
2438
  this.visitFunctionBody(valueNode, '');
1395
2439
  }
1396
2440
  if (extractObjectMethods && objectOfFns) {
1397
2441
  this.extractObjectLiteralFunctions(objectOfFns);
1398
2442
  }
2443
+ if (rtkEndpoints) {
2444
+ this.extractRtkEndpoints(rtkEndpoints);
2445
+ }
2446
+ if (piniaSetup) {
2447
+ this.extractPiniaSetupBody(piniaSetup);
2448
+ }
2449
+ for (const coll of storeCollections) {
2450
+ this.extractObjectLiteralFunctions(coll);
2451
+ }
1399
2452
  }
1400
2453
  }
1401
2454
  }
@@ -1404,7 +2457,9 @@ class TreeSitterExtractor {
1404
2457
  // Python/Ruby assignment: left = right
1405
2458
  const left = (0, tree_sitter_helpers_1.getChildByField)(node, 'left') || node.namedChild(0);
1406
2459
  const right = (0, tree_sitter_helpers_1.getChildByField)(node, 'right') || node.namedChild(1);
1407
- if (left && left.type === 'identifier') {
2460
+ // Ruby constant assignments (`MAX = 3`) have a `constant`-typed LHS, not
2461
+ // `identifier`; without this they were never extracted as symbols at all.
2462
+ if (left && (left.type === 'identifier' || left.type === 'constant')) {
1408
2463
  const name = (0, tree_sitter_helpers_1.getNodeText)(left, this.source);
1409
2464
  // Skip if name starts with lowercase and looks like a function call result
1410
2465
  // Python constants are usually UPPER_CASE
@@ -1422,12 +2477,13 @@ class TreeSitterExtractor {
1422
2477
  const specs = node.namedChildren.filter(c => c.type === 'var_spec' || c.type === 'const_spec');
1423
2478
  for (const spec of specs) {
1424
2479
  const nameNode = spec.namedChild(0);
2480
+ let varNode = null;
1425
2481
  if (nameNode && nameNode.type === 'identifier') {
1426
2482
  const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
1427
2483
  const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null;
1428
2484
  const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
1429
2485
  const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
1430
- this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
2486
+ varNode = this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
1431
2487
  docstring,
1432
2488
  signature: initSignature,
1433
2489
  });
@@ -1437,9 +2493,18 @@ class TreeSitterExtractor {
1437
2493
  // implementations) or `var c = pkg.New()` are extracted as
1438
2494
  // instantiates/calls dependencies — the body walker only covers
1439
2495
  // initializers inside functions, not these top-level declarations.
2496
+ // Scope the walk to the declared symbol so a call inside an anonymous
2497
+ // func_literal initializer — a cobra `RunE: func(){…}` handler, a
2498
+ // goroutine or callback closure — attributes to the var instead of
2499
+ // leaking to the file node (which reads as "no caller"), issue #693.
1440
2500
  const valueField = (0, tree_sitter_helpers_1.getChildByField)(spec, 'value');
1441
- if (valueField)
1442
- this.visitFunctionBody(valueField, '');
2501
+ if (valueField) {
2502
+ if (varNode)
2503
+ this.nodeStack.push(varNode.id);
2504
+ this.visitFunctionBody(valueField, varNode?.id ?? '');
2505
+ if (varNode)
2506
+ this.nodeStack.pop();
2507
+ }
1443
2508
  }
1444
2509
  // Handle short_var_declaration (:=)
1445
2510
  if (node.type === 'short_var_declaration') {
@@ -1481,6 +2546,65 @@ class TreeSitterExtractor {
1481
2546
  this.createNode(kind, name, nameNode, { docstring, signature: initSignature, isExported });
1482
2547
  });
1483
2548
  }
2549
+ else if (this.language === 'c') {
2550
+ // C: a `declaration` node's name nests inside the `declarator` field —
2551
+ // `init_declarator` (with value) or bare/pointer/array declarators (no
2552
+ // value); a `function_declarator` is a prototype, not a variable. The
2553
+ // generic fallback below only finds a *direct* identifier child, which C
2554
+ // never has, so file-scope consts/globals went unextracted entirely (and
2555
+ // so had no impact-radius edges). Only file-scope declarations are tracked
2556
+ // — locals inside a function body are skipped (a `static const` table read
2557
+ // by same-file functions is the value the impact graph wants, not every
2558
+ // block-local). C allows several declarators per declaration
2559
+ // (`int a = 1, b = 2;`), so iterate them.
2560
+ if (!hasFunctionAncestor(node)) {
2561
+ for (let i = 0; i < node.namedChildCount; i++) {
2562
+ const child = node.namedChild(i);
2563
+ if (!child)
2564
+ continue;
2565
+ // Accept only `init_declarator` (has a value) and pointer/array
2566
+ // declarators. A *bare* `identifier` declarator is deliberately
2567
+ // skipped: an unknown leading macro (`CURL_EXTERN`, `XXH_PUBLIC_API`)
2568
+ // makes tree-sitter-c misparse a prototype `MACRO RetType fn(args);`
2569
+ // as a declaration whose "variable" is the bare return-type
2570
+ // identifier, splitting `fn(args)` off as a bogus expression — minting
2571
+ // a spurious type-named global for every macro-prefixed prototype in a
2572
+ // header. Those misparses are always bare identifiers; real
2573
+ // consts/tables always carry an initializer. The only legit loss is
2574
+ // uninitialized scalar globals (`static int g;`).
2575
+ if (child.type !== 'init_declarator' &&
2576
+ child.type !== 'pointer_declarator' &&
2577
+ child.type !== 'array_declarator') {
2578
+ continue;
2579
+ }
2580
+ const nameNode = cDeclaratorIdentifier(child);
2581
+ if (!nameNode)
2582
+ continue;
2583
+ const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
2584
+ if (!name)
2585
+ continue;
2586
+ const valueNode = child.type === 'init_declarator' ? (0, tree_sitter_helpers_1.getChildByField)(child, 'value') : null;
2587
+ const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
2588
+ const initSignature = initValue
2589
+ ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}`
2590
+ : undefined;
2591
+ this.createNode(kind, name, child, { docstring, signature: initSignature, isExported });
2592
+ }
2593
+ }
2594
+ }
2595
+ else if (this.language === 'swift') {
2596
+ // Swift top-level property (`let X = …` / `var Y = …`). The name nests in
2597
+ // a `pattern`, which the generic fallback can't read, so top-level Swift
2598
+ // constants/globals went unextracted. A top-level `let`→`constant`,
2599
+ // `var`→`variable`; a computed property (getter, no value) is skipped.
2600
+ const { nameNode, isLet, isComputed } = swiftPropertyInfo(node, this.source);
2601
+ if (nameNode && !isComputed) {
2602
+ this.createNode(isLet ? 'constant' : 'variable', (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source), node, {
2603
+ docstring,
2604
+ isExported,
2605
+ });
2606
+ }
2607
+ }
1484
2608
  else {
1485
2609
  // Generic fallback for other languages
1486
2610
  // Try to find identifier children
@@ -1603,6 +2727,9 @@ class TreeSitterExtractor {
1603
2727
  // an unrelated class method picked by path-proximity (#359).
1604
2728
  if (this.language === 'typescript' || this.language === 'tsx') {
1605
2729
  this.extractTsTypeAliasMembers(value, typeAliasNode);
2730
+ // `type List = [ Service<'name', Req, Resp>, … ]` — surface each
2731
+ // entry's string-literal name as a searchable member (issue #634).
2732
+ this.extractTsTupleContractNames(value, typeAliasNode);
1606
2733
  }
1607
2734
  }
1608
2735
  }
@@ -1689,6 +2816,82 @@ class TreeSitterExtractor {
1689
2816
  }
1690
2817
  this.nodeStack.pop();
1691
2818
  }
2819
+ /**
2820
+ * Surface the string-literal "names" of a TypeScript service/contract
2821
+ * registry written as a tuple of generic instantiations:
2822
+ *
2823
+ * type MyServiceList = [
2824
+ * Service<'query_apply_record', Req, Resp>,
2825
+ * Service<'apply_confirm', Req, Resp>,
2826
+ * ];
2827
+ *
2828
+ * Each `Service<'name', …>` tags an entry with a string-literal name that a
2829
+ * dynamic factory (`createService<MyServiceList>()`) turns into a callable
2830
+ * property (`api.query_apply_record(…)`). Static extraction otherwise never
2831
+ * sees that name — it's a type argument, not a declaration — so
2832
+ * `homegraph query query_apply_record` returned nothing (issue #634). We emit
2833
+ * each name as a `method` node under the type alias (qualifiedName
2834
+ * `MyServiceList::query_apply_record`) so it's searchable and resolvable as a
2835
+ * symbol. (A call through the proxy, `api.query_apply_record(…)`, still
2836
+ * resolves to the imported `api` binding — the receiver's type isn't known —
2837
+ * so this fixes discoverability, not the per-method call edge.)
2838
+ *
2839
+ * Scope is deliberately narrow to avoid noise: only a string literal that is
2840
+ * a DIRECT type argument of a `generic_type` that is itself a DIRECT element
2841
+ * of a `tuple_type`. This excludes utility types (`Pick`/`Omit`/`Record` are
2842
+ * never written as tuples) and string args nested deeper
2843
+ * (`Service<'a', Pick<U, 'id'>>` yields only `a`, never `id`). Names must be
2844
+ * valid identifiers, which also rules out route paths / arbitrary strings.
2845
+ */
2846
+ extractTsTupleContractNames(value, typeAliasNode) {
2847
+ const tuples = [];
2848
+ const collectTuples = (n, depth) => {
2849
+ if (depth > 6)
2850
+ return; // a type expression is shallow; cap defensively
2851
+ if (n.type === 'tuple_type')
2852
+ tuples.push(n);
2853
+ for (let i = 0; i < n.namedChildCount; i++) {
2854
+ const c = n.namedChild(i);
2855
+ if (c)
2856
+ collectTuples(c, depth + 1);
2857
+ }
2858
+ };
2859
+ collectTuples(value, 0);
2860
+ if (tuples.length === 0)
2861
+ return;
2862
+ this.nodeStack.push(typeAliasNode.id);
2863
+ for (const tuple of tuples) {
2864
+ for (let i = 0; i < tuple.namedChildCount; i++) {
2865
+ const entry = tuple.namedChild(i);
2866
+ if (!entry || entry.type !== 'generic_type')
2867
+ continue;
2868
+ const typeArgs = (0, tree_sitter_helpers_1.getChildByField)(entry, 'type_arguments');
2869
+ if (!typeArgs)
2870
+ continue;
2871
+ for (let j = 0; j < typeArgs.namedChildCount; j++) {
2872
+ const arg = typeArgs.namedChild(j);
2873
+ if (!arg || arg.type !== 'literal_type')
2874
+ continue;
2875
+ // literal_type wraps the actual literal; only a string is a name.
2876
+ const strNode = arg.namedChild(0);
2877
+ if (!strNode || strNode.type !== 'string')
2878
+ continue;
2879
+ const name = (0, tree_sitter_helpers_1.getNodeText)(strNode, this.source)
2880
+ .trim()
2881
+ .replace(/^['"`]/, '')
2882
+ .replace(/['"`]$/, '');
2883
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
2884
+ continue;
2885
+ const signature = (0, tree_sitter_helpers_1.getNodeText)(entry, this.source).replace(/\s+/g, ' ').trim().slice(0, 120);
2886
+ this.createNode('method', name, entry, {
2887
+ signature,
2888
+ qualifiedName: `${typeAliasNode.name}::${name}`,
2889
+ });
2890
+ }
2891
+ }
2892
+ }
2893
+ this.nodeStack.pop();
2894
+ }
1692
2895
  /**
1693
2896
  * `foo: () => T` → property_signature whose type_annotation contains a
1694
2897
  * `function_type`. Treat that as a method-shaped contract member, since
@@ -2202,6 +3405,57 @@ class TreeSitterExtractor {
2202
3405
  // single-dot receiver regex fails. Pull out the immediate field after `this.`
2203
3406
  // so the receiver is the field name (`userbo`), which the resolver can then
2204
3407
  // look up in the enclosing class's field declarations.
3408
+ // PHP static-factory fluent chain: `Cls::for($x)->method()` — the receiver
3409
+ // is itself a static call, so resolution must infer the method's class
3410
+ // from what `Cls::for` RETURNS (its `: self` / `: static` / `: Type`),
3411
+ // #608 (mirrors the C++ chain fix in #645). Encode `<Cls::factory>().<method>`;
3412
+ // the `().` marker lets the PHP resolver split it. The receiver text
3413
+ // (`Cls::for('x')`) carries the args, so without this it degrades to an
3414
+ // unresolvable string and the call edge is dropped.
3415
+ if (methodName && this.language === 'php' && objectField.type === 'scoped_call_expression') {
3416
+ const innerScope = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'scope');
3417
+ const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
3418
+ if (innerScope && innerName) {
3419
+ calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerScope, this.source)}::${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
3420
+ }
3421
+ else {
3422
+ calleeName = methodName;
3423
+ }
3424
+ if (calleeName) {
3425
+ this.unresolvedReferences.push({
3426
+ fromNodeId: callerId,
3427
+ referenceName: calleeName,
3428
+ referenceKind: 'calls',
3429
+ line: node.startPosition.row + 1,
3430
+ column: node.startPosition.column,
3431
+ });
3432
+ }
3433
+ return;
3434
+ }
3435
+ // Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
3436
+ // receiver is itself a method call, so resolution must infer bar's class
3437
+ // from what `Foo.getInstance` RETURNS (its declared return type), the
3438
+ // #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
3439
+ // the `().` marker lets the Java chain resolver split it, and normalizing to
3440
+ // empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
3441
+ // otherwise leave a `(cfg)` in the receiver text and break the split.
3442
+ if (methodName &&
3443
+ this.language === 'java' &&
3444
+ objectField.type === 'method_invocation') {
3445
+ const innerObj = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
3446
+ const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
3447
+ if (innerObj && innerName) {
3448
+ calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerObj, this.source)}.${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
3449
+ this.unresolvedReferences.push({
3450
+ fromNodeId: callerId,
3451
+ referenceName: calleeName,
3452
+ referenceKind: 'calls',
3453
+ line: node.startPosition.row + 1,
3454
+ column: node.startPosition.column,
3455
+ });
3456
+ return;
3457
+ }
3458
+ }
2205
3459
  let receiverName;
2206
3460
  if (objectField.type === 'field_access') {
2207
3461
  const inner = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
@@ -2290,6 +3544,39 @@ class TreeSitterExtractor {
2290
3544
  calleeName = methodName;
2291
3545
  }
2292
3546
  }
3547
+ else if (receiverField && receiverField.type === 'message_expression' && /^\w+$/.test(methodName)) {
3548
+ // Chained message send `[[Foo create] doIt]` — the receiver is itself a
3549
+ // class message. Recover the inner `Class.selector` and encode
3550
+ // `Class.selector().doIt` so resolution infers doIt's class from what
3551
+ // `Class.selector` RETURNS (#645/#608). Only a CLASS-factory chain
3552
+ // (capitalized inner receiver); a unary outer selector is required
3553
+ // because the chain resolver's method part is `\w+` (no `:`). An
3554
+ // instance chain (`[[obj foo] bar]`, lowercase inner) stays bare.
3555
+ const innerRecv = (0, tree_sitter_helpers_1.getChildByField)(receiverField, 'receiver');
3556
+ const innerRecvName = innerRecv ? (0, tree_sitter_helpers_1.getNodeText)(innerRecv, this.source) : '';
3557
+ if (innerRecv?.type === 'identifier' && /^[A-Z]/.test(innerRecvName)) {
3558
+ const innerKw = [];
3559
+ for (let i = 0; i < receiverField.namedChildCount; i++) {
3560
+ if (receiverField.fieldNameForNamedChild(i) === 'method') {
3561
+ const kw = receiverField.namedChild(i);
3562
+ if (kw)
3563
+ innerKw.push((0, tree_sitter_helpers_1.getNodeText)(kw, this.source));
3564
+ }
3565
+ }
3566
+ let innerColon = false;
3567
+ for (let i = 0; i < receiverField.childCount; i++) {
3568
+ if (receiverField.child(i)?.type === ':') {
3569
+ innerColon = true;
3570
+ break;
3571
+ }
3572
+ }
3573
+ const innerSelector = innerColon ? innerKw.map((k) => `${k}:`).join('') : innerKw[0];
3574
+ calleeName = innerSelector ? `${innerRecvName}.${innerSelector}().${methodName}` : methodName;
3575
+ }
3576
+ else {
3577
+ calleeName = methodName;
3578
+ }
3579
+ }
2293
3580
  else {
2294
3581
  calleeName = methodName;
2295
3582
  }
@@ -2334,6 +3621,67 @@ class TreeSitterExtractor {
2334
3621
  calleeName = methodName;
2335
3622
  }
2336
3623
  }
3624
+ else if ((this.language === 'cpp' ||
3625
+ this.language === 'c' ||
3626
+ this.language === 'kotlin' ||
3627
+ this.language === 'swift' ||
3628
+ this.language === 'rust' ||
3629
+ this.language === 'go' ||
3630
+ this.language === 'scala') &&
3631
+ receiver &&
3632
+ receiver.type === 'call_expression') {
3633
+ // Receiver that is itself a call — `Foo::instance().bar()`,
3634
+ // `openSession()->run()`, `mgr.view().render()` (C/C++),
3635
+ // `Foo.getInstance().bar()` (Kotlin) / `Foo.make().draw()` (Swift),
3636
+ // `Foo::new().bar()` (Rust), or `New().Method()` (Go). Keep the inner
3637
+ // call so resolution can infer bar()'s class from what the inner call
3638
+ // RETURNS (#645/#608). Encode as `<innerCallee>().<method>`; the `().`
3639
+ // marker never appears in an ordinary ref, so the resolver can detect
3640
+ // and split it. Other languages keep the bare-name behavior below.
3641
+ let innerCallee;
3642
+ let reencode;
3643
+ if (this.language === 'kotlin' || this.language === 'swift') {
3644
+ // tree-sitter-kotlin/swift expose the inner callee as the
3645
+ // call_expression's first named child (a navigation_expression
3646
+ // `Foo.getInstance`, or a bare identifier for a free/constructor call).
3647
+ const innerNav = receiver.namedChild(0);
3648
+ innerCallee = innerNav ? (0, tree_sitter_helpers_1.getNodeText)(innerNav, this.source).replace(/\s+/g, '') : '';
3649
+ // Only re-encode a CLASS / companion-factory / constructor chain,
3650
+ // whose receiver chain starts with a capitalized type
3651
+ // (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain
3652
+ // (`list.filter{}.map{}`) has a lowercase receiver whose type we
3653
+ // can't recover here — re-encoding it would only drop the edge (no
3654
+ // chain resolution, no bare-name fallback), regressing recall in
3655
+ // fluent codebases. Leave those to the bare-name path.
3656
+ reencode = /^[A-Z]/.test(innerCallee);
3657
+ }
3658
+ else {
3659
+ const innerFn = (0, tree_sitter_helpers_1.getChildByField)(receiver, 'function');
3660
+ innerCallee = innerFn
3661
+ ? (0, tree_sitter_helpers_1.getNodeText)(innerFn, this.source).replace(/->/g, '.').replace(/\s+/g, '')
3662
+ : '';
3663
+ // Rust: only re-encode an associated-function chain
3664
+ // (`Foo::new().bar()`), whose inner callee is a path/`scoped_identifier`.
3665
+ // Go: only a bare package-level factory chain (`New().Method()`),
3666
+ // whose inner callee is an `identifier`. An instance chain
3667
+ // (`x.foo().bar()` Rust, `obj.Method().Other()` Go) keeps bare-name —
3668
+ // the resolver can't recover a variable's type, so re-encoding would
3669
+ // only drop the edge. C/C++ re-encode any inner.
3670
+ if (this.language === 'rust')
3671
+ reencode = innerFn?.type === 'scoped_identifier';
3672
+ else if (this.language === 'go')
3673
+ reencode = innerFn?.type === 'identifier';
3674
+ // Scala: only a companion-factory / case-class-apply chain whose
3675
+ // receiver chain starts with a capitalized type (`Foo.create().bar()`,
3676
+ // `Foo(args).bar()`). An instance chain (`list.map().filter()`) has a
3677
+ // lowercase receiver whose type we can't recover — leave it bare.
3678
+ else if (this.language === 'scala')
3679
+ reencode = /^[A-Z]/.test(innerCallee);
3680
+ else
3681
+ reencode = !!innerCallee;
3682
+ }
3683
+ calleeName = reencode ? `${innerCallee}().${methodName}` : methodName;
3684
+ }
2337
3685
  else {
2338
3686
  calleeName = methodName;
2339
3687
  }
@@ -2343,6 +3691,24 @@ class TreeSitterExtractor {
2343
3691
  // Scoped call: Module::function()
2344
3692
  calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
2345
3693
  }
3694
+ else if (this.language === 'csharp' && func.type === 'member_access_expression') {
3695
+ // C# member call `recv.Method(...)`. When the receiver is itself a call
3696
+ // — a chained factory `Foo.Create(args).Bar()` — encode `inner().Bar`
3697
+ // with normalized empty parens so resolution can infer Bar's class from
3698
+ // what `Foo.Create` RETURNS (#645/#608). A non-call receiver keeps the
3699
+ // full member-access text (the existing `recv.Method` behavior).
3700
+ const recv = (0, tree_sitter_helpers_1.getChildByField)(func, 'expression');
3701
+ const nameNode = (0, tree_sitter_helpers_1.getChildByField)(func, 'name');
3702
+ const methodName = nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : '';
3703
+ if (recv && recv.type === 'invocation_expression' && methodName) {
3704
+ const innerFunc = (0, tree_sitter_helpers_1.getChildByField)(recv, 'function');
3705
+ const innerCallee = innerFunc ? (0, tree_sitter_helpers_1.getNodeText)(innerFunc, this.source).replace(/\s+/g, '') : '';
3706
+ calleeName = innerCallee ? `${innerCallee}().${methodName}` : methodName;
3707
+ }
3708
+ else {
3709
+ calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
3710
+ }
3711
+ }
2346
3712
  else {
2347
3713
  calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
2348
3714
  }
@@ -2796,6 +4162,9 @@ class TreeSitterExtractor {
2796
4162
  return;
2797
4163
  const visitForCallsAndStructure = (node) => {
2798
4164
  const nodeType = node.type;
4165
+ // Function-as-value capture (#756) — function bodies are walked here,
4166
+ // not in visitNode, so the capture hook must fire in both walkers.
4167
+ this.maybeCaptureFnRefs(node, nodeType);
2799
4168
  // Rocket route-registration macros (`routes![…]` / `catchers![…]`): the
2800
4169
  // handler paths live in a raw token tree the call walker can't see.
2801
4170
  if (nodeType === 'macro_invocation')
@@ -3926,9 +5295,29 @@ class TreeSitterExtractor {
3926
5295
  }
3927
5296
  }
3928
5297
  }
3929
- const parentId = this.methodIndex.get(fullNameKey) ||
3930
- this.methodIndex.get(shortNameKey) ||
3931
- this.nodeStack[this.nodeStack.length - 1];
5298
+ let parentId = this.methodIndex.get(fullNameKey) ||
5299
+ this.methodIndex.get(shortNameKey);
5300
+ // No existing node? This is an implementation-only **free** procedure/function
5301
+ // (`procedure Helper; begin … end;` with no interface declaration and not a
5302
+ // class method). Create a function node so its body's calls attribute to it,
5303
+ // not to the enclosing file/module. A method (`TClass.Method`, a dotted name)
5304
+ // always has a node from its class declaration, so this only fires for free
5305
+ // routines — and the methodIndex lookup above already covers interface-declared
5306
+ // free routines, so there's no duplicate.
5307
+ if (!parentId && !fullName.includes('.')) {
5308
+ const fnNode = this.createNode('function', fullName, declProc, {
5309
+ signature: this.extractor?.getSignature?.(declProc, this.source),
5310
+ visibility: this.extractor?.getVisibility?.(declProc),
5311
+ });
5312
+ if (fnNode) {
5313
+ parentId = fnNode.id;
5314
+ this.methodIndex.set(fullNameKey, fnNode.id);
5315
+ if (!this.methodIndex.has(shortNameKey))
5316
+ this.methodIndex.set(shortNameKey, fnNode.id);
5317
+ }
5318
+ }
5319
+ if (!parentId)
5320
+ parentId = this.nodeStack[this.nodeStack.length - 1];
3932
5321
  if (!parentId)
3933
5322
  return;
3934
5323
  // Visit the block for calls
@@ -3954,10 +5343,41 @@ class TreeSitterExtractor {
3954
5343
  return;
3955
5344
  let calleeName = '';
3956
5345
  if (firstChild.type === 'exprDot') {
3957
- // Qualified call: Obj.Method(...)
3958
- const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
3959
- if (identifiers.length > 0) {
3960
- calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
5346
+ // Chained static-factory call: `TFoo.GetInstance().DoIt()` — the exprDot's
5347
+ // receiver is itself an `exprCall`, so the bare identifier list would
5348
+ // collapse to just `DoIt` and mis-resolve to a same-named method on an
5349
+ // unrelated class. Encode `TFoo.GetInstance().DoIt` so resolution infers
5350
+ // DoIt's class from what `TFoo.GetInstance` RETURNS (#645/#608). Only a
5351
+ // capitalized class-factory chain; a unary outer method.
5352
+ const innerCall = firstChild.namedChildren.find((c) => c.type === 'exprCall');
5353
+ const outerId = firstChild.namedChildren.filter((c) => c.type === 'identifier').pop();
5354
+ const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
5355
+ if (innerCall && method && /^\w+$/.test(method)) {
5356
+ const innerFirst = innerCall.namedChild(0);
5357
+ let innerCallee = '';
5358
+ if (innerFirst?.type === 'exprDot') {
5359
+ innerCallee = innerFirst.namedChildren
5360
+ .filter((c) => c.type === 'identifier')
5361
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
5362
+ .join('.');
5363
+ }
5364
+ else if (innerFirst?.type === 'identifier') {
5365
+ innerCallee = (0, tree_sitter_helpers_1.getNodeText)(innerFirst, this.source);
5366
+ }
5367
+ // Gate on the Delphi type-naming convention — `TFoo` classes / `IFoo`
5368
+ // interfaces — so a class-factory chain re-encodes but a capitalized
5369
+ // VARIABLE/parameter chain (Pascal capitalizes locals too: `Curve.X().Y()`,
5370
+ // `Self.X().Y()`) stays bare and keeps its existing bare-name resolution.
5371
+ calleeName = innerCallee && /^[TI][A-Z]/.test(innerCallee)
5372
+ ? `${innerCallee}().${method}`
5373
+ : method;
5374
+ }
5375
+ else {
5376
+ // Qualified call: Obj.Method(...)
5377
+ const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
5378
+ if (identifiers.length > 0) {
5379
+ calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
5380
+ }
3961
5381
  }
3962
5382
  }
3963
5383
  else if (firstChild.type === 'identifier') {
@@ -3978,6 +5398,72 @@ class TreeSitterExtractor {
3978
5398
  this.visitPascalBlock(args);
3979
5399
  }
3980
5400
  }
5401
+ /**
5402
+ * Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
5403
+ * `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
5404
+ * parses as a bare `exprDot` (not an `exprCall`). A bare `exprDot` is
5405
+ * syntactically identical to a field/property access, so this is only ever
5406
+ * called for a STATEMENT-level exprDot (caller-gated): a bare `Obj.Field;`
5407
+ * statement is a no-op, so a statement-level dot expression is a call. (An
5408
+ * exprDot in assignment LHS/RHS or a condition is left alone — there it really
5409
+ * can be a field/property read.)
5410
+ */
5411
+ extractPascalParenlessCall(node) {
5412
+ if (this.nodeStack.length === 0)
5413
+ return;
5414
+ const callerId = this.nodeStack[this.nodeStack.length - 1];
5415
+ if (!callerId)
5416
+ return;
5417
+ const receiver = node.namedChild(0);
5418
+ const outerId = node.namedChildren.filter((c) => c.type === 'identifier').pop();
5419
+ const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
5420
+ if (!method)
5421
+ return;
5422
+ let calleeName = '';
5423
+ // Chained: the receiver is itself a call — a paren-less `TFoo.GetInstance` (an
5424
+ // inner exprDot) or a paren'd `TFoo.GetInstance()` (an exprCall). Encode the
5425
+ // chain `TFoo.GetInstance().DoIt` so resolution infers DoIt's class from what
5426
+ // the factory RETURNS (#645/#608), gated on the Delphi `TFoo`/`IFoo` type
5427
+ // convention; a capitalized VARIABLE chain stays a bare method name.
5428
+ if ((receiver?.type === 'exprDot' || receiver?.type === 'exprCall') && /^\w+$/.test(method)) {
5429
+ const innerCalleeNode = receiver.type === 'exprCall' ? receiver.namedChild(0) : receiver;
5430
+ const innerCallee = !innerCalleeNode
5431
+ ? ''
5432
+ : innerCalleeNode.type === 'identifier'
5433
+ ? (0, tree_sitter_helpers_1.getNodeText)(innerCalleeNode, this.source)
5434
+ : innerCalleeNode.namedChildren
5435
+ .filter((c) => c.type === 'identifier')
5436
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
5437
+ .join('.');
5438
+ if (innerCallee && /^[TI][A-Z]/.test(innerCallee)) {
5439
+ calleeName = `${innerCallee}().${method}`;
5440
+ // The T/I-prefixed inner is itself a real call — record it too.
5441
+ if (receiver.type === 'exprCall')
5442
+ this.extractPascalCall(receiver);
5443
+ else
5444
+ this.extractPascalParenlessCall(receiver);
5445
+ }
5446
+ else {
5447
+ calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
5448
+ }
5449
+ }
5450
+ else {
5451
+ // Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
5452
+ calleeName = node.namedChildren
5453
+ .filter((c) => c.type === 'identifier')
5454
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
5455
+ .join('.');
5456
+ }
5457
+ if (calleeName) {
5458
+ this.unresolvedReferences.push({
5459
+ fromNodeId: callerId,
5460
+ referenceName: calleeName,
5461
+ referenceKind: 'calls',
5462
+ line: node.startPosition.row + 1,
5463
+ column: node.startPosition.column,
5464
+ });
5465
+ }
5466
+ }
3981
5467
  /**
3982
5468
  * Recursively visit a Pascal block/statement tree for call expressions
3983
5469
  */
@@ -3986,15 +5472,32 @@ class TreeSitterExtractor {
3986
5472
  const child = node.namedChild(i);
3987
5473
  if (!child)
3988
5474
  continue;
5475
+ // Function-as-value capture (#756): Pascal bodies are walked here, not
5476
+ // in visitNode/visitForCallsAndStructure, so the capture hook fires here
5477
+ // — assignment RHS is the Delphi event-wiring idiom (`OnFire := Handler`).
5478
+ this.maybeCaptureFnRefs(child, child.type);
3989
5479
  if (child.type === 'exprCall') {
3990
5480
  this.extractPascalCall(child);
5481
+ // The walker doesn't descend into a call's arguments — dispatch the
5482
+ // argument container directly (`RegisterHandler(TargetCb)` / `(@Cb)`).
5483
+ const args = child.namedChildren.find((c) => c.type === 'exprArgs');
5484
+ if (args)
5485
+ this.maybeCaptureFnRefs(args, 'exprArgs');
3991
5486
  }
3992
5487
  else if (child.type === 'exprDot') {
3993
- // Check if exprDot contains an exprCall
3994
- for (let j = 0; j < child.namedChildCount; j++) {
3995
- const grandchild = child.namedChild(j);
3996
- if (grandchild?.type === 'exprCall') {
3997
- this.extractPascalCall(grandchild);
5488
+ // A STATEMENT-level bare exprDot is a paren-less call (`Obj.Free;`,
5489
+ // `TFoo.GetInstance.DoIt;`). Anywhere else (assignment side, condition,
5490
+ // expression) a bare exprDot is ambiguous with a field/property access,
5491
+ // so there we only descend for paren'd inner calls.
5492
+ if (node.type === 'statement') {
5493
+ this.extractPascalParenlessCall(child);
5494
+ }
5495
+ else {
5496
+ for (let j = 0; j < child.namedChildCount; j++) {
5497
+ const grandchild = child.namedChild(j);
5498
+ if (grandchild?.type === 'exprCall') {
5499
+ this.extractPascalCall(grandchild);
5500
+ }
3998
5501
  }
3999
5502
  }
4000
5503
  }
@@ -4026,6 +5529,11 @@ function extractFromSource(filePath, source, language, frameworkNames) {
4026
5529
  const extractor = new vue_extractor_1.VueExtractor(filePath, source);
4027
5530
  result = extractor.extract();
4028
5531
  }
5532
+ else if (detectedLanguage === 'astro') {
5533
+ // Use custom extractor for Astro (frontmatter + template delegation)
5534
+ const extractor = new astro_extractor_1.AstroExtractor(filePath, source);
5535
+ result = extractor.extract();
5536
+ }
4029
5537
  else if (detectedLanguage === 'liquid') {
4030
5538
  // Use custom extractor for Liquid
4031
5539
  const extractor = new liquid_extractor_1.LiquidExtractor(filePath, source);