@backbencher/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-KY7K552E.js +17 -0
  8. package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-2BSKD7O3.js +40 -0
  11. package/dist/chunk-2DE27DVC.js +1314 -0
  12. package/dist/chunk-2RRBMNBQ.js +322 -0
  13. package/dist/chunk-2RTN2DFU.js +711 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3LFTSMIV.js +174 -0
  17. package/dist/chunk-47HKX4AJ.js +930 -0
  18. package/dist/chunk-4H22TLOT.js +543 -0
  19. package/dist/chunk-5SD45FLC.js +818 -0
  20. package/dist/chunk-66A5ZFYA.js +1758 -0
  21. package/dist/chunk-6AIRWAO2.js +247 -0
  22. package/dist/chunk-6WNGD3NN.js +196 -0
  23. package/dist/chunk-7AN7U5IZ.js +371 -0
  24. package/dist/chunk-BHB7BYYI.js +626 -0
  25. package/dist/chunk-BQ3OVQ3P.js +125 -0
  26. package/dist/chunk-CW4XXSNN.js +1479 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DYM4FQEE.js +90 -0
  30. package/dist/chunk-EWMWK7WN.js +75 -0
  31. package/dist/chunk-FVVQTSIM.js +3427 -0
  32. package/dist/chunk-FWSL4VG6.js +1290 -0
  33. package/dist/chunk-GBI7V7KY.js +69 -0
  34. package/dist/chunk-GEFFO6O2.js +152 -0
  35. package/dist/chunk-HOWAQXJO.js +236 -0
  36. package/dist/chunk-HQSPJDHI.js +613 -0
  37. package/dist/chunk-IFZNR5UJ.js +2863 -0
  38. package/dist/chunk-J4PMBXQQ.js +336 -0
  39. package/dist/chunk-KAET47ZL.js +125 -0
  40. package/dist/chunk-KCDECRJY.js +440 -0
  41. package/dist/chunk-KLK3PBTO.js +1119 -0
  42. package/dist/chunk-L5TDYKYK.js +76 -0
  43. package/dist/chunk-MLKGABMK.js +9 -0
  44. package/dist/chunk-OWISBAOF.js +61 -0
  45. package/dist/chunk-PJTNGD3Z.js +6924 -0
  46. package/dist/chunk-QLK6ED4Q.js +235 -0
  47. package/dist/chunk-R2GW4F6R.js +19 -0
  48. package/dist/chunk-RCMTFSEF.js +87 -0
  49. package/dist/chunk-RH7KC5HU.js +12050 -0
  50. package/dist/chunk-RM3PY6TG.js +376 -0
  51. package/dist/chunk-ROMM6KMC.js +433 -0
  52. package/dist/chunk-SK6YVHFZ.js +211 -0
  53. package/dist/chunk-SM7RFQRO.js +214 -0
  54. package/dist/chunk-UFE77L67.js +782 -0
  55. package/dist/chunk-UGR4QIGS.js +67 -0
  56. package/dist/chunk-UUNHHX4M.js +13 -0
  57. package/dist/chunk-V44PGPA6.js +2104 -0
  58. package/dist/chunk-VL52PL2T.js +71 -0
  59. package/dist/chunk-VURYFIHW.js +201 -0
  60. package/dist/chunk-XJKJYIPP.js +7436 -0
  61. package/dist/chunk-YI6FF2ZR.js +340 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-642F7E5V.js +593 -0
  65. package/dist/database-TCTZDLE5.js +9 -0
  66. package/dist/doctor-GXCKSJOX.js +395 -0
  67. package/dist/edit-engine-KM7MPFEC.js +51 -0
  68. package/dist/engine-42TZFNIZ.js +11 -0
  69. package/dist/engine-LO6L4JLQ.js +18 -0
  70. package/dist/fingerprint-XDXFRWO6.js +33 -0
  71. package/dist/framework-intelligence-DWFCKV44.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-PL5YA7BA.js +21 -0
  74. package/dist/graph_request-MM4REP6H.js +350 -0
  75. package/dist/impact_request-WDE47IZ5.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-6BD7UBHR.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-4PW3AFXP.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-ZS5TS2Z6.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-DGZBIZS7.js +355 -0
  90. package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
  91. package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
  92. package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
  93. package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
  94. package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
  95. package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
  96. package/dist/real-css-analyzer-HPU277Q6.js +15 -0
  97. package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
  98. package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
  99. package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
  100. package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
  101. package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
  102. package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
  103. package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
  104. package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
  105. package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
  106. package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
  107. package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
  108. package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
  109. package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
  110. package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
  111. package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
  112. package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
  113. package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
  114. package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
  115. package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
  116. package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
  117. package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
  118. package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
  119. package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
  120. package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
  121. package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
  122. package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
  123. package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
  124. package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
  125. package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
  126. package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
  127. package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
  128. package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
  129. package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
  130. package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
  131. package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
  132. package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
  133. package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
  134. package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
  135. package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
  136. package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
  137. package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
  138. package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
  139. package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
  140. package/dist/regex-search-OPTLKUN5.js +239 -0
  141. package/dist/rules-I453JTRC.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-Y5GB5KXA.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-KB4MU4SI.js +16 -0
  146. package/dist/taint-YOTN5X55.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-G4NAGJIH.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-RF675A3C.js +32 -0
  151. package/dist/type-inference-4Y7WTMFE.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-LXGNFO37.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,2429 @@
1
+ import {
2
+ ASTEngine
3
+ } from "./chunk-V44PGPA6.js";
4
+ import "./chunk-BQ3OVQ3P.js";
5
+ import "./chunk-SK6YVHFZ.js";
6
+ import "./chunk-2BSKD7O3.js";
7
+ import "./chunk-KAET47ZL.js";
8
+ import "./chunk-KCDECRJY.js";
9
+ import "./chunk-MLKGABMK.js";
10
+
11
+ // src/ast/real-ruby-analyzer.ts
12
+ function isAsciiWhitespace(code) {
13
+ return code === 32 || code === 9 || code === 10 || code === 13 || code === 12 || code === 11;
14
+ }
15
+ var FILE_SCOPE_OWNER = "Global";
16
+ function rubyReopenIdentity(rawName, enclosingOwner) {
17
+ const sepIdx = rawName.lastIndexOf("::");
18
+ if (sepIdx < 0) return { name: rawName, owner: enclosingOwner };
19
+ const name = rawName.substring(sepIdx + 2);
20
+ const scopeOwner = rawName.substring(0, sepIdx);
21
+ if (scopeOwner.length === 0) return { name, owner: enclosingOwner };
22
+ return { name, owner: enclosingOwner ? `${enclosingOwner}::${scopeOwner}` : scopeOwner };
23
+ }
24
+ function rubyFileUnitName(filePath) {
25
+ if (!filePath) return FILE_SCOPE_OWNER;
26
+ let end = filePath.length;
27
+ while (end > 0) {
28
+ const c = filePath.charCodeAt(end - 1);
29
+ if (c === 47 || c === 92) break;
30
+ end--;
31
+ }
32
+ const tail = filePath.substring(end);
33
+ if (tail.length === 0) return FILE_SCOPE_OWNER;
34
+ const dot = tail.lastIndexOf(".");
35
+ let stem = dot > 0 ? tail.substring(0, dot) : tail;
36
+ if (stem.indexOf(".") >= 0) {
37
+ let out = "";
38
+ for (let i = 0; i < stem.length; i++) out += stem[i] === "." ? "_" : stem[i];
39
+ stem = out;
40
+ }
41
+ return stem.length > 0 ? stem : FILE_SCOPE_OWNER;
42
+ }
43
+ var RAILS_FRAMEWORK = "Ruby on Rails";
44
+ var VARIABLE_RECEIVER_TYPES = /* @__PURE__ */ new Set(["instance_variable", "class_variable", "global_variable"]);
45
+ function isRubyVisibilityDirectiveName(name) {
46
+ return name === "private" || name === "protected" || name === "public";
47
+ }
48
+ var BARE_CALL_SKIP_PARENTS = /* @__PURE__ */ new Set([
49
+ // `call` — the METHOD NAME of a call node is part of the call, not a bare
50
+ // read: `Mutex.new` must not also read as a bare `new`. The pinned grammar
51
+ // folds every call shape — paren'd (`foo(1)`), paren-less command (`foo 1`),
52
+ // and command-with-block (`foo 1 do ... end`) — into `call` nodes, and
53
+ // `command` / `command_call` are not node types it can produce. This set USED
54
+ // to carry those two as "grammar-version drift" insurance, which cost two
55
+ // dead branches (a skip entry that can never match, and a `type ===` arm
56
+ // that can never fire) and made the file cite a grammar audit as proof of a
57
+ // claim the audit could not see.
58
+ //
59
+ // Removing the two dead entries is a one-token edit with a real failure mode
60
+ // and the rack-wave fixture caught it immediately: the same edit dropped
61
+ // `call` itself, and every constructor call in a default argument
62
+ // (`def initialize(app, mutex = Mutex.new)`) began reading its `new` as a
63
+ // bare call as well — `calls initialize->new`, a fabricated edge the RB-A
64
+ // fixture has forbidden since the rack wave. A cleanup of two provably dead
65
+ // strings was never a safe place to be casual about the live one beside them.
66
+ "call",
67
+ // NOTE: `method`/`singleton_method` are deliberately ABSENT — an endless
68
+ // `def label = name` body is a DIRECT identifier child of the def node,
69
+ // and only the FIRST named child is the def name (second review: the set
70
+ // form suppressed endless-body reads on both sides). The def-name check
71
+ // lives inline at the identifier branch (positional).
72
+ // `def size=(v)` parses the NAME as a `setter` node wrapping the plain
73
+ // identifier — without this the nested `size` leaked as a bare call
74
+ // (master-review 2026-09; oracle already carried `setter`).
75
+ "setter",
76
+ // Keyword `alias new_name old_name` (any scope): its identifiers are
77
+ // method-name DECLARATIONS, never expression calls. Class-level alias
78
+ // gets the RB-E alias_method contract in the main loop; alias inside a
79
+ // method body is a runtime-definition (SyntaxError in practice) and must
80
+ // emit nothing, not two junk calls (master-review 2026-09).
81
+ "alias",
82
+ // NOT present: `pair` — a hash/argument pair's VALUE is a real expression
83
+ // (`{ x: helper }`, `foo(k: helper)`); only colon-form KEYS are
84
+ // non-expression, and those are hash_key_symbol/simple_symbol nodes, never
85
+ // identifiers. Rocket-form keys (`key => v`) also evaluate. pair was
86
+ // suppressing genuine value-position method calls on both sides — a
87
+ // shared blind spot F5 cannot see (master-review 2026-09, RUBD12).
88
+ "method_parameters",
89
+ "parameters",
90
+ "block_parameters",
91
+ "lambda_parameters",
92
+ "block_parameters",
93
+ "class",
94
+ "module",
95
+ "singleton_class"
96
+ ]);
97
+ var RUBY_BUILTINS = /* @__PURE__ */ new Set([
98
+ // Kernel / top-level
99
+ "puts",
100
+ "print",
101
+ "p",
102
+ "pp",
103
+ "p$",
104
+ "print_flush",
105
+ "putc",
106
+ "raise",
107
+ "fail",
108
+ "exit",
109
+ "exit!",
110
+ "abort",
111
+ "warn",
112
+ "gets",
113
+ "readline",
114
+ "readlines",
115
+ "rand",
116
+ "srand",
117
+ "sleep",
118
+ "loop",
119
+ "require",
120
+ "require_relative",
121
+ "load",
122
+ "autoload",
123
+ "eval",
124
+ "binding",
125
+ "local_variables",
126
+ "global_variables",
127
+ "global_variable_get",
128
+ "global_variable_set",
129
+ "caller",
130
+ "caller_locations",
131
+ "trace_var",
132
+ "untrace_var",
133
+ "method",
134
+ "public_method",
135
+ "private_method",
136
+ "protected_method",
137
+ "instance_method",
138
+ "public_instance_method",
139
+ "private_instance_method",
140
+ "respond_to",
141
+ "respond_to?",
142
+ "class",
143
+ "superclass",
144
+ "Object",
145
+ "Kernel",
146
+ "BasicObject",
147
+ "NilClass",
148
+ "TrueClass",
149
+ "FalseClass",
150
+ "Integer",
151
+ "Float",
152
+ "String",
153
+ "Array",
154
+ "Hash",
155
+ "Symbol",
156
+ "Regexp",
157
+ "Range",
158
+ "IO",
159
+ "File",
160
+ "Dir",
161
+ "Process",
162
+ "Signal",
163
+ "GC",
164
+ "ObjectSpace",
165
+ "Marshal",
166
+ "Thread",
167
+ "Fiber",
168
+ "Random",
169
+ "SecureRandom",
170
+ "Time",
171
+ "Date",
172
+ "DateTime",
173
+ "Math",
174
+ "Comparable",
175
+ "Enumerable",
176
+ "Errno",
177
+ // Ruby dunders / special methods
178
+ "super",
179
+ "method_missing",
180
+ "respond_to_missing?",
181
+ "initialize_copy",
182
+ "initialize_dup",
183
+ "initialize_clone",
184
+ "send",
185
+ "__send__",
186
+ "freeze",
187
+ "frozen?",
188
+ "dup",
189
+ "clone",
190
+ "taint",
191
+ "untaint",
192
+ "trust",
193
+ "untrust",
194
+ "hash",
195
+ "eql?",
196
+ "equal?",
197
+ "object_id",
198
+ "instance_of?",
199
+ "kind_of?",
200
+ "is_a?",
201
+ "nil?",
202
+ "instance_variable_defined?",
203
+ "instance_variables",
204
+ "instance_variable_get",
205
+ "instance_variable_set",
206
+ "class_variables",
207
+ "class_variable_defined?",
208
+ "class_variable_get",
209
+ "class_variable_set",
210
+ "const_defined?",
211
+ "const_get",
212
+ "const_set",
213
+ "const_defined?",
214
+ "method_defined?",
215
+ "private_method_defined?",
216
+ "protected_method_defined?",
217
+ "singleton_method",
218
+ "define_singleton_method",
219
+ "to_s",
220
+ "to_i",
221
+ "to_f",
222
+ "to_a",
223
+ "to_h",
224
+ "to_proc",
225
+ "inspect",
226
+ "display",
227
+ "extend",
228
+ "include",
229
+ "prepend",
230
+ // Rails macros (emit properties, not call edges)
231
+ "has_many",
232
+ "belongs_to",
233
+ "has_one",
234
+ "has_and_belongs_to_many",
235
+ "scope",
236
+ "default_scope",
237
+ "validates",
238
+ "validate",
239
+ "validates_with",
240
+ "validates_each",
241
+ "before_action",
242
+ "after_action",
243
+ "before_filter",
244
+ "after_filter",
245
+ "before_validation",
246
+ "after_validation",
247
+ "before_save",
248
+ "after_save",
249
+ "before_create",
250
+ "after_create",
251
+ "before_update",
252
+ "after_update",
253
+ "before_destroy",
254
+ "after_destroy",
255
+ "before_commit",
256
+ "after_commit",
257
+ "after_rollback",
258
+ "attr_reader",
259
+ "attr_writer",
260
+ "attr_accessor",
261
+ "alias_method",
262
+ "remove_method",
263
+ "undef_method",
264
+ "delegate",
265
+ "delegate_missing_to",
266
+ "event",
267
+ "transition",
268
+ "state_machine",
269
+ "module_function",
270
+ "using",
271
+ // Module-level visibility MODIFIER METHODS. `private_constant :X` is not a
272
+ // keyword in this grammar — it is an ordinary `call` on Module — and it was
273
+ // missing from this list, so it published `calls <owner> -> private_constant`:
274
+ // an edge whose target is a stdlib method no project declares, which is the same
275
+ // shape as every other entry above and the reason they are here. Found by the
276
+ // oracle-agreement suite, whose negative case asserts the modifier is not a
277
+ // call: the oracle suppressed it (it lists both names among its visibility
278
+ // modifiers) and the analyzer did not, and the analyzer is the side whose
279
+ // suppression list IS the contract — `RUBY_BUILTINS` is a list of things the
280
+ // payload does not carry as call targets, not a judgement about Ruby.
281
+ //
282
+ // The DECLARATION side is not lost: `private_constant`'s effect on the export
283
+ // surface is read by `collectPrivateConstants`. So the pair is the same shape as
284
+ // `attr_reader` (declares a member, no call edge) and `module_function`
285
+ // (declares, no call edge). Measured on the corpus: 142 `private_constant` and
286
+ // 2 `public_constant` sites.
287
+ "private_constant",
288
+ "public_constant",
289
+ "ruby2_keywords",
290
+ "no_touching"
291
+ ]);
292
+ function isRubyInstanceVariableWrite(node) {
293
+ const parent = node.parent;
294
+ if (!parent) return false;
295
+ if (parent.type === "assignment" || parent.type === "operator_assignment") {
296
+ const left = parent.childForFieldName("left");
297
+ if (!left || left.id !== node.id) return false;
298
+ return left.type !== "call" && left.type !== "element_reference";
299
+ }
300
+ if (parent.type === "left_assignment_list") {
301
+ const grand = parent.parent;
302
+ if (!grand || grand.type !== "assignment" && grand.type !== "operator_assignment") return false;
303
+ const left = grand.childForFieldName("left");
304
+ if (!left || left.id !== parent.id) return false;
305
+ const _nc1 = parent.namedChildCount;
306
+ for (let i = 0; i < _nc1; i++) {
307
+ const c = parent.namedChild(i);
308
+ if (c && c.id === node.id) return c.type !== "call" && c.type !== "element_reference";
309
+ }
310
+ }
311
+ return false;
312
+ }
313
+ function isRubyStaticSuperclassName(text) {
314
+ if (!text) return false;
315
+ const t = text.trim();
316
+ const isConstChar = (c) => {
317
+ const code = c.charCodeAt(0);
318
+ return code >= 65 && code <= 90;
319
+ };
320
+ const isNameChar = (c) => {
321
+ const code = c.charCodeAt(0);
322
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || c === "_";
323
+ };
324
+ let i = 0;
325
+ const readConst = () => {
326
+ if (i >= t.length || !isConstChar(t[i])) return false;
327
+ i++;
328
+ while (i < t.length && isNameChar(t[i])) i++;
329
+ return true;
330
+ };
331
+ if (t.startsWith("::")) i = 2;
332
+ if (!readConst()) return false;
333
+ while (i < t.length) {
334
+ while (i < t.length && (t[i] === " " || t[i] === " ")) i++;
335
+ if (i >= t.length) break;
336
+ if (t[i] !== ":") return false;
337
+ if (t[i + 1] !== ":") return false;
338
+ i += 2;
339
+ while (i < t.length && (t[i] === " " || t[i] === " ")) i++;
340
+ if (!readConst()) return false;
341
+ }
342
+ return true;
343
+ }
344
+ function isRubyIdentifierShaped(name) {
345
+ if (!name) return false;
346
+ const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 36;
347
+ const isDigit = (c) => c >= 48 && c <= 57;
348
+ if (!isAlpha(name.charCodeAt(0))) return false;
349
+ for (let i = 1; i < name.length; i++) {
350
+ const c = name.charCodeAt(i);
351
+ if (!isAlpha(c) && !isDigit(c)) return false;
352
+ }
353
+ return true;
354
+ }
355
+ var RAILS_MACRO_METHODS = /* @__PURE__ */ new Set([
356
+ "has_many",
357
+ "belongs_to",
358
+ "has_one",
359
+ "has_and_belongs_to_many",
360
+ "scope",
361
+ "default_scope",
362
+ "validates",
363
+ "validate",
364
+ "validates_with",
365
+ "validates_each",
366
+ "attr_reader",
367
+ "attr_writer",
368
+ "attr_accessor",
369
+ "alias_method",
370
+ "remove_method",
371
+ "undef_method",
372
+ "delegate",
373
+ "delegate_missing_to",
374
+ "event",
375
+ "transition",
376
+ "before_action",
377
+ "after_action",
378
+ "before_validation",
379
+ "after_validation",
380
+ "before_save",
381
+ "after_save",
382
+ "before_create",
383
+ "after_create",
384
+ "before_update",
385
+ "after_update",
386
+ "before_destroy",
387
+ "after_destroy",
388
+ "before_commit",
389
+ "after_commit",
390
+ "after_rollback",
391
+ "define_method",
392
+ "module_function"
393
+ ]);
394
+ var RealRubyAnalyzer = class {
395
+ analyze(rootNode, content, filePath, context) {
396
+ const symbols = [];
397
+ const imports = [];
398
+ const exports = [];
399
+ const relationships = [];
400
+ const metadata = {
401
+ detectedFrameworks: [],
402
+ frameworkPatterns: [],
403
+ cyclomaticComplexity: 1,
404
+ linesOfCode: 0
405
+ };
406
+ if (!rootNode) return { symbols, imports, errors: [] };
407
+ if (rootNode.hasError) {
408
+ const errStack = [rootNode];
409
+ while (errStack.length > 0) {
410
+ const n = errStack.pop();
411
+ if (n.type === "ERROR" || n.isMissing) {
412
+ if (!metadata.errors) metadata.errors = [];
413
+ metadata.errors.push({
414
+ message: `Syntax ${n.type} detected.`,
415
+ range: this.nodeToRange(n),
416
+ severity: "warning"
417
+ });
418
+ }
419
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
420
+ const child = n.namedChild(i);
421
+ if (child) errStack.push(child);
422
+ }
423
+ }
424
+ }
425
+ let loc = 1;
426
+ for (let i = 0; i < content.length; i++) {
427
+ if (content[i] === "\n") loc++;
428
+ }
429
+ metadata.linesOfCode = loc;
430
+ const visibilityOverrides = /* @__PURE__ */ new Map();
431
+ const stack = [{
432
+ node: rootNode,
433
+ owner: void 0,
434
+ ownerKind: void 0,
435
+ visibility: "public",
436
+ isSingleton: false,
437
+ inSingletonClass: false
438
+ }];
439
+ while (stack.length > 0) {
440
+ const frame = stack.pop();
441
+ const { node, owner, ownerKind, visibility, isSingleton, anonBinding, inSingletonClass } = frame;
442
+ const type = node.type;
443
+ if (type === "class") {
444
+ const nameNode = node.childForFieldName("name");
445
+ if (!nameNode) continue;
446
+ const rawName = nameNode.text;
447
+ const { name, owner: effOwner } = rubyReopenIdentity(rawName, owner);
448
+ const sepIdx = rawName.lastIndexOf("::");
449
+ const superClass = this.extractSuperclass(node);
450
+ const isRailsModel = this.isRailsModelName(superClass);
451
+ const isRailsController = this.isRailsControllerName(superClass);
452
+ if (isRailsModel || isRailsController) {
453
+ this.recordFramework(metadata, RAILS_FRAMEWORK);
454
+ }
455
+ const sym = {
456
+ name,
457
+ kind: ASTEngine.SymbolKind.Class,
458
+ range: this.nodeToRange(node),
459
+ ownerName: effOwner,
460
+ metadata: {
461
+ superClass,
462
+ isRailsModel,
463
+ isRailsController,
464
+ // A class declared inside a `class << obj` body lands on that object's
465
+ // SINGLETON, so no consumer can reach it by name. The module branch
466
+ // below carries the same marker for the same reason, and the export
467
+ // pass reads it for both.
468
+ ...inSingletonClass ? { declaredInSingletonClass: true } : {},
469
+ // Full declaration path for reopen spellings (`Rake::NameSpace`)
470
+ // so consumers that want the lexical path can recover it even
471
+ // though the identity is leaf+owner (php-family precedent).
472
+ ...sepIdx >= 0 ? { fqn: rawName } : {}
473
+ }
474
+ };
475
+ symbols.push(sym);
476
+ if (superClass && this.isStaticSuperclassName(superClass)) {
477
+ const leaf = this.leafName(superClass);
478
+ relationships.push({
479
+ source: name,
480
+ target: leaf,
481
+ type: ASTEngine.RelationshipType.Extends,
482
+ confidence: 1,
483
+ // #825: the base type's own node, NOT the `superclass` node — that
484
+ // one starts at `<`, so a wrapped `class X <\n Base` would stamp the
485
+ // edge one line above the type it names (measured, not assumed).
486
+ line_number: this.nodeLine(this.superclassTargetNode(node)),
487
+ metadata: {}
488
+ });
489
+ }
490
+ const body = this.findChild(node, "body_statement");
491
+ if (body) {
492
+ const bodyFrames = [];
493
+ let bodyVisibility = "public";
494
+ const _nc2 = body.namedChildCount;
495
+ for (let i = 0; i < _nc2; i++) {
496
+ const child = body.namedChild(i);
497
+ if (!child) continue;
498
+ bodyFrames.push({
499
+ node: child,
500
+ owner: name,
501
+ ownerKind: "class",
502
+ visibility: bodyVisibility,
503
+ isSingleton: false,
504
+ // A named class declared inside an anonymous block is a REAL
505
+ // class with a real name, so the anonymous binding does not
506
+ // carry into it.
507
+ anonBinding: void 0,
508
+ // Nor does a singleton-class scope: `class << obj; class K; end;
509
+ // end` declares K in the file, not on obj's singleton.
510
+ inSingletonClass: false
511
+ });
512
+ if (this.isNoArgumentVisibilityMarker(child)) {
513
+ bodyVisibility = child.text;
514
+ }
515
+ }
516
+ for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
517
+ }
518
+ const superNode = this.findChild(node, "superclass");
519
+ if (superNode) {
520
+ const _nc3 = superNode.namedChildCount;
521
+ for (let i = 0; i < _nc3; i++) {
522
+ const sc = superNode.namedChild(i);
523
+ if (sc && sc.type === "call") {
524
+ stack.push({ node: sc, owner, ownerKind, visibility, isSingleton: false, inSingletonClass });
525
+ }
526
+ }
527
+ }
528
+ continue;
529
+ }
530
+ if (type === "module") {
531
+ const nameNode = node.childForFieldName("name");
532
+ if (!nameNode) continue;
533
+ const rawName = nameNode.text;
534
+ const { name, owner: effOwner } = rubyReopenIdentity(rawName, owner);
535
+ const sepIdx = rawName.lastIndexOf("::");
536
+ const sym = {
537
+ name,
538
+ kind: ASTEngine.SymbolKind.Module,
539
+ range: this.nodeToRange(node),
540
+ ownerName: effOwner,
541
+ metadata: {
542
+ ...sepIdx >= 0 ? { fqn: rawName } : {},
543
+ // A module declared inside a `class << obj` body lands on that object's
544
+ // singleton; the class branch above carries the same marker for the
545
+ // same reason.
546
+ ...inSingletonClass ? { declaredInSingletonClass: true } : {}
547
+ }
548
+ };
549
+ symbols.push(sym);
550
+ const body = this.findChild(node, "body_statement");
551
+ if (body) {
552
+ const bodyFrames = [];
553
+ let bodyVisibility = "public";
554
+ const _nc4 = body.namedChildCount;
555
+ for (let i = 0; i < _nc4; i++) {
556
+ const child = body.namedChild(i);
557
+ if (!child) continue;
558
+ bodyFrames.push({
559
+ node: child,
560
+ owner: name,
561
+ ownerKind: "module",
562
+ visibility: bodyVisibility,
563
+ isSingleton: false,
564
+ anonBinding: void 0,
565
+ inSingletonClass: false
566
+ });
567
+ if (this.isNoArgumentVisibilityMarker(child)) {
568
+ bodyVisibility = child.text;
569
+ }
570
+ }
571
+ for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
572
+ }
573
+ continue;
574
+ }
575
+ if (type === "method") {
576
+ const nameNode = node.childForFieldName("name");
577
+ if (!nameNode) continue;
578
+ const name = nameNode.text;
579
+ const params = this.extractParameters(node);
580
+ const isTopLevel = !owner;
581
+ const sym = {
582
+ name,
583
+ kind: isTopLevel ? ASTEngine.SymbolKind.Function : ASTEngine.SymbolKind.Method,
584
+ range: this.nodeToRange(node),
585
+ ownerName: owner,
586
+ parameters: params,
587
+ visibility,
588
+ isStatic: isSingleton || void 0,
589
+ // Per-symbol cyclomatic complexity (managed-OO family contract —
590
+ // csharp #264 wave precedent). Mirrors the file-level counter's
591
+ // node set (if/unless/while/until/case/rescue/elsif), scoped to
592
+ // this method's subtree; base 1.
593
+ complexity: this.calculateNodeComplexity(node),
594
+ metadata: {
595
+ ...isSingleton ? { isSingleton: true } : {},
596
+ // Declares that `ownerName` is the LEXICAL owner, not the class
597
+ // this method actually belongs to: the method is defined in the
598
+ // block of an anonymous dynamic class bound to `anonBinding`.
599
+ ...anonBinding ? { anonymousScopeBinding: anonBinding } : {},
600
+ // ...and that the method belongs to ONE object's singleton, which is
601
+ // not the same statement: `ownerName` can be right while the method
602
+ // is still unreachable by any name from outside.
603
+ ...inSingletonClass ? { declaredInSingletonClass: true } : {}
604
+ }
605
+ };
606
+ symbols.push(sym);
607
+ const enclosingMethod = name;
608
+ const locals = new Set((params ?? []).map((p) => p.name));
609
+ for (const bound of this.extractParamDefaultBindings(node)) locals.add(bound);
610
+ this.walkMethodBody(
611
+ node,
612
+ this.findChild(node, "body_statement"),
613
+ symbols,
614
+ relationships,
615
+ imports,
616
+ metadata,
617
+ owner,
618
+ ownerKind,
619
+ enclosingMethod,
620
+ visibilityOverrides,
621
+ new Set(locals)
622
+ );
623
+ this.walkParamDefaults(
624
+ node,
625
+ symbols,
626
+ relationships,
627
+ imports,
628
+ metadata,
629
+ owner,
630
+ ownerKind,
631
+ enclosingMethod,
632
+ visibilityOverrides,
633
+ new Set(locals)
634
+ );
635
+ continue;
636
+ }
637
+ if (type === "singleton_class") {
638
+ const body = this.findChild(node, "body_statement");
639
+ if (body) {
640
+ const bodyFrames = [];
641
+ let bodyVisibility = "public";
642
+ const _nc5 = body.namedChildCount;
643
+ for (let i = 0; i < _nc5; i++) {
644
+ const child = body.namedChild(i);
645
+ if (!child) continue;
646
+ bodyFrames.push({
647
+ node: child,
648
+ owner,
649
+ ownerKind: "class",
650
+ visibility: bodyVisibility,
651
+ isSingleton: true,
652
+ anonBinding: void 0,
653
+ inSingletonClass: true
654
+ });
655
+ if (this.isNoArgumentVisibilityMarker(child)) bodyVisibility = child.text;
656
+ }
657
+ for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
658
+ }
659
+ continue;
660
+ }
661
+ if (type === "singleton_method") {
662
+ const nameNode = node.childForFieldName("name");
663
+ if (!nameNode) continue;
664
+ const name = nameNode.text;
665
+ const params = this.extractParameters(node);
666
+ const isTopLevel = !owner;
667
+ const sym = {
668
+ name,
669
+ kind: isTopLevel ? ASTEngine.SymbolKind.Function : ASTEngine.SymbolKind.Method,
670
+ range: this.nodeToRange(node),
671
+ ownerName: owner,
672
+ parameters: params,
673
+ visibility,
674
+ isStatic: true,
675
+ // Per-symbol cyclomatic complexity (managed-OO family contract —
676
+ // csharp #264 wave precedent). Same node set as the file-level
677
+ // counter, scoped to this method's subtree; base 1.
678
+ complexity: this.calculateNodeComplexity(node),
679
+ metadata: {
680
+ isSingleton: true,
681
+ ...anonBinding ? { anonymousScopeBinding: anonBinding } : {},
682
+ ...inSingletonClass ? { declaredInSingletonClass: true } : {}
683
+ }
684
+ };
685
+ symbols.push(sym);
686
+ const locals = new Set((params ?? []).map((p) => p.name));
687
+ for (const bound of this.extractParamDefaultBindings(node)) locals.add(bound);
688
+ this.walkMethodBody(
689
+ node,
690
+ this.findChild(node, "body_statement"),
691
+ symbols,
692
+ relationships,
693
+ imports,
694
+ metadata,
695
+ owner,
696
+ ownerKind,
697
+ name,
698
+ visibilityOverrides,
699
+ new Set(locals)
700
+ );
701
+ const _nc6 = node.namedChildCount;
702
+ for (let i = 0; i < _nc6; i++) {
703
+ const c = node.namedChild(i);
704
+ if (!c || c.type !== "instance_variable") continue;
705
+ const nm = node.childForFieldName("name");
706
+ if (nm && c.id === nm.id) continue;
707
+ const p0 = node.namedChild(0);
708
+ if (p0 && c.id === p0.id && node.childForFieldName("name") == null) continue;
709
+ relationships.push({
710
+ source: name,
711
+ target: c.text,
712
+ type: ASTEngine.RelationshipType.Uses,
713
+ confidence: 0.9,
714
+ // #825: the member-read node names the edge.
715
+ line_number: this.nodeLine(c),
716
+ metadata: { memberRead: true }
717
+ });
718
+ }
719
+ this.walkParamDefaults(
720
+ node,
721
+ symbols,
722
+ relationships,
723
+ imports,
724
+ metadata,
725
+ owner,
726
+ ownerKind,
727
+ name,
728
+ visibilityOverrides,
729
+ new Set(locals)
730
+ );
731
+ continue;
732
+ }
733
+ if (type === "alias") {
734
+ const names = [];
735
+ const nameNodes = [];
736
+ let oldName;
737
+ for (let i = 0; i < node.namedChildCount && names.length < 2; i++) {
738
+ const c = node.namedChild(i);
739
+ if (!c) continue;
740
+ if (c.type === "identifier") {
741
+ names.push(c.text);
742
+ nameNodes.push(c);
743
+ } else if (c.type === "simple_symbol") {
744
+ const n = this.rubyNameFromLiteral(c);
745
+ if (n !== void 0 && n.length > 0) {
746
+ names.push(n);
747
+ nameNodes.push(c);
748
+ }
749
+ } else if (names.length === 1 && oldName === void 0) {
750
+ oldName = c.text;
751
+ }
752
+ }
753
+ if (names.length >= 1) {
754
+ relationships.push({
755
+ source: owner || FILE_SCOPE_OWNER,
756
+ target: names[0],
757
+ type: ASTEngine.RelationshipType.Calls,
758
+ confidence: 0.9,
759
+ line_number: this.nodeLine(nameNodes[0]),
760
+ metadata: { aliasedTo: names[1] ?? oldName ?? "", keywordAlias: true }
761
+ });
762
+ }
763
+ continue;
764
+ }
765
+ if (type === "identifier" && isRubyVisibilityDirectiveName(node.text)) {
766
+ continue;
767
+ }
768
+ if (type === "call") {
769
+ this.processCall(
770
+ node,
771
+ symbols,
772
+ relationships,
773
+ imports,
774
+ metadata,
775
+ owner,
776
+ void 0,
777
+ visibilityOverrides,
778
+ anonBinding
779
+ );
780
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
781
+ const child = node.namedChild(i);
782
+ if (child) stack.push({ node: child, owner, ownerKind, visibility, isSingleton: false, anonBinding, inSingletonClass });
783
+ }
784
+ continue;
785
+ }
786
+ if (type === "program") {
787
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
788
+ const child = node.namedChild(i);
789
+ if (child) stack.push({ node: child, owner, ownerKind, visibility, isSingleton: false, inSingletonClass });
790
+ }
791
+ continue;
792
+ }
793
+ if (type === "assignment" && (owner || this.isDynamicClassFactoryRhs(node))) {
794
+ const left = node.childForFieldName("left");
795
+ let factoryOwner;
796
+ let anonBindingForFrames = anonBinding;
797
+ if (left) {
798
+ const leftType = left.type;
799
+ if (leftType === "constant") {
800
+ const right = node.childForFieldName("right");
801
+ const factoryKind = right ? this.dynamicClassFactoryKind(right) : null;
802
+ if (factoryKind) {
803
+ symbols.push({
804
+ name: left.text,
805
+ kind: factoryKind === "Module" ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
806
+ range: this.nodeToRange(node),
807
+ ownerName: owner,
808
+ visibility,
809
+ metadata: { definedBy: factoryKind === "Module" ? "Module.new" : "classFactory" }
810
+ });
811
+ factoryOwner = left.text;
812
+ const factoryBaseNode = this.factorySuperclassNode(right);
813
+ const factoryBase = factoryBaseNode ? factoryBaseNode.text.trim() : void 0;
814
+ if (factoryBase && this.isStaticSuperclassName(factoryBase)) {
815
+ relationships.push({
816
+ source: left.text,
817
+ target: this.leafName(factoryBase),
818
+ type: ASTEngine.RelationshipType.Extends,
819
+ confidence: 1,
820
+ // #825: the base type's own node names the edge, which is the
821
+ // first argument of the factory call.
822
+ line_number: this.nodeLine(factoryBaseNode),
823
+ metadata: {
824
+ // Both ends are WRITTEN — the constant on the left and the
825
+ // constant in the call — so this is structural evidence with
826
+ // no inferred name, unlike `instantiates`'
827
+ // `resolvedTarget: "enclosing-class"`. What the edge ASSERTS is
828
+ // `Class.new`'s documented meaning, which is Ruby rather than
829
+ // this file, and there is no `superclass` clause on the line
830
+ // for a reader to check; the mechanism is recorded so that is
831
+ // visible rather than implied.
832
+ definedBy: "Class.new"
833
+ }
834
+ });
835
+ }
836
+ } else if (owner) {
837
+ symbols.push({
838
+ name: left.text,
839
+ kind: ASTEngine.SymbolKind.Constant,
840
+ range: this.nodeToRange(node),
841
+ ownerName: owner,
842
+ visibility,
843
+ metadata: { variableType: "constant" }
844
+ });
845
+ }
846
+ } else if (leftType === "identifier") {
847
+ const right = node.childForFieldName("right");
848
+ if (right && this.dynamicClassFactoryKind(right)) {
849
+ anonBindingForFrames = left.text;
850
+ }
851
+ }
852
+ }
853
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
854
+ const child = node.namedChild(i);
855
+ if (child) stack.push({ node: child, owner: factoryOwner ?? owner, ownerKind, visibility, isSingleton: false, anonBinding: anonBindingForFrames, inSingletonClass });
856
+ }
857
+ continue;
858
+ }
859
+ if (type === "instance_variable") {
860
+ if (!isRubyInstanceVariableWrite(node)) {
861
+ relationships.push({
862
+ source: owner || FILE_SCOPE_OWNER,
863
+ target: node.text,
864
+ type: ASTEngine.RelationshipType.Uses,
865
+ confidence: 0.9,
866
+ // #825: the read node names the edge (top-level/DSL member read).
867
+ line_number: this.nodeLine(node),
868
+ metadata: { memberRead: true }
869
+ });
870
+ }
871
+ }
872
+ {
873
+ const childFrames = [];
874
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
875
+ const child = node.namedChild(i);
876
+ if (child) childFrames.push({ node: child, owner, ownerKind, visibility, isSingleton: false, anonBinding, inSingletonClass });
877
+ }
878
+ for (const cf of childFrames) stack.push(cf);
879
+ }
880
+ }
881
+ for (const symbol of symbols) {
882
+ if (!symbol.ownerName) continue;
883
+ const override = visibilityOverrides.get(`${symbol.ownerName}\0${symbol.name}`);
884
+ if (override) symbol.visibility = override;
885
+ }
886
+ const privateConstants = content.indexOf("private_constant") >= 0 ? this.collectPrivateConstants(rootNode) : /* @__PURE__ */ new Map();
887
+ const fileUnit = rubyFileUnitName(filePath);
888
+ const exportedNames = /* @__PURE__ */ new Set();
889
+ let exportDuplicate = 0;
890
+ let exportPrivateConstant = 0;
891
+ let exportUnnameable = 0;
892
+ let exportMainSingleton = 0;
893
+ let exportSingletonMember = 0;
894
+ for (const s of symbols) {
895
+ if (s.ownerName) continue;
896
+ if (s.kind !== ASTEngine.SymbolKind.Class && s.kind !== ASTEngine.SymbolKind.Module && s.kind !== ASTEngine.SymbolKind.Constant && s.kind !== ASTEngine.SymbolKind.Function) continue;
897
+ const name = String(s.name ?? "");
898
+ if (name.length === 0 || name.trim().length === 0 || name.indexOf("#{") >= 0) {
899
+ exportUnnameable++;
900
+ continue;
901
+ }
902
+ if (s.isStatic) {
903
+ exportMainSingleton++;
904
+ continue;
905
+ }
906
+ if (s.metadata?.declaredInSingletonClass === true) {
907
+ exportSingletonMember++;
908
+ continue;
909
+ }
910
+ const hidden = privateConstants.get(FILE_SCOPE_OWNER);
911
+ if (hidden?.has(name)) {
912
+ exportPrivateConstant++;
913
+ continue;
914
+ }
915
+ if (exportedNames.has(name)) {
916
+ exportDuplicate++;
917
+ continue;
918
+ }
919
+ exportedNames.add(name);
920
+ exports.push({
921
+ name,
922
+ type: "export_surface",
923
+ kind: s.kind,
924
+ range: s.range,
925
+ metadata: { unit: fileUnit, unitKind: "file", definedBy: s.metadata?.definedBy }
926
+ });
927
+ relationships.push({
928
+ source: fileUnit,
929
+ target: name,
930
+ type: ASTEngine.RelationshipType.Exports,
931
+ confidence: 1,
932
+ line_number: s.range?.startLine,
933
+ metadata: {
934
+ // The unit name comes from the PATH, not from a declaration in the
935
+ // file. Declared, so a consumer can tell an inferred unit from a
936
+ // written one — the same convention `instantiates` uses.
937
+ resolvedTarget: "file-unit",
938
+ exportKind: s.kind
939
+ }
940
+ });
941
+ }
942
+ metadata.exportsExcluded = {
943
+ duplicateReopen: exportDuplicate,
944
+ privateConstant: exportPrivateConstant,
945
+ unnameable: exportUnnameable,
946
+ mainSingleton: exportMainSingleton,
947
+ singletonMember: exportSingletonMember,
948
+ // Owned symbols are refused by RULE (the exports-family ruling), not by a
949
+ // filter that could drift, so this is a constant rather than a count. It is
950
+ // stated here so a reader does not have to infer it from the absence of a
951
+ // number.
952
+ memberByContains: "owned symbols are containment, not importable units"
953
+ };
954
+ const baseByClass = /* @__PURE__ */ new Map();
955
+ for (const s of symbols) {
956
+ if (s.kind === ASTEngine.SymbolKind.Class && s.metadata?.superClass) {
957
+ const superText = String(s.metadata.superClass);
958
+ if (this.isStaticSuperclassName(superText)) {
959
+ baseByClass.set(s.name, this.leafName(superText));
960
+ }
961
+ }
962
+ }
963
+ const methodsByOwner = /* @__PURE__ */ new Map();
964
+ for (const s of symbols) {
965
+ if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
966
+ let names = methodsByOwner.get(s.ownerName);
967
+ if (!names) {
968
+ names = /* @__PURE__ */ new Set();
969
+ methodsByOwner.set(s.ownerName, names);
970
+ }
971
+ names.add(s.name);
972
+ }
973
+ for (const s of symbols) {
974
+ if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
975
+ const base = baseByClass.get(s.ownerName);
976
+ if (!base) continue;
977
+ const inFileMethods = base === s.ownerName ? void 0 : methodsByOwner.get(base);
978
+ const baseMethods = inFileMethods ?? context?.overrideCompanion?.get(base);
979
+ if (baseMethods?.has(s.name)) {
980
+ relationships.push({
981
+ source: `${s.ownerName}.${s.name}`,
982
+ target: `${base}.${s.name}`,
983
+ type: ASTEngine.RelationshipType.Overrides,
984
+ confidence: 0.95,
985
+ // #825: a symbol-named edge carries the SYMBOL's own declaration line,
986
+ // so the edge moves with its symbol.
987
+ line_number: s.range?.startLine,
988
+ metadata: { isOverride: true }
989
+ });
990
+ } else if (!inFileMethods && !context?.overrideCompanion?.get(base)) {
991
+ relationships.push({
992
+ source: `${s.ownerName}.${s.name}`,
993
+ target: `${base}.${s.name}`,
994
+ type: ASTEngine.RelationshipType.Overrides,
995
+ confidence: 0.5,
996
+ line_number: s.range?.startLine,
997
+ metadata: {
998
+ isOverride: true,
999
+ reasonCode: "first-parent-guess",
1000
+ // THE REASON, ROUTED WHERE THE ARBITER READS IT.
1001
+ //
1002
+ // The lane has stated why since M37.10 — `reasonCode: "first-parent-guess"` — and the
1003
+ // incoming gate never saw it: its declared-cause channel reads `baseBeyondRunScope`
1004
+ // (`script/m14/incoming-rule.ts:274`), so `causeOK` is 0 fleet-wide and the residual
1005
+ // reads as an unstated hole. The claim is the TRUE half of what this branch already
1006
+ // knows: the base is not declared in this file AND the companion index gave no opinion
1007
+ // (it is never populated in production, M37.10), so the run could not look. It is a
1008
+ // claim about the RUN, not about the project, which is why the arbiter reads it only
1009
+ // where the target names nothing and why it can never raise `resolved`.
1010
+ //
1011
+ // Three conditions, each of them the arbiter's own (`classifyEdge`):
1012
+ // · the value must be IDENTIFIER-SHAPED and must BE the target's owner, or the
1013
+ // claim is REFUTED (`causeFAIL`, counted against the lane, clause 4);
1014
+ // · `base === s.ownerName` is the leaf-collapse self-collision guard's case
1015
+ // (`class Rake::TestCase < Test::Unit::TestCase`): the run DID reach that
1016
+ // spelling — it is the class's own leaf — so "beyond this run's reach" would be
1017
+ // the wrong statement and that edge stays silent;
1018
+ // · it is MUTUALLY EXCLUSIVE with every external marker, by the arbiter's rule.
1019
+ ...base !== s.ownerName && isRubyIdentifierShaped(base) ? { baseBeyondRunScope: base } : {}
1020
+ }
1021
+ });
1022
+ }
1023
+ }
1024
+ for (const s of symbols) {
1025
+ const _owner = s.ownerName;
1026
+ if (_owner) {
1027
+ relationships.push({
1028
+ source: _owner,
1029
+ target: s.name,
1030
+ type: ASTEngine.RelationshipType.Contains,
1031
+ confidence: 1,
1032
+ line_number: s.range?.startLine,
1033
+ metadata: {}
1034
+ });
1035
+ }
1036
+ }
1037
+ const containsPairs = /* @__PURE__ */ new Set();
1038
+ for (const r of relationships) {
1039
+ if (r.type === ASTEngine.RelationshipType.Contains) containsPairs.add(`${r.source}\0${r.target}`);
1040
+ }
1041
+ let exportDuplicateOfContains = 0;
1042
+ for (const r of relationships) {
1043
+ if (r.type === ASTEngine.RelationshipType.Exports && containsPairs.has(`${r.source}\0${r.target}`)) {
1044
+ exportDuplicateOfContains++;
1045
+ }
1046
+ }
1047
+ metadata.exportsOverlappingContains = exportDuplicateOfContains;
1048
+ const indexFacts = this.applyProjectIndexFacts(relationships, context);
1049
+ if (indexFacts !== void 0) metadata.projectIndex = indexFacts;
1050
+ this.classifyEvidenceTier(relationships);
1051
+ return {
1052
+ symbols,
1053
+ imports,
1054
+ relationships,
1055
+ exports,
1056
+ metadata,
1057
+ errors: metadata.errors || []
1058
+ };
1059
+ }
1060
+ /**
1061
+ * Walk a method's body: the body_statement when present, else (endless
1062
+ * `def f = expr`) every direct child except the name and parameter nodes.
1063
+ * Endless bodies are real executable code in the method's scope with its
1064
+ * params bound — skipping them dropped all calls/reads inside.
1065
+ */
1066
+ walkMethodBody(node, body, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals) {
1067
+ if (body) {
1068
+ this.walkBody(
1069
+ body,
1070
+ symbols,
1071
+ relationships,
1072
+ imports,
1073
+ metadata,
1074
+ owner,
1075
+ ownerKind,
1076
+ enclosingMethod,
1077
+ visibilityOverrides,
1078
+ locals
1079
+ );
1080
+ return;
1081
+ }
1082
+ const _nc7 = node.namedChildCount;
1083
+ for (let i = 0; i < _nc7; i++) {
1084
+ const c = node.namedChild(i);
1085
+ if (!c) continue;
1086
+ if (c.type === "method_parameters" || c.type === "block_parameters") continue;
1087
+ const fld = node.childForFieldName?.("name");
1088
+ if (fld && c.id === fld.id) continue;
1089
+ if (c.type === "identifier" && i === 0) {
1090
+ continue;
1091
+ }
1092
+ this.walkBody(
1093
+ c,
1094
+ symbols,
1095
+ relationships,
1096
+ imports,
1097
+ metadata,
1098
+ owner,
1099
+ ownerKind,
1100
+ enclosingMethod,
1101
+ visibilityOverrides,
1102
+ new Set(locals)
1103
+ );
1104
+ }
1105
+ }
1106
+ // ========================================================================
1107
+ // BODY WALKER — processes statements inside a method/function body
1108
+ // Handles: calls, require/require_relative, include/extend/prepend,
1109
+ // attr_*, define_method, alias_method, etc.
1110
+ // ========================================================================
1111
+ walkBody(node, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals = /* @__PURE__ */ new Set()) {
1112
+ const localNames = new Set(locals);
1113
+ const scanStack = [node];
1114
+ while (scanStack.length > 0) {
1115
+ const sn = scanStack.pop();
1116
+ if (sn.type === "unary") {
1117
+ const op = sn.childCount > 0 ? sn.child(0) : null;
1118
+ if (op && !op.isNamed && op.type === "defined?") continue;
1119
+ }
1120
+ if (sn.type === "assignment" || sn.type === "operator_assignment") {
1121
+ const left = sn.childForFieldName("left") ?? sn.namedChild(0);
1122
+ if (left) {
1123
+ if (left.type === "identifier") {
1124
+ localNames.add(left.text);
1125
+ }
1126
+ const _nc8 = left.namedChildCount;
1127
+ for (let i = 0; i < _nc8; i++) {
1128
+ const c = left.namedChild(i);
1129
+ if (c?.type === "identifier") localNames.add(c.text);
1130
+ }
1131
+ }
1132
+ }
1133
+ if (sn.type === "method_parameters") {
1134
+ const inner = [sn];
1135
+ while (inner.length > 0) {
1136
+ const q = inner.pop();
1137
+ if (q.type === "identifier") localNames.add(q.text);
1138
+ for (let j = 0; j < q.namedChildCount; j++) {
1139
+ const d = q.namedChild(j);
1140
+ if (d) inner.push(d);
1141
+ }
1142
+ }
1143
+ }
1144
+ if (sn.type === "block_parameters" || sn.type === "lambda_parameters") {
1145
+ const _nc9 = sn.namedChildCount;
1146
+ for (let i = 0; i < _nc9; i++) {
1147
+ const c = sn.namedChild(i);
1148
+ if (!c) continue;
1149
+ if (c.type === "identifier") {
1150
+ localNames.add(c.text);
1151
+ } else if (c.type === "destructured_parameter") {
1152
+ const inner = [c];
1153
+ while (inner.length > 0) {
1154
+ const q = inner.pop();
1155
+ if (q.type === "identifier") localNames.add(q.text);
1156
+ for (let j = 0; j < q.namedChildCount; j++) {
1157
+ const d = q.namedChild(j);
1158
+ if (d) inner.push(d);
1159
+ }
1160
+ }
1161
+ } else {
1162
+ const inner = [c];
1163
+ while (inner.length > 0) {
1164
+ const q = inner.pop();
1165
+ if (q.type === "identifier") localNames.add(q.text);
1166
+ for (let j = 0; j < q.namedChildCount; j++) {
1167
+ const d = q.namedChild(j);
1168
+ if (d) inner.push(d);
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ if (sn.type === "exception_variable") {
1175
+ const _nc10 = sn.namedChildCount;
1176
+ for (let i = 0; i < _nc10; i++) {
1177
+ const c = sn.namedChild(i);
1178
+ if (c?.type === "identifier") localNames.add(c.text);
1179
+ }
1180
+ }
1181
+ if (sn.type === "array_pattern" || sn.type === "find_pattern" || sn.type === "as_pattern") {
1182
+ const _nc11 = sn.namedChildCount;
1183
+ for (let i = 0; i < _nc11; i++) {
1184
+ const c = sn.namedChild(i);
1185
+ if (!c) continue;
1186
+ if (c.type === "identifier") {
1187
+ localNames.add(c.text);
1188
+ } else if (c.type === "splat_parameter") {
1189
+ const nm = c.childForFieldName("name") ?? c.namedChild(0);
1190
+ if (nm?.type === "identifier") localNames.add(nm.text);
1191
+ }
1192
+ }
1193
+ } else if (sn.type === "keyword_pattern") {
1194
+ const _nc12 = sn.namedChildCount;
1195
+ for (let i = 0; i < _nc12; i++) {
1196
+ const c = sn.namedChild(i);
1197
+ if (!c) continue;
1198
+ if (c.type === "identifier") localNames.add(c.text);
1199
+ else if (c.type === "hash_key_symbol") localNames.add(this.stripSymbolColon(c.text));
1200
+ }
1201
+ } else if (sn.type === "in_clause") {
1202
+ const first = sn.namedChild(0);
1203
+ if (first?.type === "identifier") localNames.add(first.text);
1204
+ }
1205
+ for (let i = sn.namedChildCount - 1; i >= 0; i--) {
1206
+ const child = sn.namedChild(i);
1207
+ if (child) scanStack.push(child);
1208
+ }
1209
+ }
1210
+ const anonRanges = [];
1211
+ {
1212
+ const scan = [node];
1213
+ while (scan.length > 0) {
1214
+ const c = scan.pop();
1215
+ if (c.type === "assignment") {
1216
+ const left = c.childForFieldName("left");
1217
+ const right = c.childForFieldName("right");
1218
+ if (left && left.type === "identifier" && right && this.dynamicClassFactoryKind(right)) {
1219
+ anonRanges.push({ lo: right.startIndex, hi: right.endIndex, binding: left.text });
1220
+ }
1221
+ }
1222
+ for (let j = 0; j < c.namedChildCount; j++) {
1223
+ const d = c.namedChild(j);
1224
+ if (d) scan.push(d);
1225
+ }
1226
+ }
1227
+ }
1228
+ anonRanges.sort((a, b) => a.hi - a.lo - (b.hi - b.lo));
1229
+ const anonBindingAt = (startIndex) => {
1230
+ for (const r of anonRanges) {
1231
+ if (startIndex >= r.lo && startIndex <= r.hi) return r.binding;
1232
+ }
1233
+ return void 0;
1234
+ };
1235
+ const stack = [node];
1236
+ while (stack.length > 0) {
1237
+ const n = stack.pop();
1238
+ const type = n.type;
1239
+ if (type === "unary") {
1240
+ const op = n.childCount > 0 ? n.child(0) : null;
1241
+ if (op && !op.isNamed && op.type === "defined?") continue;
1242
+ }
1243
+ if (type === "call") {
1244
+ this.processCall(
1245
+ n,
1246
+ symbols,
1247
+ relationships,
1248
+ imports,
1249
+ metadata,
1250
+ owner,
1251
+ enclosingMethod,
1252
+ visibilityOverrides,
1253
+ anonBindingAt(n.startIndex)
1254
+ );
1255
+ }
1256
+ if (type === "identifier") {
1257
+ const parentType = n.parent?.type;
1258
+ const nameField = parentType === "method" || parentType === "singleton_method" ? n.parent?.childForFieldName?.("name") ?? null : null;
1259
+ const isDefName = nameField ? nameField.id === n.id : (parentType === "method" || parentType === "singleton_method") && n.parent.namedChild(0)?.id === n.id;
1260
+ if (isDefName) continue;
1261
+ if (!BARE_CALL_SKIP_PARENTS.has(parentType) && !localNames.has(n.text)) {
1262
+ this.pushBareCall(n.text, n, owner, enclosingMethod, relationships);
1263
+ }
1264
+ }
1265
+ if (type === "instance_variable") {
1266
+ if (!isRubyInstanceVariableWrite(n)) {
1267
+ const src = enclosingMethod || owner || FILE_SCOPE_OWNER;
1268
+ relationships.push({
1269
+ source: src,
1270
+ target: n.text,
1271
+ type: ASTEngine.RelationshipType.Uses,
1272
+ confidence: 0.9,
1273
+ line_number: this.nodeLine(n),
1274
+ metadata: { memberRead: true }
1275
+ });
1276
+ }
1277
+ }
1278
+ if (type === "if" || type === "unless" || type === "while" || type === "until" || type === "case" || type === "rescue" || type === "elsif" || type === "if_modifier" || type === "unless_modifier" || type === "while_modifier" || type === "until_modifier" || type === "rescue_modifier") {
1279
+ if (metadata.cyclomaticComplexity !== void 0) {
1280
+ metadata.cyclomaticComplexity++;
1281
+ }
1282
+ }
1283
+ if (type === "class_variable" || type === "global_variable") {
1284
+ metadata.classAndGlobalVarsSkipped = (metadata.classAndGlobalVarsSkipped ?? 0) + 1;
1285
+ }
1286
+ if (type === "undef") continue;
1287
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1288
+ const child = n.namedChild(i);
1289
+ if (child) stack.push(child);
1290
+ }
1291
+ }
1292
+ }
1293
+ /**
1294
+ * Names a method's PARAMETER DEFAULTS assign, which are locals of the method
1295
+ * scope because Ruby evaluates a default expression in that scope.
1296
+ *
1297
+ * `def all(all_queries: (seen = true))` binds `seen`; a later
1298
+ * `return nil if seen` reads a local, not a method. walkBody's pre-scan only
1299
+ * ever saw the BODY subtree, so these names were unbound and every read of
1300
+ * one became a fabricated bare call — measured on rails in
1301
+ * activerecord/lib/active_record/scoping/named.rb:23, where the analyzer
1302
+ * published `calls all -> all_queries_not_set` for a name the signature on
1303
+ * line 22 itself binds.
1304
+ *
1305
+ * Only ASSIGNMENT targets are collected, and only from the VALUE position of
1306
+ * an `optional_parameter` / `keyword_parameter` — a bare call in a default
1307
+ * (`def f(lock = Mutex.new)`) is a real call and must stay one, so the value
1308
+ * is scanned for what it BINDS and the call walk is left to walkParamDefaults.
1309
+ * The left-hand side forms mirror the body pre-scan's: a plain identifier, and
1310
+ * the identifiers under a `left_assignment_list` (tree-sitter-ruby has no
1311
+ * `multiple_assignment` node, so `a, b = 1, 2` is one assignment whose left
1312
+ * is a list).
1313
+ */
1314
+ extractParamDefaultBindings(node) {
1315
+ const names = /* @__PURE__ */ new Set();
1316
+ const paramsNode = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
1317
+ if (!paramsNode) return names;
1318
+ const _nc13 = paramsNode.namedChildCount;
1319
+ for (let i = 0; i < _nc13; i++) {
1320
+ const pc = paramsNode.namedChild(i);
1321
+ if (!pc) continue;
1322
+ if (pc.type !== "optional_parameter" && pc.type !== "keyword_parameter") continue;
1323
+ if (pc.namedChildCount < 2) continue;
1324
+ const stack = [];
1325
+ for (let j = 1; j < pc.namedChildCount; j++) {
1326
+ const c = pc.namedChild(j);
1327
+ if (c) stack.push(c);
1328
+ }
1329
+ while (stack.length > 0) {
1330
+ const n = stack.pop();
1331
+ if (n.type === "assignment" || n.type === "operator_assignment") {
1332
+ const left = n.childForFieldName("left") ?? n.namedChild(0);
1333
+ if (left) {
1334
+ if (left.type === "identifier") {
1335
+ names.add(left.text);
1336
+ } else {
1337
+ for (let j = 0; j < left.namedChildCount; j++) {
1338
+ const s = left.namedChild(j);
1339
+ if (s && s.type === "identifier") names.add(s.text);
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ if (n.type === "do_block" || n.type === "block") continue;
1345
+ for (let j = 0; j < n.namedChildCount; j++) {
1346
+ const c = n.namedChild(j);
1347
+ if (c) stack.push(c);
1348
+ }
1349
+ }
1350
+ }
1351
+ return names;
1352
+ }
1353
+ /**
1354
+ * Walk the default-argument VALUE expressions of a method's parameters
1355
+ * (`optional_parameter` `x = v`, `keyword_parameter` `k: v`) in the method's
1356
+ * own scope. Ruby evaluates these at call time, so their calls, member
1357
+ * reads and instantiations belong to the method — only the body was walked
1358
+ * before, silently dropping `def f(app, lock = Lock.new)`-style call sites
1359
+ * (RB-B, rack wave: mutex = Mutex.new, params = Rack::Utils.default_query_parser,
1360
+ * app = @app). Mirrors the oracle truth, which walks parameter values.
1361
+ */
1362
+ walkParamDefaults(node, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals) {
1363
+ const paramsNode = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
1364
+ if (!paramsNode) return;
1365
+ const _nc14 = paramsNode.namedChildCount;
1366
+ for (let i = 0; i < _nc14; i++) {
1367
+ const pc = paramsNode.namedChild(i);
1368
+ if (!pc) continue;
1369
+ if (pc.type === "optional_parameter" || pc.type === "keyword_parameter") {
1370
+ if (pc.namedChildCount >= 2) {
1371
+ const value = pc.namedChild(pc.namedChildCount - 1);
1372
+ if (value) {
1373
+ this.walkBody(
1374
+ value,
1375
+ symbols,
1376
+ relationships,
1377
+ imports,
1378
+ metadata,
1379
+ owner,
1380
+ ownerKind,
1381
+ enclosingMethod,
1382
+ visibilityOverrides,
1383
+ locals
1384
+ );
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ }
1390
+ // ========================================================================
1391
+ // LOAD FACTS — one `require`-family site, THREE surfaces
1392
+ // ========================================================================
1393
+ /**
1394
+ * Everything a receiverless `require` / `require_relative` / `load` /
1395
+ * `autoload` of a literal path states. One function because the four call
1396
+ * sites this replaces were four copies of the same three pushes, and the
1397
+ * `references`/`imports` PAIR is exactly the kind of thing that must not be
1398
+ * half-applied: an earlier round of this file already lost a fix to a branch
1399
+ * that claimed a rule "already existed" above the code that implemented it.
1400
+ *
1401
+ * 1. `ImportInfo` — the brain_import row. Reaches the file-dependency engine.
1402
+ * 2. `references` — "this file mentions this path". Source is the declared
1403
+ * file-scope owner. Pinned by three committed fixtures; unchanged.
1404
+ * 3. `imports` — "this file DEPENDS ON this path". The same target, a second
1405
+ * label, and the reason is not cosmetic:
1406
+ *
1407
+ * - it is the label the dependency surfaces and `bb_graph`/`bb_impact`/
1408
+ * `bb_migrate` read. A graph query that narrows to `imports` sees
1409
+ * nothing for ruby today, so "ruby has no file dependencies" is
1410
+ * answerable from the graph and it is false.
1411
+ * - `metadata.fileLevel` is what makes the SOURCE bind to the importing
1412
+ * FILE: `parallel-indexer.ts` binds `source_external` to the
1413
+ * project-relative path when the source names no symbol and the edge
1414
+ * declares `fileLevel`. The `references` edge does not declare it, so
1415
+ * its source stays the scope sentinel and no surface can answer "what
1416
+ * does this file require".
1417
+ *
1418
+ * bash emits the same pair for `source` (`isSourceImport` +
1419
+ * `isSourceDependency` on the same normalized path), so this is the family
1420
+ * precedent and not a ruby invention.
1421
+ *
1422
+ * The TARGET is the bare require path, not the `<path>:<item>` form the
1423
+ * js/python/dart lanes use. That is a property of the language, not a choice:
1424
+ * `require "json/parser"` names a FILE and Ruby has no syntax for requiring one
1425
+ * member of it, so there is no item to write after a colon. The consequence is
1426
+ * measured and declared: `import-resolution.ts` only considers `imports` rows
1427
+ * whose target contains a `:`, so these rows reach the graph and the dependency
1428
+ * engine but NOT that resolver. Inventing a `:*` suffix would buy admission to
1429
+ * a pass that then refuses `*` by design ("namespace-star-no-single-target") —
1430
+ * bytes and a target string that is not a path, for nothing.
1431
+ */
1432
+ pushLoadFacts(node, arg, cleanText, methodName, relationships, imports) {
1433
+ if (cleanText.length === 0) return;
1434
+ imports.push({
1435
+ module: cleanText,
1436
+ names: [],
1437
+ range: this.nodeToRange(node),
1438
+ isWildcard: true,
1439
+ metadata: { requireMethod: methodName }
1440
+ });
1441
+ relationships.push({
1442
+ // GAD2 twin (groovy wave, standing rule): the require edge's
1443
+ // source used a hardcoded "root" while calls/uses/instan-
1444
+ // tiates at the same top level use "Global" — two sentinels
1445
+ // for one role in one file. Unified on "Global" (the pinned
1446
+ // convention; no fixture ever pinned "root") — and now unified
1447
+ // through FILE_SCOPE_OWNER, which is what made the SECOND
1448
+ // divergent spelling ("unknown", four sites: alias,
1449
+ // alias_method, mixin, association) visible at all.
1450
+ source: FILE_SCOPE_OWNER,
1451
+ target: cleanText,
1452
+ type: ASTEngine.RelationshipType.References,
1453
+ confidence: 0.9,
1454
+ // #825: the required path's string node names the edge.
1455
+ line_number: this.nodeLine(arg),
1456
+ metadata: { requireMethod: methodName }
1457
+ });
1458
+ relationships.push({
1459
+ source: FILE_SCOPE_OWNER,
1460
+ target: cleanText,
1461
+ type: ASTEngine.RelationshipType.Imports,
1462
+ confidence: 1,
1463
+ line_number: this.nodeLine(arg),
1464
+ metadata: {
1465
+ requireMethod: methodName,
1466
+ // The importer is the FILE, not a scope — see the note above.
1467
+ fileLevel: true,
1468
+ // HOW the path is resolved, which is decidable per method and is the
1469
+ // only classification that is a fact rather than a guess:
1470
+ // `require_relative` is relative to the requiring file by definition, so
1471
+ // its target is a path in the project; a bare `require` is resolved
1472
+ // against `$LOAD_PATH`, which can name a gem OR a project file, so its
1473
+ // target's location is unknown. `is_external` is therefore NOT set:
1474
+ // parallel-indexer reads it to keep external packages out of internal
1475
+ // resolution, and asserting `false` for a load-path require would be a
1476
+ // claim this file cannot make.
1477
+ resolution: methodName === "require_relative" ? "file-relative" : "load-path"
1478
+ }
1479
+ });
1480
+ }
1481
+ // ========================================================================
1482
+ // CALL PROCESSOR — resolves a single `call` node
1483
+ // ========================================================================
1484
+ processCall(node, symbols, relationships, imports, metadata, owner, enclosingMethod, visibilityOverrides, anonBinding) {
1485
+ const methodNode = node.childForFieldName("method");
1486
+ if (!methodNode) return;
1487
+ const methodName = methodNode.text;
1488
+ if (!methodName) return;
1489
+ let receiver;
1490
+ let receiverLeaf;
1491
+ let receiverNode = null;
1492
+ let hasExpressionReceiver = false;
1493
+ const _nc15 = node.namedChildCount;
1494
+ for (let i = 0; i < _nc15; i++) {
1495
+ const child = node.namedChild(i);
1496
+ if (child && child.id === methodNode.id) break;
1497
+ if (!child) continue;
1498
+ if (child.type === "identifier" || child.type === "constant" || child.type === "scope_resolution") {
1499
+ receiver = child.text;
1500
+ receiverLeaf = this.leafName(child.text);
1501
+ receiverNode = child;
1502
+ } else if (VARIABLE_RECEIVER_TYPES.has(child.type)) {
1503
+ receiver = child.text;
1504
+ receiverNode = child;
1505
+ } else if (child.type === "self") {
1506
+ } else {
1507
+ hasExpressionReceiver = true;
1508
+ }
1509
+ }
1510
+ if (methodName === "private" || methodName === "protected" || methodName === "public") {
1511
+ const args = this.findChild(node, "argument_list");
1512
+ if (args && owner && args.namedChildCount > 0) {
1513
+ const _nc16 = args.namedChildCount;
1514
+ for (let i = 0; i < _nc16; i++) {
1515
+ const arg = args.namedChild(i);
1516
+ if (!arg || arg.type !== "simple_symbol") continue;
1517
+ const targetName = this.stripSymbolColon(arg.text);
1518
+ visibilityOverrides.set(`${owner}\0${targetName}`, methodName);
1519
+ }
1520
+ }
1521
+ return;
1522
+ }
1523
+ if ((methodName === "require" || methodName === "require_relative" || methodName === "load") && !receiver && receiverNode === null && !hasExpressionReceiver) {
1524
+ const args = this.findChild(node, "argument_list");
1525
+ if (args) {
1526
+ for (let i = 0; i < args.childCount; i++) {
1527
+ const arg = args.child(i);
1528
+ if (arg && arg.type === "string") {
1529
+ const cleanText = this.stripQuotes(arg.text);
1530
+ if (cleanText.indexOf("#{") >= 0) continue;
1531
+ this.pushLoadFacts(node, arg, cleanText, methodName, relationships, imports);
1532
+ }
1533
+ }
1534
+ }
1535
+ return;
1536
+ }
1537
+ if (methodName === "autoload" && !receiver && receiverNode === null && !hasExpressionReceiver) {
1538
+ const args = this.findChild(node, "argument_list");
1539
+ if (args) {
1540
+ for (let i = 0; i < args.childCount; i++) {
1541
+ const arg = args.child(i);
1542
+ if (arg && arg.type === "string") {
1543
+ const cleanText = this.stripQuotes(arg.text);
1544
+ if (cleanText.indexOf("#{") >= 0) continue;
1545
+ this.pushLoadFacts(node, arg, cleanText, methodName, relationships, imports);
1546
+ }
1547
+ }
1548
+ }
1549
+ return;
1550
+ }
1551
+ if (methodName === "include" || methodName === "extend" || methodName === "prepend") {
1552
+ const relType = ASTEngine.RelationshipType.MixesIn;
1553
+ const args = this.findChild(node, "argument_list");
1554
+ const selfKind = this.mixinSelfKind(node);
1555
+ const hasExplicitReceiver = receiver !== void 0 || hasExpressionReceiver;
1556
+ if (selfKind === "instance" || selfKind === "top" || hasExplicitReceiver) {
1557
+ return;
1558
+ }
1559
+ if (!owner) {
1560
+ return;
1561
+ }
1562
+ if (args) {
1563
+ const _nc17 = args.namedChildCount;
1564
+ for (let i = 0; i < _nc17; i++) {
1565
+ const arg = args.namedChild(i);
1566
+ if (arg && (arg.type === "constant" || arg.type === "scope_resolution")) {
1567
+ const target = this.leafName(arg.text);
1568
+ relationships.push({
1569
+ source: owner || FILE_SCOPE_OWNER,
1570
+ target,
1571
+ type: relType,
1572
+ confidence: 1,
1573
+ // #825: the mixin CONSTANT's node names the edge.
1574
+ line_number: this.nodeLine(arg),
1575
+ metadata: { mixinType: methodName }
1576
+ });
1577
+ }
1578
+ }
1579
+ }
1580
+ return;
1581
+ }
1582
+ if (methodName === "attr_reader" || methodName === "attr_writer" || methodName === "attr_accessor") {
1583
+ const args = this.findChild(node, "argument_list");
1584
+ if (args) {
1585
+ const _nc18 = args.namedChildCount;
1586
+ for (let i = 0; i < _nc18; i++) {
1587
+ const arg = args.namedChild(i);
1588
+ if (arg && (arg.type === "simple_symbol" || arg.type === "string")) {
1589
+ const propName = this.rubyNameFromLiteral(arg);
1590
+ if (propName === void 0 || propName.length === 0) continue;
1591
+ symbols.push({
1592
+ name: propName,
1593
+ kind: ASTEngine.SymbolKind.Property,
1594
+ range: this.nodeToRange(arg),
1595
+ ownerName: owner,
1596
+ visibility: "public",
1597
+ metadata: { accessorType: methodName }
1598
+ });
1599
+ }
1600
+ }
1601
+ }
1602
+ return;
1603
+ }
1604
+ if (methodName === "define_method") {
1605
+ const args = this.findChild(node, "argument_list");
1606
+ const nameArg = args && args.namedChildCount > 0 ? args.namedChild(0) : null;
1607
+ if (nameArg && (nameArg.type === "simple_symbol" || nameArg.type === "string")) {
1608
+ const name = this.rubyNameFromLiteral(nameArg);
1609
+ if (name !== void 0 && name.length > 0 && name.indexOf("#{") < 0) {
1610
+ symbols.push({
1611
+ name,
1612
+ kind: ASTEngine.SymbolKind.Method,
1613
+ range: this.nodeToRange(nameArg),
1614
+ ownerName: owner,
1615
+ visibility: "public",
1616
+ metadata: { isDynamicMethod: true }
1617
+ });
1618
+ }
1619
+ }
1620
+ return;
1621
+ }
1622
+ if (methodName === "alias_method") {
1623
+ const args = this.findChild(node, "argument_list");
1624
+ if (args) {
1625
+ const names = [];
1626
+ const argNodes = [];
1627
+ for (let i = 0; i < args.namedChildCount && names.length < 2; i++) {
1628
+ const arg = args.namedChild(i);
1629
+ if (arg && arg.type === "simple_symbol") {
1630
+ const n = this.rubyNameFromLiteral(arg);
1631
+ if (n === void 0 || n.length === 0) continue;
1632
+ names.push(n);
1633
+ argNodes.push(arg);
1634
+ }
1635
+ }
1636
+ if (names.length === 2) {
1637
+ relationships.push({
1638
+ source: owner || FILE_SCOPE_OWNER,
1639
+ target: names[0],
1640
+ type: ASTEngine.RelationshipType.Calls,
1641
+ confidence: 0.9,
1642
+ // #825: the `:new_name` argument's node names the edge.
1643
+ line_number: this.nodeLine(argNodes[0]),
1644
+ metadata: { aliasedTo: names[1] }
1645
+ });
1646
+ }
1647
+ }
1648
+ return;
1649
+ }
1650
+ if (methodName === "redirect_to" || methodName === "render" || methodName === "before_action" || methodName === "after_action") {
1651
+ this.recordFramework(metadata, RAILS_FRAMEWORK);
1652
+ }
1653
+ if (methodName === "before_action" || methodName === "after_action") {
1654
+ this.recordFramework(metadata, RAILS_FRAMEWORK, "Callbacks");
1655
+ }
1656
+ if (RAILS_MACRO_METHODS.has(methodName)) {
1657
+ if (["has_many", "belongs_to", "has_one", "has_and_belongs_to_many"].includes(methodName)) {
1658
+ const args = this.findChild(node, "argument_list");
1659
+ if (args) {
1660
+ const _nc20 = args.namedChildCount;
1661
+ for (let i = 0; i < _nc20; i++) {
1662
+ const arg = args.namedChild(i);
1663
+ if (arg && (arg.type === "simple_symbol" || arg.type === "string")) {
1664
+ const name = this.rubyNameFromLiteral(arg);
1665
+ if (name === void 0 || name.length === 0) continue;
1666
+ const anonMd = anonBinding ? { anonymousScopeBinding: anonBinding } : {};
1667
+ symbols.push({
1668
+ name,
1669
+ kind: ASTEngine.SymbolKind.Property,
1670
+ range: this.nodeToRange(arg),
1671
+ ownerName: owner,
1672
+ visibility: "public",
1673
+ metadata: { associationType: methodName, ...anonMd }
1674
+ });
1675
+ relationships.push({
1676
+ source: owner || FILE_SCOPE_OWNER,
1677
+ target: name,
1678
+ type: ASTEngine.RelationshipType.Uses,
1679
+ confidence: 0.8,
1680
+ // #825: the association's own argument node names the edge.
1681
+ line_number: this.nodeLine(arg),
1682
+ metadata: { activerecordMacro: methodName, ...anonMd }
1683
+ });
1684
+ }
1685
+ }
1686
+ }
1687
+ } else if (methodName === "scope" || methodName === "default_scope") {
1688
+ const args = this.findChild(node, "argument_list");
1689
+ if (args && args.namedChildCount > 0) {
1690
+ const firstArg = args.namedChild(0);
1691
+ if (firstArg && firstArg.type === "simple_symbol") {
1692
+ const scopeName = this.rubyNameFromLiteral(firstArg);
1693
+ if (scopeName === void 0 || scopeName.length === 0) return;
1694
+ symbols.push({
1695
+ name: scopeName,
1696
+ kind: ASTEngine.SymbolKind.Method,
1697
+ range: this.nodeToRange(firstArg),
1698
+ ownerName: owner,
1699
+ visibility: "public",
1700
+ metadata: { scopeType: methodName }
1701
+ });
1702
+ }
1703
+ }
1704
+ }
1705
+ return;
1706
+ }
1707
+ if (RUBY_BUILTINS.has(methodName)) return;
1708
+ if (methodName === "class" && receiver === "self") return;
1709
+ const source = enclosingMethod || owner || FILE_SCOPE_OWNER;
1710
+ if (methodName === "new") {
1711
+ if (hasExpressionReceiver) return;
1712
+ const target = receiverLeaf || (!receiver ? owner : void 0);
1713
+ const firstChar = target?.charCodeAt(0) ?? 0;
1714
+ if (target && (!receiverLeaf || firstChar >= 65 && firstChar <= 90)) {
1715
+ relationships.push({
1716
+ source,
1717
+ target,
1718
+ fromSymbol: source,
1719
+ toSymbol: target,
1720
+ type: ASTEngine.RelationshipType.Instantiates,
1721
+ // #825: the constructed TYPE names the edge — its receiver node when
1722
+ // there is one, else the call node itself (bare `new`).
1723
+ line_number: this.nodeLine(receiverNode ?? node),
1724
+ confidence: 0.9,
1725
+ // When no receiver supplied the name, the target is the ENCLOSING
1726
+ // class: Ruby's `self.new`, `self.class.new` and a receiverless `new`
1727
+ // all construct the class they appear in. The line is the call site
1728
+ // (correct), but the class name is IMPLIED rather than written there,
1729
+ // so the inference is declared instead of left to look like a written
1730
+ // name. The M59 oracle reads this marker to classify the claim as
1731
+ // structurally uncheckable-by-token rather than as a failed claim.
1732
+ metadata: receiverLeaf ? {} : { resolvedTarget: "enclosing-class" }
1733
+ });
1734
+ return;
1735
+ }
1736
+ if (!receiver && !owner) return;
1737
+ }
1738
+ const qualified = receiver ? `${receiver}.${methodName}` : methodName;
1739
+ relationships.push({
1740
+ source,
1741
+ target: methodName,
1742
+ fromSymbol: source,
1743
+ toSymbol: qualified,
1744
+ type: ASTEngine.RelationshipType.Calls,
1745
+ confidence: 0.9,
1746
+ // #825: the METHOD field's node, never the call node — a call node starts
1747
+ // at its receiver, which is the line ABOVE the callee in `a.b.c` chains.
1748
+ line_number: this.nodeLine(methodNode),
1749
+ metadata: {}
1750
+ });
1751
+ }
1752
+ /**
1753
+ * Emit a calls relationship for a bare identifier used as a method call
1754
+ * (tree-sitter-ruby parses `foo` with no receiver/args as an identifier).
1755
+ * Mirrors processCall's regular-call tail: builtins are skipped and the
1756
+ * same confidence/metadata shape is used so the graph stays consistent.
1757
+ */
1758
+ pushBareCall(name, nameNode, owner, enclosingMethod, relationships) {
1759
+ if (RUBY_BUILTINS.has(name)) return;
1760
+ if (name === "self" || name === "nil" || name === "true" || name === "false") return;
1761
+ if (isRubyVisibilityDirectiveName(name)) return;
1762
+ const firstChar = name.charCodeAt(0);
1763
+ if (firstChar >= 65 && firstChar <= 90) return;
1764
+ if (name.startsWith("__") && name.endsWith("__")) return;
1765
+ const source = enclosingMethod || owner || FILE_SCOPE_OWNER;
1766
+ relationships.push({
1767
+ source,
1768
+ target: name,
1769
+ fromSymbol: source,
1770
+ toSymbol: name,
1771
+ type: ASTEngine.RelationshipType.Calls,
1772
+ confidence: 0.9,
1773
+ // #825: the identifier's own node names the bare call.
1774
+ line_number: this.nodeLine(nameNode),
1775
+ metadata: { bareCall: true }
1776
+ });
1777
+ }
1778
+ // ========================================================================
1779
+ // HELPERS
1780
+ // ========================================================================
1781
+ /**
1782
+ * True when an `assignment` node's RHS is a dynamic class/module factory
1783
+ * call (`Struct.new`, `Data.define`, `Class.new`, `Module.new`, possibly
1784
+ * with blocks/args). Used to admit top-level factory assignments that
1785
+ * the owner-gated constant rule would otherwise skip.
1786
+ */
1787
+ isDynamicClassFactoryRhs(node) {
1788
+ const _nc21 = node.namedChildCount;
1789
+ for (let i = 0; i < _nc21; i++) {
1790
+ const c = node.namedChild(i);
1791
+ if (!c) continue;
1792
+ if (c.type === "call") {
1793
+ if (this.dynamicClassFactoryKind(c)) return true;
1794
+ }
1795
+ }
1796
+ return false;
1797
+ }
1798
+ /**
1799
+ * The NAME a `simple_symbol` or `string` literal declares, or `undefined` when
1800
+ * the analyzer cannot state it.
1801
+ *
1802
+ * ## Why this exists
1803
+ *
1804
+ * `stripQuotes` removes the OUTER quotes and stops there, so a Ruby escape inside
1805
+ * the literal survives into the published name. Ruby's
1806
+ * `define_method("test_safe_load_marshal String \"hello\" ivar")` defines a method
1807
+ * whose name is `test_safe_load_marshal String "hello" ivar` — with a plain quote,
1808
+ * no backslash — and the analyzer published the name with the backslash still in
1809
+ * it. That name matches no method in the program, and it propagated: the symbol
1810
+ * carried it, `contains` carried it, and `overrides` carried it again as
1811
+ * `TestCase.<that name>`. Measured by the full-corpus R7 sweep: 19 distinct
1812
+ * escaped names, 14 `contains` edges and 14 `overrides` edges with a quote in a
1813
+ * target, every one in test/rubygems/test_gem_safe_marshal.rb — generated
1814
+ * round-trip test methods, so the names are real but the escapes were not
1815
+ * resolved.
1816
+ *
1817
+ * ## The escape set, and why it is closed
1818
+ *
1819
+ * The corpus says which escapes occur, and the inventory is four pairs:
1820
+ *
1821
+ * \" 56 \\ 21 \x 21 \u 1
1822
+ *
1823
+ * `\"`, `\\` and the single-character control escapes resolve to text a name can
1824
+ * carry, so they are resolved here and the name is published correct. `\x` and
1825
+ * `\u` are NOT resolved, and that is a decision rather than an omission: `\x61`
1826
+ * is the BYTE 0x61 and `\u{3042}` is a non-ASCII character, so resolving them
1827
+ * would publish a name built from raw bytes or a non-Latin script — a string that
1828
+ * no consumer can bind and that the parallel-indexer's own name filters would
1829
+ * then treat as garbage. A name this analyzer cannot state is not published,
1830
+ * which is the same doctrine the `#{…}` template refusal above applies and the
1831
+ * reason that refusal exists.
1832
+ *
1833
+ * So the rule is: resolve the escapes that yield a name, refuse the name when it
1834
+ * carries an escape that does not. `\q` — an escape Ruby does not define at all
1835
+ * — takes the same path, and returns `undefined` rather than passing the backslash
1836
+ * through.
1837
+ *
1838
+ * ## Character scan, no pattern
1839
+ *
1840
+ * The escapes are matched by character code against a switch, and the scan walks
1841
+ * the string once. A table keyed on a substring would be a lookup dressed as a
1842
+ * parse; this reads the backslash, then the character it escapes, and decides on
1843
+ * those two codes.
1844
+ */
1845
+ rubyNameFromLiteral(node) {
1846
+ const raw = node.text;
1847
+ let s = raw;
1848
+ if (node.type === "string") {
1849
+ s = this.stripQuotes(raw);
1850
+ } else {
1851
+ s = this.stripSymbolColon(raw);
1852
+ }
1853
+ if (s.indexOf("\\") < 0) return s;
1854
+ let out = "";
1855
+ for (let i = 0; i < s.length; i++) {
1856
+ const c = s.charCodeAt(i);
1857
+ if (c !== 92) {
1858
+ out += s[i];
1859
+ continue;
1860
+ }
1861
+ if (i + 1 >= s.length) return void 0;
1862
+ const e = s[i + 1];
1863
+ if (e === "\\" || e === '"' || e === "'" || e === "#") {
1864
+ out += e;
1865
+ i++;
1866
+ continue;
1867
+ }
1868
+ if (e === "a") {
1869
+ out += "\x07";
1870
+ i++;
1871
+ continue;
1872
+ }
1873
+ if (e === "b") {
1874
+ out += "\b";
1875
+ i++;
1876
+ continue;
1877
+ }
1878
+ if (e === "e") {
1879
+ out += "\x1B";
1880
+ i++;
1881
+ continue;
1882
+ }
1883
+ if (e === "f") {
1884
+ out += "\f";
1885
+ i++;
1886
+ continue;
1887
+ }
1888
+ if (e === "n") {
1889
+ out += "\n";
1890
+ i++;
1891
+ continue;
1892
+ }
1893
+ if (e === "r") {
1894
+ out += "\r";
1895
+ i++;
1896
+ continue;
1897
+ }
1898
+ if (e === "s") {
1899
+ out += " ";
1900
+ i++;
1901
+ continue;
1902
+ }
1903
+ if (e === "t") {
1904
+ out += " ";
1905
+ i++;
1906
+ continue;
1907
+ }
1908
+ if (e === "v") {
1909
+ out += "\v";
1910
+ i++;
1911
+ continue;
1912
+ }
1913
+ if (e === "0") {
1914
+ out += "\0";
1915
+ i++;
1916
+ continue;
1917
+ }
1918
+ return void 0;
1919
+ }
1920
+ return out;
1921
+ }
1922
+ /**
1923
+ * The node that NAMES the base of a `K = Class.new(Base)` factory, or null.
1924
+ *
1925
+ * Only `Class.new` counts, and only when the base is the FIRST argument and is
1926
+ * written as a constant or a scope path. `Struct.new(:a, :b)` and
1927
+ * `Data.define(:a, :b)` take member names there, not a superclass — neither
1928
+ * class has a superclass parameter — so recognising them as a base would
1929
+ * fabricate an `extends` edge against a symbol name. `Module.new` takes no
1930
+ * argument at all, which the method check below also rules out.
1931
+ *
1932
+ * The RHS may wrap the call (`Class.new(Base) { … }.new`, which
1933
+ * `dynamicClassFactoryKind` already recurses through), so the search for the
1934
+ * `Class.new` head repeats the same bounded walk rather than assuming the call
1935
+ * sits at the top. The recursion is bounded by the grammar: the inner node it
1936
+ * recurses into must itself be a `.new` call whose own receiver is a constant.
1937
+ */
1938
+ factorySuperclassNode(rhs) {
1939
+ if (!rhs) return null;
1940
+ let callNode = rhs.type === "call" ? rhs : null;
1941
+ if (!callNode) {
1942
+ for (let i = 0; i < rhs.namedChildCount; i++) {
1943
+ const c = rhs.namedChild(i);
1944
+ if (c && c.type === "call") {
1945
+ callNode = c;
1946
+ break;
1947
+ }
1948
+ }
1949
+ }
1950
+ if (!callNode) return null;
1951
+ const methodNode = callNode.childForFieldName("method");
1952
+ if (!methodNode || methodNode.text !== "new") return null;
1953
+ let receiver;
1954
+ for (let i = 0; i < callNode.namedChildCount; i++) {
1955
+ const c = callNode.namedChild(i);
1956
+ if (!c) continue;
1957
+ if (c.id === methodNode.id) break;
1958
+ if (c.type === "constant" || c.type === "scope_resolution") {
1959
+ receiver = c.text;
1960
+ continue;
1961
+ }
1962
+ if (c.type === "call") return this.factorySuperclassNode(c);
1963
+ return null;
1964
+ }
1965
+ if (receiver !== "Class") return null;
1966
+ const args = this.findChild(callNode, "argument_list");
1967
+ if (!args || args.namedChildCount === 0) return null;
1968
+ const first = args.namedChild(0);
1969
+ if (!first) return null;
1970
+ if (first.type !== "constant" && first.type !== "scope_resolution") return null;
1971
+ return first;
1972
+ }
1973
+ /**
1974
+ * If `node` is a `X.new`/`X.define` call on a class-factory receiver,
1975
+ * return "Class" (Struct/Data/Class) or "Module" (Module.new); else null.
1976
+ * Receiver must be the bare constant (RHS of the assignment may wrap in
1977
+ * blocks — only the call head matters).
1978
+ */
1979
+ dynamicClassFactoryKind(node) {
1980
+ let callNode = node.type === "call" ? node : null;
1981
+ if (!callNode) {
1982
+ const _nc22 = node.namedChildCount;
1983
+ for (let i = 0; i < _nc22; i++) {
1984
+ const c = node.namedChild(i);
1985
+ if (c && c.type === "call") {
1986
+ callNode = c;
1987
+ break;
1988
+ }
1989
+ }
1990
+ }
1991
+ if (!callNode) return null;
1992
+ const methodNode = callNode.childForFieldName("method");
1993
+ if (!methodNode) return null;
1994
+ let methodName = methodNode.text;
1995
+ let receiver;
1996
+ const _nc23 = callNode.namedChildCount;
1997
+ for (let i = 0; i < _nc23; i++) {
1998
+ const child = callNode.namedChild(i);
1999
+ if (child && child.id === methodNode.id) break;
2000
+ if (!child) continue;
2001
+ if (child.type === "constant" || child.type === "scope_resolution") {
2002
+ receiver = child.text;
2003
+ } else if (!receiver && child.type === "call" && (methodName === "new" || methodName === "define")) {
2004
+ const inner = this.dynamicClassFactoryKind(child);
2005
+ if (inner) return inner;
2006
+ }
2007
+ }
2008
+ if (!receiver) return null;
2009
+ if (methodName !== "new" && methodName !== "define") return null;
2010
+ const leaf = receiver.split("::").filter(Boolean).pop() ?? receiver;
2011
+ if (leaf === "Module" && methodName === "new") return "Module";
2012
+ if ((leaf === "Struct" || leaf === "Data" || leaf === "Class") && (methodName === "new" || methodName === "define")) return "Class";
2013
+ return null;
2014
+ }
2015
+ /**
2016
+ * The constants a scope removes from its public surface with
2017
+ * `private_constant :A, :B`, keyed by the OWNER NAME THE SYMBOL TABLE USES.
2018
+ *
2019
+ * Ruby's constant visibility has its own keyword and the method family does not,
2020
+ * so the lexical `visibility` this file tracks for methods says nothing about a
2021
+ * constant — a `private_constant :VERSION` member would otherwise be published
2022
+ * as part of the export surface, which is precisely the members the language
2023
+ * declares unexported. Measured on the corpus: 142 sites in 73 files.
2024
+ *
2025
+ * It is collected in its own walk rather than in `processCall` because it needs
2026
+ * the ENCLOSING SCOPE, and threading a map through `walkBody` /
2027
+ * `walkMethodBody` / `walkParamDefaults` / `processCall` — eight signatures and
2028
+ * eight call sites — to carry one set is a large diff in a hot file for a small
2029
+ * fact. The scope rule it needs is the same one the main walk uses, so it calls
2030
+ * `rubyReopenIdentity` rather than restating it: a second copy of the reopen
2031
+ * rule is exactly how the two existing copies came to disagree.
2032
+ *
2033
+ * Keyed by the same owner string the export pass reads off `ownerName`, and
2034
+ * missing a scope costs a wrong edge, so the walk descends into every node type
2035
+ * rather than only the ones a declaration can sit under.
2036
+ */
2037
+ collectPrivateConstants(root) {
2038
+ const out = /* @__PURE__ */ new Map();
2039
+ const stack = [{ node: root, owner: void 0 }];
2040
+ while (stack.length > 0) {
2041
+ const { node, owner } = stack.pop();
2042
+ const type = node.type;
2043
+ let nextOwner = owner;
2044
+ if (type === "class" || type === "module" || type === "singleton_class") {
2045
+ const nameNode = node.childForFieldName("name");
2046
+ if (nameNode) nextOwner = rubyReopenIdentity(nameNode.text, owner).name;
2047
+ if (type === "singleton_class") nextOwner = owner;
2048
+ } else if (type === "call") {
2049
+ const methodNode = node.childForFieldName("method");
2050
+ if (methodNode?.text === "private_constant") {
2051
+ const args = this.findChild(node, "argument_list");
2052
+ if (args) {
2053
+ const key = owner ?? FILE_SCOPE_OWNER;
2054
+ let set = out.get(key);
2055
+ if (!set) {
2056
+ set = /* @__PURE__ */ new Set();
2057
+ out.set(key, set);
2058
+ }
2059
+ for (let i = 0; i < args.namedChildCount; i++) {
2060
+ const arg = args.namedChild(i);
2061
+ if (!arg) continue;
2062
+ if (arg.type === "simple_symbol" || arg.type === "string") {
2063
+ const n = this.rubyNameFromLiteral(arg);
2064
+ if (n !== void 0 && n.length > 0) set.add(n);
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2069
+ }
2070
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
2071
+ const child = node.namedChild(i);
2072
+ if (child) stack.push({ node: child, owner: nextOwner });
2073
+ }
2074
+ }
2075
+ return out;
2076
+ }
2077
+ /**
2078
+ * Extract superclass from class node — pure char scan, no regex
2079
+ */
2080
+ /**
2081
+ * The node that NAMES the base type of a `class X < Base` declaration: the
2082
+ * first named child of the `superclass` node, i.e. `Base` as written. The
2083
+ * `superclass` node itself begins at the `<` token, which sits on the previous
2084
+ * line whenever the base is wrapped — so it is the wrong anchor for a line.
2085
+ */
2086
+ superclassTargetNode(node) {
2087
+ const superNode = this.findChild(node, "superclass");
2088
+ if (!superNode || superNode.namedChildCount === 0) return null;
2089
+ return superNode.namedChild(0);
2090
+ }
2091
+ extractSuperclass(node) {
2092
+ const superNode = this.findChild(node, "superclass");
2093
+ if (!superNode) return void 0;
2094
+ const target = this.superclassTargetNode(node);
2095
+ if (target?.text) return target.text.trim();
2096
+ const rawText = superNode.text;
2097
+ let i = rawText.charCodeAt(0) === 60 ? 1 : 0;
2098
+ while (i < rawText.length && isAsciiWhitespace(rawText.charCodeAt(i))) i++;
2099
+ const text = rawText.substring(i).trim();
2100
+ return text.length > 0 ? text : void 0;
2101
+ }
2102
+ /** Extract parameters from method node */
2103
+ extractParameters(node) {
2104
+ const params = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
2105
+ if (!params) return void 0;
2106
+ const result = [];
2107
+ const _nc24 = params.namedChildCount;
2108
+ for (let i = 0; i < _nc24; i++) {
2109
+ const child = params.namedChild(i);
2110
+ if (!child) continue;
2111
+ const childType = child.type;
2112
+ if (childType === "identifier") {
2113
+ result.push({ name: child.text });
2114
+ } else if (childType === "optional_parameter") {
2115
+ const name = child.childForFieldName("name");
2116
+ if (name) result.push({ name: name.text });
2117
+ } else if (childType === "keyword_parameter" || childType === "keyword_rest") {
2118
+ const name = child.childForFieldName("name");
2119
+ if (name) result.push({ name: name.text });
2120
+ } else if (childType === "splat_parameter" || childType === "hash_splat_parameter") {
2121
+ const name = child.childForFieldName("name");
2122
+ if (name) result.push({ name: name.text });
2123
+ } else if (childType === "block_parameter") {
2124
+ const name = child.childForFieldName("name") ?? child.namedChild(0);
2125
+ if (name && name.type === "identifier") result.push({ name: name.text });
2126
+ } else if (childType === "destructured_parameter") {
2127
+ for (let j = 0; j < child.namedChildCount; j++) {
2128
+ const sub = child.namedChild(j);
2129
+ if (sub && sub.type === "identifier") {
2130
+ result.push({ name: sub.text });
2131
+ }
2132
+ }
2133
+ }
2134
+ }
2135
+ return result.length > 0 ? result : void 0;
2136
+ }
2137
+ /** Return true for a bare visibility directive that affects following methods. */
2138
+ isNoArgumentVisibilityMarker(node) {
2139
+ if (node.type === "identifier") {
2140
+ return node.text === "private" || node.text === "protected" || node.text === "public";
2141
+ }
2142
+ if (node.type !== "call") return false;
2143
+ const method = node.childForFieldName("method");
2144
+ if (!method || method.text !== "private" && method.text !== "protected" && method.text !== "public") return false;
2145
+ const args = this.findChild(node, "argument_list");
2146
+ return !args || args.namedChildCount === 0;
2147
+ }
2148
+ /** Per-symbol cyclomatic complexity (managed-OO family contract —
2149
+ * csharp #264 wave precedent). Mirrors the file-level counter's node set
2150
+ * (if/unless/while/until/case/rescue/elsif PLUS the postfix *modifier
2151
+ * forms), scoped to one method/function subtree; base 1.
2152
+ *
2153
+ * Postfix conditionals MUST count: `return false if amount <= 0` is a
2154
+ * genuine branch — semantically identical to the block form, and the most
2155
+ * idiomatic Ruby. tree-sitter-ruby emits a DISTINCT node type
2156
+ * (if_modifier/unless_modifier/while_modifier/until_modifier/
2157
+ * rescue_modifier) for postfix forms; before this fix the analyzer counted
2158
+ * only the block forms, under-reporting every modifier-heavy method
2159
+ * (verified: fixture authorize has 2 if_modifier + 1 while = complexity 4,
2160
+ * previously pinned at the wrong value 2). */
2161
+ calculateNodeComplexity(node) {
2162
+ if (!node) return 1;
2163
+ let complexity = 1;
2164
+ const stack = [node];
2165
+ let guard = 0;
2166
+ while (stack.length > 0 && guard++ < 2e5) {
2167
+ const n = stack.pop();
2168
+ if (n.type === "if" || n.type === "unless" || n.type === "while" || n.type === "until" || n.type === "case" || n.type === "rescue" || n.type === "elsif" || n.type === "if_modifier" || n.type === "unless_modifier" || n.type === "while_modifier" || n.type === "until_modifier" || n.type === "rescue_modifier") {
2169
+ complexity++;
2170
+ }
2171
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
2172
+ const child = n.namedChild(i);
2173
+ if (child) stack.push(child);
2174
+ }
2175
+ }
2176
+ return complexity;
2177
+ }
2178
+ /**
2179
+ * Evidence-tier classification — same rules as the swift/dart/csharp/objc
2180
+ * family (see swift wave #266 for the full rationale).
2181
+ */
2182
+ /**
2183
+ * PROJECT INDEX CONSUMPTION (task-22 phase 2) — facts, never fabrications.
2184
+ *
2185
+ * Behind `BB_RUBY_INDEX_CONSUME=1` ONLY, and FAIL-CLOSED: no context, no index, or `index.complete`
2186
+ * not strictly true ⇒ touches nothing and the file's output is byte-identical to the pre-index run.
2187
+ * What it does, in the pre-committed shape of the phase-1 sizing (4,611-row bound):
2188
+ * · overrides — VERIFY the first-parent guess: base type declared AND the base declares the member
2189
+ * (`typeKinds` + `typeMembers`, NOT counts) ⇒ `metadata.projectIndexVerified`/`ownerEvidence`,
2190
+ * confidence raised to structural; otherwise DECLINED with a reason (the 102 none-base rows stay
2191
+ * declared causes, they are not overwritten).
2192
+ * · instantiates/extends/mixes_in — a static constant path that the index declares gets
2193
+ * `metadata.projectIndexDeclared` (module existence is all mixes_in needs; typeBases is not read).
2194
+ * · calls — NOT TOUCHED. Name-only binding would bind decoys (the ruby D4 lesson); ruby has no
2195
+ * static receiver typing, so the index may not manufacture a call binding.
2196
+ * Declined rows are COUNTED BY REASON in `metadata.projectIndex.declined`, never merged into a result.
2197
+ */
2198
+ applyProjectIndexFacts(relationships, context) {
2199
+ if (process.env.BB_RUBY_INDEX_CONSUME !== "1") return void 0;
2200
+ const index = context?.projectIndex;
2201
+ if (!index || index.complete !== true) return void 0;
2202
+ const leafOf = (name) => {
2203
+ const at = name.lastIndexOf("::");
2204
+ return at >= 0 ? name.slice(at + 2) : name;
2205
+ };
2206
+ const hasType = (name) => index.typeKinds.has(name) || index.typeKinds.has(leafOf(name));
2207
+ const membersOf = (name) => index.typeMembers.get(name) ?? index.typeMembers.get(leafOf(name));
2208
+ let verifiedOverrides = 0;
2209
+ let declaredTargets = 0;
2210
+ const declined = {};
2211
+ const decline = (reason) => {
2212
+ declined[reason] = (declined[reason] ?? 0) + 1;
2213
+ };
2214
+ for (const r of relationships) {
2215
+ const type = String(r.type ?? "");
2216
+ const md = r.metadata ??= {};
2217
+ if (type === "overrides") {
2218
+ const target = String(r.target ?? "");
2219
+ const dot = target.lastIndexOf(".");
2220
+ if (dot <= 0) {
2221
+ decline("overrideTargetUnnameable");
2222
+ continue;
2223
+ }
2224
+ const base = target.slice(0, dot);
2225
+ const method = target.slice(dot + 1);
2226
+ if (!hasType(base)) {
2227
+ decline("baseNotDeclared");
2228
+ continue;
2229
+ }
2230
+ const members = membersOf(base);
2231
+ if (!members || !members.has(method)) {
2232
+ decline("memberNotOnBase");
2233
+ continue;
2234
+ }
2235
+ md.projectIndexVerified = true;
2236
+ md.ownerEvidence = "projectIndex";
2237
+ r.confidence = Math.max(r.confidence ?? 0, 0.95);
2238
+ verifiedOverrides++;
2239
+ } else if (type === "instantiates" || type === "extends" || type === "mixes_in") {
2240
+ if (!hasType(String(r.target ?? ""))) {
2241
+ decline(`${type}:typeNotDeclared`);
2242
+ continue;
2243
+ }
2244
+ md.projectIndexDeclared = true;
2245
+ declaredTargets++;
2246
+ }
2247
+ }
2248
+ return {
2249
+ complete: true,
2250
+ verifiedOverrides,
2251
+ declaredTargets,
2252
+ callsUntouched: true,
2253
+ declined
2254
+ };
2255
+ }
2256
+ classifyEvidenceTier(relationships) {
2257
+ for (const r of relationships) {
2258
+ const type = r.type ?? r.relationshipType ?? "";
2259
+ const md = r.metadata ?? {};
2260
+ if (type === "contains") {
2261
+ md.evidenceClass = "structural";
2262
+ continue;
2263
+ }
2264
+ if (type === "imports" || type === "references") {
2265
+ md.evidenceClass = "import_based";
2266
+ continue;
2267
+ }
2268
+ if (type === "exports") {
2269
+ md.evidenceClass = "structural";
2270
+ continue;
2271
+ }
2272
+ if (type === "calls" || type === "uses") {
2273
+ md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
2274
+ continue;
2275
+ }
2276
+ if (type === "instantiates" || type === "extends" || type === "implements" || type === "mixes_in") {
2277
+ md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
2278
+ continue;
2279
+ }
2280
+ if (type === "overrides") {
2281
+ md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
2282
+ continue;
2283
+ }
2284
+ md.evidenceClass = "usage_contextual";
2285
+ }
2286
+ }
2287
+ /**
2288
+ * THE STATIC SELF-TYPE AT A CALL SITE — ruby's analogue of dart's "declared return type".
2289
+ *
2290
+ * `include`, `extend` and `prepend` are Module's methods, so a call with one of those names
2291
+ * composes a mixin into the ENCLOSING CLASS only if `self` IS that class at that point. The
2292
+ * static self-type is a NODE-TYPE question, never a name question:
2293
+ *
2294
+ * · a class / module / `class << self` body → "class" (`self` IS the class)
2295
+ * · a `Class.new { }` / `Module.new { }` block → "class" (the block is class_eval'd, so
2296
+ * `self` is the NEW class whatever encloses it lexically — and the enclosing call is the
2297
+ * block's PARENT, because the block node's own first child is the first statement of its body)
2298
+ * · a `singleton_method` node (`def self.x`, `def x.y`) → "class" (`self` IS the class)
2299
+ * · a plain `method` node (`def x`) → "instance" (`self` IS an INSTANCE)
2300
+ * · nothing enclosing it → "top" (`self` IS `main`)
2301
+ *
2302
+ * ⇒ MEASURED over the whole admitted population (989 files): of the 543 published `mixes_in` rows,
2303
+ * 416 sit where `self` is the class, 29 in a singleton method, 18 in a plain instance method, and
2304
+ * 80 at the file's top level. **⇒ THE LAST TWO CLASSES ARE IMPOSTORS: 127 rows, 23.4 %.** The 80
2305
+ * top-level rows are RSpec's example-group `include` — the same NAME, a different construct.
2306
+ */
2307
+ mixinSelfKind(node) {
2308
+ let cur = node.parent ?? null;
2309
+ while (cur) {
2310
+ if (cur.type === "method") return "instance";
2311
+ if (cur.type === "singleton_method") return "singleton";
2312
+ if (cur.type === "class" || cur.type === "module" || cur.type === "singleton_class") return "class";
2313
+ if (cur.type === "do_block" || cur.type === "block") {
2314
+ const p = cur.parent;
2315
+ if (p && p.type === "call") {
2316
+ const recv = p.childForFieldName?.("receiver");
2317
+ const meth = p.childForFieldName?.("method");
2318
+ const rt = recv ? recv.text : "";
2319
+ const mt = meth ? meth.text : "";
2320
+ if ((rt === "Class" || rt === "Module") && mt === "new") return "class";
2321
+ }
2322
+ }
2323
+ cur = cur.parent ?? null;
2324
+ }
2325
+ return "top";
2326
+ }
2327
+ findChild(node, type) {
2328
+ const _nc25 = node.namedChildCount;
2329
+ for (let i = 0; i < _nc25; i++) {
2330
+ const child = node.namedChild(i);
2331
+ if (child && child.type === type) return child;
2332
+ }
2333
+ return null;
2334
+ }
2335
+ /** Strip leading ': ' from symbol text — pure char scan */
2336
+ /**
2337
+ * Record a detected framework EXACTLY ONCE, and publish the primary pick (#749).
2338
+ *
2339
+ * Both accumulators are arrays published into the graph, and every caller runs once
2340
+ * per class and once per call, so a bare `.push` published one entry per trigger hit:
2341
+ * measured, `["Ruby on Rails", "Ruby on Rails", …]` five times on a five-model file
2342
+ * and three times on `rack/test.rb`. `metadata.framework` — the contract's
2343
+ * deterministic primary pick — was published for nothing at all, so every file that
2344
+ * DID detect Rails read as framework-less downstream.
2345
+ */
2346
+ recordFramework(metadata, name, pattern) {
2347
+ const detected = metadata.detectedFrameworks ??= [];
2348
+ if (!detected.includes(name)) detected.push(name);
2349
+ metadata.framework ??= name;
2350
+ if (!pattern) return;
2351
+ const patterns = metadata.frameworkPatterns ??= [];
2352
+ if (!patterns.some((p) => p.framework === name)) patterns.push({ framework: name, patterns: [pattern] });
2353
+ }
2354
+ stripSymbolColon(text) {
2355
+ let start = 0;
2356
+ while (start < text.length && text[start] === ":") start++;
2357
+ return start < text.length ? text.substring(start) : text;
2358
+ }
2359
+ /** Strip surrounding quotes — pure char scan */
2360
+ stripQuotes(text) {
2361
+ if (text.length >= 2) {
2362
+ const first = text.charCodeAt(0);
2363
+ const last = text.charCodeAt(text.length - 1);
2364
+ if (first === 34 && last === 34 || first === 39 && last === 39) {
2365
+ return text.substring(1, text.length - 1);
2366
+ }
2367
+ }
2368
+ return text;
2369
+ }
2370
+ /** Extract the leaf name from a possibly-qualified name like A::B */
2371
+ leafName(name) {
2372
+ const idx = name.lastIndexOf("::");
2373
+ return idx >= 0 ? name.substring(idx + 2) : name;
2374
+ }
2375
+ isStaticSuperclassName(text) {
2376
+ return isRubyStaticSuperclassName(text);
2377
+ }
2378
+ isRailsModelName(superClass) {
2379
+ if (!superClass) return false;
2380
+ const leaf = this.leafName(superClass);
2381
+ return leaf === "Base" && superClass.includes("ActiveRecord") || leaf === "ApplicationRecord";
2382
+ }
2383
+ isRailsControllerName(superClass) {
2384
+ if (!superClass) return false;
2385
+ const leaf = this.leafName(superClass);
2386
+ return leaf === "Base" && superClass.includes("ActionController") || leaf === "ApplicationController";
2387
+ }
2388
+ /**
2389
+ * M59.A7 (#825): the 1-based line of the node that NAMES an edge's target.
2390
+ *
2391
+ * The contract is the family's, not this file's: a relationship's `line_number`
2392
+ * is the line where the TARGET is written — the callee for a call, the read for
2393
+ * a member use, the constructed type for an instantiation, the symbol's own
2394
+ * declaration for contains/overrides. It is checked independently by
2395
+ * script/m59/oracle-callsite.ts, which re-derives every edge from the tree and
2396
+ * requires the target token to appear on the claimed line.
2397
+ *
2398
+ * Two shapes are traps and both were measured on the python lane first:
2399
+ * * a call's node starts at its RECEIVER, so `items.strip.upper` stamped the
2400
+ * whole call on the `items` line — hence the method field's node below,
2401
+ * which in this grammar IS the leaf that names the callee;
2402
+ * * `undefined` is a first-class answer. A node that does not exist (a dynamic
2403
+ * mixin, a computed callee) contributes NO line rather than a fabricated 0,
2404
+ * which is the rule `0 means unknown and unknown must be absent` (#820/#821)
2405
+ * applied to this field.
2406
+ */
2407
+ nodeLine(node) {
2408
+ if (!node) return void 0;
2409
+ return node.startPosition.row + 1;
2410
+ }
2411
+ nodeToRange(node) {
2412
+ return {
2413
+ startLine: node.startPosition.row + 1,
2414
+ startColumn: node.startPosition.column + 1,
2415
+ endLine: node.endPosition.row + 1,
2416
+ endColumn: node.endPosition.column + 1,
2417
+ startIndex: node.startIndex,
2418
+ endIndex: node.endIndex
2419
+ };
2420
+ }
2421
+ };
2422
+ export {
2423
+ RAILS_MACRO_METHODS,
2424
+ RUBY_BUILTINS,
2425
+ RealRubyAnalyzer,
2426
+ isRubyIdentifierShaped,
2427
+ isRubyInstanceVariableWrite,
2428
+ isRubyStaticSuperclassName
2429
+ };