@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.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,1731 @@
1
+ import {
2
+ ASTEngine
3
+ } from "./chunk-3ZTOHHSW.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-erlang-analyzer.ts
12
+ var OTP_BEHAVIOURS = /* @__PURE__ */ new Set(["gen_server", "gen_statem", "gen_event", "supervisor", "application"]);
13
+ var SPAWN_BIFS = /* @__PURE__ */ new Set(["spawn", "spawn_link", "spawn_monitor"]);
14
+ var TYPE_SCOPE_NODES = /* @__PURE__ */ new Set([
15
+ "spec",
16
+ "callback",
17
+ "type_alias",
18
+ "opaque",
19
+ "nominal",
20
+ "type_sig",
21
+ "fun_type_sig",
22
+ "ann_type",
23
+ "ann_var",
24
+ "field_type",
25
+ "fun_type",
26
+ "range_type",
27
+ "var_args",
28
+ "type_name",
29
+ "type_guards",
30
+ "pipe"
31
+ ]);
32
+ var ARM_NODES = /* @__PURE__ */ new Set(["cr_clause", "if_clause", "catch_clause", "receive_after"]);
33
+ var COMPREHENSION_NODES = /* @__PURE__ */ new Set(["list_comprehension", "binary_comprehension", "map_comprehension"]);
34
+ var PRE_PASS_DESCEND = /* @__PURE__ */ new Set([
35
+ "pp_if",
36
+ "pp_ifdef",
37
+ "pp_ifndef",
38
+ "pp_else",
39
+ "pp_elif",
40
+ "pp_endif",
41
+ "ERROR"
42
+ ]);
43
+ var MATCH_OPERATORS = /* @__PURE__ */ new Set(["=", "?=", "<-", "<:=", "<="]);
44
+ var DECL_NODES = /* @__PURE__ */ new Set([
45
+ "module_attribute",
46
+ "fun_decl",
47
+ "spec",
48
+ "record_decl",
49
+ "pp_define",
50
+ "type_alias",
51
+ "opaque",
52
+ "nominal",
53
+ "callback"
54
+ ]);
55
+ var DEFAULT_EVIDENCE_CLASS = {
56
+ contains: "structural",
57
+ exports: "structural",
58
+ implements: "structural",
59
+ instantiates: "structural",
60
+ overrides: "structural",
61
+ extends: "structural",
62
+ imports: "import_based",
63
+ references: "reference_qualified",
64
+ calls: "usage_contextual",
65
+ uses: "usage_contextual"
66
+ };
67
+ function evidenceClassFor(type, confidence, target) {
68
+ const t = String(type ?? "");
69
+ if (t === "calls") return target.indexOf(":") !== -1 ? "reference_qualified" : "usage_contextual";
70
+ return DEFAULT_EVIDENCE_CLASS[t] ?? (confidence >= 0.85 ? "structural" : "inferred");
71
+ }
72
+ var RealErlangAnalyzer = class {
73
+ analyze(rootNode, content, filePath) {
74
+ const ctx = {
75
+ filePath,
76
+ symbols: [],
77
+ relationships: [],
78
+ imports: [],
79
+ exports: [],
80
+ errors: [],
81
+ patterns: {},
82
+ seenRels: /* @__PURE__ */ new Set(),
83
+ funDecls: /* @__PURE__ */ new Map(),
84
+ declaredFnKeys: /* @__PURE__ */ new Set(),
85
+ clauseCountByKey: /* @__PURE__ */ new Map(),
86
+ fnBranches: /* @__PURE__ */ new Map(),
87
+ specs: /* @__PURE__ */ new Map(),
88
+ macroAtoms: /* @__PURE__ */ new Map(),
89
+ nifEntries: /* @__PURE__ */ new Map(),
90
+ onLoadEntries: /* @__PURE__ */ new Map(),
91
+ functionSymbolByKey: /* @__PURE__ */ new Map(),
92
+ moduleName: void 0,
93
+ typeDepth: 0,
94
+ macroDepth: 0,
95
+ declStack: [],
96
+ currentFun: void 0,
97
+ totalBranches: 0,
98
+ optionalCallbacks: [],
99
+ moduleSymbol: void 0,
100
+ stats: { dynamicRemote: 0, unresolvedNifs: 0, macroHeadDecls: 0 },
101
+ grammarDamaged: false
102
+ };
103
+ this.collectPrePass(rootNode, ctx);
104
+ const DECLARATION_FORMS = /* @__PURE__ */ new Set([
105
+ "fun_decl",
106
+ "module_attribute",
107
+ "pp_define",
108
+ "record_decl",
109
+ "type_alias",
110
+ "opaque",
111
+ "nominal",
112
+ "import_attribute",
113
+ "export_attribute",
114
+ "export_type_attribute",
115
+ // `-include` / `-include_lib` are declarations too. An include-ONLY header has
116
+ // no symbol to publish but a real dependency to record, so calling it
117
+ // "grammar damaged" would be as false as calling a comment-only header
118
+ // damaged was.
119
+ "pp_include",
120
+ "pp_include_lib"
121
+ ]);
122
+ const hasErrorNodes = rootNode.hasError === true;
123
+ if (hasErrorNodes && !this.prePassForms(rootNode).some((f) => DECLARATION_FORMS.has(f.type))) {
124
+ ctx.grammarDamaged = true;
125
+ }
126
+ const stack = [{ node: rootNode }];
127
+ while (stack.length > 0) {
128
+ const { node, isExit } = stack.pop();
129
+ const type = node.type;
130
+ const named = node.isNamed;
131
+ if (isExit) {
132
+ if (named && TYPE_SCOPE_NODES.has(type)) ctx.typeDepth--;
133
+ if (named && type === "pp_define") ctx.macroDepth--;
134
+ if (named && DECL_NODES.has(type)) ctx.declStack.pop();
135
+ if (named && type === "fun_decl") {
136
+ const done = ctx.currentFun;
137
+ if (done) ctx.fnBranches.set(done.key, (ctx.fnBranches.get(done.key) ?? 0) + done.branches);
138
+ ctx.currentFun = void 0;
139
+ }
140
+ continue;
141
+ }
142
+ let pushScope = false;
143
+ if (named && TYPE_SCOPE_NODES.has(type)) ctx.typeDepth++;
144
+ if (named && type === "pp_define") ctx.macroDepth++;
145
+ if (named && (TYPE_SCOPE_NODES.has(type) || DECL_NODES.has(type) || type === "pp_define")) pushScope = true;
146
+ if (type === "ERROR" || node.isMissing) {
147
+ ctx.errors.push({
148
+ message: `Erlang syntax ${node.isMissing ? "MISSING" : "ERROR"} node.`,
149
+ range: this.nodeToRange(node),
150
+ severity: "warning"
151
+ });
152
+ }
153
+ if (named) {
154
+ switch (type) {
155
+ // ---- module-level declarations ----
156
+ case "module_attribute":
157
+ this.processModuleAttribute(node, ctx, filePath);
158
+ break;
159
+ case "behaviour_attribute":
160
+ this.processBehaviourAttribute(node, ctx, filePath);
161
+ break;
162
+ case "export_attribute":
163
+ this.processFaListAttribute(node, ctx, filePath, ASTEngine.SymbolKind.Function);
164
+ break;
165
+ case "export_type_attribute":
166
+ this.processFaListAttribute(node, ctx, filePath, ASTEngine.SymbolKind.Type);
167
+ break;
168
+ case "import_attribute":
169
+ this.processImportAttribute(node, ctx, filePath);
170
+ break;
171
+ case "optional_callbacks_attribute":
172
+ this.processOptionalCallbacksAttribute(node, ctx);
173
+ break;
174
+ // ---- preprocessor ----
175
+ case "pp_include":
176
+ case "pp_include_lib":
177
+ this.processInclude(node, ctx, type === "pp_include", filePath);
178
+ break;
179
+ case "pp_define":
180
+ this.processDefine(node, ctx);
181
+ break;
182
+ // ---- type declarations: the type graph ----
183
+ case "type_alias":
184
+ case "opaque":
185
+ case "nominal":
186
+ this.processTypeDeclaration(node, ctx, type, filePath);
187
+ break;
188
+ case "callback":
189
+ this.processCallback(node, ctx, filePath);
190
+ break;
191
+ case "spec":
192
+ this.processSpecTypes(node, ctx);
193
+ break;
194
+ case "record_decl":
195
+ this.processRecordDecl(node, ctx, filePath);
196
+ break;
197
+ case "field_type":
198
+ this.processTypeReferences(node, void 0, this.currentSource(ctx) ?? "", ctx);
199
+ break;
200
+ // ---- functions ----
201
+ case "fun_decl":
202
+ this.enterFunDecl(node, ctx, filePath);
203
+ break;
204
+ // ---- evaluation-time edges ----
205
+ case "call":
206
+ this.processCall(node, ctx);
207
+ break;
208
+ case "record_expr":
209
+ this.processRecordExpr(node, ctx);
210
+ break;
211
+ case "record_field_expr":
212
+ case "record_index_expr":
213
+ this.processRecordRead(node, ctx);
214
+ break;
215
+ case "record_update_expr":
216
+ this.processRecordUpdate(node, ctx);
217
+ break;
218
+ case "macro_call_expr":
219
+ this.processMacroUse(node, ctx);
220
+ break;
221
+ case "internal_fun":
222
+ this.processFunReference(node, ctx, false);
223
+ break;
224
+ case "external_fun":
225
+ this.processFunReference(node, ctx, true);
226
+ break;
227
+ // ---- control constructs: patterns + decision points ----
228
+ case "binary_op_expr":
229
+ this.processBinaryOp(node, ctx);
230
+ break;
231
+ case "receive_expr":
232
+ ctx.patterns["Message Reception (receive)"] = true;
233
+ ctx.patterns["Actor Model (OTP)"] = true;
234
+ break;
235
+ case "case_expr":
236
+ case "if_expr":
237
+ case "try_expr":
238
+ case "maybe_expr":
239
+ ctx.patterns["Pattern Matching"] = true;
240
+ break;
241
+ case "anonymous_fun":
242
+ ctx.patterns["Higher-Order Functions"] = true;
243
+ break;
244
+ default:
245
+ if (ARM_NODES.has(type)) this.countBranch(ctx);
246
+ else if (COMPREHENSION_NODES.has(type)) {
247
+ ctx.patterns["Comprehensions"] = true;
248
+ this.countBranch(ctx);
249
+ }
250
+ break;
251
+ }
252
+ }
253
+ if (pushScope) stack.push({ node, isExit: true });
254
+ for (let i = node.childCount - 1; i >= 0; i--) {
255
+ const child = node.child(i);
256
+ if (child) stack.push({ node: child });
257
+ }
258
+ }
259
+ for (const [key, branches] of ctx.fnBranches) {
260
+ const sym = ctx.functionSymbolByKey.get(key);
261
+ if (sym) sym.complexity = 1 + branches;
262
+ }
263
+ this.annotateNativeFunctions(ctx);
264
+ if (ctx.grammarDamaged) {
265
+ return {
266
+ language: "erlang",
267
+ symbols: [],
268
+ relationships: [],
269
+ imports: [],
270
+ exports: [],
271
+ errors: ctx.errors,
272
+ metadata: {
273
+ complexity: 0,
274
+ linesOfCode: content.split("\n").length,
275
+ patterns: [],
276
+ framework: "Erlang",
277
+ hasFrameworkIntelligence: false,
278
+ hasTypeInference: false,
279
+ grammarDamaged: true
280
+ }
281
+ };
282
+ }
283
+ this.emitOwnership(ctx);
284
+ if (ctx.moduleName) {
285
+ ctx.patterns["OTP Module"] = true;
286
+ const ms = ctx.moduleSymbol;
287
+ if (ms && ctx.optionalCallbacks.length > 0) {
288
+ ms.metadata = { ...ms.metadata ?? {}, optionalCallbacks: ctx.optionalCallbacks };
289
+ }
290
+ }
291
+ const pa = [];
292
+ for (const k in ctx.patterns) pa.push(k);
293
+ for (const rel of ctx.relationships) {
294
+ const md = rel.metadata ?? {};
295
+ if (typeof md.evidenceClass !== "string" || md.evidenceClass.length === 0) {
296
+ md.evidenceClass = evidenceClassFor(rel.type, Number(rel.confidence), String(rel.target ?? ""));
297
+ rel.metadata = md;
298
+ }
299
+ }
300
+ return {
301
+ language: "erlang",
302
+ symbols: ctx.symbols,
303
+ relationships: ctx.relationships,
304
+ imports: ctx.imports,
305
+ exports: ctx.exports,
306
+ errors: ctx.errors,
307
+ metadata: {
308
+ complexity: 1 + ctx.totalBranches,
309
+ linesOfCode: content.split("\n").length,
310
+ patterns: pa,
311
+ framework: "Erlang",
312
+ hasFrameworkIntelligence: true,
313
+ hasTypeInference: false
314
+ }
315
+ };
316
+ }
317
+ // ==========================================================================
318
+ // Pre-pass
319
+ // ==========================================================================
320
+ /**
321
+ * ER-D29: every DECLARATION is a direct child of `source_file`. Measured over
322
+ * the full 4,326-file census: module_attribute, behaviour_attribute,
323
+ * export_attribute, export_type_attribute, import_attribute,
324
+ * optional_callbacks_attribute, pp_include, pp_include_lib, pp_define,
325
+ * type_alias, opaque, nominal, spec, callback, record_decl — every one of them
326
+ * has `source_file` as its ONLY parent (the census's other "parent" entries are
327
+ * the anonymous keyword token that shares the type's name, e.g. the `spec`
328
+ * inside a `spec`).
329
+ *
330
+ * So the pre-pass reads the top level instead of walking all ~4,000 nodes per
331
+ * file again. The one place a declaration can hide is inside conditional
332
+ * compilation (`-ifdef … -endif`), whose bodies are top-level forms too, so
333
+ * those containers are descended into explicitly.
334
+ */
335
+ collectPrePass(rootNode, ctx) {
336
+ for (const form of this.prePassForms(rootNode)) {
337
+ if (form.type !== "pp_define") continue;
338
+ const lhs = this.getChildByType(form, "macro_lhs");
339
+ const nameNode = lhs ? this.getChildByType(lhs, "var") ?? this.getChildByType(lhs, "atom") : void 0;
340
+ if (!nameNode?.text) continue;
341
+ for (let i = 0; i < form.namedChildCount; i++) {
342
+ const body = form.namedChild(i);
343
+ if (!body || body.type !== "atom" || !body.text) continue;
344
+ ctx.macroAtoms.set(nameNode.text, body.text);
345
+ break;
346
+ }
347
+ }
348
+ for (const top of this.prePassForms(rootNode)) {
349
+ if (top.type !== "module_attribute") continue;
350
+ const atom = this.getChildByType(top, "atom");
351
+ if (atom?.text) {
352
+ ctx.moduleName = atom.text;
353
+ break;
354
+ }
355
+ const expanded = this.expandModuleNameMacro(top, ctx);
356
+ if (expanded) {
357
+ ctx.moduleName = expanded;
358
+ break;
359
+ }
360
+ }
361
+ const forms = this.prePassForms(rootNode);
362
+ const stack = [];
363
+ for (let i = forms.length - 1; i >= 0; i--) {
364
+ const top = forms[i];
365
+ if (top) stack.push(top);
366
+ }
367
+ while (stack.length > 0) {
368
+ const node = stack.pop();
369
+ if (!node.isNamed) continue;
370
+ const type = node.type;
371
+ if (type === "fun_decl") {
372
+ const clauses = this.collectChildrenByType(node, "function_clause");
373
+ const head = this.readClauseHead(clauses[0], ctx.moduleName);
374
+ if (head) {
375
+ ctx.funDecls.set(node.startIndex, {
376
+ name: head.name,
377
+ arity: head.arity,
378
+ clauseCount: clauses.length,
379
+ macroHead: head.macroHead,
380
+ startIndex: node.startIndex
381
+ });
382
+ const key = this.fnKey(head.name, head.arity);
383
+ ctx.declaredFnKeys.add(key);
384
+ ctx.clauseCountByKey.set(key, (ctx.clauseCountByKey.get(key) ?? 0) + clauses.length);
385
+ if (head.macroHead) ctx.stats.macroHeadDecls++;
386
+ }
387
+ } else if (type === "spec") {
388
+ const nameNode = this.getChildByType(node, "atom");
389
+ const sig = this.getChildByType(node, "type_sig");
390
+ if (nameNode?.text && sig) {
391
+ const params = this.namedChildrenTexts(this.getChildByType(sig, "expr_args"));
392
+ ctx.specs.set(this.fnKey(nameNode.text, params.length), {
393
+ params,
394
+ ret: this.typeSigReturnText(sig),
395
+ line: nameNode.startPosition.row + 1
396
+ });
397
+ }
398
+ } else if (type === "wild_attribute") {
399
+ const attrName = this.attrNameText(node);
400
+ if (attrName === "nifs") this.collectAttributeFaEntries(node, ctx.nifEntries, ctx);
401
+ else if (attrName === "on_load") this.collectAttributeFaEntries(node, ctx.onLoadEntries, ctx);
402
+ } else if (PRE_PASS_DESCEND.has(type)) {
403
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
404
+ const c = node.namedChild(i);
405
+ if (c) stack.push(c);
406
+ }
407
+ }
408
+ }
409
+ }
410
+ /**
411
+ * The forms the pre-pass reads: `source_file`'s named children, with any
412
+ * conditional-compilation or grammar-ERROR container's children spliced in
413
+ * (ER-D29). Every DECLARATION lives here — measured over the 4,326-file
414
+ * census — so the pre-pass costs O(declarations) instead of a second walk of
415
+ * all ~4,000 nodes per file.
416
+ */
417
+ prePassForms(rootNode) {
418
+ const out = [];
419
+ const stack = [];
420
+ for (let i = rootNode.namedChildCount - 1; i >= 0; i--) {
421
+ const c = rootNode.namedChild(i);
422
+ if (c) stack.push(c);
423
+ }
424
+ while (stack.length > 0) {
425
+ const n = stack.pop();
426
+ if (!n.isNamed) continue;
427
+ if (PRE_PASS_DESCEND.has(n.type)) {
428
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
429
+ const c = n.namedChild(i);
430
+ if (c) stack.push(c);
431
+ }
432
+ continue;
433
+ }
434
+ out.push(n);
435
+ }
436
+ return out;
437
+ }
438
+ fnKey(name, arity) {
439
+ return `${name}/${arity}`;
440
+ }
441
+ /**
442
+ * The inverse of `fnKey`, for the two places that must go back from a collected
443
+ * "name/arity" entry to a symbol. Erlang function names cannot contain `/`, so
444
+ * the LAST slash is the separator and a name like `mod/1` never appears here.
445
+ */
446
+ splitFnKey(key) {
447
+ const i = key.lastIndexOf("/");
448
+ return i === -1 ? [key, ""] : [key.slice(0, i), key.slice(i + 1)];
449
+ }
450
+ /**
451
+ * A clause head's declared name and arity.
452
+ *
453
+ * ER-D25: the name is normally the head's `atom`, but a MACRO-headed
454
+ * definition carries a `macro_call_expr` instead, and the real function name
455
+ * only exists AFTER the preprocessor expands it. Measured over the whole
456
+ * corpus, every one of the 79 such clauses is `?MODULE() -> …` — the OTP test
457
+ * suite idiom — and `?MODULE` is a PREDEFINED macro whose value is this file's
458
+ * module atom, so the expansion is exact: the function is `<module>/<arity>`.
459
+ * That is the OTP `yes_2.erl` / `stacktrace.erl` shape, and resolving it
460
+ * recovers 79 function definitions that previously produced NO symbol at all
461
+ * (the "unreadable clause name" the pre-ER3 oracle had to mirror).
462
+ *
463
+ * Any OTHER macro head is refused on purpose. `-define(DOUBLE(X), helper(X))`
464
+ * plus `?DOUBLE(X) -> X` declares `helper/1`, not `DOUBLE/1`: naming the macro
465
+ * would FABRICATE a function that does not exist, and the analyzer does not
466
+ * expand macros. The clause's body is then attributed to the module, and the
467
+ * parse is reported by the error channel.
468
+ */
469
+ readClauseHead(clause, moduleName) {
470
+ if (!clause) return void 0;
471
+ const arity = this.countArgs(this.getChildByType(clause, "expr_args"));
472
+ const direct = this.getChildByType(clause, "atom") ?? this.getChildByType(clause, "var");
473
+ if (direct?.text) return { name: direct.text, arity, macroHead: false };
474
+ const macro = this.getChildByType(clause, "macro_call_expr");
475
+ const macroName = macro ? (this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom"))?.text : void 0;
476
+ if (macroName === "MODULE" && moduleName) return { name: moduleName, arity, macroHead: true };
477
+ return void 0;
478
+ }
479
+ /**
480
+ * A parameter list's arity: the named children of `expr_args`, comments
481
+ * excluded. ONE counter serves a clause head, a call site and a `fa`, so
482
+ * `f/1` and `f/2` can never be confused anywhere in this analyzer (ER-D2).
483
+ */
484
+ countArgs(args) {
485
+ if (!args) return 0;
486
+ let n = 0;
487
+ for (let i = 0; i < args.namedChildCount; i++) {
488
+ const c = args.namedChild(i);
489
+ if (c && c.type !== "comment") n++;
490
+ }
491
+ return n;
492
+ }
493
+ namedChildrenTexts(args) {
494
+ if (!args) return [];
495
+ const out = [];
496
+ for (let i = 0; i < args.namedChildCount; i++) {
497
+ const c = args.namedChild(i);
498
+ if (c && c.type !== "comment") out.push(c.text);
499
+ }
500
+ return out;
501
+ }
502
+ /** The text after a type signature's `->`, or undefined when it has none. */
503
+ typeSigReturnText(sig) {
504
+ for (let i = 0; i < sig.childCount; i++) {
505
+ const c = sig.child(i);
506
+ if (c?.type !== "->") continue;
507
+ const t = sig.text.slice(c.endIndex - sig.startIndex).trim();
508
+ return t.length > 0 ? t : void 0;
509
+ }
510
+ return void 0;
511
+ }
512
+ // ==========================================================================
513
+ // Module-level declarations
514
+ // ==========================================================================
515
+ /**
516
+ * The module name a `-module(?MACRO)` attribute expands to, or undefined when
517
+ * it cannot be known without running the preprocessor.
518
+ *
519
+ * ER-D34. Common Test suites name themselves through a macro by convention, so
520
+ * this is not exotic: otp/lib/kernel/test/file_SUITE.erl writes
521
+ * `-module(?FILE_SUITE).` and is the only such file in the 4,326-file corpus
522
+ * — rare, but the failure was silent and total. The attribute parsed as a
523
+ * module_attribute with a macro_call_expr child, and a reader that required an
524
+ * `atom` emitted no module symbol at all, so the file fell back to being owned
525
+ * by its path: wrong owner on every edge, no module in the store, exports
526
+ * unmatched.
527
+ */
528
+ expandModuleNameMacro(attr, ctx) {
529
+ const macro = this.getChildByType(attr, "macro_call_expr") ?? this.getChildByType(attr, "macro_expr");
530
+ if (!macro) return void 0;
531
+ const nameNode = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
532
+ if (!nameNode?.text) return void 0;
533
+ return ctx.macroAtoms.get(nameNode.text);
534
+ }
535
+ processModuleAttribute(node, ctx, filePath) {
536
+ if (ctx.moduleSymbol) return;
537
+ let nameNode = this.getChildByType(node, "atom");
538
+ let moduleNameOverride;
539
+ if (!nameNode?.text) {
540
+ const macro = this.getChildByType(node, "macro_call_expr") ?? this.getChildByType(node, "macro_expr");
541
+ const macroName = macro ? this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom") : void 0;
542
+ const expanded = this.expandModuleNameMacro(node, ctx);
543
+ if (!macroName?.text) return;
544
+ const source = expanded ?? this.ownerNameOf(ctx) ?? node.text;
545
+ this.pushRel(
546
+ ctx,
547
+ source,
548
+ macroName.text,
549
+ ASTEngine.RelationshipType.References,
550
+ 0.9,
551
+ {
552
+ erlangSpecific: true,
553
+ context: "macro use",
554
+ isModuleNameMacro: true,
555
+ moduleNameResolved: !!expanded
556
+ },
557
+ macroName.startPosition.row + 1
558
+ );
559
+ if (!expanded) return;
560
+ moduleNameOverride = expanded;
561
+ nameNode = macroName;
562
+ }
563
+ const moduleName = moduleNameOverride ?? nameNode.text;
564
+ const symbol = {
565
+ name: moduleName,
566
+ kind: ASTEngine.SymbolKind.Module,
567
+ range: this.nodeToRange(node),
568
+ metadata: {
569
+ nodeType: "module_attribute",
570
+ line: nameNode.startPosition.row + 1,
571
+ ...moduleNameOverride ? { nameFromMacro: nameNode.text, expandsTo: moduleNameOverride } : {}
572
+ }
573
+ };
574
+ ctx.moduleSymbol = symbol;
575
+ ctx.symbols.push(symbol);
576
+ ctx.moduleName = moduleName;
577
+ ctx.declStack.push({ source: moduleName, kind: "module" });
578
+ void filePath;
579
+ }
580
+ processBehaviourAttribute(node, ctx, filePath) {
581
+ const beNode = this.getChildByType(node, "atom");
582
+ if (!beNode?.text) return;
583
+ this.pushRel(
584
+ ctx,
585
+ ctx.moduleName ?? filePath,
586
+ beNode.text,
587
+ ASTEngine.RelationshipType.Implements,
588
+ 1,
589
+ { erlangSpecific: true, behaviour: beNode.text, context: "OTP behaviour callback implementation" },
590
+ beNode.startPosition.row + 1
591
+ );
592
+ if (OTP_BEHAVIOURS.has(beNode.text)) {
593
+ ctx.patterns["Actor Model (OTP)"] = true;
594
+ ctx.patterns["OTP Behaviours"] = true;
595
+ }
596
+ }
597
+ /** `-export` / `-export_type`: a list of `fa` nodes. */
598
+ processFaListAttribute(node, ctx, filePath, kind) {
599
+ const owner = ctx.moduleName ?? filePath;
600
+ for (let i = 0; i < node.namedChildCount; i++) {
601
+ const child = node.namedChild(i);
602
+ if (child?.type !== "fa") continue;
603
+ const fa = this.readFa(child, ctx.moduleName);
604
+ if (!fa) continue;
605
+ ctx.exports.push({
606
+ name: fa.name,
607
+ arity: fa.arity,
608
+ file: filePath,
609
+ line: fa.line,
610
+ kind,
611
+ ...kind === ASTEngine.SymbolKind.Type ? { metadata: { isTypeExport: true } } : {}
612
+ });
613
+ this.pushRel(
614
+ ctx,
615
+ owner,
616
+ fa.name,
617
+ ASTEngine.RelationshipType.Exports,
618
+ 1,
619
+ {
620
+ erlangSpecific: true,
621
+ arity: fa.arity,
622
+ exportForm: kind === ASTEngine.SymbolKind.Type ? "export_type" : "export",
623
+ isTypeExport: kind === ASTEngine.SymbolKind.Type
624
+ },
625
+ fa.line
626
+ );
627
+ }
628
+ }
629
+ processImportAttribute(node, ctx, filePath) {
630
+ const modNode = this.getChildByType(node, "atom");
631
+ if (!modNode?.text) return;
632
+ const funcs = [];
633
+ for (let i = 0; i < node.namedChildCount; i++) {
634
+ const child = node.namedChild(i);
635
+ if (child?.type !== "fa") continue;
636
+ const fa = this.readFa(child, ctx.moduleName);
637
+ if (fa) funcs.push(fa.name);
638
+ }
639
+ ctx.imports.push({
640
+ module: modNode.text,
641
+ path: modNode.text,
642
+ importType: "named",
643
+ // ER-D20: `-import(lists, …)` names an OTP module, which by construction
644
+ // cannot be declared in this file — M60/#910's `is_external` contract,
645
+ // which the file-dependency engine reads to keep system dependencies out
646
+ // of internal resolution. The old code wrote no classification at all.
647
+ isExternal: true,
648
+ importedNames: funcs,
649
+ items: funcs,
650
+ file: filePath,
651
+ line: modNode.startPosition.row + 1,
652
+ isNif: false
653
+ });
654
+ this.pushRel(
655
+ ctx,
656
+ ctx.moduleName ?? filePath,
657
+ modNode.text,
658
+ ASTEngine.RelationshipType.Imports,
659
+ 1,
660
+ { erlangSpecific: true, isModuleImport: true, importedFunctions: funcs, isExternal: true },
661
+ modNode.startPosition.row + 1
662
+ );
663
+ this.pushRel(
664
+ ctx,
665
+ ctx.moduleName ?? filePath,
666
+ modNode.text,
667
+ ASTEngine.RelationshipType.References,
668
+ 1,
669
+ { erlangSpecific: true, isModuleImport: true, importedFunctions: funcs },
670
+ modNode.startPosition.row + 1
671
+ );
672
+ }
673
+ /**
674
+ * `-optional_callbacks([f/2, …])` names callbacks a behaviour MAY omit. It
675
+ * declares nothing, so no symbol and no edge; the names are recorded on the
676
+ * module symbol, which is the only place they are true.
677
+ */
678
+ processOptionalCallbacksAttribute(node, ctx) {
679
+ for (let i = 0; i < node.namedChildCount; i++) {
680
+ const child = node.namedChild(i);
681
+ if (child?.type !== "fa") continue;
682
+ const fa = this.readFa(child, ctx.moduleName);
683
+ if (fa) ctx.optionalCallbacks.push(this.fnKey(fa.name, fa.arity));
684
+ }
685
+ }
686
+ // ==========================================================================
687
+ // Preprocessor
688
+ // ==========================================================================
689
+ /**
690
+ * `-include("x.hrl")` / `-include_lib("kernel/include/x.hrl")`.
691
+ *
692
+ * ER-D46 (2026-10-23): THE LABEL IS `part_of`, NOT `references`. An include is
693
+ * FILE INCLUSION: the header is textually spliced into THIS module's single
694
+ * compilation scope, it cannot be compiled as a unit of its own, and it
695
+ * imports nothing — which is the vocabulary's `part_of` contract
696
+ * (`src/ast/types.ts:PartOf`: "a library unit INCLUDES a part file … one
697
+ * compilation unit"), the same construct php's include/require and fsharp's
698
+ * `#load` were relabelled to. `references` said the path was MENTIONED; it
699
+ * cannot say the file became part of this unit.
700
+ *
701
+ * DIRECTION AND ROLE follow the directive's own: source = the file carrying
702
+ * the directive (its module symbol, else its path — ER-D32); target = the
703
+ * path as written. `metadata.partRole:"library"` because THIS file is the
704
+ * including library and names the part. `isExternal` keeps ER-D20's
705
+ * classification: `-include_lib` resolves through the code path (a system
706
+ * header), `-include` is this project's.
707
+ *
708
+ * NOT `imports`: `-import(Mod, [f/a])` stays imports + references (ER-D41) —
709
+ * a name import creates binding scope, an include splices text. The two
710
+ * constructs were distinguished by the fixture `imports-and-try` before this
711
+ * change and are pinned apart by the `extended-part-of` fixture after it.
712
+ *
713
+ * THE TIER FOLLOWS THE CONSTRUCT, as php's `part_of` relabel did: a file
714
+ * inclusion directive is `import_based`, confidence 1.0 (the directive IS the
715
+ * syntax) — not the old row's 0.9/`reference_qualified`, which was the
716
+ * mention reading the relabel exists to replace.
717
+ *
718
+ * M59 (#825): the line belongs to the NAME-BEARING node — the path STRING,
719
+ * never the `-include` keyword. The fixture erlang/callsite-lines-825 splits
720
+ * the attribute across three lines precisely to pin that difference.
721
+ */
722
+ processInclude(node, ctx, isPlain, filePath) {
723
+ const fNode = this.getChildByType(node, "string");
724
+ if (!fNode) return;
725
+ const includePath = this.stripQuotes(fNode.text);
726
+ const line = fNode.startPosition.row + 1;
727
+ ctx.imports.push({
728
+ module: includePath,
729
+ path: includePath,
730
+ importType: isPlain ? "include" : "include_lib",
731
+ // ER-D20: `-include_lib("kernel/include/…")` resolves through the code
732
+ // path — a system dependency. `-include("local.hrl")` is this project's.
733
+ isExternal: !isPlain,
734
+ file: includePath,
735
+ line,
736
+ isNif: false
737
+ });
738
+ this.pushRel(
739
+ ctx,
740
+ ctx.moduleName ?? filePath,
741
+ includePath,
742
+ ASTEngine.RelationshipType.PartOf,
743
+ 1,
744
+ {
745
+ erlangSpecific: true,
746
+ // THE MANDATORY ROLE FIELD (part_of's contract): this file carries the
747
+ // directive and names the part, so it is the library side of the pair.
748
+ partRole: "library",
749
+ isInclude: isPlain,
750
+ isLibInclude: !isPlain,
751
+ // A PATH TARGET, not a symbol: the include names a file. Consumers that
752
+ // resolve names must not read it as one, and the declaration's
753
+ // resolution caveat relies on this marker.
754
+ isFileInclusion: true,
755
+ // The tier follows the construct (ER-D46) — see the doc block above.
756
+ evidenceClass: "import_based"
757
+ },
758
+ line
759
+ );
760
+ }
761
+ processDefine(node, ctx) {
762
+ const lhs = this.getChildByType(node, "macro_lhs");
763
+ if (!lhs) return;
764
+ const nameNode = this.getChildByType(lhs, "var") ?? this.getChildByType(lhs, "atom");
765
+ if (!nameNode?.text) return;
766
+ const params = this.getChildByType(lhs, "var_args");
767
+ const isFunctionLike = params !== void 0;
768
+ ctx.symbols.push({
769
+ name: nameNode.text,
770
+ kind: isFunctionLike ? ASTEngine.SymbolKind.Macro : ASTEngine.SymbolKind.Constant,
771
+ range: this.nodeToRange(node),
772
+ ownerName: this.ownerNameOf(ctx),
773
+ ownerKind: ASTEngine.SymbolKind.Module,
774
+ metadata: {
775
+ nodeType: "macro_definition",
776
+ isMacro: true,
777
+ isFunctionLike,
778
+ isFunctionLikeMacro: isFunctionLike,
779
+ parameterCount: this.countArgs(params),
780
+ line: nameNode.startPosition.row + 1
781
+ }
782
+ });
783
+ ctx.declStack.push({ source: nameNode.text, kind: "macro" });
784
+ }
785
+ // ==========================================================================
786
+ // Types
787
+ // ==========================================================================
788
+ processTypeDeclaration(node, ctx, type, filePath) {
789
+ const typeNameNode = this.getChildByType(node, "type_name");
790
+ const nameNode = typeNameNode ? this.getChildByType(typeNameNode, "atom") : this.getChildByType(node, "atom");
791
+ const typeName = nameNode?.text ?? (typeNameNode ? this.stripFirstParen(typeNameNode.text) : void 0);
792
+ if (!typeName) return;
793
+ const params = typeNameNode ? this.getChildByType(typeNameNode, "var_args") : void 0;
794
+ ctx.symbols.push({
795
+ name: typeName,
796
+ kind: ASTEngine.SymbolKind.Type,
797
+ range: this.nodeToRange(node),
798
+ ownerName: this.ownerNameOf(ctx),
799
+ ownerKind: ASTEngine.SymbolKind.Module,
800
+ metadata: {
801
+ nodeType: type,
802
+ isOpaque: type === "opaque",
803
+ isNominal: type === "nominal",
804
+ arity: this.countArgs(params),
805
+ line: (nameNode ?? typeNameNode ?? node).startPosition.row + 1
806
+ }
807
+ });
808
+ ctx.declStack.push({ source: typeName, kind: "type" });
809
+ this.processTypeReferences(node, typeNameNode, typeName, ctx);
810
+ }
811
+ /**
812
+ * ER-G3: a behaviour's callback contract is a Method owned by the module.
813
+ * The arity is the type signature's parameter count. A behaviour that also
814
+ * DEFINES its callback (`-callback init/1` plus `init/1 -> …`) yields ONE
815
+ * symbol: both declare the same (name, arity) identity.
816
+ */
817
+ processCallback(node, ctx, filePath) {
818
+ const nameNode = this.getChildByType(node, "atom");
819
+ if (!nameNode?.text) return;
820
+ const sig = this.getChildByType(node, "type_sig");
821
+ const key = this.fnKey(nameNode.text, this.countArgs(sig ? this.getChildByType(sig, "expr_args") : void 0));
822
+ const existing = ctx.functionSymbolByKey.get(key);
823
+ if (existing) {
824
+ existing.metadata = {
825
+ ...existing.metadata ?? {},
826
+ isCallback: true,
827
+ callbackLine: nameNode.startPosition.row + 1
828
+ };
829
+ } else {
830
+ const symbol = {
831
+ name: nameNode.text,
832
+ kind: ASTEngine.SymbolKind.Method,
833
+ range: this.nodeToRange(node),
834
+ ownerName: this.ownerNameOf(ctx),
835
+ ownerKind: ASTEngine.SymbolKind.Module,
836
+ metadata: {
837
+ nodeType: "callback",
838
+ isCallback: true,
839
+ arity: this.countArgs(sig ? this.getChildByType(sig, "expr_args") : void 0),
840
+ line: nameNode.startPosition.row + 1
841
+ }
842
+ };
843
+ ctx.functionSymbolByKey.set(key, symbol);
844
+ ctx.symbols.push(symbol);
845
+ }
846
+ this.processTypeReferences(node, nameNode, nameNode.text, ctx);
847
+ ctx.declStack.push({ source: nameNode.text, kind: "callback" });
848
+ }
849
+ processSpecTypes(node, ctx) {
850
+ const nameNode = this.getChildByType(node, "atom");
851
+ if (!nameNode?.text) return;
852
+ this.processTypeReferences(node, nameNode, nameNode.text, ctx);
853
+ ctx.declStack.push({ source: nameNode.text, kind: "spec" });
854
+ }
855
+ /**
856
+ * A record is the struct analog: one Struct symbol owned by the module, one
857
+ * Property per field owned by the record. Field DEFAULTS are EVALUATIONS and
858
+ * field TYPES are TYPE references, both attributed to the RECORD (ER-D12) —
859
+ * `-record(state, {t = erlang:make_ref()})` calls a function from the record's
860
+ * own declaration, and `-record(r, {f :: pid()})` names a type from it.
861
+ */
862
+ processRecordDecl(node, ctx, filePath) {
863
+ const nameNode = this.getChildByType(node, "atom");
864
+ if (!nameNode?.text) return;
865
+ const recordName = nameNode.text;
866
+ ctx.declStack.push({ source: recordName, kind: "record" });
867
+ const fields = [];
868
+ for (let i = 0; i < node.namedChildCount; i++) {
869
+ const child = node.namedChild(i);
870
+ if (child?.type !== "record_field") continue;
871
+ const fieldName = this.getChildByType(child, "atom");
872
+ if (!fieldName?.text) continue;
873
+ fields.push(fieldName.text);
874
+ const fieldType = this.getChildByType(child, "field_type");
875
+ const defaultExpr = this.getChildByType(child, "field_expr");
876
+ ctx.symbols.push({
877
+ name: fieldName.text,
878
+ kind: ASTEngine.SymbolKind.Property,
879
+ range: this.nodeToRange(child),
880
+ ownerName: recordName,
881
+ ownerKind: ASTEngine.SymbolKind.Struct,
882
+ metadata: {
883
+ nodeType: "record_field",
884
+ isRecordField: true,
885
+ line: fieldName.startPosition.row + 1,
886
+ ...defaultExpr ? { defaultValue: defaultExpr.text } : {},
887
+ ...fieldType ? { fieldType: fieldType.text } : {}
888
+ }
889
+ });
890
+ if (fieldType) this.processTypeReferences(fieldType, void 0, recordName, ctx);
891
+ }
892
+ ctx.symbols.push({
893
+ name: recordName,
894
+ kind: ASTEngine.SymbolKind.Struct,
895
+ range: this.nodeToRange(node),
896
+ ownerName: this.ownerNameOf(ctx),
897
+ ownerKind: ASTEngine.SymbolKind.Module,
898
+ metadata: { nodeType: "record_decl", fields, line: nameNode.startPosition.row + 1 }
899
+ });
900
+ }
901
+ // ==========================================================================
902
+ // Functions
903
+ // ==========================================================================
904
+ enterFunDecl(node, ctx, filePath) {
905
+ const info = ctx.funDecls.get(node.startIndex);
906
+ if (!info) {
907
+ ctx.declStack.push({ source: ctx.moduleName ?? filePath, kind: "module" });
908
+ return;
909
+ }
910
+ const name = info.name;
911
+ const arity = info.arity;
912
+ const key = this.fnKey(name, arity);
913
+ const clause = this.collectChildrenByType(node, "function_clause")[0];
914
+ let symbol = ctx.functionSymbolByKey.get(key);
915
+ if (symbol) {
916
+ this.extendRange(symbol, node);
917
+ if (symbol.kind === ASTEngine.SymbolKind.Method && symbol.metadata?.isCallback) {
918
+ symbol.kind = ASTEngine.SymbolKind.Function;
919
+ symbol.metadata = {
920
+ ...symbol.metadata ?? {},
921
+ definedHere: true,
922
+ ...clause ? { definitionLine: clause.startPosition.row + 1, line: clause.startPosition.row + 1 } : {}
923
+ };
924
+ if (clause) {
925
+ const r = this.nodeToRange(clause);
926
+ symbol.range.startLine = r.startLine;
927
+ symbol.range.startColumn = r.startColumn;
928
+ if (symbol.range.startIndex !== void 0 && r.startIndex !== void 0) {
929
+ symbol.range.startIndex = r.startIndex;
930
+ }
931
+ }
932
+ }
933
+ } else {
934
+ symbol = this.makeFunctionSymbol(ctx, name, arity, clause, node, info, filePath);
935
+ ctx.functionSymbolByKey.set(key, symbol);
936
+ ctx.symbols.push(symbol);
937
+ }
938
+ ctx.currentFun = { name, key, symbol, branches: 0 };
939
+ const extra = (ctx.clauseCountByKey.get(key) ?? 1) - 1;
940
+ for (let i = 0; i < extra; i++) this.countBranch(ctx);
941
+ ctx.declStack.push({ source: name, kind: "function" });
942
+ }
943
+ extendRange(symbol, node) {
944
+ const r = this.nodeToRange(node);
945
+ symbol.range.endLine = Math.max(symbol.range.endLine, r.endLine);
946
+ symbol.range.endColumn = Math.max(symbol.range.endColumn, r.endColumn);
947
+ if (symbol.range.endIndex !== void 0 && r.endIndex !== void 0) {
948
+ symbol.range.endIndex = Math.max(symbol.range.endIndex, r.endIndex);
949
+ }
950
+ }
951
+ /**
952
+ * The name of the macro sitting in a remote-module position, or undefined when
953
+ * the module is a plain atom.
954
+ *
955
+ * ER-D25 vs ER-D35. `?MODULE:f(X)` is the idiomatic self-dispatch and resolves
956
+ * to this file's module. `?ANYTHING_ELSE:f(X)` does not: the macro is an
957
+ * ordinary build-time constant whose value this analyzer does not expand, and
958
+ * treating it as `?MODULE` fabricates a self-call. The distinction is the
959
+ * macro's NAME, and the full-corpus draw is what forced it — otp's
960
+ * `file_SUITE.erl` writes `?FILE_MODULE:read_file_info(Priv)`, and before this
961
+ * check every one of those calls was reported as a call into its own module.
962
+ */
963
+ remoteMacroModuleName(remoteModule) {
964
+ if (!remoteModule) return void 0;
965
+ const macro = this.getChildByType(remoteModule, "macro_call_expr") ?? this.getChildByType(remoteModule, "macro_expr");
966
+ if (!macro) return void 0;
967
+ const nameNode = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
968
+ return nameNode?.text;
969
+ }
970
+ makeFunctionSymbol(ctx, name, arity, clause, declNode, info, filePath) {
971
+ const key = this.fnKey(name, arity);
972
+ const spec = ctx.specs.get(key);
973
+ const params = this.namedChildrenOf(
974
+ clause ? this.getChildByType(clause, "expr_args") : void 0
975
+ ).map((c, i) => ({
976
+ // ER-D21: a clause head is a PATTERN, not a declaration. The binding's
977
+ // name is carried when the pattern is a plain variable; the pattern itself
978
+ // is always carried, because that is what the head really says.
979
+ name: c.type === "var" ? c.text : "",
980
+ pattern: c.text,
981
+ ...spec?.params[i] ? { type: spec.params[i] } : {}
982
+ }));
983
+ return {
984
+ name,
985
+ kind: ASTEngine.SymbolKind.Function,
986
+ range: this.nodeToRange(declNode),
987
+ ownerName: this.ownerNameOf(ctx),
988
+ ownerKind: ASTEngine.SymbolKind.Module,
989
+ parameters: params,
990
+ ...spec?.ret ? { returnType: spec.ret } : {},
991
+ complexity: 1,
992
+ metadata: {
993
+ nodeType: "function_clause",
994
+ arity,
995
+ clauseCount: ctx.clauseCountByKey.get(key) ?? 1,
996
+ ...clause ? { line: clause.startPosition.row + 1 } : {},
997
+ ...info?.macroHead ? { isMacroHead: true } : {},
998
+ ...spec ? { hasSpec: true, specLine: spec.line } : {}
999
+ }
1000
+ };
1001
+ }
1002
+ /**
1003
+ * ER-D22: `-nifs([f/1])` and `-on_load([g/0])` are ATTRIBUTES about functions,
1004
+ * not declarations. The old code minted a second Function symbol per entry —
1005
+ * and could not have: `attr_name.text` is `-nifs` (the dash is INSIDE the
1006
+ * node), so its `=== "nifs"` test never matched, and the entries are
1007
+ * `binary_op_expr` (`f/1`), not the `fa` the branch then looked for. Two
1008
+ * independent defects hid the same behaviour. The entries now annotate the
1009
+ * declared function, which is the honest surface: a NIF always has an
1010
+ * Erlang-visible stub.
1011
+ */
1012
+ annotateNativeFunctions(ctx) {
1013
+ const owner = this.ownerNameOf(ctx);
1014
+ for (const [key, line] of ctx.nifEntries) {
1015
+ const sym = ctx.functionSymbolByKey.get(key);
1016
+ if (sym) {
1017
+ sym.metadata = { ...sym.metadata ?? {}, isNif: true };
1018
+ continue;
1019
+ }
1020
+ const [name, arityText] = this.splitFnKey(key);
1021
+ const arity = Number(arityText);
1022
+ if (!name || !Number.isFinite(arity)) {
1023
+ ctx.stats.unresolvedNifs++;
1024
+ continue;
1025
+ }
1026
+ const published = {
1027
+ name,
1028
+ kind: ASTEngine.SymbolKind.Function,
1029
+ range: { startLine: line, startColumn: 1, endLine: line, endColumn: 1 },
1030
+ ownerName: owner,
1031
+ ownerKind: ASTEngine.SymbolKind.Module,
1032
+ metadata: {
1033
+ nodeType: "nif_declaration",
1034
+ arity,
1035
+ clauseCount: 0,
1036
+ isNif: true,
1037
+ declaredByNifsAttribute: true,
1038
+ line
1039
+ }
1040
+ };
1041
+ ctx.symbols.push(published);
1042
+ ctx.functionSymbolByKey.set(key, published);
1043
+ }
1044
+ for (const [key] of ctx.onLoadEntries) {
1045
+ const sym = ctx.functionSymbolByKey.get(key);
1046
+ if (sym) sym.metadata = { ...sym.metadata ?? {}, isOnLoad: true };
1047
+ }
1048
+ }
1049
+ // ==========================================================================
1050
+ // Calls
1051
+ // ==========================================================================
1052
+ processCall(node, ctx) {
1053
+ const callee = node.namedChild(0);
1054
+ if (!callee) return;
1055
+ if (callee.type === "atom" && SPAWN_BIFS.has(callee.text)) {
1056
+ ctx.patterns["Actor Model (OTP)"] = true;
1057
+ ctx.patterns["Process Spawning"] = true;
1058
+ return;
1059
+ }
1060
+ if (ctx.typeDepth > 0) return;
1061
+ if (ctx.macroDepth > 0) return;
1062
+ const source = this.currentSource(ctx);
1063
+ if (!source) return;
1064
+ const arity = this.countArgs(this.getChildByType(node, "expr_args"));
1065
+ const remote = this.getChildByType(node, "remote");
1066
+ if (remote) {
1067
+ const remoteModule = this.getChildByType(remote, "remote_module");
1068
+ const modAtom = remoteModule ? this.getChildByType(remoteModule, "atom") : void 0;
1069
+ const macroMod = this.remoteMacroModuleName(remoteModule);
1070
+ const fnAtom = this.getChildByType(remote, "atom");
1071
+ if (!fnAtom?.text) {
1072
+ ctx.stats.dynamicRemote++;
1073
+ return;
1074
+ }
1075
+ if (modAtom?.text) {
1076
+ this.pushRel(
1077
+ ctx,
1078
+ source,
1079
+ `${modAtom.text}:${fnAtom.text}`,
1080
+ ASTEngine.RelationshipType.Calls,
1081
+ 0.9,
1082
+ { erlangSpecific: true, context: "remote function call", callArity: arity },
1083
+ fnAtom.startPosition.row + 1
1084
+ );
1085
+ return;
1086
+ }
1087
+ if (macroMod) {
1088
+ const mod = macroMod === "MODULE" ? ctx.moduleName : void 0;
1089
+ if (!mod) {
1090
+ ctx.stats.dynamicRemote++;
1091
+ return;
1092
+ }
1093
+ this.pushRel(
1094
+ ctx,
1095
+ source,
1096
+ `${mod}:${fnAtom.text}`,
1097
+ ASTEngine.RelationshipType.Calls,
1098
+ 0.9,
1099
+ { erlangSpecific: true, context: "remote function call", callArity: arity, macroModule: true },
1100
+ fnAtom.startPosition.row + 1
1101
+ );
1102
+ return;
1103
+ }
1104
+ ctx.stats.dynamicRemote++;
1105
+ return;
1106
+ }
1107
+ if (callee.type !== "atom" || !callee.text) return;
1108
+ if (!ctx.declaredFnKeys.has(this.fnKey(callee.text, arity))) return;
1109
+ this.pushRel(
1110
+ ctx,
1111
+ source,
1112
+ callee.text,
1113
+ ASTEngine.RelationshipType.Calls,
1114
+ 0.9,
1115
+ { erlangSpecific: true, context: "local function call", callArity: arity },
1116
+ callee.startPosition.row + 1
1117
+ );
1118
+ }
1119
+ // ==========================================================================
1120
+ // Records
1121
+ // ==========================================================================
1122
+ processRecordExpr(node, ctx) {
1123
+ const nameNode = this.getChildByType(node, "record_name");
1124
+ const atom = nameNode ? this.getChildByType(nameNode, "atom") : void 0;
1125
+ if (!atom?.text) return;
1126
+ const source = this.currentSource(ctx);
1127
+ if (!source) return;
1128
+ const line = atom.startPosition.row + 1;
1129
+ if (ctx.typeDepth > 0) {
1130
+ this.pushRel(
1131
+ ctx,
1132
+ source,
1133
+ atom.text,
1134
+ ASTEngine.RelationshipType.References,
1135
+ 1,
1136
+ // The context is the generic type-dependency label so the whole family
1137
+ // reads uniformly, and the record-ness is an explicit flag rather than a
1138
+ // second context string: a consumer asking "is this a dependency on a
1139
+ // record?" then has a boolean to read instead of a string to match.
1140
+ { erlangSpecific: true, context: "type reference", isRecordType: true },
1141
+ line
1142
+ );
1143
+ return;
1144
+ }
1145
+ if (this.isPatternPosition(node)) return;
1146
+ this.pushRel(
1147
+ ctx,
1148
+ source,
1149
+ atom.text,
1150
+ ASTEngine.RelationshipType.Instantiates,
1151
+ 1,
1152
+ { erlangSpecific: true, isRecordExpr: true },
1153
+ line
1154
+ );
1155
+ }
1156
+ /**
1157
+ * ER-D24 (user finding #1, ER-G4): a member READ is the dominant OTP idiom —
1158
+ * every gen_server state transition reads the state record. Reads and index
1159
+ * reads emit `uses` to the RECORD and to the touched FIELD (java/kotlin
1160
+ * member-read parity, #739/#740). A read in PATTERN position is a match and
1161
+ * emits nothing.
1162
+ */
1163
+ processRecordRead(node, ctx) {
1164
+ if (ctx.typeDepth > 0 || this.isPatternPosition(node)) return;
1165
+ const source = this.currentSource(ctx);
1166
+ if (!source) return;
1167
+ const recordAtom = this.atomIn(this.getChildByType(node, "record_name"));
1168
+ if (recordAtom) {
1169
+ this.pushRel(
1170
+ ctx,
1171
+ source,
1172
+ recordAtom.text,
1173
+ ASTEngine.RelationshipType.Uses,
1174
+ 1,
1175
+ { erlangSpecific: true, context: "record member read" },
1176
+ recordAtom.startPosition.row + 1
1177
+ );
1178
+ }
1179
+ const fieldAtom = this.atomIn(this.getChildByType(node, "record_field_name"));
1180
+ if (fieldAtom) {
1181
+ this.pushRel(
1182
+ ctx,
1183
+ source,
1184
+ fieldAtom.text,
1185
+ ASTEngine.RelationshipType.Uses,
1186
+ 1,
1187
+ { erlangSpecific: true, context: "record member read" },
1188
+ fieldAtom.startPosition.row + 1
1189
+ );
1190
+ }
1191
+ }
1192
+ /**
1193
+ * ER-D4: an UPDATE is member mutation of an existing value, never a
1194
+ * construction — so `uses`, never `instantiates` — and it touches the RECORD
1195
+ * plus every ASSIGNED field.
1196
+ */
1197
+ processRecordUpdate(node, ctx) {
1198
+ if (ctx.typeDepth > 0 || this.isPatternPosition(node)) return;
1199
+ const source = this.currentSource(ctx);
1200
+ if (!source) return;
1201
+ const recordAtom = this.atomIn(this.getChildByType(node, "record_name"));
1202
+ if (recordAtom) {
1203
+ this.pushRel(
1204
+ ctx,
1205
+ source,
1206
+ recordAtom.text,
1207
+ ASTEngine.RelationshipType.Uses,
1208
+ 1,
1209
+ { erlangSpecific: true, context: "record field update" },
1210
+ recordAtom.startPosition.row + 1
1211
+ );
1212
+ }
1213
+ for (let i = 0; i < node.namedChildCount; i++) {
1214
+ const child = node.namedChild(i);
1215
+ if (child?.type !== "record_field") continue;
1216
+ const fieldAtom = this.getChildByType(child, "atom");
1217
+ if (!fieldAtom?.text) continue;
1218
+ this.pushRel(
1219
+ ctx,
1220
+ source,
1221
+ fieldAtom.text,
1222
+ ASTEngine.RelationshipType.Uses,
1223
+ 1,
1224
+ { erlangSpecific: true, context: "record field update" },
1225
+ fieldAtom.startPosition.row + 1
1226
+ );
1227
+ }
1228
+ }
1229
+ // ==========================================================================
1230
+ // Macros and fun values
1231
+ // ==========================================================================
1232
+ /**
1233
+ * ER-D9 (user finding #2, ER-G1): every `?MACRO` use is a reference to the
1234
+ * macro's Constant symbol, per site, whether or not the macro is defined in
1235
+ * this file. A use of a header-defined macro lands as an EXTERNAL target,
1236
+ * which is the honest answer — the declaration lives in another file, and the
1237
+ * fixture erlang/refs-er2 pins exactly that.
1238
+ *
1239
+ * The name is the `var` child when the macro takes no arguments and the `atom`
1240
+ * child when it does (`?FOO(A)` → atom + macro_call_args). Reading only the
1241
+ * `var` would drop every function-like macro use (measured: 2,691 of the 6,612
1242
+ * macro_call_expr sites in 300 corpus files carry a macro_call_args).
1243
+ */
1244
+ processMacroUse(node, ctx) {
1245
+ if (node.parent?.type === "module_attribute") return;
1246
+ const nameNode = this.getChildByType(node, "var") ?? this.getChildByType(node, "atom");
1247
+ if (!nameNode?.text) return;
1248
+ const source = this.currentSource(ctx);
1249
+ if (!source) return;
1250
+ this.pushRel(
1251
+ ctx,
1252
+ source,
1253
+ nameNode.text,
1254
+ ASTEngine.RelationshipType.References,
1255
+ 1,
1256
+ { erlangSpecific: true, context: "macro use" },
1257
+ nameNode.startPosition.row + 1
1258
+ );
1259
+ }
1260
+ /**
1261
+ * ER-D10 (user finding #4, ER-G2): `fun f/1` and `fun lists:map/2` name a
1262
+ * function VALUE. Taking a reference is not calling it, so these are
1263
+ * `references`, never `calls` (the fixture forbids a calls edge at the same
1264
+ * site). A qualified reference keeps its module (ER-D1); `fun ?MODULE:g/0`
1265
+ * expands to this module exactly like the remote-call rule.
1266
+ */
1267
+ processFunReference(node, ctx, external) {
1268
+ if (ctx.typeDepth > 0 || ctx.macroDepth > 0) return;
1269
+ const source = this.currentSource(ctx);
1270
+ if (!source) return;
1271
+ const arityNode = this.getChildByType(node, "arity");
1272
+ const arity = arityNode ? this.digitsOf(this.getChildByType(arityNode, "integer")?.text) : 0;
1273
+ if (!external) {
1274
+ const nameAtom = this.getChildByType(node, "atom");
1275
+ if (!nameAtom?.text) return;
1276
+ this.pushRel(
1277
+ ctx,
1278
+ source,
1279
+ nameAtom.text,
1280
+ ASTEngine.RelationshipType.References,
1281
+ 0.9,
1282
+ { erlangSpecific: true, context: "fun value reference", callArity: arity },
1283
+ nameAtom.startPosition.row + 1
1284
+ );
1285
+ return;
1286
+ }
1287
+ const fnAtom = this.getChildByType(node, "atom");
1288
+ if (!fnAtom?.text) return;
1289
+ const modNode = this.getChildByType(node, "module");
1290
+ const modAtom = modNode ? this.getChildByType(modNode, "atom") : void 0;
1291
+ const macroMod = this.remoteMacroModuleName(modNode);
1292
+ if (modAtom?.text) {
1293
+ this.pushRel(
1294
+ ctx,
1295
+ source,
1296
+ `${modAtom.text}:${fnAtom.text}`,
1297
+ ASTEngine.RelationshipType.References,
1298
+ 0.9,
1299
+ { erlangSpecific: true, context: "fun value reference", callArity: arity },
1300
+ fnAtom.startPosition.row + 1
1301
+ );
1302
+ return;
1303
+ }
1304
+ if (macroMod === "MODULE" && ctx.moduleName) {
1305
+ this.pushRel(
1306
+ ctx,
1307
+ source,
1308
+ `${ctx.moduleName}:${fnAtom.text}`,
1309
+ ASTEngine.RelationshipType.References,
1310
+ 0.9,
1311
+ { erlangSpecific: true, context: "fun value reference", callArity: arity, macroModule: true },
1312
+ fnAtom.startPosition.row + 1
1313
+ );
1314
+ }
1315
+ }
1316
+ // ==========================================================================
1317
+ // Operators
1318
+ // ==========================================================================
1319
+ processBinaryOp(node, ctx) {
1320
+ const op = node.child(1)?.text;
1321
+ if (op === "!") {
1322
+ ctx.patterns["Message Passing"] = true;
1323
+ ctx.patterns["Actor Model (OTP)"] = true;
1324
+ return;
1325
+ }
1326
+ if ((op === "andalso" || op === "orelse") && ctx.macroDepth === 0) {
1327
+ ctx.patterns["Pattern Matching"] = true;
1328
+ this.countBranch(ctx);
1329
+ }
1330
+ }
1331
+ countBranch(ctx) {
1332
+ if (ctx.macroDepth > 0) return;
1333
+ ctx.totalBranches++;
1334
+ if (ctx.currentFun) ctx.currentFun.branches++;
1335
+ }
1336
+ // ==========================================================================
1337
+ // Type references (the type graph)
1338
+ // ==========================================================================
1339
+ /**
1340
+ * ER-D10 (user finding #6, ER-G5's type half): a declared type's BODY names
1341
+ * other types, and that dependency graph is the whole value of reading a
1342
+ * `-type`. Inside a type context every `atom` and every applied `call` head is
1343
+ * a type name; `var` is a type parameter and `string` is a literal, so both are
1344
+ * silent.
1345
+ *
1346
+ * The declared type's OWN `type_name` is skipped by SPAN CONTAINMENT
1347
+ * (startIndex/endIndex), never by object identity: web-tree-sitter hands out a
1348
+ * FRESH wrapper per access, so `child !== skip` never matches and the type
1349
+ * would reference itself. That was a real defect the M14 hygiene gate caught
1350
+ * (KAD2/TSD2 class); the mirrored copy in the oracle was invisible because
1351
+ * both sides agreed.
1352
+ *
1353
+ * A self-reference that survives this filter is LEGAL Erlang (a recursive
1354
+ * type, `-type t() :: {t, integer()}`) and is emitted — the span filter removes
1355
+ * the declaration's name, not a genuine recursive dependency.
1356
+ */
1357
+ processTypeReferences(declNode, skipSpan, declName, ctx) {
1358
+ const stack = [declNode];
1359
+ while (stack.length > 0) {
1360
+ const node = stack.pop();
1361
+ if (node.isNamed && node.type === "record_name") continue;
1362
+ if (node.isNamed && (node.type === "remote" || node.type === "remote_module")) continue;
1363
+ if (node.isNamed && node.type === "call") {
1364
+ let name;
1365
+ let nameNode;
1366
+ if (node.type === "call") {
1367
+ const remote = this.getChildByType(node, "remote");
1368
+ if (remote) {
1369
+ const remoteModule = this.getChildByType(remote, "remote_module");
1370
+ const modAtom = remoteModule ? this.getChildByType(remoteModule, "atom") : void 0;
1371
+ const fnAtom = this.getChildByType(remote, "atom");
1372
+ if (modAtom?.text && fnAtom?.text) {
1373
+ name = `${modAtom.text}:${fnAtom.text}`;
1374
+ nameNode = fnAtom;
1375
+ }
1376
+ } else {
1377
+ nameNode = node.namedChild(0) ?? void 0;
1378
+ name = nameNode?.text;
1379
+ }
1380
+ }
1381
+ const inDeclarationName = skipSpan !== void 0 && nameNode !== void 0 && this.spanContains(skipSpan, nameNode);
1382
+ if (name && !inDeclarationName) {
1383
+ this.pushRel(
1384
+ ctx,
1385
+ declName,
1386
+ name,
1387
+ ASTEngine.RelationshipType.References,
1388
+ 0.9,
1389
+ { erlangSpecific: true, context: "type reference" },
1390
+ (nameNode ?? node).startPosition.row + 1
1391
+ );
1392
+ }
1393
+ }
1394
+ for (let i = node.childCount - 1; i >= 0; i--) {
1395
+ const child = node.child(i);
1396
+ if (child) stack.push(child);
1397
+ }
1398
+ }
1399
+ }
1400
+ // ==========================================================================
1401
+ // Ownership
1402
+ // ==========================================================================
1403
+ /**
1404
+ * `contains` for every owned symbol: module -> its functions, methods, types,
1405
+ * records and macros; record -> its fields. The line is the member's own
1406
+ * declaration line, which is where bb_graph's call hierarchy and the M59
1407
+ * symbol-line contract read it from.
1408
+ */
1409
+ /**
1410
+ * The owner of a module-scope declaration.
1411
+ *
1412
+ * Erlang has an honest gap here: a source file may declare NO `-module` at
1413
+ * all. OTP's own `lib/syntax_tools/examples/merl/basic_test.erl` is one — its
1414
+ * `-module` line is commented out because the file is textually included into
1415
+ * another module. Nothing in the file names the module it will end up in, and
1416
+ * the analyzer cannot know it. So the owner falls back to the FILE PATH: the
1417
+ * outermost container that definitely holds the declaration, and the only
1418
+ * identity the file has. (The alternative — inventing a module name from the
1419
+ * filename — would fabricate a module that does not exist.)
1420
+ *
1421
+ * Every module-scope declaration must route through here. The 400-file corpus
1422
+ * draw caught the one site that did not: macros took bare `ctx.moduleName`
1423
+ * while functions, records, types and callbacks took the fallback, so a
1424
+ * macro in a module-less file was emitted with NO owner while the function
1425
+ * beside it had one. A fallback that is duplicated per declaration is a
1426
+ * fallback that will drift.
1427
+ */
1428
+ ownerNameOf(ctx) {
1429
+ return ctx.moduleName ?? ctx.filePath;
1430
+ }
1431
+ emitOwnership(ctx) {
1432
+ for (const s of ctx.symbols) {
1433
+ const owner = s.ownerName;
1434
+ if (!owner) continue;
1435
+ const nameLine = s.metadata?.line;
1436
+ const line = typeof nameLine === "number" && nameLine > 0 ? nameLine : s.range.startLine;
1437
+ this.pushRel(
1438
+ ctx,
1439
+ owner,
1440
+ s.name,
1441
+ ASTEngine.RelationshipType.Contains,
1442
+ 1,
1443
+ { erlangSpecific: true, isModuleMember: s.ownerKind === ASTEngine.SymbolKind.Module },
1444
+ line
1445
+ );
1446
+ }
1447
+ }
1448
+ // ==========================================================================
1449
+ // Relationship emission
1450
+ // ==========================================================================
1451
+ /**
1452
+ * One edge per (source, type, target, line, callArity).
1453
+ *
1454
+ * ER-D2: call-site arity is part of call identity — `pick(1)` and `pick(1,2)`
1455
+ * are different callees and must not collapse into one row.
1456
+ * M59 (#825): the line is part of identity for per-SITE edges — `run/0` calling
1457
+ * `target/0` on four lines is four call sites, and the fixture pins all four.
1458
+ * The line also makes the ancestor-wins rule deterministic: in
1459
+ * `S#state{count = S#state.count + 1}` the update and the inner read share
1460
+ * (source, target, line), and because the outer node is visited first its
1461
+ * "record field update" edge is the one that survives.
1462
+ *
1463
+ * ER-D43 (measured 2026-10-03): the PUBLISHED IDENTITY is part of identity too. An
1464
+ * `exports` edge publishes a module's interface, and the interface is keyed by
1465
+ * `name/arity` (ER-D2), so `-export([start/0, start/1, complete/1, complete/0])` on
1466
+ * ONE line is four publications. The key carried `callArity` — which an `exports` edge
1467
+ * does not set — and `line`, which is the same line for all four, so the second
1468
+ * publication of each name was dropped and the module's interface was published at half
1469
+ * its real size with no error and no counter. 340 sites in a 204-file corpus draw;
1470
+ * `otp/lib/asn1/examples/test_driver.erl:10` and
1471
+ * `otp/lib/common_test/src/ct_doctest.erl:318` are two of them. `metadata.arity` is set
1472
+ * on the `exports` edge and on nothing else, so reading it here widens the key for that
1473
+ * one label and changes no other.
1474
+ */
1475
+ pushRel(ctx, source, target, type, confidence, metadata = {}, line) {
1476
+ const publishedIdentity = metadata.callArity ?? metadata.arity ?? "";
1477
+ const key = `${source}|${type}|${target}|${line ?? ""}|${publishedIdentity}`;
1478
+ if (ctx.seenRels.has(key)) return;
1479
+ ctx.seenRels.add(key);
1480
+ ctx.relationships.push({
1481
+ source,
1482
+ target,
1483
+ type,
1484
+ confidence,
1485
+ metadata,
1486
+ ...typeof line === "number" && line > 0 ? { line_number: line } : {}
1487
+ });
1488
+ }
1489
+ // ==========================================================================
1490
+ // Context helpers
1491
+ // ==========================================================================
1492
+ /** The symbol every edge inside the current declaration belongs to. */
1493
+ currentSource(ctx) {
1494
+ const top = ctx.declStack[ctx.declStack.length - 1];
1495
+ return top?.source ?? this.ownerNameOf(ctx);
1496
+ }
1497
+ /**
1498
+ * Is this node in PATTERN position — a clause head, a `case`/`receive`/`try`
1499
+ * arm pattern, the left side of a `=` / `?=` match, or a comprehension
1500
+ * generator's left side?
1501
+ *
1502
+ * A pattern is a MATCH, not an evaluation: `S#state.count` in a head is
1503
+ * destructuring, while `State#state{…}` in a body is a real update. The rule
1504
+ * climbs to the nearest boundary. A CLAUSE HEAD makes everything below it a
1505
+ * pattern (`case X of Y = #state{} -> …` binds, it does not construct). An
1506
+ * EVALUATION context (clause body, guard) makes only the pattern side of a
1507
+ * match or generator a pattern.
1508
+ */
1509
+ /**
1510
+ * Is this node in PATTERN position?
1511
+ *
1512
+ * A pattern is a MATCH, not an evaluation. The rule is "the NEAREST enclosing
1513
+ * boundary decides", which is the only formulation that survives Erlang's
1514
+ * nesting — three corpus shapes decided it, and each one is a case a
1515
+ * cumulative flag gets wrong:
1516
+ *
1517
+ * `D#r.f == []` in a GUARD -> evaluation (a guard computes).
1518
+ * Climbing to the clause head would call it a pattern; the boundary is the
1519
+ * guard, and a guard evaluates.
1520
+ * `[C || #cert{der = C} <- Certs]` -> pattern. The generator's left
1521
+ * side binds, whatever expression holds the comprehension. The nearest
1522
+ * boundary is the generator and the node is on its `<-` side, so the
1523
+ * enclosing `= unzip4(…)` match is never consulted.
1524
+ * `Y = #r{} -> …` in a case arm -> pattern. The node is on the
1525
+ * match's VALUE side, but the match itself sits in an arm head, so the
1526
+ * rule re-asks the boundary's own context.
1527
+ * `X = #r{}` in a clause body -> evaluation.
1528
+ */
1529
+ isPatternPosition(node) {
1530
+ let probe = node;
1531
+ for (; ; ) {
1532
+ const boundary = this.nearestPatternBoundary(probe.parent);
1533
+ if (!boundary) return false;
1534
+ if (boundary.kind === "head") return true;
1535
+ if (boundary.kind === "body" || boundary.kind === "guard") return false;
1536
+ if (this.isOnPatternSide(node, boundary.node)) return true;
1537
+ probe = boundary.node;
1538
+ }
1539
+ }
1540
+ nearestPatternBoundary(from) {
1541
+ let cur = from;
1542
+ while (cur) {
1543
+ const t = cur.type;
1544
+ if (t === "function_clause" || t === "fun_clause" || t === "cr_clause") return { node: cur, kind: "head" };
1545
+ if (t === "clause_body") return { node: cur, kind: "body" };
1546
+ if (t === "guard" || t === "guard_clause") return { node: cur, kind: "guard" };
1547
+ if (t === "match_expr" || t === "cond_match_expr" || t === "lc_or_zc_expr") return { node: cur, kind: "match" };
1548
+ cur = cur.parent;
1549
+ }
1550
+ return void 0;
1551
+ }
1552
+ /**
1553
+ * Does `node` sit left of the boundary operator of `owner`?
1554
+ *
1555
+ * The `<-` of a comprehension generator is NOT a direct child of the
1556
+ * `lc_or_zc_expr` — the grammar wraps it in a `generator` node (measured shape:
1557
+ * `lc_or_zc_expr → generator → {LHS, <-, RHS}`). Both this analyzer AND the
1558
+ * independent oracle first missed that and agreed with each other on the wrong
1559
+ * answer, which is precisely the case a mirror cannot see: only the authored
1560
+ * fixture `erlang/patterns-er3` catches it. The `generator` / `map_generator` /
1561
+ * `b_generator` wrapper is resolved here.
1562
+ *
1563
+ * A `lc_or_zc_expr` with NO generator is a comprehension FILTER, which
1564
+ * computes — no operator, so the answer is false.
1565
+ */
1566
+ isOnPatternSide(node, owner) {
1567
+ let target = owner;
1568
+ if (owner.type === "lc_or_zc_expr") {
1569
+ const gen = this.getChildByType(owner, "generator") ?? this.getChildByType(owner, "map_generator") ?? this.getChildByType(owner, "b_generator");
1570
+ if (gen) target = gen;
1571
+ }
1572
+ for (let i = 0; i < target.childCount; i++) {
1573
+ const c = target.child(i);
1574
+ if (c && MATCH_OPERATORS.has(c.type)) return node.startIndex < c.startIndex;
1575
+ }
1576
+ return false;
1577
+ }
1578
+ /** Span containment — the only correct way to compare two tree-sitter nodes. */
1579
+ spanContains(outer, inner) {
1580
+ return outer.startIndex <= inner.startIndex && inner.endIndex <= outer.endIndex;
1581
+ }
1582
+ // ==========================================================================
1583
+ // Node readers
1584
+ // ==========================================================================
1585
+ /**
1586
+ * `name/arity` from an `fa`, with the NAME's line (M59).
1587
+ *
1588
+ * An `fa`'s name has THREE spellings in this grammar and a reader that takes only the
1589
+ * first loses rows SILENTLY — no error, no counter, and the label simply has fewer
1590
+ * members than the file publishes:
1591
+ *
1592
+ * atom `-export([run/0])`
1593
+ * macro_call_expr `-export([?MODULE/0])` — ER-D28: how OTP test suites export the
1594
+ * function their `?MODULE() -> …` clause declares. `?MODULE`
1595
+ * expands to the module atom, so the entry resolves exactly like
1596
+ * the clause head does. Any other macro in an `fa` needs expansion
1597
+ * and is refused.
1598
+ * var `-export([HEAD/1])` — ER-D42, measured 2026-10-03. An
1599
+ * identifier beginning with an UPPERCASE letter is a VARIABLE in
1600
+ * Erlang, so the grammar types it `var`, not `atom`, and this
1601
+ * reader required `atom`. Every all-caps export name was therefore
1602
+ * dropped: `-export([HEAD/1])` in the lane's own
1603
+ * `macros-er3.erl` fixture produced no `exports` row at all, and the
1604
+ * fixture did not catch it because nothing pinned the row. A macro
1605
+ * is a real Erlang function name here — `-define(HEAD(X), X).` plus
1606
+ * `-export([HEAD/1])` is ordinary code — so the name is read and the
1607
+ * arity with it.
1608
+ */
1609
+ readFa(fa, moduleName) {
1610
+ const nameNode = this.getChildByType(fa, "atom");
1611
+ const arityNode = this.getChildByType(fa, "arity");
1612
+ const arity = arityNode ? this.digitsOf(this.getChildByType(arityNode, "integer")?.text) : 0;
1613
+ if (nameNode?.text) return { name: nameNode.text, arity, line: nameNode.startPosition.row + 1 };
1614
+ const macro = this.getChildByType(fa, "macro_call_expr");
1615
+ const macroName = macro ? (this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom"))?.text : void 0;
1616
+ if (macroName === "MODULE" && moduleName) {
1617
+ const at = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
1618
+ return { name: moduleName, arity, line: (at ?? macro).startPosition.row + 1 };
1619
+ }
1620
+ const varNode = this.getChildByType(fa, "var");
1621
+ if (varNode?.text) return { name: varNode.text, arity, line: varNode.startPosition.row + 1 };
1622
+ return void 0;
1623
+ }
1624
+ /**
1625
+ * `-nifs([f/1])` / `-on_load([g/0])` entries. The list holds
1626
+ * `binary_op_expr` (`f` `/` `1`), NOT an `fa` — the second half of the
1627
+ * dead-branch defect ER-D22 records. Both spellings are read, because the
1628
+ * grammar's own `-record`-in-`wild_attribute` error path produces `fa` nodes
1629
+ * for some vendor attributes.
1630
+ */
1631
+ collectAttributeFaEntries(node, out, ctx) {
1632
+ const line = node.startPosition.row + 1;
1633
+ const stack = [node];
1634
+ while (stack.length > 0) {
1635
+ const n = stack.pop();
1636
+ if (n.isNamed && n.type === "binary_op_expr" && n.child(1)?.type === "/") {
1637
+ const nameNode = this.getChildByType(n, "atom");
1638
+ if (nameNode?.text) {
1639
+ out.set(this.fnKey(nameNode.text, this.digitsOf(this.getChildByType(n, "integer")?.text)), line);
1640
+ }
1641
+ continue;
1642
+ }
1643
+ if (n.isNamed && n.type === "fa") {
1644
+ const fa = this.readFa(n, ctx.moduleName);
1645
+ if (fa) out.set(this.fnKey(fa.name, fa.arity), line);
1646
+ continue;
1647
+ }
1648
+ for (let i = n.childCount - 1; i >= 0; i--) {
1649
+ const c = n.child(i);
1650
+ if (c) stack.push(c);
1651
+ }
1652
+ }
1653
+ }
1654
+ /** `-nifs`, not `nifs`: the dash is INSIDE the `attr_name` node. */
1655
+ attrNameText(node) {
1656
+ const attrName = this.getChildByType(node, "attr_name");
1657
+ if (!attrName) return void 0;
1658
+ return this.getChildByType(attrName, "atom")?.text ?? attrName.text;
1659
+ }
1660
+ atomIn(node) {
1661
+ return node ? this.getChildByType(node, "atom") : void 0;
1662
+ }
1663
+ namedChildrenOf(args) {
1664
+ if (!args) return [];
1665
+ const out = [];
1666
+ for (let i = 0; i < args.namedChildCount; i++) {
1667
+ const c = args.namedChild(i);
1668
+ if (c && c.type !== "comment") out.push(c);
1669
+ }
1670
+ return out;
1671
+ }
1672
+ getChildByType(node, targetType) {
1673
+ if (!node) return void 0;
1674
+ for (let i = 0; i < node.childCount; i++) {
1675
+ const child = node.child(i);
1676
+ if (child?.type === targetType) return child;
1677
+ }
1678
+ return void 0;
1679
+ }
1680
+ collectChildrenByType(node, targetType) {
1681
+ const results = [];
1682
+ for (let i = 0; i < node.namedChildCount; i++) {
1683
+ const child = node.namedChild(i);
1684
+ if (child && child.type === targetType) results.push(child);
1685
+ }
1686
+ return results;
1687
+ }
1688
+ digitsOf(text) {
1689
+ if (!text) return 0;
1690
+ const digits = this.keepDigits(text);
1691
+ return digits.length > 0 ? parseInt(digits, 10) || 0 : 0;
1692
+ }
1693
+ // ==========================================================================
1694
+ // Pure-string helpers (no regex — M12 census, M49 #849)
1695
+ // ==========================================================================
1696
+ stripQuotes(text) {
1697
+ let out = "";
1698
+ for (let i = 0; i < text.length; i++) {
1699
+ const ch = text[i];
1700
+ if (ch === '"' || ch === "'") continue;
1701
+ out += ch;
1702
+ }
1703
+ return out;
1704
+ }
1705
+ keepDigits(text) {
1706
+ let out = "";
1707
+ for (let i = 0; i < text.length; i++) {
1708
+ const ch = text[i];
1709
+ if (ch >= "0" && ch <= "9") out += ch;
1710
+ }
1711
+ return out;
1712
+ }
1713
+ /** `name()` -> `name`; `maybe(T)` -> `maybe`. */
1714
+ stripFirstParen(text) {
1715
+ const paren = text.indexOf("(");
1716
+ return (paren >= 0 ? text.slice(0, paren) : text).trim();
1717
+ }
1718
+ nodeToRange(node) {
1719
+ return {
1720
+ startLine: node.startPosition.row + 1,
1721
+ startColumn: node.startPosition.column + 1,
1722
+ endLine: node.endPosition.row + 1,
1723
+ endColumn: node.endPosition.column + 1,
1724
+ startIndex: node.startIndex,
1725
+ endIndex: node.endIndex
1726
+ };
1727
+ }
1728
+ };
1729
+ export {
1730
+ RealErlangAnalyzer
1731
+ };