@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,1957 @@
1
+ import {
2
+ FILE_CLAIM_MAX_BYTES,
3
+ dirOf,
4
+ fileClaimForTarget,
5
+ foldProjectPath
6
+ } from "./chunk-EWMWK7WN.js";
7
+ import {
8
+ ASTEngine
9
+ } from "./chunk-V44PGPA6.js";
10
+ import "./chunk-BQ3OVQ3P.js";
11
+ import "./chunk-SK6YVHFZ.js";
12
+ import "./chunk-2BSKD7O3.js";
13
+ import "./chunk-KAET47ZL.js";
14
+ import "./chunk-KCDECRJY.js";
15
+ import "./chunk-MLKGABMK.js";
16
+
17
+ // src/ast/real-lua-analyzer.ts
18
+ import * as path from "path";
19
+ var LUA54_TABLES = /* @__PURE__ */ new Set([
20
+ "string",
21
+ "table",
22
+ "math",
23
+ "io",
24
+ "os",
25
+ "coroutine",
26
+ "debug",
27
+ "utf8"
28
+ ]);
29
+ var LUA54_GLOBALS = /* @__PURE__ */ new Set([
30
+ "assert",
31
+ "collectgarbage",
32
+ "dofile",
33
+ "error",
34
+ "getmetatable",
35
+ "ipairs",
36
+ "load",
37
+ "loadfile",
38
+ "next",
39
+ "pairs",
40
+ "pcall",
41
+ "print",
42
+ "rawequal",
43
+ "rawget",
44
+ "rawlen",
45
+ "rawset",
46
+ "require",
47
+ "select",
48
+ "setmetatable",
49
+ "tonumber",
50
+ "tostring",
51
+ "type",
52
+ "warn",
53
+ "xpcall",
54
+ "_G",
55
+ "_VERSION"
56
+ ]);
57
+ function luaModuleBases(fileRel) {
58
+ const dir = dirOf(fileRel);
59
+ const segs = fileRel.split("/");
60
+ const all = [{ label: "root", base: "" }, { label: "fileDir", base: dir }];
61
+ for (let i = 0; i < segs.length - 1; i++) {
62
+ if (segs[i] === "lua") {
63
+ all.push({ label: "luaRoot", base: segs.slice(0, i + 1).join("/") + "/" });
64
+ break;
65
+ }
66
+ }
67
+ all.push({ label: "fileDir+lua", base: dir + "lua/" });
68
+ const seen = /* @__PURE__ */ new Set();
69
+ return all.filter((b) => {
70
+ if (seen.has(b.base)) return false;
71
+ seen.add(b.base);
72
+ return true;
73
+ });
74
+ }
75
+ function luaModuleFileCandidates(moduleName, fileRel) {
76
+ const stem = moduleName.trim().split(".").join("/");
77
+ if (stem.length === 0) return [];
78
+ const out = [];
79
+ for (const { base } of luaModuleBases(fileRel)) {
80
+ for (const suffix of [".lua", "/init.lua"]) {
81
+ const folded = foldProjectPath(base, stem + suffix);
82
+ if (folded !== null && !out.includes(folded)) out.push(folded);
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ var RealLuaAnalyzer = class {
88
+ analyze(rootNode, content, filePath, context) {
89
+ const projectRootRaw = String(context?.projectRoot ?? "");
90
+ const filePathRaw = String(filePath ?? "");
91
+ const rooted = projectRootRaw.length > 0 && filePathRaw.length > 0 && path.isAbsolute(projectRootRaw) && path.isAbsolute(filePathRaw);
92
+ const ctx = {
93
+ symbols: [],
94
+ relationships: [],
95
+ imports: [],
96
+ exports: [],
97
+ errors: [],
98
+ globalVariables: /* @__PURE__ */ new Set(),
99
+ declaredTables: /* @__PURE__ */ new Set(),
100
+ unpublishedSurface: [],
101
+ frameworks: /* @__PURE__ */ new Set(),
102
+ patterns: /* @__PURE__ */ new Set(),
103
+ seenRelationships: /* @__PURE__ */ new Set(),
104
+ coroutineCalls: 0,
105
+ declarationsByLabel: /* @__PURE__ */ new Map(),
106
+ membersByOwner: /* @__PURE__ */ new Map(),
107
+ supertypeLinks: /* @__PURE__ */ new Map(),
108
+ factorySupertypeLines: /* @__PURE__ */ new Map(),
109
+ declaredFunctions: /* @__PURE__ */ new Set(),
110
+ wrapperFactories: /* @__PURE__ */ new Set(),
111
+ stdlibCandidates: [],
112
+ stdlibGlobalCandidates: [],
113
+ ...rooted ? { projectRoot: projectRootRaw, filePath: filePathRaw } : {},
114
+ moduleFileClaims: /* @__PURE__ */ new Map()
115
+ };
116
+ this.collectErrors(rootNode, ctx);
117
+ this.collectDeclaredTables(rootNode, ctx);
118
+ this.collectSupertypeLinks(rootNode, ctx);
119
+ this.collectFactorySupertypeLinks(rootNode, ctx);
120
+ this.collectDecoratorFacts(rootNode, ctx);
121
+ const rootFrame = { localNames: /* @__PURE__ */ new Set(), inFunction: false };
122
+ const stack = [{ node: rootNode, frame: rootFrame }];
123
+ while (stack.length > 0) {
124
+ const entry = stack.pop();
125
+ const node = entry.node;
126
+ const frame = entry.frame;
127
+ if (node.isMissing) continue;
128
+ this.processNode(node, frame, ctx);
129
+ let childFrame = frame;
130
+ if (node.type === "function_declaration" || node.type === "function_definition") {
131
+ const functionName = node.type === "function_declaration" ? this.functionName(node) : void 0;
132
+ const locals = new Set(frame.localNames);
133
+ for (const parameter of this.extractParameters(node)) locals.add(parameter.name);
134
+ if (functionName || node.type === "function_definition") {
135
+ const nameNode = node.childForFieldName("name");
136
+ const bareOwner = (nameNode ? this.functionOwner(nameNode) : void 0) ?? functionName ?? frame.owner ?? frame.callable;
137
+ const selfName = (nameNode ? this.splitMemberName(nameNode, functionName ?? "").name : functionName) || frame.selfName;
138
+ childFrame = {
139
+ callable: functionName ?? frame.callable,
140
+ owner: bareOwner,
141
+ selfName,
142
+ localNames: locals,
143
+ inFunction: true
144
+ };
145
+ }
146
+ }
147
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
148
+ const child = node.namedChild(i);
149
+ if (child) stack.push({ node: child, frame: childFrame });
150
+ }
151
+ }
152
+ this.addContainsRelationships(ctx);
153
+ this.addFactorySupertypeEdges(ctx);
154
+ this.addOverrideRelationships(ctx);
155
+ this.markStdlibExternal(ctx);
156
+ const detectedFrameworks = [...ctx.frameworks];
157
+ const detectedPatterns = [...ctx.patterns];
158
+ return {
159
+ language: "lua",
160
+ symbols: ctx.symbols,
161
+ relationships: ctx.relationships,
162
+ imports: ctx.imports,
163
+ exports: ctx.exports,
164
+ errors: ctx.errors,
165
+ metadata: {
166
+ // M60 LUA-35. Present ONLY when there is something to say, and every field
167
+ // is derived from the parse: `shape` from the node type, `detail` from the
168
+ // node's own text, `line` from its start position, `why` a fixed reason per
169
+ // shape. Nothing here is a constant shaped like a measurement, which is the
170
+ // LUA-11 rule — a count or score that did not vary with the file would be
171
+ // worse than an absent field, and this is a list that is empty whenever the
172
+ // module's surface is nameable.
173
+ ...ctx.unpublishedSurface.length ? { unpublishedSurface: ctx.unpublishedSurface } : {},
174
+ complexity: ctx.symbols.length + ctx.relationships.length,
175
+ linesOfCode: this.lineCount(content),
176
+ cyclomaticComplexity: this.calculateCyclomaticComplexity(rootNode),
177
+ framework: detectedFrameworks[0],
178
+ patterns: detectedPatterns,
179
+ detectedFrameworks,
180
+ frameworkPatterns: detectedFrameworks.map((framework) => ({
181
+ framework,
182
+ patterns: [{ type: "usage", detected: true, description: `${framework} usage detected` }]
183
+ })),
184
+ architecturalInsights: this.generateArchitecturalInsights(ctx),
185
+ architecturalScore: this.calculateArchitecturalScore(ctx),
186
+ codeQuality: this.assessCodeQuality(ctx),
187
+ hasTypeInference: false,
188
+ // M60 LUA-11: performanceScore and memoryEfficiency were the literals
189
+ // 85 and 85 — the two most checkable-looking numbers in this block and
190
+ // the two that were invented. They are gone under those NAMES as well as
191
+ // those values, because a derived number published as "performanceScore"
192
+ // is still a lie: it claims to measure performance, and this analyzer
193
+ // measures no time at all. What replaces them is named for what it is —
194
+ // relationshipDensity and retainedRowKb — and each is either derived
195
+ // from the analysed file or ABSENT.
196
+ relationshipDensity: this.relationshipDensity(ctx),
197
+ retainedRowKb: this.retainedRowKb(ctx),
198
+ // M60 LUA-02: counted from the CALLS the walk actually saw, not from a
199
+ // symbol flag nothing ever set — which made this a structural zero.
200
+ coroutineUsage: this.countCoroutineSymbols(ctx),
201
+ analysisVersion: "4.0.0-lua-m12.32"
202
+ }
203
+ };
204
+ }
205
+ processNode(node, frame, ctx) {
206
+ switch (node.type) {
207
+ case "function_declaration":
208
+ this.extractFunction(node, frame, ctx, this.isLocalFunction(node));
209
+ break;
210
+ case "variable_declaration":
211
+ if (this.isLocalVariableDeclaration(node)) this.extractLocalDeclaration(node, frame, ctx);
212
+ this.extractDecoratorApplication(node, frame, ctx);
213
+ break;
214
+ case "assignment_statement":
215
+ this.extractAssignment(node, frame, ctx);
216
+ this.extractDecoratorApplication(node, frame, ctx);
217
+ break;
218
+ case "function_call":
219
+ this.processCall(node, frame, ctx);
220
+ break;
221
+ case "return_statement":
222
+ this.extractExport(node, frame, ctx);
223
+ break;
224
+ case "for_statement":
225
+ this.registerLoopBindings(node, frame);
226
+ break;
227
+ case "identifier":
228
+ this.detectFrameworks(node.text, ctx);
229
+ this.emitNameRead(node, frame, ctx);
230
+ break;
231
+ case "dot_index_expression":
232
+ case "method_index_expression":
233
+ case "bracket_index_expression":
234
+ this.emitMemberRead(node, frame, ctx);
235
+ break;
236
+ }
237
+ }
238
+ /**
239
+ * M60 LUA-40 — `uses`: a name this scope READS.
240
+ *
241
+ * Measured before it was written (script/m60/lua-ontology-census.ts, probe
242
+ * B4-bare-name-read): 295,019 bare-name reads across 3,250 corpus files. The
243
+ * type was absent, so the largest read surface the language has was invisible.
244
+ *
245
+ * The rule is a ROLE, read off the parent node, never a text shape. Each
246
+ * exclusion is a different relationship that is already published or is not a
247
+ * declaration at all:
248
+ *
249
+ * callee of a function_call -> that is `calls`, published by processCall
250
+ * a `variable_list` child -> a DECLARATION, published as a symbol and
251
+ * as `contains`
252
+ * the LHS of an assignment -> a WRITE, not a read
253
+ * a `field`'s key -> a table-literal KEY, a declaration site
254
+ * a `function_declaration` name -> a member DECLARATION
255
+ * the RECEIVER of an index expr -> part of a qualified name; that read is
256
+ * `references` (LUA-41), and publishing both
257
+ * would double-count one fact under two labels
258
+ * a `parameters` child -> a parameter BINDING, not a use of it
259
+ *
260
+ * Confidence is 0.9, not 1.0, and the reason is not a hedge: the node proves
261
+ * the file READS A NAME. Which binding that name denotes — a local, an
262
+ * upvalue, a global, a stdlib function, a field of a required module — is not
263
+ * written anywhere in the tree. The machine-readable reason code the honesty
264
+ * contract §4 requires for anything under 1.0 is `evidenceClass:
265
+ * "usage_contextual"`, which is this codebase's existing word for a link
266
+ * established by a NAME in a value position (real-css-analyzer.ts:737).
267
+ */
268
+ /**
269
+ * M60 LUA-45: node identity, the only way that works on this runtime.
270
+ *
271
+ * `web-tree-sitter` returns a FRESH wrapper object on every access, so
272
+ * `parent.namedChild(0) === node` is false even when both name the same span —
273
+ * measured on validation/m14/fixtures/lua/name-reads.lua, where the receiver
274
+ * test silently never fired and published 3 edges it exists to suppress:
275
+ * `apply -> config` at L22, `apply -> helper` at L24 and `never -> helper` at
276
+ * L29. Every one of them states a qualified access twice under two labels.
277
+ *
278
+ * The identity that IS stable is the SPAN, which is what this compares. It is
279
+ * the family's own idiom and not a local invention: real-c-analyzer.ts:2336,
280
+ * :2525 and :6012 all compare `startIndex`/`endIndex` for exactly this reason.
281
+ * `type` is compared too because a span can be covered by more than one node
282
+ * type in a grammar that reuses a token, and a wrong-type match here would
283
+ * suppress a real edge.
284
+ */
285
+ isSameNode(a, b) {
286
+ if (!a || !b) return false;
287
+ return a.type === b.type && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
288
+ }
289
+ emitNameRead(node, frame, ctx) {
290
+ const parent = node.parent;
291
+ if (!parent) return;
292
+ const pt = parent.type;
293
+ if (pt === "function_call") {
294
+ if (this.isSameNode(parent.childForFieldName("name"), node) || this.isSameNode(parent.namedChild(0), node)) return;
295
+ } else if (pt === "variable_list" || pt === "parameters") {
296
+ return;
297
+ } else if (pt === "assignment_statement") {
298
+ if (this.isSameNode(parent.namedChild(0), node)) return;
299
+ } else if (pt === "field") {
300
+ if (this.isSameNode(parent.childForFieldName("name"), node)) return;
301
+ } else if (pt === "function_declaration") {
302
+ if (this.isSameNode(parent.childForFieldName("name"), node)) return;
303
+ } else if (pt === "dot_index_expression" || pt === "method_index_expression") {
304
+ if (this.isSameNode(parent.namedChild(0), node)) return;
305
+ if (this.isSameNode(parent.namedChild(parent.namedChildCount - 1), node)) return;
306
+ } else if (pt === "bracket_index_expression") {
307
+ if (this.isSameNode(parent.namedChild(0), node)) return;
308
+ }
309
+ this.addRelationship(
310
+ ctx,
311
+ frame.callable || "global",
312
+ node.text,
313
+ ASTEngine.RelationshipType.Uses,
314
+ { luaSpecific: true, relationshipType: "lua_name_read" },
315
+ node,
316
+ { confidence: 0.9 }
317
+ );
318
+ }
319
+ /**
320
+ * M60 LUA-41 — `references`: a MEMBER of a table read without calling it.
321
+ *
322
+ * Measured first, the same way: probe B3-member-read counted 198,720 qualified
323
+ * reads across 2,643 files. A qualified read is the shape the family's
324
+ * `references` is for and the shape this lane was missing: css's `var(--x)`
325
+ * asserts a dependency on a NAME that may be declared in another file, and
326
+ * `M.field` asserts exactly that — `M` is written, `field` is written, and
327
+ * which module owns `field` is not written anywhere.
328
+ *
329
+ * The same four exclusions as `calls`/`contains` already rely on, each for the
330
+ * same reason: a member's CALLEE is a `calls` edge (processCall), a member's
331
+ * DECLARATION is a symbol plus a `contains` edge, a member ASSIGNED TO is a
332
+ * write, and a `field`'s key is a table-literal key.
333
+ *
334
+ * A COMPUTED index (`dao[method or context .. "_by_" .. endpoint_key]`) has no
335
+ * name, and LUA-22's payload lesson applies unchanged: a literal STRING index
336
+ * does (`dao["rows"]`, read as `rows`), a number and a variable-held key do not.
337
+ *
338
+ * The TARGET is the member's own field name, which is what every other lua edge
339
+ * uses for a member (LUA-13/LUA-15: `obj:method()` yields target `method`), and
340
+ * the RECEIVER is recorded in metadata so a consumer that needs the qualified
341
+ * path has it without this edge having to claim an identity it cannot prove.
342
+ * The cost of that choice is measured and declared: two tables in one file can
343
+ * both have a member `run`, and the bare name fuses them. The qualified path is
344
+ * recorded per edge, so the ambiguity is recoverable rather than lost.
345
+ */
346
+ emitMemberRead(node, frame, ctx) {
347
+ const parent = node.parent;
348
+ if (parent) {
349
+ const pt = parent.type;
350
+ if (pt === "function_call" && (this.isSameNode(parent.childForFieldName("name"), node) || this.isSameNode(parent.namedChild(0), node))) return;
351
+ if (pt === "function_declaration" && this.isSameNode(parent.childForFieldName("name"), node)) return;
352
+ if (pt === "field" && this.isSameNode(parent.childForFieldName("name"), node)) return;
353
+ if (pt === "variable_list") return;
354
+ }
355
+ let member;
356
+ let memberNode;
357
+ if (node.type === "bracket_index_expression") {
358
+ const key = node.childForFieldName("field");
359
+ if (key?.type !== "string") return;
360
+ member = this.stringValue(key);
361
+ memberNode = key;
362
+ } else {
363
+ const last = node.namedChildCount > 0 ? node.namedChild(node.namedChildCount - 1) : void 0;
364
+ if (last?.type !== "identifier") return;
365
+ member = last.text;
366
+ memberNode = last;
367
+ }
368
+ if (!member || !memberNode) return;
369
+ const receiver = this.nameBearing(node.childForFieldName("table"));
370
+ this.addRelationship(
371
+ ctx,
372
+ frame.callable || "global",
373
+ member,
374
+ ASTEngine.RelationshipType.References,
375
+ {
376
+ luaSpecific: true,
377
+ relationshipType: "lua_member_read",
378
+ ...receiver ? { receiver } : {}
379
+ },
380
+ memberNode,
381
+ { confidence: 0.9 }
382
+ );
383
+ }
384
+ extractFunction(node, frame, ctx, isLocal) {
385
+ const nameNode = node.childForFieldName("name") || node.namedChild(0);
386
+ if (!nameNode) return;
387
+ const label = nameNode.text;
388
+ if (!label) return;
389
+ const { name, owner } = this.splitMemberName(nameNode, label);
390
+ const parameters = this.extractParameters(node);
391
+ const isMethod = !!owner;
392
+ const lexicallyLocal = isLocal || !!frame.callable;
393
+ const symbol = {
394
+ name,
395
+ kind: isMethod ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
396
+ range: this.nodeToRange(node),
397
+ scope: lexicallyLocal ? frame.callable || "global" : "global",
398
+ visibility: lexicallyLocal ? "private" : "public",
399
+ parameters,
400
+ ownerName: owner,
401
+ ownerKind: owner ? ASTEngine.SymbolKind.Class : void 0,
402
+ // M60 LUA-37: the enclosing body's own symbol name, so a declaration nested
403
+ // in this one can be contained BY IT rather than by its label. See
404
+ // addContainsRelationships, which reads this and never reads `scope`.
405
+ frameSelfName: frame.selfName,
406
+ // qualifiedName keeps the full path, which is what it is FOR. The `name`
407
+ // field is the identity; this is the path. Before the fix the path was
408
+ // doing the identity's job and no path was recorded at all.
409
+ qualifiedName: label,
410
+ metadata: { luaSpecific: true, isLocal: lexicallyLocal, isMethod, ownerTable: owner, qualifiedName: label }
411
+ };
412
+ ctx.symbols.push(symbol);
413
+ if (owner) {
414
+ ctx.declaredTables.add(owner);
415
+ ctx.patterns.add("Object-Oriented Programming");
416
+ const members = ctx.membersByOwner.get(owner) ?? /* @__PURE__ */ new Map();
417
+ if (!members.has(name)) members.set(name, node.startPosition.row + 1);
418
+ ctx.membersByOwner.set(owner, members);
419
+ }
420
+ const byLabel = ctx.declarationsByLabel.get(label) ?? [];
421
+ byLabel.push({ line: node.startPosition.row + 1, owner: owner ?? null, member: owner ? name : null });
422
+ ctx.declarationsByLabel.set(label, byLabel);
423
+ if (lexicallyLocal) ctx.globalVariables.delete(name);
424
+ }
425
+ // M60 LUA-15: split a declaration's name into its OWN name and its OWNER.
426
+ //
427
+ // In this grammar `function Buffer:initialize()` and `function
428
+ // Buffer.readUInt8()` both put an index expression in the `name` field, with
429
+ // the RECEIVER first and the FIELD last:
430
+ // method_index_expression
431
+ // table: (identifier "Buffer")
432
+ // : (identifier "initialize")
433
+ // so the field is the LAST named child and the receiver the first. Taking the
434
+ // first yields "Buffer" — the table, not the method.
435
+ splitMemberName(nameNode, label) {
436
+ if (nameNode.type !== "dot_index_expression" && nameNode.type !== "method_index_expression") {
437
+ return { name: label };
438
+ }
439
+ const first = nameNode.namedChild(0);
440
+ const last = nameNode.namedChildCount > 1 ? nameNode.namedChild(nameNode.namedChildCount - 1) : void 0;
441
+ const owner = first?.type === "identifier" ? first.text : void 0;
442
+ return { name: last?.text || label, owner };
443
+ }
444
+ extractLocalDeclaration(node, frame, ctx) {
445
+ const first = node.type === "variable_declaration" ? node.namedChild(0) : node;
446
+ const assignment = first?.type === "assignment_statement" ? first : void 0;
447
+ const variableList = assignment ? this.childOfType(assignment, "variable_list") : first?.type === "variable_list" ? first : void 0;
448
+ if (!variableList) return;
449
+ for (let i = 0; i < variableList.namedChildCount; i++) {
450
+ const variable = variableList.namedChild(i);
451
+ const name = this.plainVariableName(variable);
452
+ if (!variable || !name) continue;
453
+ frame.localNames.add(name);
454
+ const value = assignment ? this.assignmentValue(assignment) : void 0;
455
+ const isTable = this.isTableNode(value);
456
+ if (isTable) ctx.declaredTables.add(name);
457
+ ctx.symbols.push({
458
+ name,
459
+ kind: ASTEngine.SymbolKind.Variable,
460
+ range: this.nodeToRange(variable),
461
+ scope: frame.callable || "global",
462
+ // M60 LUA-37: a declaration inside a body is contained by the FUNCTION whose
463
+ // body it is, so the owner is that function's own name (`selfName`), not the
464
+ // function's own owner. `function fs.appendFile()` has owner "fs" and self
465
+ // "appendFile", and a local inside it must read `appendFile -> localName`:
466
+ // "fs" is the owner of the METHOD, and using it filed the local as a sibling
467
+ // of the method rather than a member of it, which cost 525 unexplained
468
+ // contains edges on an 83-file kong slice.
469
+ ownerName: frame.selfName ?? frame.owner ?? frame.callable,
470
+ frameSelfName: frame.selfName,
471
+ ownerKind: frame.callable ? ASTEngine.SymbolKind.Function : void 0,
472
+ visibility: frame.callable ? "private" : "public",
473
+ metadata: { luaSpecific: true, isLocal: true, isTable, tableFields: isTable ? this.tableFieldNames(value) : [] }
474
+ });
475
+ }
476
+ }
477
+ /**
478
+ * M60 LUA-49: a for-clause BINDS a name, and the frame never heard of it.
479
+ *
480
+ * `extractAssignment` suppresses a local re-assignment with
481
+ * `frame.localNames.has(name)` — the one line standing between a re-assignment and
482
+ * a fabricated declaration. `localNames` was filled from exactly one place,
483
+ * `variable_declaration` (real-lua-analyzer.ts:649), so every OTHER binding form in
484
+ * the language fell through it. A generic-for clause is the common one:
485
+ *
486
+ * for _, line in ipairs(lines) do
487
+ * line = line:match("(.-)%%") or line <- a re-assignment
488
+ * end
489
+ *
490
+ * `line` is bound by the clause, so the statement above re-assigns a local and
491
+ * declares nothing. The analyzer had no record of the binding, published a
492
+ * `variable` SYMBOL for it, and `addContainsRelationships` then published
493
+ * `contains get_packages -> line` — a containment edge for a declaration that never
494
+ * happened, sitting on the assignment's line rather than the clause's.
495
+ *
496
+ * Found by the ORACLE rather than by reading: `lua-truth-oracle.ts` records
497
+ * for-clause names in `localsIn` (for its own LUA-26 exemption), so it correctly
498
+ * declined to derive the re-assignment, and the two disagreed. Four unexplained
499
+ * `contains` edges over ~600 corpus files, every one this shape:
500
+ * `contains get_packages -> line` (snacks.nvim/lua/snacks/image/doc.lua:131),
501
+ * `contains json -> item` (snacks.nvim/lua/snacks/picker/source/proc.lua:172),
502
+ * and one each in kong and luarocks.
503
+ *
504
+ * WHAT THIS DOES NOT DO, stated because the omission is the other half of the
505
+ * defect: the loop BINDING itself is still not published as a declaration. Lua
506
+ * binds `line` there, and neither a symbol nor a `contains` edge says so — the
507
+ * edge that used to appear was on the wrong line and for the wrong reason.
508
+ * Publishing the binding is the larger change and it is not made here: `for k, v in
509
+ * pairs(t)` is one of the most frequent constructs in Lua, so it is a symbol-count
510
+ * and payload decision for the whole graph surface, not a lane-local fix. Reported
511
+ * rather than done (§8 of the workstream, and limitation 14 of the doc).
512
+ *
513
+ * Scope: the name is recorded WITH THE LOOP'S END OFFSET, and the re-assignment
514
+ * test asks whether that loop is still lexically open at the assignment. That is
515
+ * not a refinement, it is the whole correctness of the rule: Lua scopes a loop
516
+ * variable to the loop body, so
517
+ *
518
+ * for _, v in ipairs(t) do v = f(v) end -- a RE-assignment
519
+ * v = 5 -- a DIFFERENT binding
520
+ *
521
+ * are different facts. Recording the name in `localNames` unconditionally, which is
522
+ * what the first version of this fix did, got the first line right and the second
523
+ * one wrong: it suppressed 18 real `contains` edges on kong alone. The oracle caught
524
+ * it as 18 MISSING, which is the oracle working — it derives the post-loop
525
+ * assignment because the loop really has closed.
526
+ *
527
+ * Lua has no block scope for locals — a `do ... end` does not open one — so the
528
+ * binding goes in the ENCLOSING FUNCTION's frame rather than a block frame, and the
529
+ * loop's own extent is what bounds it. At file scope `frame.callable` is undefined
530
+ * and `extractAssignment`'s existing `frame.callable &&` guard still lets a
531
+ * file-scope assignment declare a global, which is correct: after
532
+ * `for _, v in ...` ends at file scope, `v = 5` assigns a global.
533
+ */
534
+ registerLoopBindings(node, frame) {
535
+ const clause = node.namedChild(0);
536
+ if (!clause) return;
537
+ const end = node.endIndex;
538
+ const add = (n) => {
539
+ if (n?.type !== "identifier") return;
540
+ let list = frame.loopBindings;
541
+ if (!list) frame.loopBindings = list = [];
542
+ list.push({ name: n.text, endIndex: end });
543
+ };
544
+ if (clause.type === "for_numeric_clause") {
545
+ add(clause.childForFieldName("name"));
546
+ return;
547
+ }
548
+ if (clause.type !== "for_generic_clause") return;
549
+ for (let i = 0; i < clause.namedChildCount; i++) {
550
+ const list = clause.namedChild(i);
551
+ if (list?.type !== "variable_list") continue;
552
+ for (let j = 0; j < list.namedChildCount; j++) add(list.namedChild(j));
553
+ }
554
+ }
555
+ /** Is `name` bound by a for-clause whose loop is still open at byte offset `at`? */
556
+ inOpenLoopBinding(frame, name, at) {
557
+ const list = frame.loopBindings;
558
+ if (!list) return false;
559
+ for (let i = list.length - 1; i >= 0; i--) {
560
+ if (list[i].name === name && at < list[i].endIndex) return true;
561
+ }
562
+ return false;
563
+ }
564
+ extractAssignment(node, frame, ctx) {
565
+ if (node.parent?.type === "variable_declaration" && this.isLocalVariableDeclaration(node.parent)) return;
566
+ const variableList = this.childOfType(node, "variable_list");
567
+ if (!variableList) return;
568
+ const value = this.assignmentValue(node);
569
+ for (let i = 0; i < variableList.namedChildCount; i++) {
570
+ const variable = variableList.namedChild(i);
571
+ const name = this.plainVariableName(variable);
572
+ if (!variable || !name) continue;
573
+ if (frame.callable && frame.localNames.has(name) || this.inOpenLoopBinding(frame, name, node.startIndex)) continue;
574
+ const isTable = this.isTableNode(value);
575
+ if (isTable) ctx.declaredTables.add(name);
576
+ ctx.symbols.push({
577
+ name,
578
+ kind: ASTEngine.SymbolKind.Variable,
579
+ range: this.nodeToRange(variable),
580
+ scope: frame.callable || "global",
581
+ ownerName: frame.selfName ?? frame.owner ?? frame.callable,
582
+ frameSelfName: frame.selfName,
583
+ ownerKind: frame.callable ? ASTEngine.SymbolKind.Function : void 0,
584
+ visibility: frame.callable ? "private" : "public",
585
+ metadata: { luaSpecific: true, isGlobal: !frame.callable, isTable, tableFields: isTable ? this.tableFieldNames(value) : [] }
586
+ });
587
+ if (!frame.callable) ctx.globalVariables.add(name);
588
+ }
589
+ }
590
+ processCall(node, frame, ctx) {
591
+ const functionNode = node.childForFieldName("name") || node.namedChild(0);
592
+ if (!functionNode) return;
593
+ const callee = this.callee(functionNode);
594
+ if (!callee) return;
595
+ const callName = callee.name;
596
+ const qualified = functionNode.text;
597
+ this.detectFrameworks(qualified, ctx);
598
+ const values = this.callArguments(node);
599
+ const firstString = this.stringValue(values[0]);
600
+ if (callName === "require" || callName === "module") {
601
+ if (firstString) this.addImport(firstString, frame, node, ctx);
602
+ return;
603
+ }
604
+ if (callName === "pcall" || callName === "xpcall") {
605
+ if (values[0]?.text === "require") {
606
+ const moduleName = this.stringValue(values[1]);
607
+ if (moduleName) this.addImport(moduleName, frame, node, ctx);
608
+ }
609
+ }
610
+ const source = frame.callable || "global";
611
+ if (callName !== "pcall" && callName !== "xpcall") {
612
+ this.addRelationship(ctx, source, callName, ASTEngine.RelationshipType.Calls, { luaSpecific: true, ...callee.shapeMark }, callee.nameNode);
613
+ }
614
+ if (qualified === "coroutine.create" || qualified === "coroutine.wrap") {
615
+ ctx.patterns.add("Coroutines");
616
+ ctx.coroutineCalls++;
617
+ }
618
+ if (callName === "setmetatable") {
619
+ ctx.patterns.add("Object-Oriented Programming (Metatables)");
620
+ const target = values[0];
621
+ const metatable = values[1];
622
+ const srcName = this.nameBearing(target);
623
+ const mtName = this.nameBearing(metatable);
624
+ if (target && metatable && !this.isTableNode(target) && srcName && mtName && srcName !== mtName) {
625
+ this.addRelationship(ctx, srcName, mtName, ASTEngine.RelationshipType.Extends, { luaSpecific: true, relationshipType: "lua_metatable" }, metatable);
626
+ }
627
+ if (metatable?.type === "identifier") {
628
+ const tableName = metatable.text;
629
+ ctx.declaredTables.add(tableName);
630
+ }
631
+ }
632
+ const constructor = this.constructorTarget(functionNode, ctx);
633
+ if (constructor) {
634
+ this.addRelationship(ctx, source, constructor, ASTEngine.RelationshipType.Instantiates, { luaSpecific: true, relationshipType: "instantiates" }, callee.nameNode);
635
+ }
636
+ if (callName === "class" || qualified === "Object:extend" || qualified === "Object.extend") {
637
+ const className = firstString;
638
+ if (className) {
639
+ ctx.symbols.push({
640
+ name: className,
641
+ kind: ASTEngine.SymbolKind.Class,
642
+ range: this.nodeToRange(node),
643
+ scope: source,
644
+ ownerName: frame.selfName ?? frame.owner ?? frame.callable,
645
+ frameSelfName: frame.selfName,
646
+ metadata: { luaSpecific: true, isLibraryClass: true }
647
+ });
648
+ }
649
+ ctx.patterns.add("Object-Oriented Programming (Libraries)");
650
+ }
651
+ }
652
+ addImport(moduleName, frame, node, ctx) {
653
+ ctx.imports.push({
654
+ module: moduleName,
655
+ items: [],
656
+ isDefaultImport: true,
657
+ isNamespaceImport: false,
658
+ range: this.nodeToRange(node),
659
+ metadata: { luaSpecific: true }
660
+ });
661
+ const fileClaim = this.moduleFileClaim(moduleName, ctx);
662
+ this.addRelationship(ctx, frame.callable || "global", moduleName, ASTEngine.RelationshipType.Imports, {
663
+ luaSpecific: true,
664
+ relationshipType: "lua_require",
665
+ ...fileClaim !== null ? { importProjectFile: fileClaim } : {}
666
+ }, node);
667
+ if (moduleName.startsWith("resty")) ctx.frameworks.add("OpenResty");
668
+ if (moduleName === "socket" || moduleName === "lfs" || moduleName === "cjson" || moduleName === "ssl") ctx.frameworks.add("LuaRocks Core Modules");
669
+ if (moduleName === "lapis") ctx.frameworks.add("Lapis");
670
+ if (moduleName === "middleclass" || moduleName === "classic" || moduleName === "30log") ctx.patterns.add("Object-Oriented Programming (Libraries)");
671
+ if (moduleName === "hs" || moduleName.startsWith("hs.")) ctx.frameworks.add("Hammerspoon");
672
+ if (moduleName.startsWith("lovr")) ctx.frameworks.add("L\xD6VR (VR/AR)");
673
+ }
674
+ /**
675
+ * THE FILE CLAIM for one module name, or null when no candidate can be confirmed.
676
+ *
677
+ * WHAT IT ASSERTS, in the exact words of the guard's own contract: the project carries an admitted
678
+ * `.lua` file that this `require` names. It asserts NOTHING about symbols, so it can never raise
679
+ * `resolved`, and it is written ONLY where the guard confirmed a candidate on disk — a claim the
680
+ * project would refute is worse than no claim, because the gate checks a claim BEFORE the count.
681
+ *
682
+ * WHEN IT IS SILENT, and both silences are deliberate:
683
+ * * NO ROOT, NO CLAIM. `analyze` without an absolute `projectRoot` (the fixture runner, a
684
+ * one-off parse) states no claim at all rather than guessing a base. A caller that wants the
685
+ * claim must hand the analyzer the project it is reading inside;
686
+ * * NO CANDIDATE, NO CLAIM. `require("luarocks.fs")`, `require("tap")` and the rest of the
687
+ * residue name files this corpus does not carry — those edges keep their target and stay
688
+ * unresolved, which is TRUE, rather than being marked external to make a column smaller.
689
+ *
690
+ * The candidate arithmetic is `luaModuleFileCandidates`; the ADMITTED-EXTENSION clause, the
691
+ * inside-the-project clause, the size cap and the `statSync` are the shared guard's, so this lane
692
+ * cannot relax them by editing its own spelling of them.
693
+ */
694
+ moduleFileClaim(moduleName, ctx) {
695
+ const { projectRoot, filePath } = ctx;
696
+ if (projectRoot === void 0 || filePath === void 0) return null;
697
+ const cached = ctx.moduleFileClaims?.get(moduleName);
698
+ if (cached !== void 0) return cached;
699
+ const fileRel = path.relative(projectRoot, filePath).split(path.sep).join("/");
700
+ const claim = fileClaimForTarget({
701
+ projectRoot,
702
+ filePath,
703
+ // THE TARGET AS WRITTEN. The guard folds it itself before the extras below; the claim is a
704
+ // statement ABOUT this string and never a rewrite of it.
705
+ written: moduleName,
706
+ // THE LANE'S OWN ADMITTED LIST out of the router: lua admits `.lua` alone, so a candidate of
707
+ // any other extension can never verify while a miss still refutes — which is why this clause
708
+ // is the guard's and not this file's.
709
+ admittedExtensions: ["lua"],
710
+ extraCandidates: luaModuleFileCandidates(moduleName, fileRel),
711
+ // The indexer's own admission cap: a file above it is not in the population a claim is
712
+ // checked against, so a claim about it would be REFUTED rather than unverified.
713
+ maxFileSizeBytes: FILE_CLAIM_MAX_BYTES
714
+ });
715
+ ctx.moduleFileClaims?.set(moduleName, claim);
716
+ return claim;
717
+ }
718
+ // M60 LUA-06: this was ORDER-DEPENDENT, and therefore wrong. `declaredTables`
719
+ // was filled during the same single traversal that read it, so whether a
720
+ // constructor call produced an edge depended on whether the table's
721
+ // declaration happened to appear EARLIER IN THE FILE. Proven by probe, with
722
+ // the two files differing only in statement order:
723
+ //
724
+ // local A = {} A.new()
725
+ // function A.new() end local A = {}
726
+ // A.new() function A.new() end
727
+ //
728
+ // first -> instantiates global->A (edge present)
729
+ // second -> NO EDGE (same semantics, no edge)
730
+ //
731
+ // A relationship that depends on where its declaration sits is not a fact
732
+ // about the program, it is a fact about the traversal. The fix is a PRE-PASS:
733
+ // collect every name this file binds to a table, before any call is examined.
734
+ // One traversal, order-independent.
735
+ constructorTarget(functionNode, ctx) {
736
+ if (!["dot_index_expression", "method_index_expression"].includes(functionNode.type)) return void 0;
737
+ const method = functionNode.childForFieldName("method") || functionNode.childForFieldName("field");
738
+ const table = functionNode.childForFieldName("table");
739
+ if ((method?.text === "new" || method?.text === "constructor") && table?.text && ctx.declaredTables.has(table.text)) return table.text;
740
+ return void 0;
741
+ }
742
+ // M60 LUA-06: collect the file's table-valued names BEFORE the walk, so a
743
+ // constructor call does not depend on the order of the declaration.
744
+ collectDeclaredTables(root, ctx) {
745
+ const stack = [root];
746
+ while (stack.length > 0) {
747
+ const node = stack.pop();
748
+ if (node.type === "variable_declaration" || node.type === "assignment_statement") {
749
+ const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
750
+ const list = inner?.type === "assignment_statement" ? inner.namedChild(0) : void 0;
751
+ const values = inner?.type === "assignment_statement" ? inner.namedChild(1) : void 0;
752
+ const first = values?.namedChild(0);
753
+ if (list?.type === "variable_list" && first?.type === "table_constructor") {
754
+ for (let i = 0; i < list.namedChildCount; i++) {
755
+ const id = list.namedChild(i);
756
+ if (id?.type === "identifier") ctx.declaredTables.add(id.text);
757
+ }
758
+ }
759
+ }
760
+ if (node.type === "function_declaration") {
761
+ const nm = node.childForFieldName("name");
762
+ if (nm && (nm.type === "dot_index_expression" || nm.type === "method_index_expression")) {
763
+ const recv = nm.namedChild(0);
764
+ if (recv?.type === "identifier") ctx.declaredTables.add(recv.text);
765
+ }
766
+ }
767
+ for (let i = 0; i < node.namedChildCount; i++) {
768
+ const c = node.namedChild(i);
769
+ if (c) stack.push(c);
770
+ }
771
+ }
772
+ }
773
+ /**
774
+ * M60 LUA-44: derived table -> the tables its member lookup falls back to.
775
+ *
776
+ * Lua has NO inheritance keyword, so a grammar inventory can never find this
777
+ * relation — it is a CONVENTION over three ordinary constructs, and the
778
+ * ontology-coverage gate's own json ruling says in as many words that a grammar
779
+ * cannot see a convention. All three forms are read here because all three mean
780
+ * the same thing for an override:
781
+ *
782
+ * setmetatable(D, B) D's members fall back to B's (direct)
783
+ * setmetatable(D, {__index = B}) the same, through an inline metatable
784
+ * local Mt = {__index = B}; setmetatable(D, Mt) the same, through a named
785
+ * metatable — kong's idiom, and the shape
786
+ * probe B2 actually found
787
+ * D.__index = B the same, assigned after the fact
788
+ *
789
+ * The named-metatable form needs TWO HOPS and is why this method does not simply
790
+ * read the second argument: `setmetatable(_lmdb, _lmdb_mt)` names a METATABLE,
791
+ * and reading it as a supertype would make every metatable in the corpus look
792
+ * like a base class. Resolving `Mt -> Mt.__index -> Base` is the difference
793
+ * between the two, and it is a derivation over parse facts rather than a guess:
794
+ * both hops are written in the file.
795
+ *
796
+ * The third form is deliberately absent from the B2 probe in
797
+ * script/m60/lua-ontology-census.ts, which reads only the inline
798
+ * `setmetatable` forms and therefore UNDER-counts (1 occurrence). That probe is
799
+ * the falsifiable instrument for the ruling; this is the analyzer's wider read,
800
+ * and the gap between them is disclosed rather than reconciled by widening the
801
+ * probe until the numbers agree.
802
+ */
803
+ collectSupertypeLinks(root, ctx) {
804
+ const link = (derived, base) => {
805
+ if (!derived || !base || derived === base) return;
806
+ if (ctx.declaredTables.has(derived) || ctx.declaredTables.has(base)) {
807
+ const set = ctx.supertypeLinks.get(derived) ?? /* @__PURE__ */ new Set();
808
+ set.add(base);
809
+ ctx.supertypeLinks.set(derived, set);
810
+ }
811
+ };
812
+ const indexField = (table) => {
813
+ if (table?.type !== "table_constructor") return void 0;
814
+ for (let i = 0; i < table.namedChildCount; i++) {
815
+ const f = table.namedChild(i);
816
+ if (f?.type !== "field") continue;
817
+ const key = f.childForFieldName("name") ?? f.namedChild(0);
818
+ const value = f.namedChild(1);
819
+ if (key?.text === "__index" && value?.type === "identifier") return value.text;
820
+ }
821
+ return void 0;
822
+ };
823
+ const metatableIndex = /* @__PURE__ */ new Map();
824
+ const pending = [];
825
+ const stack = [root];
826
+ while (stack.length > 0) {
827
+ const node = stack.pop();
828
+ if (node.type === "function_call") {
829
+ const fn = node.childForFieldName("name");
830
+ const args = node.childForFieldName("arguments");
831
+ if (fn?.type === "identifier" && (fn.text === "setmetatable" || fn.text === "setmetatable2")) {
832
+ const a0 = args?.namedChild(0) ?? null;
833
+ pending.push({ derived: a0?.type === "identifier" ? a0.text : void 0, second: args?.namedChild(1) ?? null });
834
+ }
835
+ }
836
+ if (node.type === "variable_declaration" || node.type === "assignment_statement") {
837
+ const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
838
+ if (inner?.type === "assignment_statement") {
839
+ const list = inner.namedChild(0);
840
+ const values = inner.namedChild(1);
841
+ const table = values?.type === "expression_list" ? values.namedChild(0) : void 0;
842
+ const base = indexField(table);
843
+ if (base && list?.type === "variable_list") {
844
+ for (let i = 0; i < list.namedChildCount; i++) {
845
+ const id = list.namedChild(i);
846
+ if (id?.type === "identifier") metatableIndex.set(id.text, base);
847
+ }
848
+ }
849
+ }
850
+ }
851
+ for (let i = 0; i < node.namedChildCount; i++) {
852
+ const c = node.namedChild(i);
853
+ if (c) stack.push(c);
854
+ }
855
+ }
856
+ for (const site of pending) {
857
+ const second = site.second;
858
+ if (second?.type === "identifier") {
859
+ link(site.derived, metatableIndex.get(second.text) ?? second.text);
860
+ } else {
861
+ const base = indexField(second);
862
+ if (base) link(site.derived, base);
863
+ }
864
+ }
865
+ const stack2 = [root];
866
+ while (stack2.length > 0) {
867
+ const node = stack2.pop();
868
+ if (node.type === "assignment_statement") {
869
+ const list = node.namedChild(0);
870
+ const values = node.namedChild(1);
871
+ const lhs = list?.type === "variable_list" ? list.namedChild(0) ?? null : null;
872
+ const rhs = values?.type === "expression_list" ? values.namedChild(0) ?? null : null;
873
+ if ((lhs?.type === "dot_index_expression" || lhs?.type === "method_index_expression") && lhs.namedChild(lhs.namedChildCount - 1)?.text === "__index" && rhs?.type === "identifier") {
874
+ const receiver = lhs.namedChild(0);
875
+ link(receiver?.type === "identifier" ? receiver.text : void 0, rhs.text);
876
+ }
877
+ }
878
+ for (let i = 0; i < node.namedChildCount; i++) {
879
+ const c = node.namedChild(i);
880
+ if (c) stack2.push(c);
881
+ }
882
+ }
883
+ }
884
+ /**
885
+ * M60 LUA-51 — a WRITTEN supertype CLAUSE, expressed as an ordinary call argument.
886
+ *
887
+ * MEASURED BEFORE THIS METHOD EXISTED (script/m60/lua-supertype-arg-probe.ts, over
888
+ * the 3,250 admitted corpus files): 7 written supertype arguments across 5 files,
889
+ * of which 2 are inheritance and neither was emitted. The two:
890
+ *
891
+ * luarocks/vendor/argparse.lua:277 local Command = class({...}, {...}, Parser)
892
+ * luarocks/vendor/argparse.lua:328 local Option = class({...}, {...}, Argument)
893
+ *
894
+ * `extends` read only `setmetatable` (real-lua-analyzer.ts:924) and the `class`
895
+ * branch read only `firstString`, the class's own NAME — so a parent passed in ANY
896
+ * argument position was invisible.
897
+ *
898
+ * ## Why the position is READ and not GUESSED
899
+ *
900
+ * The obvious implementation is "the third argument of a `class` call is the
901
+ * parent", and it is wrong in a way the corpus demonstrates: middleclass's own form
902
+ * is `class("Name", Super)` — second position — and luarocks' is
903
+ * `class(prototype, properties, parent)` — third. A fixed index publishes a wrong
904
+ * edge for one of them and misses the other.
905
+ *
906
+ * So the position comes from the CALLEE'S OWN PARAMETER LIST, which the same file
907
+ * writes down:
908
+ *
909
+ * local function class(prototype, properties, parent)
910
+ * 0 1 2 <- the parent slot
911
+ *
912
+ * A parameter whose NAME states the supertype role (`parent`, `super`, `base`,
913
+ * `supertype`, `superclass`, …) identifies its own slot. If the file does not
914
+ * declare such a parameter there is nothing to read, and no edge is published — so
915
+ * the rule cannot invent a relation from a call that never claimed one.
916
+ *
917
+ * ## Why this is `extends` and not `implements`
918
+ *
919
+ * Because the callee's body composes the parent into the object it returns:
920
+ *
921
+ * if parent then cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
922
+ *
923
+ * The new class's prototype is built FROM the parent's. That is inheritance, and it
924
+ * is the same relation the `setmetatable` forms already publish, so it carries the
925
+ * same label and feeds the same `supertypeLinks` map that `overrides` reads.
926
+ *
927
+ * ## Confidence
928
+ *
929
+ * 0.95, `usage_contextual` — NOT 1.0 and NOT `structural`, and deliberately so.
930
+ * Both endpoint NAMES are written in the file, which is what earns `extends` 1.0
931
+ * elsewhere; what is a convention rather than a fact is the ROLE: that a parameter
932
+ * named `parent` means the supertype. `overrides`' `lua_inherited_member` takes the
933
+ * same 0.95 for the same reason and cites the same family precedent
934
+ * (real-objective-c-analyzer.ts:1020). The corpus shows the cost of reading it as
935
+ * certain: `snacks.nvim/lua/snacks/layout.lua` and three other files declare a
936
+ * `parent` parameter for TREE TRAVERSAL (`_each(widget, parent)`,
937
+ * `add(result, parent)`), and the probe returns all 5 of those alongside the 2 real
938
+ * ones.
939
+ *
940
+ * So the read is narrowed to the case the corpus proves is inheritance: the factory
941
+ * must be ASSIGNED to a top-level name, because `local Command = class(...)` is what
942
+ * makes the edge's SOURCE a name a consumer can follow, and the derived and base
943
+ * names must differ.
944
+ */
945
+ collectFactorySupertypeLinks(root, ctx) {
946
+ const SUPERTYPE_ROLES = /* @__PURE__ */ new Set([
947
+ "parent",
948
+ "super",
949
+ "base",
950
+ "supertype",
951
+ "superclass",
952
+ "super_class",
953
+ "parentclass",
954
+ "parent_class"
955
+ ]);
956
+ const paramNames = (params) => {
957
+ const out = [];
958
+ if (!params) return out;
959
+ for (let i = 0; i < params.namedChildCount; i++) {
960
+ const c = params.namedChild(i);
961
+ if (!c) continue;
962
+ if (c.type === "identifier") out.push(c.text);
963
+ else if (c.type === "vararg_expression") continue;
964
+ else out.push("");
965
+ }
966
+ return out;
967
+ };
968
+ const foldsIntoMetamethod = (fn, param) => {
969
+ let found = false;
970
+ const isRole = (n) => n?.type === "identifier" && n.text === param;
971
+ const visit = (n) => {
972
+ if (found) return;
973
+ if (n.type === "dot_index_expression" || n.type === "method_index_expression") {
974
+ if (isRole(n.namedChild(0))) {
975
+ const last = n.namedChild(n.namedChildCount - 1);
976
+ if (last?.text?.startsWith("__")) {
977
+ found = true;
978
+ return;
979
+ }
980
+ }
981
+ }
982
+ if (n.type === "assignment_statement") {
983
+ const lhs = n.namedChild(0);
984
+ const lhsNode = lhs?.type === "variable_list" ? lhs.namedChild(0) : void 0;
985
+ if (lhsNode && (lhsNode.type === "dot_index_expression" || lhsNode.type === "method_index_expression")) {
986
+ const field = lhsNode.namedChild(lhsNode.namedChildCount - 1);
987
+ const rhs = n.namedChild(1);
988
+ const rhsNode = rhs?.type === "expression_list" ? rhs.namedChild(0) : void 0;
989
+ if (field?.text?.startsWith("__") && isRole(rhsNode)) {
990
+ found = true;
991
+ return;
992
+ }
993
+ }
994
+ }
995
+ for (let i = 0; i < n.namedChildCount; i++) {
996
+ const c = n.namedChild(i);
997
+ if (c) visit(c);
998
+ }
999
+ };
1000
+ const body = fn.childForFieldName("body") ?? fn;
1001
+ for (let i = 0; i < body.namedChildCount; i++) {
1002
+ const c = body.namedChild(i);
1003
+ if (c) visit(c);
1004
+ }
1005
+ return found;
1006
+ };
1007
+ const factoryParams = /* @__PURE__ */ new Map();
1008
+ const stack0 = [root];
1009
+ while (stack0.length > 0) {
1010
+ const node = stack0.pop();
1011
+ if (node.type === "function_declaration" || node.type === "function_definition") {
1012
+ const nameNode = node.childForFieldName("name");
1013
+ if (nameNode?.type === "identifier" && nameNode.text && !factoryParams.has(nameNode.text)) {
1014
+ const ps = paramNames(node.childForFieldName("parameters"));
1015
+ if (ps.some((p) => SUPERTYPE_ROLES.has(p.toLowerCase()) && foldsIntoMetamethod(node, p))) {
1016
+ factoryParams.set(nameNode.text, ps);
1017
+ }
1018
+ }
1019
+ }
1020
+ for (let i = 0; i < node.namedChildCount; i++) {
1021
+ const c = node.namedChild(i);
1022
+ if (c) stack0.push(c);
1023
+ }
1024
+ }
1025
+ if (factoryParams.size === 0) return;
1026
+ const stack1 = [root];
1027
+ while (stack1.length > 0) {
1028
+ const node = stack1.pop();
1029
+ if (node.type === "variable_declaration" || node.type === "assignment_statement") {
1030
+ const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
1031
+ const call = inner?.type === "assignment_statement" ? inner.namedChild(1) : node;
1032
+ const value = call?.type === "expression_list" ? call.namedChild(0) : void 0;
1033
+ if (value?.type === "function_call" && inner) {
1034
+ const callee = value.childForFieldName("name");
1035
+ const args = value.childForFieldName("arguments");
1036
+ if (callee?.type === "identifier" && args) {
1037
+ const ps = factoryParams.get(callee.text);
1038
+ if (ps) {
1039
+ const list = inner.namedChild(0);
1040
+ const derived = list?.type === "variable_list" ? list.namedChild(0) : void 0;
1041
+ const derivedName = derived?.type === "identifier" ? derived.text : void 0;
1042
+ ps.forEach((p, slot) => {
1043
+ if (!SUPERTYPE_ROLES.has(p.toLowerCase())) return;
1044
+ const argNode = args.namedChild(slot);
1045
+ if (argNode?.type !== "identifier") return;
1046
+ const baseName = argNode.text;
1047
+ if (!derivedName || derivedName === baseName) return;
1048
+ const existing = ctx.supertypeLinks.get(derivedName) ?? /* @__PURE__ */ new Set();
1049
+ existing.add(baseName);
1050
+ ctx.supertypeLinks.set(derivedName, existing);
1051
+ ctx.factorySupertypeLines.set(`${derivedName}|${baseName}`, {
1052
+ derived: derivedName,
1053
+ base: baseName,
1054
+ line: argNode.startPosition.row + 1
1055
+ });
1056
+ });
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ for (let i = 0; i < node.namedChildCount; i++) {
1062
+ const c = node.namedChild(i);
1063
+ if (c) stack1.push(c);
1064
+ }
1065
+ }
1066
+ }
1067
+ /**
1068
+ * M60 LUA-51 — publish the `extends` edge for a factory supertype clause.
1069
+ *
1070
+ * `extends` was emitted only from the `setmetatable` call site, so a link collected
1071
+ * by `collectFactorySupertypeLinks` reached `overrides` and nothing else. This runs
1072
+ * beside it and emits the same relation, deduplicated against the `extends` edges
1073
+ * already in the result so a link both readers find is published once.
1074
+ *
1075
+ * It iterates `factorySupertypeLines` and NOT `supertypeLinks`, and that is the whole
1076
+ * correction. The first version walked `supertypeLinks`, which is the union of THREE
1077
+ * conventions, and so published the third one — `D.__index = Base` — under this
1078
+ * method's `lua_factory_supertype` label. Measured on the file that exposed it:
1079
+ *
1080
+ * luarocks/vendor/argparse.lua:99 class_metatable.__index = parent
1081
+ *
1082
+ * produced `class_metatable -[extends 0.95 lua_factory_supertype]-> parent` — a link
1083
+ * the walk had deliberately never published, carrying a label that says "factory" for
1084
+ * a construct with no factory in it. `factorySupertypeLines` is keyed only by links
1085
+ * `collectFactorySupertypeLinks` created, so iterating it cannot reach the other two.
1086
+ */
1087
+ addFactorySupertypeEdges(ctx) {
1088
+ const seen = /* @__PURE__ */ new Set();
1089
+ for (const e of ctx.relationships) {
1090
+ if (e.type !== ASTEngine.RelationshipType.Extends) continue;
1091
+ seen.add(`${e.source}|${e.target}`);
1092
+ }
1093
+ for (const [key, link] of ctx.factorySupertypeLines) {
1094
+ const { derived, base, line } = link;
1095
+ if (seen.has(key)) continue;
1096
+ seen.add(key);
1097
+ this.addRelationship(
1098
+ ctx,
1099
+ derived,
1100
+ base,
1101
+ ASTEngine.RelationshipType.Extends,
1102
+ { luaSpecific: true, relationshipType: "lua_factory_supertype", evidenceClass: "usage_contextual" },
1103
+ null,
1104
+ { confidence: 0.95, symbolRange: { startLine: line } }
1105
+ );
1106
+ }
1107
+ }
1108
+ /**
1109
+ * `decorates` — THE LUA DECORATOR IDIOM, and the measurement that made it a
1110
+ * relation instead of a ruling.
1111
+ *
1112
+ * ## What the construct is
1113
+ *
1114
+ * Lua has no annotation syntax. Its decorator idiom is the PYTHON DESUGARING
1115
+ * WRITTEN OUT: a declared function is wrapped by a declared higher-order
1116
+ * function and rebound to the same name at file scope.
1117
+ *
1118
+ * function fib(n) ... end
1119
+ * function cache(f) -- a WRAPPER FACTORY: returns a function
1120
+ * local c = {}
1121
+ * return function(x) ... end
1122
+ * end
1123
+ * fib = cache(fib) -- redis/deps/lua/test/fib.lua:39
1124
+ *
1125
+ * `@cache def fib` in Python compiles to exactly this sequence — the function
1126
+ * object is created, then `cache` is applied to it, then the name is rebound. So
1127
+ * the relation is a real runtime application to a DECLARATION, and the edge is
1128
+ * `fib -> cache` (the vocabulary's direction: decorated declaration -> decorator).
1129
+ *
1130
+ * ## Why the discriminator is structural, and what each clause excludes
1131
+ *
1132
+ * MEASURED over the whole admitted population (3,255 tracked files, artifact
1133
+ * validation/m14/measurements/lua-extended-census.json): 1,185 `X = f(X)` sites;
1134
+ * 186 whose name is declared somewhere in the file; 3 at file scope; **1** that is
1135
+ * a decoration. The clauses below are what separate that one from the other 1,184:
1136
+ *
1137
+ * FILE SCOPE ONLY. A decorator is applied while the declaration is created, and
1138
+ * in Lua that is chunk scope. An in-function `X = f(X)` is a value assignment —
1139
+ * measured class: `value = sub(value, 2, -2)`, `payload = json_encode(payload)`.
1140
+ * Publishing those would attach a decoration to a local parameter.
1141
+ *
1142
+ * X IS A DECLARED FUNCTION. The decorated endpoint must be a declaration this
1143
+ * file writes (`function X(...)`, including `local function X`). A rebinding of
1144
+ * a table or a scalar is not a declaration being decorated.
1145
+ *
1146
+ * f IS A DECLARED WRAPPER FACTORY. `f`'s own body must RETURN a function
1147
+ * definition — that is the structural evidence it is a decorator and not a
1148
+ * transform. `cache` returns `function(x) ... end`; `sub`, `concat`,
1149
+ * `json_encode` return values. This clause is the whole difference between the
1150
+ * 185 value rebindings and the 1 decoration, and a name-based filter would not
1151
+ * find it (the dart lane's lesson: a filter on the NAME does not separate the
1152
+ * construct from its impostors).
1153
+ *
1154
+ * THE ARGUMENT IS THE DECORATED NAME. `f(X)` with the same identifier that is
1155
+ * rebound; `fib = cache(x)` decorates `x`'s value, not `fib`.
1156
+ *
1157
+ * ## Confidence and tier
1158
+ *
1159
+ * 0.95 and `usage_contextual`, NOT 1.0 and NOT `structural`. The application is a
1160
+ * real call site and both names are written in the file; what is a CONVENTION is
1161
+ * the READING — that a wrapper-factory application to a declaration is a
1162
+ * decoration rather than a memoization rebinding. Same number and tier this
1163
+ * analyzer already assigns its two other convention edges (`lua_factory_supertype`,
1164
+ * `lua_inherited_member`), and the same reason: "a 0.95 edge must not claim
1165
+ * `structural`" (see `addFactorySupertypeEdges`).
1166
+ */
1167
+ extractDecoratorApplication(node, frame, ctx) {
1168
+ if (frame.inFunction) return;
1169
+ const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
1170
+ if (inner?.type !== "assignment_statement") return;
1171
+ const list = this.childOfType(inner, "variable_list");
1172
+ const values = this.childOfType(inner, "expression_list");
1173
+ const lhs = list?.namedChild(0);
1174
+ const call = values?.namedChild(0);
1175
+ if (lhs?.type !== "identifier" || call?.type !== "function_call") return;
1176
+ const callee = call.childForFieldName("name");
1177
+ if (callee?.type !== "identifier") return;
1178
+ const args = call.childForFieldName("arguments");
1179
+ const arg0 = args?.namedChild(0);
1180
+ if (arg0?.type !== "identifier" || arg0.text !== lhs.text) return;
1181
+ if (callee.text === lhs.text) return;
1182
+ if (!ctx.declaredFunctions.has(lhs.text)) return;
1183
+ if (!ctx.wrapperFactories.has(callee.text)) return;
1184
+ this.addRelationship(
1185
+ ctx,
1186
+ lhs.text,
1187
+ callee.text,
1188
+ ASTEngine.RelationshipType.Decorates,
1189
+ {
1190
+ luaSpecific: true,
1191
+ relationshipType: "lua_decorator_application",
1192
+ decorator: callee.text,
1193
+ evidenceClass: "usage_contextual"
1194
+ },
1195
+ call,
1196
+ { confidence: 0.95 }
1197
+ );
1198
+ }
1199
+ /**
1200
+ * THE PRE-PASS for `decorates`: the file's declared function names and, of those,
1201
+ * the ones whose body RETURNS a function (wrapper factories).
1202
+ *
1203
+ * Order-independent by construction, for the same reason `collectDeclaredTables`
1204
+ * and `collectSupertypeLinks` are: a file may apply a factory declared below the
1205
+ * site, and a relation that depends on traversal order is a fact about the
1206
+ * traversal, not the program.
1207
+ *
1208
+ * `function X()` and `local function X()` both parse as `function_declaration` with
1209
+ * an `identifier` name (verified on the pinned build), so both are declarations for
1210
+ * this purpose. A `function M.x()` binds a FIELD — reachable only through the
1211
+ * table and not a bare name a decoration site could rebind — and is not collected.
1212
+ */
1213
+ collectDecoratorFacts(root, ctx) {
1214
+ const stack = [root];
1215
+ while (stack.length > 0) {
1216
+ const node = stack.pop();
1217
+ if (node.type === "function_declaration") {
1218
+ const nm = node.childForFieldName("name");
1219
+ if (nm?.type === "identifier") {
1220
+ ctx.declaredFunctions.add(nm.text);
1221
+ if (this.returnsFunctionDefinition(node)) ctx.wrapperFactories.add(nm.text);
1222
+ }
1223
+ }
1224
+ for (let i = 0; i < node.namedChildCount; i++) {
1225
+ const c = node.namedChild(i);
1226
+ if (c) stack.push(c);
1227
+ }
1228
+ }
1229
+ }
1230
+ /**
1231
+ * Does this function's own body RETURN a function definition, at ITS OWN level?
1232
+ *
1233
+ * The walk stops at a nested function body, because a return inside it belongs to
1234
+ * that inner function. `function cache(f) ... return function(x) ... end end`
1235
+ * answers true; `function sub(s, i) return s end` answers false; a factory whose
1236
+ * inner wrapper itself returns a value (`return function(x) return f(x) end end`)
1237
+ * answers true from the OUTER return, which is the one that matters.
1238
+ */
1239
+ returnsFunctionDefinition(fn) {
1240
+ const body = fn.childForFieldName("body");
1241
+ if (!body) return false;
1242
+ const visit = (n) => {
1243
+ if (n.type === "return_statement") {
1244
+ const list = this.childOfType(n, "expression_list");
1245
+ const first = list?.namedChild(0) ?? n.namedChild(0);
1246
+ if (first && (first.type === "function_definition" || first.type === "function_declaration")) return true;
1247
+ return false;
1248
+ }
1249
+ if (n.type === "function_definition" || n.type === "function_declaration") return false;
1250
+ for (let i = 0; i < n.namedChildCount; i++) {
1251
+ const c = n.namedChild(i);
1252
+ if (c && visit(c)) return true;
1253
+ }
1254
+ return false;
1255
+ };
1256
+ for (let i = 0; i < body.namedChildCount; i++) {
1257
+ const c = body.namedChild(i);
1258
+ if (c && visit(c)) return true;
1259
+ }
1260
+ return false;
1261
+ }
1262
+ extractExport(node, frame, ctx) {
1263
+ if (frame.inFunction) return;
1264
+ const values = this.childOfType(node, "expression_list");
1265
+ const target = values?.namedChild(0);
1266
+ if (!target) return;
1267
+ if (target.type === "identifier") {
1268
+ ctx.exports.push({ name: target.text, isDefault: true, range: this.nodeToRange(node), metadata: { luaSpecific: true } });
1269
+ this.addExportEdge(ctx, target.text, target);
1270
+ return;
1271
+ }
1272
+ if (target.type === "function_call") {
1273
+ const nameNode = target.childForFieldName("name") ?? target.namedChild(0);
1274
+ const calleeName = nameNode ? this.callee(nameNode)?.name : void 0;
1275
+ if (calleeName === "setmetatable") {
1276
+ const first = this.callArgument(target, 0);
1277
+ const named = this.nameBearing(first);
1278
+ if (named) {
1279
+ ctx.exports.push({ name: named, isDefault: true, range: this.nodeToRange(first), metadata: { luaSpecific: true, viaMetatable: true } });
1280
+ this.addExportEdge(ctx, named, first ?? null);
1281
+ } else {
1282
+ ctx.unpublishedSurface.push({ shape: "call-result", detail: calleeName ?? null, line: target.startPosition.row + 1, why: "setmetatable's first argument is an expression, so the surface has no name" });
1283
+ }
1284
+ } else if (calleeName === "require") {
1285
+ const arg = this.callArgument(target, 0);
1286
+ const path2 = arg?.type === "string" ? this.stringValue(arg) : null;
1287
+ ctx.unpublishedSurface.push({ shape: "re-export", detail: path2 ?? null, line: target.startPosition.row + 1, why: "the surface is another module's, not this module's" });
1288
+ } else {
1289
+ ctx.unpublishedSurface.push({ shape: "call-result", detail: calleeName ?? null, line: target.startPosition.row + 1, why: "the result of a call is statically unknowable" });
1290
+ }
1291
+ return;
1292
+ }
1293
+ if (target.type === "string") {
1294
+ ctx.unpublishedSurface.push({ shape: "value-literal", detail: this.stringValue(target) ?? null, line: target.startPosition.row + 1, why: "a value is not a name" });
1295
+ return;
1296
+ }
1297
+ if (target.type === "function_definition") {
1298
+ ctx.unpublishedSurface.push({ shape: "anonymous-function", detail: null, line: target.startPosition.row + 1, why: "an anonymous function has no name" });
1299
+ return;
1300
+ }
1301
+ if (target.type === "table_constructor") {
1302
+ for (let i = 0; i < target.namedChildCount; i++) {
1303
+ const f = target.namedChild(i);
1304
+ if (!f || f.type !== "field") continue;
1305
+ const key = f.childForFieldName("name");
1306
+ if (!key) continue;
1307
+ let name;
1308
+ if (key.type === "identifier") name = key.text;
1309
+ else if (key.type === "string") name = this.stringValue(key);
1310
+ if (!name) continue;
1311
+ ctx.exports.push({ name, isDefault: false, range: this.nodeToRange(key), metadata: { luaSpecific: true } });
1312
+ this.addExportEdge(ctx, name, key);
1313
+ }
1314
+ }
1315
+ }
1316
+ /** M60 LUA-42: the `exports` edge for one nameable module-surface name.
1317
+ *
1318
+ * Confidence 1.0 and `evidenceClass: "structural"`, because both halves of the
1319
+ * claim are written in the file: a `return` at chunk scope names this name. That
1320
+ * is the difference from `uses`/`references` (LUA-40/LUA-41), where the name is
1321
+ * written but the BINDING it denotes is not — the reason those two carry 0.9
1322
+ * and those two only.
1323
+ */
1324
+ addExportEdge(ctx, name, nameNode) {
1325
+ this.addRelationship(
1326
+ ctx,
1327
+ "global",
1328
+ name,
1329
+ ASTEngine.RelationshipType.Exports,
1330
+ { luaSpecific: true, relationshipType: "lua_module_return" },
1331
+ nameNode
1332
+ );
1333
+ }
1334
+ /**
1335
+ * M60 LUA-44 — `overrides`, the type this lane was missing.
1336
+ *
1337
+ * MEASURED BEFORE THIS METHOD EXISTED, on 3,250 corpus files through
1338
+ * `parseFile` (script/m60/lua-ontology-census.ts):
1339
+ *
1340
+ * B1-override-redefinition 65 occurrences / 37 files
1341
+ * B2-override-inherited 1 occurrence / 1 file
1342
+ *
1343
+ * Both non-zero, so `overrides` could NOT be ruled and had to be built. That is
1344
+ * the whole reason this method exists: the ontology-coverage gate refuses a
1345
+ * ruling whose probe finds a non-zero count, and the lua grammar's 100 node
1346
+ * types include no `override_specifier` and no `redefinition` node, so no
1347
+ * grammar inventory would ever have found it — instrument A is silent here by
1348
+ * construction, and only instrument B could falsify the ruling.
1349
+ *
1350
+ * TWO SHAPES, two labels, because Lua expresses two different things and fusing
1351
+ * them would state one of them falsely:
1352
+ *
1353
+ * lua_redefinition the SAME name declared twice in one chunk. The later
1354
+ * declaration replaces the earlier one. 1.0, structural —
1355
+ * both endpoints are nodes this parse saw, and the
1356
+ * `overridesLine` metadata carries the prior declaration's
1357
+ * line so the two are distinguishable, exactly as
1358
+ * real-bash-analyzer.ts:343 does for the same construct.
1359
+ *
1360
+ * lua_inherited_member a member of D that D also inherits from a base B. 0.95,
1361
+ * structural — the family number for this edge is objc's
1362
+ * (real-objective-c-analyzer.ts:1020), and perl uses 0.95
1363
+ * for the same verified-base case. It is not 1.0 because
1364
+ * the corpus's inheritance is a CONVENTION over
1365
+ * `setmetatable`/`__index`, and nothing in the tree says
1366
+ * the link is meant as inheritance.
1367
+ *
1368
+ * The DIRECTION and the ENDPOINTS deliberately do NOT copy objc's
1369
+ * `Derived.member -> Base.member`. This lane's member-identity contract (LUA-15,
1370
+ * pinned by validation/m14/fixtures/lua/member-identity.expectation.json) holds
1371
+ * that a member's identity is its container plus its own name and that the
1372
+ * qualified path must not BE the name — that fixture FORBIDS `Buffer:initialize`
1373
+ * as a symbol name. So the edge runs `Derived -> Base`, the same pair `extends`
1374
+ * uses, and the overridden member travels in metadata as `member` plus the
1375
+ * qualified `memberPath`. Two tables in one file can therefore produce an
1376
+ * `overrides` and an `extends` edge between the same pair, under different type
1377
+ * labels; the store treats a distinct type on the same pair as a distinct link,
1378
+ * which is the property that keeps them from being read as one fact.
1379
+ */
1380
+ addOverrideRelationships(ctx) {
1381
+ for (const [label, decls] of ctx.declarationsByLabel) {
1382
+ if (decls.length < 2) continue;
1383
+ const byLine = [...decls].sort((a, b) => a.line - b.line);
1384
+ const first = byLine[0];
1385
+ for (let i = 1; i < byLine.length; i++) {
1386
+ const d = byLine[i];
1387
+ this.addRelationship(
1388
+ ctx,
1389
+ d.owner ?? "global",
1390
+ d.owner ? d.member ?? label : label,
1391
+ ASTEngine.RelationshipType.Overrides,
1392
+ {
1393
+ luaSpecific: true,
1394
+ relationshipType: "lua_redefinition",
1395
+ redefinition: true,
1396
+ qualifiedName: label,
1397
+ overridesLine: first.line,
1398
+ overriddenOwner: first.owner
1399
+ },
1400
+ null,
1401
+ { confidence: 1, symbolRange: { startLine: d.line } }
1402
+ );
1403
+ }
1404
+ }
1405
+ for (const [derived, bases] of ctx.supertypeLinks) {
1406
+ const derivedMembers = ctx.membersByOwner.get(derived);
1407
+ if (!derivedMembers || derivedMembers.size === 0) continue;
1408
+ for (const base of bases) {
1409
+ const baseMembers = ctx.membersByOwner.get(base);
1410
+ if (!baseMembers) continue;
1411
+ for (const [member, line] of derivedMembers) {
1412
+ if (!baseMembers.has(member)) continue;
1413
+ this.addRelationship(
1414
+ ctx,
1415
+ derived,
1416
+ base,
1417
+ ASTEngine.RelationshipType.Overrides,
1418
+ {
1419
+ luaSpecific: true,
1420
+ relationshipType: "lua_inherited_member",
1421
+ isOverride: true,
1422
+ member,
1423
+ memberPath: `${derived}.${member} -> ${base}.${member}`,
1424
+ // M60 LUA-52: the tier is `usage_contextual`, NOT the `structural` that
1425
+ // `evidenceClassFor` hands every type which is not imports/references/uses.
1426
+ // The rule this analyzer already wrote for ITSELF is at
1427
+ // `addFactorySupertypeEdges` (:1381): "a 0.95 edge must not claim
1428
+ // `structural`". `structural` asserts both endpoints are nodes this parse
1429
+ // saw, which is true here — and is not the claim. The claim is that a
1430
+ // member SHADOWS another through an `__index` chain, and Lua's inheritance
1431
+ // is a CONVENTION over `setmetatable`/`__index` with nothing in the tree
1432
+ // saying the link is meant as inheritance. Same reason, same 0.95, same
1433
+ // tier as `lua_factory_supertype`.
1434
+ //
1435
+ // MEASURED (validation/m60/lua/ontology-census.json, 3,252 files): 2 of
1436
+ // 67 `overrides` edges carried `structural` at confidence 0.95, and the
1437
+ // census counted 67 edges before and after this change — the tier is the
1438
+ // whole difference. Pinned by
1439
+ // `validation/m14/fixtures/lua/override-shapes.expectation.json`, whose
1440
+ // forbid half fails an implementation that derives the tier from the
1441
+ // type, and by the mutant in script/m60/mutate-lua-evidence-tier.ts.
1442
+ evidenceClass: "usage_contextual"
1443
+ },
1444
+ null,
1445
+ { confidence: 0.95, symbolRange: { startLine: line } }
1446
+ );
1447
+ }
1448
+ }
1449
+ }
1450
+ }
1451
+ addContainsRelationships(ctx) {
1452
+ for (const symbol of ctx.symbols) {
1453
+ const owner = symbol.ownerName || symbol.frameSelfName || (symbol.scope === "global" || !symbol.scope ? "global" : symbol.scope);
1454
+ const selfNamed = owner === symbol.name;
1455
+ const ownerWrittenAsReceiver = symbol.metadata?.isMethod === true;
1456
+ const containerExists = selfNamed && ownerWrittenAsReceiver && ctx.symbols.some(
1457
+ (o) => o.name === owner && o !== symbol && (o.range?.startLine !== symbol.range?.startLine || o.kind !== symbol.kind)
1458
+ );
1459
+ if (owner && (owner !== symbol.name || containerExists)) {
1460
+ this.addRelationship(ctx, owner, symbol.name, ASTEngine.RelationshipType.Contains, {}, void 0, { symbolRange: symbol.range });
1461
+ }
1462
+ }
1463
+ }
1464
+ addRelationship(ctx, source, target, type, metadata, nameNode, opts) {
1465
+ if (!source || !target) return;
1466
+ const key = `${type}${source}${target}`;
1467
+ if (ctx.seenRelationships.has(key)) return;
1468
+ ctx.seenRelationships.add(key);
1469
+ const relationship = {
1470
+ source,
1471
+ target,
1472
+ type,
1473
+ confidence: opts?.confidence ?? 1,
1474
+ metadata
1475
+ };
1476
+ if (nameNode) relationship.line_number = nameNode.startPosition.row + 1;
1477
+ else if (opts?.symbolRange) relationship.line_number = opts.symbolRange.startLine;
1478
+ relationship.metadata = { evidenceClass: this.evidenceClassFor(type), ...relationship.metadata };
1479
+ const receiver = nameNode ? this.stdlibReceiverOf(nameNode) : null;
1480
+ if (receiver) ctx.stdlibCandidates.push({ rel: relationship, receiver });
1481
+ const globalName = (type === ASTEngine.RelationshipType.Calls || type === ASTEngine.RelationshipType.Uses) && nameNode !== null && nameNode !== void 0 && target === nameNode.text ? this.stdlibGlobalNameOf(nameNode) : null;
1482
+ if (globalName !== null && LUA54_GLOBALS.has(globalName)) {
1483
+ ctx.stdlibGlobalCandidates.push({ rel: relationship, name: globalName });
1484
+ }
1485
+ ctx.relationships.push(relationship);
1486
+ }
1487
+ /**
1488
+ * M60 LUA-54 — the standard-library receiver of a member read, or `null`.
1489
+ *
1490
+ * The name node is the edge's own target, so the test is structural rather than
1491
+ * a name comparison: walk up to the `dot_index_expression` /
1492
+ * `method_index_expression` that OWNS this node, require its receiver to be an
1493
+ * `identifier`, and return that identifier's text. `bracket_index_expression` is
1494
+ * excluded deliberately — `t["insert"]` is a member read whose key is a STRING
1495
+ * literal and never an identifier, so it cannot carry a receiver name, and
1496
+ * `stdlibReceiverOf` returning `null` for it is the correct answer rather than a
1497
+ * miss. `t[table.insert]` is a real `table.insert` read and is NOT marked: the
1498
+ * subscript is a computed value, so the name it denotes is not in the tree.
1499
+ *
1500
+ * Returns the RECEIVER's text and nothing else. Whether that name is a standard
1501
+ * library table is decided by the caller against a transcribed list, so this
1502
+ * method holds no list and cannot drift from one.
1503
+ */
1504
+ stdlibReceiverOf(nameNode) {
1505
+ const owner = nameNode.parent;
1506
+ if (!owner) return null;
1507
+ if (owner.type !== "dot_index_expression" && owner.type !== "method_index_expression") return null;
1508
+ const field = owner.childForFieldName("field") ?? owner.childForFieldName("method");
1509
+ if (!field || !this.isSameNode(field, nameNode)) return null;
1510
+ const receiver = owner.childForFieldName("table");
1511
+ if (!receiver || receiver.type !== "identifier") return null;
1512
+ return receiver.text;
1513
+ }
1514
+ /**
1515
+ * M60 LUA-57 — the node's text when the node is a BARE identifier, or `null` when
1516
+ * it is a member's name.
1517
+ *
1518
+ * The inverse question to `stdlibReceiverOf`, and it is answered structurally for
1519
+ * the same reason: `("abc"):type()` and `obj.type()` both publish target `type`
1520
+ * with a node whose TEXT is `type`, and neither is the standard library global —
1521
+ * the first because its receiver is a literal, the second because its receiver is
1522
+ * an identifier that is not a documented table. Text alone cannot tell either from
1523
+ * a bare `type(x)`, so the shape decides: a name that is the FIELD or METHOD of an
1524
+ * index expression is a member's name and never a global read. The RECEIVER half
1525
+ * of an index expression is deliberately NOT excluded — it is a genuine bare read
1526
+ * of that identifier — but no `calls`/`uses` edge is ever published under it, so
1527
+ * the case is unreachable from the only two call sites that consult this.
1528
+ */
1529
+ stdlibGlobalNameOf(nameNode) {
1530
+ if (nameNode.type !== "identifier") return null;
1531
+ const owner = nameNode.parent;
1532
+ if (owner && (owner.type === "dot_index_expression" || owner.type === "method_index_expression")) {
1533
+ const field = owner.childForFieldName("field") ?? owner.childForFieldName("method");
1534
+ if (field && this.isSameNode(field, nameNode)) return null;
1535
+ }
1536
+ return nameNode.text;
1537
+ }
1538
+ /**
1539
+ * M60 LUA-54 — mark the standard-library member edges, from ONE post-pass, after
1540
+ * every symbol this file declares is known.
1541
+ *
1542
+ * ## WHAT IT CLAIMS, AND THE MEASUREMENT BEHIND THE CLAIM
1543
+ *
1544
+ * An edge is marked `external` when its target is the field of a member read
1545
+ * whose receiver is an identifier naming one of the eight documented Lua 5.4
1546
+ * standard tables (`string`, `table`, `math`, `io`, `os`, `coroutine`, `debug`,
1547
+ * `utf8` — reference manual §6, transcribed into `LUA54_TABLES`), AND this file
1548
+ * publishes no symbol under that receiver.
1549
+ *
1550
+ * Measured on the whole-corpus draw of 3,252 .lua files from 199 repos
1551
+ * (drawDigest 502ef2f4..., 3,250 parsed, 219,981 relationships, production
1552
+ * `parseFile`): 3,525 edges are marked — 2,738 `calls` and 787 `references` —
1553
+ * out of 219,981, by `script/m60/verify-lua-stdlib-external.mts`. It changes NO
1554
+ * edge count and creates NO edge: the edge digest is `bc0c38b6...` with and
1555
+ * without this method, because the key is additive metadata and every consumer
1556
+ * that ignores it sees the identical graph.
1557
+ *
1558
+ * `script/m60/lua-external-marking.ts` puts the same rule at 3,956 from outside
1559
+ * the analyzer. The 431 difference is the outer instrument keying on LINE,
1560
+ * which credits a hit to every edge on that line — including the 398 `contains`
1561
+ * edges that share a line with a `references` edge and carry no node at all. The
1562
+ * two agree exactly on `calls` at 2,725, so the gap sits entirely in the types
1563
+ * with no node to test, which is what the analyzer can prove.
1564
+ *
1565
+ * ## THE RULE THAT WAS REJECTED, because it is the interesting half
1566
+ *
1567
+ * The first version of this marked any target whose bare name is in a
1568
+ * transcribed stdlib surface — 16,195 edges, 4.1x the above — and it is a
1569
+ * NAME-MATCH GUESS. This analyzer publishes the FINAL SEGMENT of a member read
1570
+ * (LUA-13), so `table.insert(x)` arrives as target `insert`, and a bare
1571
+ * `insert(x)` with no receiver arrives as target `insert` too. Lua 5.4
1572
+ * publishes no global named `insert`, so the name alone cannot tell the standard
1573
+ * library from a user's own global, and marking the second case `external`
1574
+ * would move it out of `unresolved` without moving anything toward truth — the
1575
+ * arbiter's PASS would then mean less than it did. Measured: **0** edge targets
1576
+ * a dotted `table.field` spelling anywhere in the corpus, which is why the name
1577
+ * list cannot recover the receiver from the target.
1578
+ *
1579
+ * ## WHAT IS DELIBERATELY LEFT UNMARKED
1580
+ *
1581
+ * `imports` targets (6,601 on the same draw) are Lua MODULE paths, not
1582
+ * declarations. One file cannot tell a sibling module from a luarocks rock, and
1583
+ * `classb-origin` measured both populations — 2,425 dotted targets whose module
1584
+ * file is in the corpus against 3,060 whose file is absent. Marking all 6,601
1585
+ * would assert the second count for the first.
1586
+ *
1587
+ * UPDATED 2026-10-11 (LUA-55), because the sentence that used to close this
1588
+ * paragraph — "the gate has no bucket for 'this type's target is a module', so they
1589
+ * are reported rather than swept" — stopped being true, and a comment that outlives
1590
+ * its code is this lane's most expensive defect class (LUA-21, 41811754). The gate
1591
+ * HAS a bucket now: `metadata.importProjectFile`, the shared file claim, and
1592
+ * `addImport` writes it. The split above is unchanged and is still why an `isExternal`
1593
+ * MARK is wrong here (1,560 of 5,485 REFUTED) — but the file claim is a different
1594
+ * statement about the same edge, and it is checkable rather than asserted. See
1595
+ * LUA-55 in docs/analyzers/lua.md.
1596
+ */
1597
+ markStdlibExternal(ctx) {
1598
+ if (ctx.stdlibCandidates.length === 0 && ctx.stdlibGlobalCandidates.length === 0) return;
1599
+ const declaredAt = /* @__PURE__ */ new Map();
1600
+ for (const s of ctx.symbols) {
1601
+ const line = Number(s.range?.startLine ?? s.startLine ?? 0);
1602
+ if (!s.name) continue;
1603
+ const arr = declaredAt.get(s.name) ?? [];
1604
+ arr.push(line);
1605
+ declaredAt.set(s.name, arr);
1606
+ }
1607
+ for (const { rel, receiver } of ctx.stdlibCandidates) {
1608
+ if (!LUA54_TABLES.has(receiver)) continue;
1609
+ const line = Number(rel.line_number ?? 0);
1610
+ const lines = declaredAt.get(receiver);
1611
+ if (lines && lines.some((l) => l <= 0 || line > 0 && l <= line)) continue;
1612
+ rel.metadata.external = true;
1613
+ rel.metadata.externalKind = "lua54-stdlib";
1614
+ rel.metadata.externalReceiver = receiver;
1615
+ }
1616
+ for (const { rel, name } of ctx.stdlibGlobalCandidates) {
1617
+ if (!LUA54_GLOBALS.has(name)) continue;
1618
+ const line = Number(rel.line_number ?? 0);
1619
+ const lines = declaredAt.get(name);
1620
+ if (lines && lines.some((l) => l <= 0 || line > 0 && l <= line)) continue;
1621
+ rel.metadata.external = true;
1622
+ rel.metadata.externalKind = "lua54-global";
1623
+ }
1624
+ }
1625
+ /** M60 LUA-43: the evidence tier for a relationship type. See the call site for
1626
+ * why these three words and not a fourth. */
1627
+ evidenceClassFor(type) {
1628
+ if (type === ASTEngine.RelationshipType.Imports || type === ASTEngine.RelationshipType.References) return "import_based";
1629
+ if (type === ASTEngine.RelationshipType.Uses) return "usage_contextual";
1630
+ return "structural";
1631
+ }
1632
+ functionName(node) {
1633
+ return node.childForFieldName("name")?.text || node.namedChild(0)?.text;
1634
+ }
1635
+ hasChildType(node, type) {
1636
+ for (let i = 0; i < node.childCount; i++) if (node.child(i)?.type === type) return true;
1637
+ return false;
1638
+ }
1639
+ isLocalFunction(node) {
1640
+ return this.hasChildType(node, "local");
1641
+ }
1642
+ isLocalVariableDeclaration(node) {
1643
+ return this.hasChildType(node, "local");
1644
+ }
1645
+ /**
1646
+ * The resolvable CONTAINER of a member: the HEAD of the receiver path.
1647
+ *
1648
+ * M60 LUA-37. `function Buffer:initialize()` has receiver `Buffer`; `function
1649
+ * Buffer.meta.index()` has receiver `Buffer.meta`. Returning the receiver's
1650
+ * text verbatim makes the second an owner of `"Buffer.meta"`, which no symbol is
1651
+ * filed under, so the owner still does not resolve. The head is the only part
1652
+ * of the path that is a declared name, and the analyzer already files
1653
+ * `Buffer` as a symbol. Returns undefined for a bare name — a plain function
1654
+ * has no container, and its own name is its scope.
1655
+ */
1656
+ functionOwner(nameNode) {
1657
+ if (!["dot_index_expression", "method_index_expression"].includes(nameNode.type)) return void 0;
1658
+ const recv = nameNode.childForFieldName("table")?.text;
1659
+ if (!recv) return void 0;
1660
+ const head = recv.split(".")[0];
1661
+ return head.length > 0 ? head : void 0;
1662
+ }
1663
+ extractParameters(node) {
1664
+ const list = node.childForFieldName("parameters");
1665
+ if (!list) return [];
1666
+ const parameters = [];
1667
+ for (let i = 0; i < list.namedChildCount; i++) {
1668
+ const child = list.namedChild(i);
1669
+ if (!child) continue;
1670
+ if (child.type === "identifier") parameters.push({ name: child.text, type: "Any" });
1671
+ else if (child.type === "vararg_expression") parameters.push({ name: "...", type: "Any", isVariadic: true });
1672
+ }
1673
+ return parameters;
1674
+ }
1675
+ plainVariableName(node) {
1676
+ return node?.type === "identifier" ? node.text : void 0;
1677
+ }
1678
+ isTableNode(node) {
1679
+ return node?.type === "table_constructor";
1680
+ }
1681
+ assignmentValue(node) {
1682
+ const expressionList = node.childForFieldName("value") || node.namedChild(1);
1683
+ return expressionList?.namedChild(0) || expressionList || void 0;
1684
+ }
1685
+ callArguments(node) {
1686
+ const argumentList = node.childForFieldName("arguments");
1687
+ if (!argumentList) return [];
1688
+ const source = argumentList;
1689
+ const values = [];
1690
+ for (let i = 0; i < source.namedChildCount; i++) {
1691
+ const child = source.namedChild(i);
1692
+ if (child) values.push(child);
1693
+ }
1694
+ return values;
1695
+ }
1696
+ callArgument(node, index) {
1697
+ return this.callArguments(node)[index];
1698
+ }
1699
+ stringValue(node) {
1700
+ if (!node || node.type !== "string") return void 0;
1701
+ return this.cleanLuaString(node.text);
1702
+ }
1703
+ tableFieldNames(node) {
1704
+ const result = [];
1705
+ const fieldList = node;
1706
+ for (let i = 0; i < (fieldList?.namedChildCount || 0); i++) {
1707
+ const field = fieldList?.namedChild(i);
1708
+ if (!field || field.type !== "field") continue;
1709
+ const key = field.childForFieldName("name") || field.namedChild(0);
1710
+ if (key?.text) result.push(key.text);
1711
+ }
1712
+ return result;
1713
+ }
1714
+ // M60 LUA-13 + LUA-01: the callee's NAME and the node that CARRIES ITS LINE
1715
+ // must be the same node, returned together so they cannot drift apart.
1716
+ //
1717
+ // Getting this wrong took three measurements, and each wrong answer was
1718
+ // plausible enough to nearly ship:
1719
+ // 1. `node.text` on the callee — on a call split across lines that is a
1720
+ // SOURCE SNIPPET ("obj\n .method"), which the pool-worker's
1721
+ // garbage-target filter deletes (pool-worker.ts:391 rejects any target
1722
+ // containing a newline). Consequence: `obj\n.method()` produced NO EDGE
1723
+ // while the identical one-line call produced one. A line break silently
1724
+ // removed a call from the graph.
1725
+ // 2. the first identifier in the index expression — that is the RECEIVER
1726
+ // (`obj`), not the thing called.
1727
+ // 3. the index expression itself — its start is the receiver's line, so
1728
+ // `obj\n .method()` reported the `obj` line and the gate caught it.
1729
+ //
1730
+ // The callee of `table . field` / `table : field` is `field`, and `field` is
1731
+ // the LAST named child, so its own start position is the line the method name
1732
+ // is written on — which is exactly M59 (#825)'s rule and exactly what java
1733
+ // gets from `childForFieldName("name")` on its invocation node.
1734
+ // M60 LUA-32: the ONE rule for "is this node a name", shared by the callee
1735
+ // whitelist (LUA-29/LUA-30) and the setmetatable path, so the two cannot drift
1736
+ // apart the way `table_index_expression` did (LUA-27). Three node forms carry a
1737
+ // name; everything else is an expression, and an expression used as a target is
1738
+ // a payload — unmatchable, unreferenceable, and unbounded in shape.
1739
+ //
1740
+ // `dot_index_expression` and `method_index_expression` yield their FULL TEXT
1741
+ // (`Account.Meta`), which is a path rather than a bare name. That is kept
1742
+ // deliberately: these are relationship ENDPOINTS, not symbols, and the
1743
+ // member-identity rule that forbids '.' in a SYMBOL name (verify-lua-member-identity)
1744
+ // is a different contract from what a reference to `Account.Meta` may look like.
1745
+ nameBearing(node) {
1746
+ if (!node) return void 0;
1747
+ if (node.type === "identifier") return node.text;
1748
+ if (node.type === "dot_index_expression" || node.type === "method_index_expression") return node.text;
1749
+ return void 0;
1750
+ }
1751
+ // M60 LUA-56 — WHY THE SHAPE IS RETURNED WITH THE NAME. `calls` is this lane's largest
1752
+ // remaining cause (42,889 not-resolved edges, 30,370 ambiguous + 12,519 unresolved, measured
1753
+ // on the whole admitted population), and the class is TWO mechanisms wearing one label:
1754
+ //
1755
+ // `foo()` an IDENTIFIER callee — the name the language looks up through the file's
1756
+ // lexical scope and then `_ENV`. This is the only shape about which an
1757
+ // externality question can even be asked, because the target IS the name.
1758
+ // `M.foo()` an INDEX callee — `callee()` publishes the FIELD (LUA-13), so the target is a
1759
+ // `M:foo()` member name whose CONTAINER the edge does not carry. "declared nowhere in the
1760
+ // `M["foo"]()` project" is then a statement about a field, not about a global.
1761
+ //
1762
+ // The distinction is DECIDED BY THE BRANCH THAT PRODUCES THE NAME — the same node test that
1763
+ // returns the name decides which form it is — so it is returned from here rather than
1764
+ // re-derived at the call site, where a second copy of the node test could silently drift from
1765
+ // this one. That is the "one spelling, two places" hazard this lane has paid for repeatedly.
1766
+ //
1767
+ // THE TWO KEYS ARE A PARTITION of the `calls` population (every edge carries exactly one), and
1768
+ // they are marker KEYS rather than one value-carrying key on purpose: the gate's marker table
1769
+ // and this lane's own cause reader both group by KEY ONLY ("a value would split one marker into
1770
+ // a signature per symbol it names"), so a fact that must be separable there has to be a key.
1771
+ //
1772
+ // THE THIRD STATE IS DELIBERATE. The fallthrough below publishes a name taken from arbitrary
1773
+ // node text — the payload-as-a-name defect LUA-22/LUA-29/LUA-30 closed for every form the
1774
+ // pinned grammar can produce — and it carries NO shape key. An edge in that state is therefore
1775
+ // visible as a hole (the lane's `calls-file-scope-pricing.mts` reports it as UNPUBLISHED, and a
1776
+ // non-zero reading there means the grammar grew a callee form this whitelist has not seen)
1777
+ // rather than being silently filed under one of the two shapes.
1778
+ callee(functionNode) {
1779
+ if (functionNode.type === "identifier") return { name: functionNode.text, nameNode: functionNode, shapeMark: { calleeIdentifier: true } };
1780
+ if (functionNode.type === "dot_index_expression" || functionNode.type === "method_index_expression") {
1781
+ const last = functionNode.namedChildCount > 0 ? functionNode.namedChild(functionNode.namedChildCount - 1) : void 0;
1782
+ if (last?.text) return { name: last.text, nameNode: last, shapeMark: { calleeIndexed: true } };
1783
+ }
1784
+ if (functionNode.type === "bracket_index_expression") {
1785
+ const field = functionNode.childForFieldName("field");
1786
+ if (field?.type !== "string") return void 0;
1787
+ const literal = this.stringValue(field);
1788
+ return literal ? { name: literal, nameNode: field, shapeMark: { calleeIndexed: true } } : void 0;
1789
+ }
1790
+ if (functionNode.type === "function_call" || functionNode.type === "table_constructor" || functionNode.type === "binary_expression" || functionNode.type === "parenthesized_expression" || functionNode.type === "string" || functionNode.type === "number") return void 0;
1791
+ if (functionNode.text) return { name: functionNode.text, nameNode: functionNode, shapeMark: {} };
1792
+ return void 0;
1793
+ }
1794
+ // M60 LUA-01: reach a child by node TYPE. The five field literals this
1795
+ // analyzer used to ask for (`name_list`, `function`, `expression_list`, `key`)
1796
+ // do not exist in the pinned grammar, and every one of them sat behind a
1797
+ // `|| namedChild(0)` fallback that turned "this field does not exist" into
1798
+ // "quietly use whatever happens to be first" — a wrong answer that could not
1799
+ // raise, log, or fail. Type-directed lookup has no fallback to hide behind:
1800
+ // either the child is there or the path does not run.
1801
+ childOfType(node, type) {
1802
+ if (!node) return void 0;
1803
+ for (let i = 0; i < node.namedChildCount; i++) {
1804
+ const child = node.namedChild(i);
1805
+ if (child?.type === type) return child;
1806
+ }
1807
+ return void 0;
1808
+ }
1809
+ cleanLuaString(text) {
1810
+ const cleaned = text.trim();
1811
+ if (cleaned.length >= 2) {
1812
+ const first = cleaned[0];
1813
+ const last = cleaned[cleaned.length - 1];
1814
+ if ((first === '"' || first === "'") && first === last) return cleaned.substring(1, cleaned.length - 1);
1815
+ }
1816
+ if (cleaned.startsWith("[[") && cleaned.endsWith("]]")) return cleaned.substring(2, cleaned.length - 2);
1817
+ return cleaned;
1818
+ }
1819
+ // M60 LUA-04: framework detection fired on EVERY identifier node and matched
1820
+ // BARE NAMES — `use`, `setup`, `GetTime`, `C_Timer`, `Proto`, `UIParent`. So a
1821
+ // file containing `local setup = 1` was labelled "Neovim Configuration", and
1822
+ // the committed gate for this defect class caught a probe labelled BOTH
1823
+ // "Neovim Configuration" AND "World of Warcraft Addon" purely from three
1824
+ // ordinary variable names.
1825
+ //
1826
+ // The analyzer's own header claims "No regex fallback or source-wide scanning
1827
+ // is used". A bare-name equality test applied to every identifier is
1828
+ // source-wide scanning wearing an AST costume, so this is a contract failure
1829
+ // and not only a precision one.
1830
+ //
1831
+ // The rule now: a framework is claimed only from a QUALIFIED member access
1832
+ // (`vim.api`, `love.graphics`, `ngx.timer`) or from a `require`d module name
1833
+ // that names it. A bare identifier never names a framework. That is narrower
1834
+ // than the old rule on purpose — and the gate asserts the CONVERSE too, so it
1835
+ // cannot be satisfied by deleting detection: a file that really does use
1836
+ // `vim.api` must still be labelled Neovim.
1837
+ detectFrameworks(text, ctx) {
1838
+ if (!text.includes(".")) return;
1839
+ const head = text.split(".")[0];
1840
+ if (!head) return;
1841
+ if (head === "vim") ctx.frameworks.add("Neovim Configuration");
1842
+ if (head === "love") ctx.frameworks.add("Love2D");
1843
+ if (head === "ngx" || head === "resty") ctx.frameworks.add("OpenResty");
1844
+ if (head === "display" || head === "system" || head === "transition" || head === "Runtime") ctx.frameworks.add("Corona SDK / Solar2D");
1845
+ if (head === "lovr") ctx.frameworks.add("L\xD6VR (VR/AR)");
1846
+ if (head === "redis") ctx.frameworks.add("Redis Scripting");
1847
+ }
1848
+ collectErrors(rootNode, ctx) {
1849
+ if (!rootNode.hasError) return;
1850
+ const stack = [rootNode];
1851
+ while (stack.length) {
1852
+ const node = stack.pop();
1853
+ if (node.type === "ERROR" || node.isMissing) {
1854
+ ctx.errors.push({ message: `Syntax ${node.type} detected.`, range: this.nodeToRange(node), severity: "warning" });
1855
+ }
1856
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1857
+ const child = node.namedChild(i);
1858
+ if (child) stack.push(child);
1859
+ }
1860
+ }
1861
+ }
1862
+ calculateCyclomaticComplexity(rootNode) {
1863
+ let complexity = 1;
1864
+ const stack = [rootNode];
1865
+ while (stack.length) {
1866
+ const node = stack.pop();
1867
+ if (node.type === "if_statement" || node.type === "for_statement" || node.type === "while_statement" || node.type === "repeat_statement") complexity++;
1868
+ if (node.type === "binary_expression") {
1869
+ const operator = node.childForFieldName("operator");
1870
+ if (operator?.text === "and" || operator?.text === "or") complexity++;
1871
+ }
1872
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1873
+ const child = node.namedChild(i);
1874
+ if (child) stack.push(child);
1875
+ }
1876
+ }
1877
+ return complexity;
1878
+ }
1879
+ // M60 LUA-02: this counted `symbol.metadata?.isCoroutine`, which NO code path
1880
+ // in the analyzer ever set — processCall adds the string "Coroutines" to
1881
+ // ctx.patterns and stops. So the count could only ever read 0, and it was
1882
+ // published as a count. It is now counted from the calls the walk actually
1883
+ // saw: `coroutine.create` and `coroutine.wrap` are ordinary function_call
1884
+ // nodes, so a file that uses them reports a positive number and a file that
1885
+ // does not reports 0 that means something.
1886
+ // M60 LUA-02. Counted during the walk, not afterwards and not from the stored
1887
+ // edges: the edge carries the callee FIELD, so after the LUA-13 fix there is
1888
+ // no relationship whose target is "coroutine.create" — the field is "create".
1889
+ // Two earlier versions of this fix were wrong in ways the gate caught: the
1890
+ // first counted ctx.symbols (a call is not a declaration, so it read 0), the
1891
+ // second counted relationship targets (which no longer exist under that name,
1892
+ // so it also read 0). Counting at the point where the qualified name is still
1893
+ // in hand is the only place the fact exists.
1894
+ countCoroutineSymbols(ctx) {
1895
+ return ctx.coroutineCalls;
1896
+ }
1897
+ // M60 LUA-11: a derived figure, or absent. Returns undefined when there is
1898
+ // nothing to measure rather than a number that means nothing.
1899
+ relationshipDensity(ctx) {
1900
+ if (ctx.symbols.length === 0 && ctx.relationships.length === 0) return void 0;
1901
+ const perSymbol = ctx.symbols.length === 0 ? 0 : ctx.relationships.length / ctx.symbols.length;
1902
+ return Math.max(0, Math.min(100, Math.round(100 - perSymbol * 10)));
1903
+ }
1904
+ retainedRowKb(ctx) {
1905
+ if (ctx.symbols.length === 0 && ctx.relationships.length === 0) return void 0;
1906
+ const SYMBOL_ROW_KB = 0.5;
1907
+ const RELATIONSHIP_ROW_KB = 0.25;
1908
+ const kb = ctx.symbols.length * SYMBOL_ROW_KB + ctx.relationships.length * RELATIONSHIP_ROW_KB;
1909
+ return Math.round(kb * 100) / 100;
1910
+ }
1911
+ // M60 LUA-10: the +5 branch for "Module/Table Pattern" rewarded a string
1912
+ // nothing ever added, so a published score was computed from a dead branch —
1913
+ // and codeQuality thresholds on that score, so the error propagated into a
1914
+ // second published field. The branch is gone rather than wired up: a module
1915
+ // table IS this language's module idiom, so awarding points for it would
1916
+ // award points to most Lua files, which measures nothing.
1917
+ calculateArchitecturalScore(ctx) {
1918
+ let score = 70;
1919
+ if (ctx.relationships.length > 0) score += 10;
1920
+ if (ctx.exports.length > 0) score += 5;
1921
+ if (ctx.patterns.has("Object-Oriented Programming (Metatables)")) score += 10;
1922
+ if (ctx.patterns.has("Object-Oriented Programming (Libraries)")) score += 5;
1923
+ return Math.min(100, score);
1924
+ }
1925
+ assessCodeQuality(ctx) {
1926
+ const score = this.calculateArchitecturalScore(ctx);
1927
+ if (score >= 90) return "excellent";
1928
+ if (score >= 80) return "good";
1929
+ if (score >= 60) return "fair";
1930
+ return "poor";
1931
+ }
1932
+ generateArchitecturalInsights(ctx) {
1933
+ const insights = [];
1934
+ if (ctx.patterns.has("Object-Oriented Programming (Metatables)")) {
1935
+ insights.push({ type: "architecture", category: "lua_oop", message: "Lua object-oriented patterns detected through metatables.", severity: "info" });
1936
+ }
1937
+ return insights;
1938
+ }
1939
+ lineCount(content) {
1940
+ let count = 1;
1941
+ for (let i = 0; i < content.length; i++) if (content[i] === "\n") count++;
1942
+ return count;
1943
+ }
1944
+ nodeToRange(node) {
1945
+ return {
1946
+ startLine: node.startPosition.row + 1,
1947
+ startColumn: node.startPosition.column + 1,
1948
+ endLine: node.endPosition.row + 1,
1949
+ endColumn: node.endPosition.column + 1,
1950
+ startIndex: node.startIndex,
1951
+ endIndex: node.endIndex
1952
+ };
1953
+ }
1954
+ };
1955
+ export {
1956
+ RealLuaAnalyzer
1957
+ };