@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,3956 @@
1
+ import {
2
+ findDeclaringParent
3
+ } from "./chunk-OWISBAOF.js";
4
+ import {
5
+ ASTEngine
6
+ } from "./chunk-V44PGPA6.js";
7
+ import "./chunk-BQ3OVQ3P.js";
8
+ import "./chunk-SK6YVHFZ.js";
9
+ import "./chunk-2BSKD7O3.js";
10
+ import "./chunk-KAET47ZL.js";
11
+ import "./chunk-KCDECRJY.js";
12
+ import "./chunk-MLKGABMK.js";
13
+
14
+ // src/ast/real-kotlin-analyzer.ts
15
+ function stringIncludes(str, val) {
16
+ if (!str) return false;
17
+ return str.indexOf(val) !== -1;
18
+ }
19
+ function someSymbol(arr, predicate) {
20
+ if (!arr) return false;
21
+ for (let i = 0; i < arr.length; i++) {
22
+ if (predicate(arr[i])) return true;
23
+ }
24
+ return false;
25
+ }
26
+ function stripKotlinQuoting(text) {
27
+ if (!text.includes("`")) return text;
28
+ let out = "";
29
+ let i = 0;
30
+ while (i < text.length) {
31
+ if (text.charCodeAt(i) === 96) {
32
+ const close = text.indexOf("`", i + 1);
33
+ if (close === -1) {
34
+ out += text.slice(i);
35
+ break;
36
+ }
37
+ out += text.slice(i + 1, close);
38
+ i = close + 1;
39
+ } else {
40
+ out += text[i];
41
+ i++;
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ function kotlinSigHasTypeVar(sig) {
47
+ const n = sig.length;
48
+ let i = 0;
49
+ while (i < n) {
50
+ const c = sig[i];
51
+ if (c === "(" || c === "," || c === ":" || c === "|" || c === "[" || c === "<" || c === "&" || i === 0) {
52
+ let j = i;
53
+ while (j < n && (sig[j] === "(" || sig[j] === "[" || sig[j] === " " || sig[j] === ":" || sig[j] === ",")) j++;
54
+ if (j < n && sig[j] >= "A" && sig[j] <= "Z") {
55
+ let end = j + 1;
56
+ while (end < n && (sig[end] >= "A" && sig[end] <= "Z" || sig[end] >= "a" && sig[end] <= "z" || sig[end] >= "0" && sig[end] <= "9" || sig[end] === "_")) end++;
57
+ if (end === j + 1) return true;
58
+ i = end;
59
+ continue;
60
+ }
61
+ i = j > i ? j : i + 1;
62
+ continue;
63
+ }
64
+ i++;
65
+ }
66
+ return false;
67
+ }
68
+ function kotlinCompilationUnitName(filePath) {
69
+ if (!filePath) return "";
70
+ let norm = "";
71
+ for (let i = 0; i < filePath.length; i++) {
72
+ const c = filePath[i];
73
+ norm += c === "\\" ? "/" : c;
74
+ }
75
+ const base = norm.slice(norm.lastIndexOf("/") + 1);
76
+ const dot = base.lastIndexOf(".");
77
+ const stem = dot > 0 ? base.slice(0, dot) : base;
78
+ return stem ? stripKotlinQuoting(stem) : "";
79
+ }
80
+ var KOTLIN_EXTERNAL_IMPORT_ROOTS = [
81
+ "java.",
82
+ "javax.",
83
+ "jakarta.",
84
+ "jdk.",
85
+ "sun.",
86
+ "com.sun.",
87
+ "kotlin.",
88
+ "android.",
89
+ "androidx."
90
+ ];
91
+ var RealKotlinAnalyzer = class _RealKotlinAnalyzer {
92
+ findChildByType(node, types) {
93
+ for (let i = 0; i < node.namedChildCount; i++) {
94
+ const c = node.namedChild(i);
95
+ if (c) {
96
+ for (let j = 0; j < types.length; j++) {
97
+ if (types[j] === c.type) return c;
98
+ }
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Is this candidate TYPE name actually a Kotlin keyword the recovery handed us?
105
+ *
106
+ * MEASURED DEFECT (kotlin corpus, 405-file census). IntelliJ context-parameter
107
+ * test markup breaks a class header so badly that the real name ends up INSIDE
108
+ * an ERROR node while the constructor keyword is serialised as a direct
109
+ * `type_identifier` child of the class_declaration:
110
+ *
111
+ * analysis/low-level-api-fir/testData/lazyResolve/errors/
112
+ * constructorDuplicatedContextParameter.kt
113
+ * `class Foo { context(c<caret>: Int) context(c: String) constructor() }`
114
+ *
115
+ * class_declaration
116
+ * class (anon)
117
+ * ERROR <- contains simple_identifier "Foo"
118
+ * simple_identifier "Foo"
119
+ * ...
120
+ * type_identifier "constructor" <- the KEYWORD, a direct named child
121
+ * primary_constructor "()"
122
+ *
123
+ * A first-name-child scan therefore published a CLASS named `constructor` for a
124
+ * file that declares `Foo`, and the census then found `exports (file) ->
125
+ * constructor` and `contains constructor -> constructor` rows pointing at it.
126
+ *
127
+ * The rejection is TYPE-DECLARATION-ONLY on purpose: `constructor` is a SOFT
128
+ * keyword in Kotlin, and `fun constructor()` is legal — the corpus has one
129
+ * (compiler/psi/psi-impl/testData/psi/secondaryConstructors/basic.kt declares
130
+ * both `constructor() {}` and `@constructor fun constructor() {}` inside class
131
+ * A). Applying this to functions would delete a real declaration. No Kotlin
132
+ * TYPE may be named with any token below, which is what makes the list safe
133
+ * here: they are declaration keywords, and the recovery only ever offers one
134
+ * where a name belongs.
135
+ */
136
+ static KOTLIN_TYPE_NAME_KEYWORDS = /* @__PURE__ */ new Set([
137
+ "class",
138
+ "interface",
139
+ "object",
140
+ "fun",
141
+ "val",
142
+ "var",
143
+ "constructor",
144
+ "companion",
145
+ "enum",
146
+ "annotation",
147
+ "typealias",
148
+ "init"
149
+ ]);
150
+ isKotlinTypeNameKeyword(text) {
151
+ return _RealKotlinAnalyzer.KOTLIN_TYPE_NAME_KEYWORDS.has(text);
152
+ }
153
+ /**
154
+ * The NAME node of a declaration, or null when the declaration has none.
155
+ *
156
+ * `findChildByType` returns the first child whose TYPE matches, and a
157
+ * zero-length node matches. Tree-sitter produces exactly that under
158
+ * ERROR recovery, and Kotlin's corpus carries the recovery in real files, not
159
+ * only in broken ones — measured on the kotlin corpus (script/m14/kotlin-census.ts,
160
+ * stride 40, 405 files admitted), two declarations published an EMPTY name:
161
+ *
162
+ * analysis/analysis-api/testData/.../ambiguouslyInheritedGeneric.kt
163
+ * `abstract class <caret>Child : A<String>, B<CharSequence>`
164
+ * -> source_file child is a class_declaration whose only name-bearing child
165
+ * is a ZERO-LENGTH simple_identifier, so `className` was "".
166
+ * analysis/low-level-api-fir/testData/.../memberPropertyWithTypeInDelegateScript.kts
167
+ * `fun <expr>doSmth</expr>(i: String) = 4`
168
+ * -> same shape, an empty-name function symbol owned by class A.
169
+ *
170
+ * An empty name is not a name. It became a symbol row with `name = ""`, and
171
+ * because the same "" flowed into the derivations that read `ownerName` /
172
+ * `cls.name`, it also became FIVE EDGES THAT NAMED NOBODY: 2 `implements` and
173
+ * 3 `references` rows whose `source` is the empty string. Those rows reach
174
+ * `brain_relationship` with `source_symbol_id = NULL` and `source_external = ''`
175
+ * — an edge that asserts a relationship and identifies no participant.
176
+ *
177
+ * The fix is ABSENCE, not a placeholder (standing rule R7, ledger.json): the
178
+ * declaration is not published, and the file's ParseError already reports the
179
+ * ERROR node's line, so the malformed syntax is disclosed rather than
180
+ * silently renamed. Reading the ERROR node's own text instead would recover
181
+ * the name `Child` — deliberately not done: a name read out of an error
182
+ * recovery is a guess about what the author meant, and publishing it at the
183
+ * confidence of a real declaration is the same defect one node over.
184
+ *
185
+ * The scan is over NAMED children in child order, exactly like
186
+ * findChildByType, and only skips zero-length matches — so a declaration
187
+ * whose name node is present but empty is dropped while every other
188
+ * declaration's resolution is unchanged.
189
+ */
190
+ static NAME_NODE_TYPES = /* @__PURE__ */ new Set(["simple_identifier", "type_identifier"]);
191
+ findNameNode(node) {
192
+ for (let i = 0; i < node.namedChildCount; i++) {
193
+ const c = node.namedChild(i);
194
+ if (!c) continue;
195
+ if (!_RealKotlinAnalyzer.NAME_NODE_TYPES.has(c.type)) {
196
+ if (c.type === "variable_declaration") {
197
+ const inner = this.findNameNode(c);
198
+ if (inner) return inner;
199
+ }
200
+ continue;
201
+ }
202
+ if (c.text.length === 0) continue;
203
+ return c;
204
+ }
205
+ return null;
206
+ }
207
+ /**
208
+ * An endpoint is publishable when it names something. R7 forbids naming a
209
+ * symbol with a placeholder literal, and an empty string is the same failure
210
+ * with the placeholder removed: the row exists and identifies no participant.
211
+ *
212
+ * `unknown` is NOT special-cased here, and the reason is measured rather than
213
+ * stylistic: `unknown` is a legal Kotlin identifier, and the corpus declares
214
+ * one. `localClassWithUnresolvedSuperType.kt` contains
215
+ * `override fun unknown() {}` and calls `Bar().unknown()`, so the `unknown`
216
+ * endpoint on those two edges is a REAL member that really is called. A gate
217
+ * that convicted it would be wrong, and a gate with false positives gets
218
+ * disabled — so the rule stays on emptiness, which has no such collision.
219
+ */
220
+ static publishableEndpoint(name) {
221
+ if (typeof name !== "string") return null;
222
+ return name.length === 0 ? null : name;
223
+ }
224
+ /**
225
+ * THE KOTLIN PLATFORM'S OWN TYPE NAMES — the DEFAULT-IMPORTED set, as a CLOSED LIST.
226
+ *
227
+ * ## What this answers, and what it deliberately does not
228
+ *
229
+ * A bare `String`, `Int`, `List`, `Unit`, `Throwable` target names a declaration in
230
+ * `kotlin.*` (or one of the other default-imported packages: kotlin.collections,
231
+ * kotlin.text, kotlin.ranges, kotlin.sequences, kotlin.annotation, kotlin.comparisons,
232
+ * kotlin.io) — NOT a declaration in the project. MEASURED over this lane's 400-file gate
233
+ * population (2,067-file corpus, 6 projects), the unresolved edges whose target is one of
234
+ * these spellings number 2,936: 2,734 `references`, 172 `instantiates`, 27 `extends`,
235
+ * 3 `implements`. The top spellings are `String` 685, `Boolean` 332, `Int` 316, `Any` 315,
236
+ * `List` 269, `Unit` 238, `Long` 136, `Result` 96, `Throwable` 91, `Map` 68, `Double` 48,
237
+ * `Pair` 45, `Function` 40, `Array` 30. This is the largest ANALYZER-facing cause in the
238
+ * lane's residual; the largest cause overall (6,753 `calls` edges) is the 400-file stride
239
+ * hiding a declaration that does exist — an instrument artifact, not an analyzer defect.
240
+ *
241
+ * The membership test is DECIDABLE from a closed list, which is why it may be claimed at
242
+ * all: there is no lookup and no corpus fact behind it — a bare identifier of one of these
243
+ * spellings that is not this file's own is the platform's declaration of that name. The
244
+ * model is graphql's closed built-in scalar set.
245
+ *
246
+ * ## The three guards, each one measured rather than assumed
247
+ *
248
+ * 1. BARE SPELLINGS ONLY. A DOTTED target is not marked, and the reason is a measurement of
249
+ * the arbiter's own classifier: `incoming-rule.ts` `pathLikeTarget` reads a dotted target
250
+ * whose last segment is a source-file extension as a PATH, so `kotlin.js` would land in
251
+ * `externalUnverified` (`ext?`) — a claim no check can reach, which blocks a PASS instead
252
+ * of earning one. The dotted language-FQN channel (1,667 further edges: `kotlin.test` 228,
253
+ * `kotlin.coroutines` 141, `java.util.concurrent` 66, `java.util` 60, `kotlin.time` 33,
254
+ * `java.util.UUID` 18 …) is therefore PRICED AND LEFT, not guessed at.
255
+ * 2. NOT A NAME THIS FILE COULD MEAN ITSELF. The corpus refutes the naive version: Exposed
256
+ * declares `org.jetbrains.exposed.v1.core.Sequence` and `...core.Function`, and
257
+ * kotlinx-coroutines declares package `kotlin.coroutines.jvm.internal` — so a bare
258
+ * `Sequence` in an Exposed file that imports the project's own `Sequence` is NOT the
259
+ * platform's type, and marking it would assert something false about what the edge means.
260
+ * The marker is attached only where the file neither declares that name nor imports a
261
+ * declaration whose simple name or alias is that name. Both lists are already in hand at
262
+ * the point of the check — no new walk, no new parse. MEASURED: 40 edges vetoed.
263
+ * 3. A WILDCARD IMPORT VETOES EVERY BARE NAME. `MariaDBDialect.kt` carries
264
+ * `import org.jetbrains.exposed.v1.core.*` and writes `nextVal(seq: Sequence, …)`; an
265
+ * explicit star import beats the DEFAULT star import in Kotlin, so that `Sequence` is
266
+ * Exposed's own class. What a wildcard names cannot be enumerated from this file, so the
267
+ * file claims nothing. MEASURED: 1,518 candidate edges vetoed (54% of the naive 2,936);
268
+ * the 1,386 that remain are the change's whole yield.
269
+ *
270
+ * ## Why the marker is safe wherever it is attached
271
+ *
272
+ * The arbiter reads an external claim ONLY where the target names nothing in the project
273
+ * (`classifyEdge`, the `decls === 0` branch): on an edge whose target IS a project
274
+ * declaration the marker is inert, so it can never hide a resolution and never turn one into
275
+ * a gap. On a bare target the claim is verified by looking the target up and finding
276
+ * nothing — which is exactly the state in which it is read.
277
+ *
278
+ * TWO SPELLINGS ARE EXCLUDED, AND THE EXCLUSION IS A MEASUREMENT RATHER THAN A TASTE:
279
+ * `Sequence` and `Function` are default-imported (`kotlin.sequences.Sequence`, `kotlin.Function`)
280
+ * and this corpus declares both UNDER ITS OWN PACKAGE (`org.jetbrains.exposed.v1.core`). A
281
+ * same-package declaration needs NO import, so no per-file guard can see it: in the 400-file
282
+ * draw the marker was attached to `Alias.kt:139` — `object : Function<T>(columnType)`, which is
283
+ * Exposed's own class — and a false external is the one defect this programme has reverted a fix
284
+ * over (hcl's 29, protobuf's 37). Excluding the two spellings costs exactly TWO edges of the
285
+ * 1,388 (one of them that false one) and removes the only measured instance; the hazard CLASS
286
+ * remains declarable — a name redeclared in the same package of an unseen file — and for these
287
+ * 53 spellings a scan of all 2,346 `.kt`/`.kts` files found no other instance.
288
+ */
289
+ static KOTLIN_DEFAULT_IMPORTED_TYPES = /* @__PURE__ */ new Set([
290
+ // kotlin.* (NOTE: `Function` is deliberately absent — see the exclusion note above.)
291
+ "Any",
292
+ "Nothing",
293
+ "Unit",
294
+ "Boolean",
295
+ "Byte",
296
+ "Char",
297
+ "Double",
298
+ "Float",
299
+ "Int",
300
+ "Long",
301
+ "Short",
302
+ "String",
303
+ "Array",
304
+ "CharSequence",
305
+ "Comparable",
306
+ "Enum",
307
+ "Lazy",
308
+ "Pair",
309
+ "Triple",
310
+ "Result",
311
+ "Throwable",
312
+ "Exception",
313
+ "Error",
314
+ "RuntimeException",
315
+ "IllegalArgumentException",
316
+ "IllegalStateException",
317
+ "UnsupportedOperationException",
318
+ "IndexOutOfBoundsException",
319
+ "ArithmeticException",
320
+ "ClassCastException",
321
+ "NullPointerException",
322
+ "NoSuchElementException",
323
+ "NumberFormatException",
324
+ "ConcurrentModificationException",
325
+ "Number",
326
+ "Annotation",
327
+ "Deprecated",
328
+ "Suppress",
329
+ "OptIn",
330
+ "Comparator",
331
+ "UInt",
332
+ "ULong",
333
+ "UByte",
334
+ "UShort",
335
+ // kotlin.collections.*
336
+ "Iterable",
337
+ "MutableIterable",
338
+ "Iterator",
339
+ "MutableIterator",
340
+ "ListIterator",
341
+ "Collection",
342
+ "MutableCollection",
343
+ "List",
344
+ "MutableList",
345
+ "Set",
346
+ "MutableSet",
347
+ "Map",
348
+ "MutableMap",
349
+ "ArrayList",
350
+ "HashMap",
351
+ "HashSet",
352
+ "LinkedHashMap",
353
+ "LinkedHashSet",
354
+ "ArrayDeque",
355
+ // kotlin.text.* / kotlin.ranges.* (NOTE: `Sequence` is deliberately absent — same reason.)
356
+ "StringBuilder",
357
+ "Regex",
358
+ "MatchResult",
359
+ "IntRange",
360
+ "LongRange",
361
+ "CharRange",
362
+ "ClosedRange"
363
+ ]);
364
+ /**
365
+ * ATTACH THE PLATFORM MARKER — one pass over the finished edge list, so a new emission site
366
+ * cannot forget it (the defect class that produced this lane's two import shapes).
367
+ *
368
+ * `isStdlib` is the spelling the arbiter already recognises as an external claim
369
+ * (`incoming-rule.ts` `EXTERNAL_KEYS`), and no kotlin edge carried it before this change, so
370
+ * the column it feeds read 0 for this lane while these edges were counted as silent holes.
371
+ * Nothing else about an edge is touched: target, source, line and every pre-existing
372
+ * metadata key are byte-identical.
373
+ */
374
+ markPlatformTargets(relationships, symbols, imports) {
375
+ let hasWildcardImport = false;
376
+ const fileOwned = /* @__PURE__ */ new Set();
377
+ for (const s of symbols) {
378
+ const n = s?.name;
379
+ if (typeof n === "string" && n.length > 0) fileOwned.add(n);
380
+ }
381
+ for (const imp of imports) {
382
+ const mod = imp?.module;
383
+ if (typeof mod !== "string" || mod.length === 0) continue;
384
+ if (mod.endsWith("*") || imp?.isNamespaceImport === true) {
385
+ hasWildcardImport = true;
386
+ continue;
387
+ }
388
+ const dot = mod.lastIndexOf(".");
389
+ const leaf = dot >= 0 ? mod.slice(dot + 1) : mod;
390
+ if (leaf.length > 0) fileOwned.add(leaf);
391
+ if (typeof imp.alias === "string" && imp.alias.length > 0) fileOwned.add(imp.alias);
392
+ }
393
+ if (hasWildcardImport) return;
394
+ for (const rel of relationships) {
395
+ const t = rel.type;
396
+ if (t !== ASTEngine.RelationshipType.References && t !== ASTEngine.RelationshipType.Instantiates && t !== ASTEngine.RelationshipType.Extends && t !== ASTEngine.RelationshipType.Implements) continue;
397
+ const target = rel.target;
398
+ if (typeof target !== "string") continue;
399
+ if (!_RealKotlinAnalyzer.KOTLIN_DEFAULT_IMPORTED_TYPES.has(target)) continue;
400
+ if (fileOwned.has(target)) continue;
401
+ const md = rel.metadata;
402
+ if (!md || typeof md !== "object") continue;
403
+ if (md.isStdlib === true) continue;
404
+ md.isStdlib = true;
405
+ }
406
+ }
407
+ analyze(rootNode, content, filePath, context) {
408
+ const ctx = {
409
+ content,
410
+ currentFile: filePath,
411
+ packageStack: [],
412
+ classStack: []
413
+ };
414
+ const symbols = [];
415
+ const relationships = [];
416
+ const imports = [];
417
+ const exports = [];
418
+ const errors = [];
419
+ const importClaimEnabled = process.env.BB_KOTLIN_IMPORT_CLAIM === "1";
420
+ if (rootNode.hasError) {
421
+ const collectErrors = (node) => {
422
+ if (node.type === "ERROR" || node.type === "MISSING") {
423
+ errors.push({
424
+ message: `Syntax ${node.type} detected. The Brain Engine is bypassing this broken node.`,
425
+ // 1-based rows/columns, consistent with nodeToRange and every
426
+ // other analyzer (was 0-based: error locations off-by-one).
427
+ range: {
428
+ startLine: node.startPosition.row + 1,
429
+ startColumn: node.startPosition.column + 1,
430
+ endLine: node.endPosition.row + 1,
431
+ endColumn: node.endPosition.column + 1
432
+ },
433
+ severity: `warning`
434
+ });
435
+ }
436
+ for (let i = 0; i < node.childCount; i++) {
437
+ const child = node.child(i);
438
+ if (child) collectErrors(child);
439
+ }
440
+ };
441
+ collectErrors(rootNode);
442
+ }
443
+ this.extractSymbolsWithScoping(rootNode, ctx, symbols, content, void 0, relationships);
444
+ this.analyzeRelationships(rootNode, relationships, symbols, ctx);
445
+ this.processImportsExports(rootNode, imports, exports, relationships, symbols, ctx);
446
+ const frameworks = this.detectKotlinFrameworks(content, symbols, imports);
447
+ const patterns = this.detectKotlinPatterns(symbols, relationships, content, rootNode);
448
+ this.injectPhantomSymbols(rootNode, symbols, frameworks, ctx);
449
+ try {
450
+ const extractKotlinCalls = (n) => {
451
+ const resolveCallSource = (start) => {
452
+ let p = start;
453
+ while (p) {
454
+ if (p.type === "secondary_constructor" || p.type === "primary_constructor") {
455
+ return this.kotlinConstructorOwnerName(p) ?? "";
456
+ }
457
+ if (p.type === "object_literal") {
458
+ return this.anonymousObjectName(p);
459
+ }
460
+ if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
461
+ const nameNode = this.findNameNode(p);
462
+ if (nameNode) return nameNode.text;
463
+ }
464
+ const topLevelProp = this.kotlinTopLevelPropertyName(p);
465
+ if (topLevelProp) return topLevelProp;
466
+ p = p.parent;
467
+ }
468
+ return "Global";
469
+ };
470
+ if (this.isKotlinContextParameterHeader(n)) return;
471
+ if (n.type === "call_expression") {
472
+ const callExpr = this.buildKotlinCallExpression(n);
473
+ const targetName = callExpr;
474
+ const leaf = targetName.split(".").pop() || targetName;
475
+ const isQualified = targetName.includes(".");
476
+ const isStdlib = this.kotlinStdlibExact.has(targetName) || !isQualified && this.kotlinStdlibExact.has(leaf);
477
+ if (targetName && targetName !== "print" && targetName !== "println" && !isStdlib) {
478
+ const sourceName = resolveCallSource(n.parent);
479
+ const endpointsName = _RealKotlinAnalyzer.publishableEndpoint(leaf) !== null && _RealKotlinAnalyzer.publishableEndpoint(sourceName) !== null;
480
+ if (endpointsName) {
481
+ const isPascal = leaf.length > 0 && leaf[0] >= "A" && leaf[0] <= "Z";
482
+ if (isPascal && !isQualified) {
483
+ relationships.push({
484
+ source: sourceName,
485
+ target: leaf,
486
+ type: ASTEngine.RelationshipType.Instantiates,
487
+ confidence: 0.7,
488
+ // (#825): the constructed type is named by the callee leaf.
489
+ ...this.lineField(this.calleeLeafNode(n, leaf), n),
490
+ metadata: { kotlinSpecific: true, isInstantiation: true }
491
+ });
492
+ } else {
493
+ relationships.push({
494
+ source: sourceName,
495
+ target: leaf,
496
+ type: ASTEngine.RelationshipType.Calls,
497
+ confidence: 0.7,
498
+ // (#825): the call is written at the callee leaf.
499
+ ...this.lineField(this.calleeLeafNode(n, leaf), n),
500
+ metadata: {
501
+ kotlinSpecific: true,
502
+ // cut-1 claim eligibility: only a RECEIVER-LESS call can be the imported binding itself
503
+ // (a receiver-carrying call's target is a member leaf, not the bound name).
504
+ ...importClaimEnabled && !isQualified ? { kotlinBareCallee: true } : {}
505
+ }
506
+ });
507
+ }
508
+ }
509
+ }
510
+ } else if (n.type === "constructor_delegation_call") {
511
+ const text = n.text.trim();
512
+ const sourceName = resolveCallSource(n.parent);
513
+ let target = "super";
514
+ if (text.startsWith("this")) target = "this";
515
+ if (!_RealKotlinAnalyzer.publishableEndpoint(sourceName)) {
516
+ } else relationships.push({
517
+ source: sourceName,
518
+ target,
519
+ type: ASTEngine.RelationshipType.Calls,
520
+ confidence: 1,
521
+ // (#825): `super(...)`/`this(...)` are unnamed keyword children,
522
+ // so the delegation node's own line is the site that carries it.
523
+ line_number: n.startPosition.row + 1,
524
+ metadata: {
525
+ relationshipType: "constructor_invocation",
526
+ constructorReceiver: target,
527
+ callExpression: text
528
+ }
529
+ });
530
+ }
531
+ for (let i = 0; i < n.namedChildCount; i++) {
532
+ const child = n.namedChild(i);
533
+ if (child) extractKotlinCalls(child);
534
+ }
535
+ };
536
+ extractKotlinCalls(rootNode);
537
+ } catch (e) {
538
+ }
539
+ try {
540
+ this.extractDeclaredTypeRefs(rootNode, relationships);
541
+ this.collectExpressionUses(rootNode, relationships, symbols);
542
+ } catch (e) {
543
+ }
544
+ const typeIndex = /* @__PURE__ */ new Map();
545
+ for (const s of symbols) {
546
+ if (s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum) {
547
+ if (!typeIndex.has(s.name)) typeIndex.set(s.name, s);
548
+ }
549
+ }
550
+ const declSigs = /* @__PURE__ */ new Map();
551
+ for (const t of symbols) {
552
+ if (t.kind !== ASTEngine.SymbolKind.Method || !t.ownerName || t.metadata?.isPhantom) continue;
553
+ let nameMap = declSigs.get(t.ownerName);
554
+ if (!nameMap) {
555
+ nameMap = /* @__PURE__ */ new Map();
556
+ declSigs.set(t.ownerName, nameMap);
557
+ }
558
+ let set = nameMap.get(t.name);
559
+ if (!set) {
560
+ set = /* @__PURE__ */ new Set();
561
+ nameMap.set(t.name, set);
562
+ }
563
+ const tSig = "(" + (t.parameters ?? []).map((p) => p.type ?? "").join(",") + ")";
564
+ set.add(tSig);
565
+ }
566
+ const propSigs = /* @__PURE__ */ new Map();
567
+ for (const t of symbols) {
568
+ if (t.kind !== ASTEngine.SymbolKind.Property || !t.ownerName || t.metadata?.isPhantom) continue;
569
+ let nameMap = propSigs.get(t.ownerName);
570
+ if (!nameMap) {
571
+ nameMap = /* @__PURE__ */ new Map();
572
+ propSigs.set(t.ownerName, nameMap);
573
+ }
574
+ let set = nameMap.get(t.name);
575
+ if (!set) {
576
+ set = /* @__PURE__ */ new Set();
577
+ nameMap.set(t.name, set);
578
+ }
579
+ set.add("(" + (t.returnType ?? "") + ")");
580
+ }
581
+ for (const s of symbols) {
582
+ const _owner = s.ownerName;
583
+ if (_owner) {
584
+ const isSynthetic = s.metadata?.isPhantom === true;
585
+ relationships.push({
586
+ source: _owner,
587
+ target: s.name,
588
+ type: ASTEngine.RelationshipType.Contains,
589
+ // K1 (M60 kotlin deep review, 2026-09-27): a SYNTHESIZED member's
590
+ // name is written nowhere in the file. `data class C(val a: Int)`
591
+ // produces component1 / copy / equals / hashCode / toString, and the
592
+ // code above already knows it — `isSynthetic` is computed, and the
593
+ // LINE is correctly omitted for exactly this reason. The confidence
594
+ // was the one field left claiming otherwise.
595
+ //
596
+ // This is the same defect the java pass closed as JD-J13, verbatim in
597
+ // shape: an inference published at maximum confidence, on a branch
598
+ // that already carries the knowledge it is wrong about. Probed here:
599
+ //
600
+ // data class C(val a: Int)
601
+ // C->a conf=1.0 line=1 (declared — correct)
602
+ // C->component1 conf=1.0 line=- (synthesized)
603
+ // C->copy conf=1.0 line=- (synthesized)
604
+ // C->equals conf=1.0 line=- (synthesized)
605
+ // C->hashCode conf=1.0 line=- (synthesized)
606
+ // C->toString conf=1.0 line=- (synthesized)
607
+ //
608
+ // Five of six containment edges name a method with no declaration, at
609
+ // maximum confidence, while the one real member sits beside them at the
610
+ // same confidence with a real line. A consumer cannot tell them apart
611
+ // without knowing the `isPhantom` convention.
612
+ //
613
+ // Fixed the way java's JD-J13 was: keep the edge (the containment is
614
+ // real and useful — `C.component1` does exist), mark it, and demote
615
+ // below the 0.7 fabrication threshold so it reads as declared
616
+ // suspicion and a consumer can filter on it.
617
+ confidence: isSynthetic ? 0.6 : 1,
618
+ ...!isSynthetic && s.range?.startLine ? { line_number: s.range.startLine } : {},
619
+ metadata: { isContained: true, ...isSynthetic ? { inferred: true } : {} }
620
+ });
621
+ }
622
+ const overrideKind = s.kind === ASTEngine.SymbolKind.Method ? "method" : s.kind === ASTEngine.SymbolKind.Property ? "property" : "";
623
+ if (overrideKind !== "" && s.ownerName && !s.metadata?.isPhantom && s.metadata?.modifiers?.includes?.("override")) {
624
+ const isMethodOverride = overrideKind === "method";
625
+ const ownerSym = typeIndex.get(s.ownerName);
626
+ const parents = ownerSym?.metadata?.superTypes ?? [];
627
+ const overrideSig = isMethodOverride ? "(" + (s.parameters ?? []).map((p) => p.type ?? "").join(",") + ")" : "(" + (s.returnType ?? "") + ")";
628
+ let overrideParent = parents.length > 0 ? parents[0] : "";
629
+ let confidence = 0.5;
630
+ let reasonCode = "first-supertype-guess";
631
+ let evidence = "inferred";
632
+ if (parents.length > 0) {
633
+ const declared = (typeName) => {
634
+ const nameMap = (isMethodOverride ? declSigs : propSigs).get(typeName);
635
+ if (!nameMap) return false;
636
+ const sigs = nameMap.get(s.name);
637
+ if (!sigs || sigs.size === 0) return false;
638
+ if (overrideSig && sigs.has(overrideSig)) return true;
639
+ const hasTypeVar = (sig) => kotlinSigHasTypeVar(sig);
640
+ if (!overrideSig) return true;
641
+ for (const s2 of sigs) if (hasTypeVar(s2) || hasTypeVar(overrideSig)) return true;
642
+ return false;
643
+ };
644
+ const queue = [...parents];
645
+ const seen = /* @__PURE__ */ new Set();
646
+ let depth = 0;
647
+ while (queue.length > 0 && depth < 10) {
648
+ depth++;
649
+ const parent = queue.shift();
650
+ if (seen.has(parent)) continue;
651
+ seen.add(parent);
652
+ if (declared(parent)) {
653
+ overrideParent = parent;
654
+ confidence = 0.9;
655
+ reasonCode = "in-file-verified";
656
+ evidence = "structural";
657
+ break;
658
+ }
659
+ const gp = typeIndex.get(parent)?.metadata?.superTypes ?? [];
660
+ for (const g of gp) if (!seen.has(g)) queue.push(g);
661
+ }
662
+ }
663
+ if (isMethodOverride && context?.overrideCompanion && parents.length > 0) {
664
+ const declaring = findDeclaringParent(
665
+ s.name,
666
+ parents,
667
+ context.overrideCompanion,
668
+ (t) => typeIndex.get(t)?.metadata?.superTypes ?? []
669
+ );
670
+ if (declaring) {
671
+ overrideParent = declaring;
672
+ confidence = 0.9;
673
+ reasonCode = "companion-index";
674
+ evidence = "structural";
675
+ }
676
+ }
677
+ if (overrideParent) {
678
+ relationships.push({
679
+ source: `${s.ownerName}.${s.name}`,
680
+ target: `${overrideParent}.${s.name}`,
681
+ type: ASTEngine.RelationshipType.Overrides,
682
+ confidence,
683
+ // (#825): the override occurs at the OVERRIDING declaration.
684
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
685
+ metadata: {
686
+ isOverride: true,
687
+ overrideParent,
688
+ reasonCode,
689
+ evidenceClass: evidence,
690
+ overrideSig,
691
+ overrideMemberKind: overrideKind
692
+ }
693
+ });
694
+ }
695
+ }
696
+ }
697
+ const compilationUnit = kotlinCompilationUnitName(filePath);
698
+ for (const imp of imports) {
699
+ if (!_RealKotlinAnalyzer.publishableEndpoint(imp.module)) continue;
700
+ relationships.push({
701
+ source: "root",
702
+ target: imp.module,
703
+ type: ASTEngine.RelationshipType.References,
704
+ confidence: 0.9,
705
+ // (#825): the imported module is named on the import statement.
706
+ ...imp.range?.startLine ? { line_number: imp.range.startLine } : {},
707
+ metadata: { isImportDep: true }
708
+ });
709
+ }
710
+ this.canonicalizePublishedNames(symbols, relationships);
711
+ const primaryType = this.primaryTopLevelType(symbols);
712
+ if (primaryType) {
713
+ for (const rel of relationships) {
714
+ if (rel.type === ASTEngine.RelationshipType.References && rel.source === "root") {
715
+ rel.source = primaryType.name;
716
+ }
717
+ }
718
+ } else if (compilationUnit) {
719
+ for (const rel of relationships) {
720
+ if (rel.type === ASTEngine.RelationshipType.References && rel.source === "root") {
721
+ rel.source = compilationUnit;
722
+ }
723
+ }
724
+ }
725
+ const exportedSymbols = [];
726
+ for (let i = 0; i < symbols.length; i++) {
727
+ const s = symbols[i];
728
+ if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) continue;
729
+ if (s.metadata?.isFileLevelDeclaration !== true) continue;
730
+ if (s.visibility !== "public" && s.visibility !== void 0) continue;
731
+ if (s.ownerName) continue;
732
+ if (!_RealKotlinAnalyzer.publishableEndpoint(s.name)) continue;
733
+ const mods = s.metadata?.modifiers ?? [];
734
+ const explicitPublic = mods.indexOf("public") >= 0;
735
+ exportedSymbols.push({
736
+ name: s.name,
737
+ kind: s.kind,
738
+ range: s.range,
739
+ visibility: s.visibility || "public",
740
+ via: explicitPublic ? "explicit-public" : "default-public"
741
+ });
742
+ }
743
+ const unitIdentity = this.primaryTopLevelType(symbols);
744
+ const fileLevelSource = unitIdentity ? "(file)" : kotlinCompilationUnitName(filePath) || "(file)";
745
+ for (const e of exportedSymbols) {
746
+ exports.push({ name: e.name, kind: e.kind, range: e.range, visibility: e.visibility });
747
+ relationships.push({
748
+ source: fileLevelSource,
749
+ target: e.name,
750
+ type: ASTEngine.RelationshipType.Exports,
751
+ confidence: 1,
752
+ // (#825): the exported declaration's own line — the same site rule
753
+ // go's exports emitter uses (real-go-analyzer.ts:463).
754
+ ...e.range?.startLine ? { line_number: e.range.startLine } : {},
755
+ metadata: {
756
+ kotlinSpecific: true,
757
+ // Which fact put this name on the file's surface: the modifier that is
758
+ // written, or Kotlin's public-by-default rule. A reader can tell an
759
+ // explicit `public` from an implicit one without re-parsing.
760
+ via: e.via,
761
+ visibility: e.visibility,
762
+ symbolKind: String(e.kind),
763
+ // M60/#910: the source is the FILE, not a symbol.
764
+ fileLevel: true
765
+ }
766
+ });
767
+ }
768
+ this.classifyEvidenceTier(relationships);
769
+ this.markPlatformTargets(relationships, symbols, imports);
770
+ if (importClaimEnabled) {
771
+ this.attachKotlinImportedBindingClaims(relationships, symbols, imports);
772
+ }
773
+ return {
774
+ symbols,
775
+ relationships,
776
+ imports,
777
+ exports,
778
+ errors,
779
+ metadata: {
780
+ complexity: this.calculateRealComplexity(symbols, relationships),
781
+ linesOfCode: content.split("\n").length,
782
+ cyclomaticComplexity: this.calculateCyclomaticComplexity(rootNode),
783
+ framework: frameworks[0],
784
+ // Primary framework
785
+ patterns,
786
+ // Kotlin intelligence metadata
787
+ detectedFrameworks: frameworks,
788
+ frameworkPatterns: this.getKotlinFrameworkPatterns(frameworks, symbols),
789
+ architecturalInsights: this.generateArchitecturalInsights(symbols, relationships),
790
+ architecturalScore: this.calculateArchitecturalScore(symbols, relationships, patterns),
791
+ codeQuality: this.assessKotlinCodeQuality(symbols, relationships, patterns),
792
+ hasTypeInference: true,
793
+ hasFrameworkIntelligence: true,
794
+ // ABSENT when the file has a module identity, a COUNT when it does not:
795
+ // a zero would be a claim that the count was measured to be zero, while
796
+ // an absent field is an unknown. See the unit-identity note above.
797
+ ...unitIdentity ? {} : { exportsWithheldNoModuleIdentity: exportedSymbols.length },
798
+ analysisVersion: "3.0.0-infallible-brain"
799
+ }
800
+ };
801
+ }
802
+ /**
803
+ * PHASE 1: Real Kotlin Symbol Extraction with Scoping
804
+ * Understands Kotlin semantics: classes, data classes, sealed classes, objects, functions
805
+ */
806
+ injectPhantomSymbols(rootNode, symbols, frameworks, ctx) {
807
+ for (let i = 0; i < symbols.length; i++) {
808
+ const symbol = symbols[i];
809
+ let hasData = false;
810
+ const symModifiers = symbol.metadata?.modifiers ?? [];
811
+ for (let j = 0; j < symModifiers.length; j++) {
812
+ if (symModifiers[j] === "data") {
813
+ hasData = true;
814
+ break;
815
+ }
816
+ }
817
+ if (symbol.kind === ASTEngine.SymbolKind.Class && hasData) {
818
+ const properties = [];
819
+ for (let j = 0; j < symbols.length; j++) {
820
+ const s = symbols[j];
821
+ if (s.ownerName === symbol.name && s.kind === ASTEngine.SymbolKind.Property && s.metadata?.isConstructorProperty === true) properties.push(s);
822
+ }
823
+ for (let index = 0; index < properties.length; index++) {
824
+ const prop = properties[index];
825
+ symbols.push({
826
+ name: `component${index + 1}`,
827
+ kind: ASTEngine.SymbolKind.Method,
828
+ scope: symbol.name,
829
+ ownerName: symbol.name,
830
+ modifiers: ["phantom", "data_class_generated", "operator"],
831
+ range: symbol.range,
832
+ parameters: [],
833
+ returnType: prop.returnType || prop.type || "Any",
834
+ metadata: { isPhantom: true }
835
+ });
836
+ }
837
+ const copyParams = [];
838
+ for (let j = 0; j < properties.length; j++) {
839
+ copyParams.push({ name: properties[j].name, type: properties[j].returnType || properties[j].type || "Any" });
840
+ }
841
+ symbols.push({
842
+ name: "copy",
843
+ kind: ASTEngine.SymbolKind.Method,
844
+ scope: symbol.name,
845
+ // (#825): ownerName is what makes the member CONTAINED by its class
846
+ // — the injector set it for componentN but not for copy/equals/hashCode/
847
+ // toString, so `contains Money -> copy` never existed while
848
+ // `contains Money -> component1` did. Same class, same synthesis route,
849
+ // opposite outcomes on a real corpus file (measured on
850
+ // JsonTestsData.kt: 7 componentN containment edges, 0 for the other four
851
+ // members). java's phantoms (lombok/Spring-Data, processPhantomSymbols)
852
+ // have always carried ownerName.
853
+ ownerName: symbol.name,
854
+ modifiers: ["phantom", "data_class_generated"],
855
+ range: symbol.range,
856
+ parameters: copyParams,
857
+ returnType: symbol.name,
858
+ metadata: { isPhantom: true }
859
+ });
860
+ const stdMethods = [
861
+ { name: "equals", type: "Boolean", params: [{ name: "other", type: "Any?" }] },
862
+ { name: "hashCode", type: "Int", params: [] },
863
+ { name: "toString", type: "String", params: [] }
864
+ ];
865
+ for (let j = 0; j < stdMethods.length; j++) {
866
+ const method = stdMethods[j];
867
+ symbols.push({
868
+ name: method.name,
869
+ kind: ASTEngine.SymbolKind.Method,
870
+ scope: symbol.name,
871
+ // Same ownerName gap as `copy` above (#825): without it these
872
+ // three generated overrides were the only members of the data class
873
+ // with no containment edge at all.
874
+ ownerName: symbol.name,
875
+ modifiers: ["phantom", "data_class_generated", "override"],
876
+ range: symbol.range,
877
+ parameters: method.params,
878
+ returnType: method.type,
879
+ metadata: { isPhantom: true }
880
+ });
881
+ }
882
+ }
883
+ }
884
+ }
885
+ /**
886
+ * The TYPE that owns a constructor, by name — for attributing edges written
887
+ * inside one.
888
+ *
889
+ * MEASURED DEFECT this closes (kotlin corpus, 405-file census,
890
+ * script/m14/kotlin-census.ts). The M14 #419 K11 rule is "constructor and init
891
+ * calls attribute to the CLASS NAME", and it was implemented as
892
+ * `cls = node.parent; findNameNode(cls)`. That is TRUE for a
893
+ * `primary_constructor` — its parent IS the class_declaration — and FALSE for a
894
+ * `secondary_constructor`, whose parent is the `class_body`. So every
895
+ * `secondary_constructor` in the corpus fell through to the literal fallback
896
+ * and published `parentClass || "constructor"`: 60 edges across 405 files
897
+ * (37 calls, 13 uses, 4 instantiates, 3 exports targets, 2 references, 1
898
+ * contains) attributed to or naming a symbol spelled `constructor`, which no
899
+ * Kotlin source can declare. The rule was documented as closed and was half
900
+ * implemented.
901
+ *
902
+ * One walk fixes both shapes: skip the `class_body` and take the nearest
903
+ * enclosing type declaration's name. Returns undefined when there is none (an
904
+ * ERROR-recovered declaration whose name node is zero-length — see
905
+ * findNameNode), and the callers then publish nothing rather than a
906
+ * placeholder (R7).
907
+ */
908
+ kotlinConstructorOwnerName(node) {
909
+ let p = node.parent;
910
+ while (p) {
911
+ if (p.type === "class_declaration" || p.type === "object_declaration") {
912
+ const n = this.findNameNode(p);
913
+ if (!n || this.isKotlinTypeNameKeyword(n.text)) return void 0;
914
+ return n.text;
915
+ }
916
+ if (p.type === "source_file") break;
917
+ p = p.parent;
918
+ }
919
+ return void 0;
920
+ }
921
+ extractSymbolsWithScoping(node, ctx, symbols, content, parentClass, relationships) {
922
+ const stack = [{ n: node, pClass: parentClass }];
923
+ while (stack.length > 0) {
924
+ const entry = stack.pop();
925
+ if (entry.isPop) {
926
+ if (entry.name) {
927
+ ctx.classStack.pop();
928
+ }
929
+ continue;
930
+ }
931
+ const n = entry.n;
932
+ const pClass = entry.pClass;
933
+ let nextParentClass = pClass;
934
+ let pushedName = void 0;
935
+ const symbolsBefore = symbols.length;
936
+ const atFileLevel = n.parent?.type === "source_file";
937
+ switch (n.type) {
938
+ case "function_declaration":
939
+ {
940
+ const pt = n.parent?.type;
941
+ const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
942
+ this.extractFunction(n, symbols, ctx, isMemberPosition ? pClass : void 0);
943
+ }
944
+ break;
945
+ case "class_declaration": {
946
+ const isInterface = someSymbol(n.children, (c) => c?.type === "interface");
947
+ const isEnum = someSymbol(n.children, (c) => c?.type === "enum");
948
+ const cNameNode = this.findNameNode(n);
949
+ const cName = cNameNode && !this.isKotlinTypeNameKeyword(cNameNode.text) ? cNameNode.text : void 0;
950
+ if (cName) {
951
+ ctx.classStack.push(cName);
952
+ pushedName = cName;
953
+ nextParentClass = cName;
954
+ }
955
+ {
956
+ const pt = n.parent?.type;
957
+ const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
958
+ const ownerForDecl = isMemberPosition ? pClass : void 0;
959
+ if (isInterface) {
960
+ this.extractInterface(n, symbols, ctx, ownerForDecl);
961
+ } else if (isEnum) {
962
+ this.extractEnumClass(n, symbols, ctx, ownerForDecl);
963
+ } else {
964
+ this.extractClass(n, symbols, ctx, ownerForDecl);
965
+ }
966
+ }
967
+ break;
968
+ }
969
+ case "object_declaration": {
970
+ const oNameNode = this.findNameNode(n);
971
+ const oName = oNameNode && !this.isKotlinTypeNameKeyword(oNameNode.text) ? oNameNode.text : void 0;
972
+ if (oName) {
973
+ ctx.classStack.push(oName);
974
+ pushedName = oName;
975
+ nextParentClass = oName;
976
+ }
977
+ {
978
+ const pt = n.parent?.type;
979
+ const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
980
+ this.extractObject(n, symbols, ctx, isMemberPosition ? pClass : void 0);
981
+ }
982
+ break;
983
+ }
984
+ case "property_declaration":
985
+ if (this.isKotlinLocalDeclaration(n)) break;
986
+ {
987
+ const pt = n.parent?.type;
988
+ const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
989
+ this.extractProperty(n, symbols, ctx, isMemberPosition ? pClass : void 0, relationships);
990
+ }
991
+ break;
992
+ case "primary_constructor":
993
+ case "constructor_declaration":
994
+ case "secondary_constructor":
995
+ this.extractConstructor(n, symbols, ctx, pClass);
996
+ break;
997
+ case "package_header":
998
+ this.extractPackage(n, ctx);
999
+ break;
1000
+ case "import_header":
1001
+ this.extractImport(n, ctx);
1002
+ break;
1003
+ case "annotation":
1004
+ this.extractAnnotation(n, symbols, ctx);
1005
+ break;
1006
+ case "type_alias": {
1007
+ const pt = n.parent?.type;
1008
+ const isMemberPosition = pt === "class_body" || pt === "enum_class_body" || pt === "source_file";
1009
+ const tName = this.findNameNode(n);
1010
+ if (tName) {
1011
+ const tModifiers = this.extractModifiers(n, ctx);
1012
+ symbols.push({
1013
+ name: tName.text,
1014
+ kind: ASTEngine.SymbolKind.Type,
1015
+ range: this.symbolRange(n),
1016
+ scope: this.currentScope(ctx),
1017
+ visibility: this.determineVisibility(tModifiers, ctx),
1018
+ // M60 M3 (master review): same owner rule, same sweep. A type
1019
+ // alias is a MEMBER of a class exactly when it is declared in that
1020
+ // class's body. One declared inside a function is local to it, and
1021
+ // the class does not contain it:
1022
+ //
1023
+ // class Host3 { fun f() { typealias T = Int; val x: T = 1 } }
1024
+ // published: T[owner=Host3], contains Host3->T
1025
+ //
1026
+ // This is the same phantom containment as aa6a2beb's init-block
1027
+ // local and the local-class fix, in the one remaining ungated
1028
+ // extractor call site. Found by enumerating every site that passes
1029
+ // a parentClass, not by looking for this shape.
1030
+ ownerName: isMemberPosition ? pClass : void 0,
1031
+ ownerKind: isMemberPosition && pClass ? ASTEngine.SymbolKind.Class : void 0,
1032
+ decorators: this.extractAnnotations(n, ctx),
1033
+ metadata: {
1034
+ modifiers: tModifiers,
1035
+ annotations: this.extractAnnotations(n, ctx).map((a) => this.kotlinShortAnnotationName(a)),
1036
+ isTypeAlias: true,
1037
+ kotlinSpecific: true
1038
+ }
1039
+ });
1040
+ }
1041
+ break;
1042
+ }
1043
+ case "object_literal": {
1044
+ const deleg = this.findChildByType(n, ["delegation_specifier"]);
1045
+ let superName;
1046
+ if (deleg) {
1047
+ const findType = (node2) => {
1048
+ if (node2.type === "type_identifier") return node2;
1049
+ for (let i = 0; i < node2.namedChildCount; i++) {
1050
+ const c = node2.namedChild(i);
1051
+ if (c) {
1052
+ const r = findType(c);
1053
+ if (r) return r;
1054
+ }
1055
+ }
1056
+ return null;
1057
+ };
1058
+ const tId = findType(deleg);
1059
+ if (tId) superName = tId.text;
1060
+ }
1061
+ const anonName = this.anonymousObjectName(n);
1062
+ const anonOwner = this.anonymousObjectOwner(n, pClass);
1063
+ ctx.classStack.push(anonName);
1064
+ pushedName = anonName;
1065
+ nextParentClass = anonName;
1066
+ symbols.push({
1067
+ name: anonName,
1068
+ kind: ASTEngine.SymbolKind.Class,
1069
+ range: this.symbolRange(n),
1070
+ scope: this.currentScope(ctx),
1071
+ visibility: this.determineVisibility(this.extractModifiers(n, ctx), ctx),
1072
+ ownerName: anonOwner.name,
1073
+ ownerKind: anonOwner.kind,
1074
+ decorators: this.extractAnnotations(n, ctx),
1075
+ metadata: {
1076
+ modifiers: [],
1077
+ annotations: this.extractAnnotations(n, ctx).map((a) => this.kotlinShortAnnotationName(a)),
1078
+ isObject: true,
1079
+ isAnonymousObject: true,
1080
+ superTypes: superName ? [superName] : [],
1081
+ superTypesAreConstructorCalls: superName ? [false] : [],
1082
+ kotlinSpecific: true
1083
+ }
1084
+ });
1085
+ break;
1086
+ }
1087
+ }
1088
+ if (atFileLevel) {
1089
+ for (let i = symbolsBefore; i < symbols.length; i++) {
1090
+ const sym = symbols[i];
1091
+ sym.metadata = { ...sym.metadata ?? {}, isFileLevelDeclaration: true };
1092
+ }
1093
+ }
1094
+ if (pushedName) {
1095
+ stack.push({ n, isPop: true, name: pushedName });
1096
+ }
1097
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1098
+ const child = n.namedChild(i);
1099
+ if (child) {
1100
+ stack.push({ n: child, pClass: nextParentClass });
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Extract Kotlin function with extension functions and suspend functions
1107
+ */
1108
+ extractFunction(node, symbols, ctx, parentClass) {
1109
+ const nameNode = this.findNameNode(node);
1110
+ if (!nameNode) return;
1111
+ const functionName = nameNode.text;
1112
+ const qualifiedName = this.buildQualifiedName(functionName, ctx, parentClass);
1113
+ const modifiers = this.extractModifiers(node, ctx);
1114
+ const parameters = this.extractKotlinParameters(node, ctx);
1115
+ const returnType = this.extractReturnType(node, ctx);
1116
+ const receiverType = this.extractReceiverType(node, ctx);
1117
+ const qualifiedCalls = this.findKotlinFunctionCalls(node, ctx);
1118
+ const annotations = this.extractAnnotations(node, ctx);
1119
+ const kdoc = this.extractKDocComment(node, ctx);
1120
+ const visibility = this.determineVisibility(modifiers, ctx);
1121
+ let isInline = false, isInfix = false, isOperator = false, isSuspend = false;
1122
+ for (let i = 0; i < modifiers.length; i++) {
1123
+ if (modifiers[i] === "inline") isInline = true;
1124
+ else if (modifiers[i] === "infix") isInfix = true;
1125
+ else if (modifiers[i] === "operator") isOperator = true;
1126
+ else if (modifiers[i] === "suspend") isSuspend = true;
1127
+ }
1128
+ const isExtension = !!receiverType;
1129
+ const apiContract = this.extractKotlinApiContract(node, ctx);
1130
+ const apiContractPath = apiContract.path;
1131
+ const apiMethod = apiContract.method;
1132
+ const fnTypeParameters = this.extractTypeParameters(node, ctx);
1133
+ const symbol = {
1134
+ name: functionName,
1135
+ kind: parentClass ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
1136
+ range: this.symbolRange(node),
1137
+ scope: this.currentScope(ctx),
1138
+ visibility,
1139
+ isAsync: isSuspend,
1140
+ parameters,
1141
+ returnType,
1142
+ decorators: annotations,
1143
+ // Top-level per family contract: the DB ingester reads
1144
+ // symbol.complexity (|| 1) for gravity scoring — metadata-only values
1145
+ // left every kotlin symbol at complexity 1 in the index.
1146
+ complexity: this.calculateFunctionComplexity(node, ctx),
1147
+ comments: kdoc ? [kdoc] : [],
1148
+ qualified_calls: qualifiedCalls,
1149
+ apiContractPath,
1150
+ genericParameters: fnTypeParameters.length > 0 ? fnTypeParameters : void 0,
1151
+ ownerName: parentClass,
1152
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1153
+ metadata: {
1154
+ modifiers,
1155
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1156
+ receiverType,
1157
+ apiMethod,
1158
+ isInline,
1159
+ isInfix,
1160
+ isOperator,
1161
+ isSuspend,
1162
+ isExtension,
1163
+ complexity: this.calculateFunctionComplexity(node, ctx),
1164
+ parameterCount: parameters.length,
1165
+ hasKDoc: !!kdoc,
1166
+ ...fnTypeParameters.length > 0 ? { typeParameters: fnTypeParameters } : {},
1167
+ kotlinSpecific: true
1168
+ }
1169
+ };
1170
+ symbols.push(symbol);
1171
+ }
1172
+ /**
1173
+ * Spring mapping annotation extraction. Returns the route PATH ONLY and
1174
+ * the HTTP method separately: brain_symbol.api_contract_path is an exact
1175
+ * match key shared across languages (doc-engine queries
1176
+ * s.api_contract_path=?), so embedding "GET " into it breaks every
1177
+ * cross-language route join against analyzers that store bare paths.
1178
+ */
1179
+ extractKotlinApiContract(node, ctx) {
1180
+ const MAPPINGS = ["@GetMapping", "@PostMapping", "@PutMapping", "@DeleteMapping", "@PatchMapping", "@RequestMapping"];
1181
+ const candidates = [];
1182
+ const modifiersNode = this.findChildByType(node, ["modifiers"]);
1183
+ if (modifiersNode) {
1184
+ const stack = [modifiersNode];
1185
+ while (stack.length > 0) {
1186
+ const n = stack.pop();
1187
+ if (n.type === "annotation") candidates.push({ text: n.text, args: this.getValueArguments(n) });
1188
+ for (let i = 0; i < n.namedChildCount; i++) {
1189
+ const c = n.namedChild(i);
1190
+ if (c) stack.push(c);
1191
+ }
1192
+ }
1193
+ if (candidates.length === 0 && modifiersNode.text.includes("Mapping")) {
1194
+ candidates.push({ text: modifiersNode.text });
1195
+ }
1196
+ }
1197
+ for (let i = 0; i < node.namedChildCount; i++) {
1198
+ const mod = node.namedChild(i);
1199
+ if (mod?.type === "annotation") candidates.push({ text: mod.text, args: this.getValueArguments(mod) });
1200
+ }
1201
+ for (const { text, args } of candidates) {
1202
+ const shortName = this.kotlinShortAnnotationName(text);
1203
+ const mapping = MAPPINGS.find((m) => shortName === m.slice(1));
1204
+ if (!mapping) continue;
1205
+ const method = mapping.slice(1).replace("Mapping", "").toUpperCase();
1206
+ let path = "/";
1207
+ if (args && args.namedChildCount > 0) {
1208
+ let methodStr = "ANY";
1209
+ const paths = [];
1210
+ for (let j = 0; j < args.namedChildCount; j++) {
1211
+ const arg = args.namedChild(j);
1212
+ if (!arg || arg.type !== "value_argument") continue;
1213
+ let name = "";
1214
+ let expr = null;
1215
+ for (let k = 0; k < arg.namedChildCount; k++) {
1216
+ const c = arg.namedChild(k);
1217
+ if (!c) continue;
1218
+ if (c.type === "simple_identifier" && !expr) name = c.text;
1219
+ else if (!expr) expr = c;
1220
+ }
1221
+ const isRequestMethod = name === "method" || !name && stringIncludes(arg.text, "method");
1222
+ if (isRequestMethod) {
1223
+ const segs = arg.text.split(".");
1224
+ if (segs.length > 1) methodStr = segs[segs.length - 1].toUpperCase();
1225
+ continue;
1226
+ }
1227
+ if (name && name !== "value" && name !== "path") continue;
1228
+ if (!expr) continue;
1229
+ if (expr.type === "string_literal") {
1230
+ paths.push(expr.text.split('"').join(""));
1231
+ } else if (expr.type === "collection_literal") {
1232
+ for (let k = 0; k < expr.namedChildCount; k++) {
1233
+ const e = expr.namedChild(k);
1234
+ if (e?.type === "string_literal") paths.push(e.text.split('"').join(""));
1235
+ }
1236
+ } else if (expr.type === "call_expression") {
1237
+ for (let k = 0; k < expr.namedChildCount; k++) {
1238
+ const inner = expr.namedChild(k);
1239
+ if (inner?.type !== "call_suffix") continue;
1240
+ for (let m2 = 0; m2 < inner.namedChildCount; m2++) {
1241
+ const va = inner.namedChild(m2);
1242
+ if (va?.type !== "value_arguments") continue;
1243
+ for (let m3 = 0; m3 < va.namedChildCount; m3++) {
1244
+ const e = va.namedChild(m3);
1245
+ if (e?.type === "string_literal") paths.push(e.text.split('"').join(""));
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+ }
1251
+ if (paths.length > 0) path = paths[0];
1252
+ if (methodStr !== "ANY") {
1253
+ return { path, method: methodStr };
1254
+ }
1255
+ return { path, method };
1256
+ }
1257
+ const afterName = text.slice(mapping.length);
1258
+ const firstQuote = afterName.indexOf('"');
1259
+ if (firstQuote !== -1) {
1260
+ const close = afterName.indexOf('"', firstQuote + 1);
1261
+ if (close !== -1) path = afterName.slice(firstQuote + 1, close);
1262
+ }
1263
+ if (mapping !== "@RequestMapping") return { path, method };
1264
+ const rmIdx = afterName.indexOf("RequestMethod.");
1265
+ if (rmIdx !== -1) {
1266
+ let end = rmIdx + "RequestMethod.".length;
1267
+ while (end < afterName.length) {
1268
+ const ch = afterName[end];
1269
+ const isAlpha = ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
1270
+ if (!isAlpha) break;
1271
+ end++;
1272
+ }
1273
+ const mname = afterName.slice(rmIdx + "RequestMethod.".length, end);
1274
+ if (mname) return { path, method: mname.toUpperCase() };
1275
+ }
1276
+ return { path, method: "ANY" };
1277
+ }
1278
+ return {};
1279
+ }
1280
+ /**
1281
+ * Extract Kotlin class with data classes, sealed classes, and inner classes
1282
+ */
1283
+ extractClass(node, symbols, ctx, parentClass) {
1284
+ const nameNode = this.findNameNode(node);
1285
+ if (!nameNode) return;
1286
+ if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
1287
+ const className = nameNode.text;
1288
+ const qualifiedName = this.buildQualifiedName(className, ctx, parentClass);
1289
+ const modifiers = this.extractModifiers(node, ctx);
1290
+ const superTypes = this.extractSuperTypes(node, ctx);
1291
+ const superTypeKinds = this.extractSuperTypesWithKinds(node);
1292
+ const typeParameters = this.extractTypeParameters(node, ctx);
1293
+ const annotations = this.extractAnnotations(node, ctx);
1294
+ const kdoc = this.extractKDocComment(node, ctx);
1295
+ const visibility = this.determineVisibility(modifiers, ctx);
1296
+ let isData = false, isSealed = false, isAbstract = false, isInner = false, isOpen = false;
1297
+ for (let i = 0; i < modifiers.length; i++) {
1298
+ if (modifiers[i] === "data") isData = true;
1299
+ else if (modifiers[i] === "sealed") isSealed = true;
1300
+ else if (modifiers[i] === "abstract") isAbstract = true;
1301
+ else if (modifiers[i] === "inner") isInner = true;
1302
+ else if (modifiers[i] === "open") isOpen = true;
1303
+ }
1304
+ const symbol = {
1305
+ name: className,
1306
+ kind: ASTEngine.SymbolKind.Class,
1307
+ range: this.symbolRange(node),
1308
+ // Scope = package + enclosing types, EXCLUDING this class's own name,
1309
+ // so a class is never a container of itself.
1310
+ scope: this.enclosingScope(ctx),
1311
+ visibility,
1312
+ isAbstract,
1313
+ ownerName: parentClass,
1314
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1315
+ decorators: annotations,
1316
+ comments: kdoc ? [kdoc] : [],
1317
+ metadata: {
1318
+ modifiers,
1319
+ // Short annotation names (last dotted segment) so framework pattern
1320
+ // detection matches fully-qualified annotations exactly like bare ones.
1321
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1322
+ superTypes,
1323
+ superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
1324
+ // (#825): parallel line array — one line per supertype, in the
1325
+ // same order. Heritage edges take their line from here, not from the
1326
+ // class header (which names only the first supertype).
1327
+ superTypeLines: superTypeKinds.map((s) => s.line),
1328
+ inheritance: superTypes,
1329
+ typeParameters,
1330
+ isData,
1331
+ isDataClass: isData,
1332
+ isSealed,
1333
+ isInner,
1334
+ isOpen,
1335
+ methodCount: this.countMethods(node, ctx),
1336
+ propertyCount: this.countProperties(node, ctx),
1337
+ hasCompanionObject: this.hasCompanionObject(node, ctx),
1338
+ kotlinSpecific: true
1339
+ }
1340
+ };
1341
+ symbols.push(symbol);
1342
+ }
1343
+ /**
1344
+ * Extract Kotlin object (singleton)
1345
+ */
1346
+ extractObject(node, symbols, ctx, parentClass) {
1347
+ const nameNode = this.findNameNode(node);
1348
+ if (!nameNode) return;
1349
+ if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
1350
+ const objectName = nameNode.text;
1351
+ const modifiers = this.extractModifiers(node, ctx);
1352
+ const superTypes = this.extractSuperTypes(node, ctx);
1353
+ const superTypeKinds = this.extractSuperTypesWithKinds(node);
1354
+ const annotations = this.extractAnnotations(node, ctx);
1355
+ const symbol = {
1356
+ name: objectName,
1357
+ kind: ASTEngine.SymbolKind.Class,
1358
+ // Using Class for objects
1359
+ range: this.symbolRange(node),
1360
+ scope: this.enclosingScope(ctx),
1361
+ visibility: this.determineVisibility(modifiers, ctx),
1362
+ ownerName: parentClass,
1363
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1364
+ decorators: annotations,
1365
+ metadata: {
1366
+ modifiers,
1367
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1368
+ superTypes,
1369
+ superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
1370
+ // (#825): parallel line array — one line per supertype.
1371
+ superTypeLines: superTypeKinds.map((s) => s.line),
1372
+ isObject: true,
1373
+ isCompanionObject: (() => {
1374
+ for (let i = 0; i < modifiers.length; i++) if (modifiers[i] === "companion") return true;
1375
+ return false;
1376
+ })(),
1377
+ kotlinSpecific: true
1378
+ }
1379
+ };
1380
+ symbols.push(symbol);
1381
+ }
1382
+ /**
1383
+ * M49/M37 #867: the DECLARATION that owns an anonymous type — the innermost
1384
+ * enclosing `function_declaration` when the literal sits inside one, and the
1385
+ * enclosing class (`pClass`) otherwise.
1386
+ *
1387
+ * Ruled language-wide (not just here): `ownerName` = innermost enclosing
1388
+ * declaration. Three of the four JVM analyzers already did that before this
1389
+ * ruling — java's own source comment claimed to "mirror the kotlin family
1390
+ * convention exactly" while its code used the enclosing declaration, so the
1391
+ * comment and the majority disagreed and kotlin was the outlier. Lambdas and
1392
+ * `init` blocks are not declarations: a literal inside one is still owned by
1393
+ * the enclosing function/class, matching java (which pushes no context for a
1394
+ * lambda either).
1395
+ */
1396
+ anonymousObjectOwner(node, pClass) {
1397
+ let ancestor = node.parent;
1398
+ while (ancestor && ancestor.type !== "source_file") {
1399
+ if (ancestor.type === "function_declaration") {
1400
+ const nameNode = this.findNameNode(ancestor);
1401
+ if (nameNode) return { name: nameNode.text, kind: ASTEngine.SymbolKind.Method };
1402
+ break;
1403
+ }
1404
+ if (ancestor.type === "class_declaration" || ancestor.type === "object_declaration") break;
1405
+ ancestor = ancestor.parent;
1406
+ }
1407
+ return { name: pClass, kind: pClass ? ASTEngine.SymbolKind.Class : void 0 };
1408
+ }
1409
+ /**
1410
+ * M49/M37 #866: the name an anonymous object's members — and the calls made
1411
+ * from them — attribute to. ONE derivation, shared by the symbol emitter and
1412
+ * the call-source resolver, so a field symbol and its initialiser call can
1413
+ * never disagree about which type they belong to (java/groovy convention:
1414
+ * `anonymous_<SuperName>`).
1415
+ */
1416
+ anonymousObjectName(node) {
1417
+ const deleg = this.findChildByType(node, ["delegation_specifier"]);
1418
+ let superName;
1419
+ if (deleg) {
1420
+ const findType = (n) => {
1421
+ if (n.type === "type_identifier") return n;
1422
+ for (let i = 0; i < n.namedChildCount; i++) {
1423
+ const c = n.namedChild(i);
1424
+ if (c) {
1425
+ const r = findType(c);
1426
+ if (r) return r;
1427
+ }
1428
+ }
1429
+ return null;
1430
+ };
1431
+ const tId = findType(deleg);
1432
+ if (tId) superName = tId.text;
1433
+ }
1434
+ return superName ? `anonymous_${superName}` : "anonymous_object";
1435
+ }
1436
+ /**
1437
+ * Extract Kotlin property with getters/setters and delegation
1438
+ */
1439
+ extractProperty(node, symbols, ctx, parentClass, relationships) {
1440
+ let ancestor = node.parent;
1441
+ while (ancestor && ancestor.type !== "class_declaration" && ancestor.type !== "object_declaration" && ancestor.type !== "object_literal" && ancestor.type !== "source_file") {
1442
+ if (ancestor.type === "function_declaration") return;
1443
+ ancestor = ancestor.parent;
1444
+ }
1445
+ const declarationNode = this.findChildByType(node, ["variable_declaration"]);
1446
+ const nameNode = declarationNode ? this.findNameNode(declarationNode) : this.findNameNode(node);
1447
+ if (!nameNode) return;
1448
+ const propertyName = nameNode.text;
1449
+ const modifiers = this.extractModifiers(node, ctx);
1450
+ const propertyType = this.extractPropertyType(node, ctx);
1451
+ const annotations = this.extractAnnotations(node, ctx);
1452
+ const isVal = this.isValProperty(node);
1453
+ const isVar = this.isVarProperty(node);
1454
+ let isLateInit = false, isConst = false;
1455
+ for (let i = 0; i < modifiers.length; i++) {
1456
+ if (modifiers[i] === "lateinit") isLateInit = true;
1457
+ if (modifiers[i] === "const") isConst = true;
1458
+ }
1459
+ const hasDelegate = this.hasDelegate(node);
1460
+ let extensionReceiver;
1461
+ let extensionReceiverNode;
1462
+ {
1463
+ let receiverNode = node.childForFieldName("receiver");
1464
+ for (let i = 0; receiverNode == null && i < node.namedChildCount; i++) {
1465
+ const c = node.namedChild(i);
1466
+ if (c?.type === "simple_identifier") break;
1467
+ if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
1468
+ receiverNode = c;
1469
+ break;
1470
+ }
1471
+ }
1472
+ if (receiverNode) {
1473
+ const t = receiverNode.text;
1474
+ extensionReceiver = t.endsWith("?") ? t.slice(0, -1) : t;
1475
+ extensionReceiverNode = receiverNode;
1476
+ }
1477
+ }
1478
+ const symbol = {
1479
+ name: propertyName,
1480
+ kind: ASTEngine.SymbolKind.Property,
1481
+ range: this.symbolRange(node),
1482
+ scope: this.currentScope(ctx),
1483
+ visibility: this.determineVisibility(modifiers, ctx),
1484
+ decorators: annotations,
1485
+ returnType: propertyType,
1486
+ ownerName: parentClass,
1487
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1488
+ metadata: {
1489
+ modifiers,
1490
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1491
+ propertyType,
1492
+ isVal,
1493
+ isVar,
1494
+ isLateInit,
1495
+ isConst,
1496
+ hasDelegate,
1497
+ hasCustomGetter: this.hasCustomGetter(node),
1498
+ hasCustomSetter: this.hasCustomSetter(node),
1499
+ ...extensionReceiver ? { isExtension: true, extensionReceiver } : {},
1500
+ kotlinSpecific: true
1501
+ }
1502
+ };
1503
+ symbols.push(symbol);
1504
+ if (extensionReceiver && relationships) {
1505
+ relationships.push({
1506
+ source: propertyName,
1507
+ target: extensionReceiver,
1508
+ type: ASTEngine.RelationshipType.Uses,
1509
+ confidence: 0.9,
1510
+ // (#825): the receiver type is named by its receiver node.
1511
+ ...this.lineField(extensionReceiverNode, node),
1512
+ metadata: {
1513
+ relationshipType: "kotlin_extension_property",
1514
+ isExtension: true
1515
+ }
1516
+ });
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Kotlin-specific framework detection
1521
+ */
1522
+ detectKotlinFrameworks(content, symbols, imports) {
1523
+ const frameworks = {};
1524
+ const hasImport = (pkg) => {
1525
+ for (let i = 0; i < imports.length; i++) {
1526
+ const mod = imports[i].module;
1527
+ if (mod && (mod.startsWith(pkg) || mod === pkg)) return true;
1528
+ }
1529
+ return false;
1530
+ };
1531
+ if (hasImport("org.springframework") || hasImport("org.springframework.boot")) {
1532
+ frameworks["Spring Boot"] = true;
1533
+ frameworks["Spring Framework"] = true;
1534
+ }
1535
+ if (hasImport("io.ktor")) frameworks["Ktor"] = true;
1536
+ if (hasImport("io.micronaut")) frameworks["Micronaut"] = true;
1537
+ if (hasImport("io.reactivex.rxjava2") || hasImport("io.reactivex.rxjava3")) frameworks["RxJava"] = true;
1538
+ if (hasImport("org.hibernate") || hasImport("javax.persistence") || hasImport("jakarta.persistence"))
1539
+ frameworks["Hibernate / JPA"] = true;
1540
+ if (hasImport("kotlinx.coroutines")) frameworks["Kotlin Coroutines"] = true;
1541
+ if (hasImport("org.junit") || hasImport("kotlin.test")) frameworks["JUnit"] = true;
1542
+ if (hasImport("io.mockk") || hasImport("org.mockito")) frameworks["Mocking Framework"] = true;
1543
+ if (hasImport("androidx.compose")) frameworks["Jetpack Compose"] = true;
1544
+ if (hasImport("android.app") || hasImport("androidx")) frameworks["Android SDK"] = true;
1545
+ const annotationNames = /* @__PURE__ */ new Set();
1546
+ for (let i = 0; i < symbols.length; i++) {
1547
+ const anns = symbols[i].metadata?.annotations ?? [];
1548
+ for (let j = 0; j < anns.length; j++) annotationNames.add(anns[j]);
1549
+ }
1550
+ const hasAnnotation = (...names) => names.some((n) => annotationNames.has(n));
1551
+ if (hasAnnotation("Service", "RestController", "Controller", "Repository", "Component", "SpringBootApplication")) {
1552
+ frameworks["Spring Boot"] = true;
1553
+ frameworks["Spring Framework"] = true;
1554
+ }
1555
+ if (hasAnnotation("Entity", "Table")) frameworks["Hibernate / JPA"] = true;
1556
+ if (hasAnnotation("Dao", "Database")) frameworks["Room"] = true;
1557
+ if (hasAnnotation("Composable")) frameworks["Jetpack Compose"] = true;
1558
+ if (hasAnnotation("Test", "RunWith", "ExtendWith")) frameworks["JUnit"] = true;
1559
+ const result = [];
1560
+ for (const key in frameworks) {
1561
+ if (Object.prototype.hasOwnProperty.call(frameworks, key)) {
1562
+ result.push(key);
1563
+ }
1564
+ }
1565
+ return result;
1566
+ }
1567
+ /**
1568
+ * Detect Kotlin-specific patterns
1569
+ */
1570
+ detectKotlinPatterns(symbols, relationships, content, rootNode) {
1571
+ const patterns = [];
1572
+ let hasData = false, hasSealed = false, hasExtension = false, hasSuspend = false, hasDelegate = false, hasCompanion = false, hasOperator = false;
1573
+ for (let i = 0; i < symbols.length; i++) {
1574
+ const s = symbols[i];
1575
+ if (s.metadata?.isData) hasData = true;
1576
+ if (s.metadata?.isSealed) hasSealed = true;
1577
+ if (s.metadata?.isExtension) hasExtension = true;
1578
+ if (s.metadata?.isSuspend) hasSuspend = true;
1579
+ if (s.metadata?.hasDelegate) hasDelegate = true;
1580
+ if (s.metadata?.hasCompanionObject || s.metadata?.isCompanionObject) hasCompanion = true;
1581
+ if (s.metadata?.isOperator) hasOperator = true;
1582
+ }
1583
+ if (hasData) patterns.push("Data Classes");
1584
+ if (hasSealed) patterns.push("Sealed Classes");
1585
+ if (hasExtension) patterns.push("Extension Functions");
1586
+ if (hasSuspend) patterns.push("Coroutines");
1587
+ const astHas = /* @__PURE__ */ new Map();
1588
+ const scanAst = (n) => {
1589
+ switch (n.type) {
1590
+ case "check_expression":
1591
+ astHas.set("is", true);
1592
+ break;
1593
+ case "elvis_expression":
1594
+ astHas.set("elvis", true);
1595
+ break;
1596
+ case "lambda_literal":
1597
+ case "anonymous_function":
1598
+ astHas.set("lambda", true);
1599
+ break;
1600
+ }
1601
+ if (n.type === "navigation_expression") {
1602
+ for (let i = 0; i < n.namedChildCount; i++) {
1603
+ if (n.namedChild(i)?.type === "quest") astHas.set("nullsafe", true);
1604
+ }
1605
+ }
1606
+ if (n.type === "postfix_expression" && n.text.endsWith("!!")) astHas.set("notnull", true);
1607
+ if (n.type === "delegation_specifier" && stringIncludes(n.text, " by ")) astHas.set("by", true);
1608
+ for (let i = 0; i < n.namedChildCount; i++) {
1609
+ const c = n.namedChild(i);
1610
+ if (c) scanAst(c);
1611
+ }
1612
+ };
1613
+ if (rootNode) scanAst(rootNode);
1614
+ if (astHas.get("nullsafe") || astHas.get("notnull") || astHas.get("elvis")) patterns.push("Null Safety");
1615
+ if (astHas.get("is")) patterns.push("Smart Casts");
1616
+ if (astHas.get("lambda")) patterns.push("Higher-Order Functions");
1617
+ if (hasDelegate || astHas.get("by")) {
1618
+ patterns.push("Delegation");
1619
+ patterns.push("Property Delegation");
1620
+ }
1621
+ if (stringIncludes(content, "buildString {")) patterns.push("DSL Patterns");
1622
+ if (stringIncludes(content, "@GetMapping") || stringIncludes(content, "@PostMapping") || stringIncludes(content, "@RequestMapping")) patterns.push("REST API");
1623
+ if (hasCompanion) patterns.push("Companion Objects");
1624
+ if (hasOperator) patterns.push("Operator Overloading");
1625
+ return patterns;
1626
+ }
1627
+ extractKotlinParameters(node, ctx) {
1628
+ const parameters = [];
1629
+ let paramsNode = this.findChildByType(node, ["function_value_parameters"]);
1630
+ if (!paramsNode) return parameters;
1631
+ const kids = [];
1632
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
1633
+ const c = paramsNode.namedChild(i);
1634
+ if (c) kids.push(c);
1635
+ }
1636
+ for (let i = 0; i < kids.length; i++) {
1637
+ const paramNode = kids[i];
1638
+ if (paramNode.type !== "parameter") continue;
1639
+ const next = kids[i + 1];
1640
+ const defaultNode = next && next.type !== "parameter" ? next : null;
1641
+ const nameNode = this.findNameNode(paramNode);
1642
+ const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
1643
+ if (nameNode && nameNode.text.length > 0) {
1644
+ parameters.push({
1645
+ name: nameNode.text,
1646
+ type: typeNode ? this.cleanKotlinType(typeNode.text) : void 0,
1647
+ isOptional: !!defaultNode,
1648
+ defaultValue: defaultNode ? defaultNode.text : void 0
1649
+ });
1650
+ }
1651
+ }
1652
+ return parameters;
1653
+ }
1654
+ buildQualifiedName(name, ctx, parentClass) {
1655
+ const scope = parentClass || this.currentScope(ctx);
1656
+ return scope ? `${scope}.${name}` : name;
1657
+ }
1658
+ currentScope(ctx) {
1659
+ return [...ctx.packageStack, ...ctx.classStack].join(".");
1660
+ }
1661
+ /**
1662
+ * Scope of a type declaration = package + ENCLOSING types, excluding the
1663
+ * declaration's own name (which has already been pushed onto classStack by
1664
+ * the time the extractor runs). A container must never contain itself.
1665
+ */
1666
+ enclosingScope(ctx) {
1667
+ const enclosing = ctx.classStack.slice(0, ctx.classStack.length - 1);
1668
+ return [...ctx.packageStack, ...enclosing].join(".");
1669
+ }
1670
+ /**
1671
+ * M14 #299 - evidence-tier classification for EVERY emitted relationship.
1672
+ * Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
1673
+ * field access, companion-apply idioms) | import_based | reference_qualified
1674
+ * | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
1675
+ * so no emission site can forget it.
1676
+ */
1677
+ classifyEvidenceTier(relationships) {
1678
+ for (const r of relationships) {
1679
+ const md = r.metadata ?? (r.metadata = {});
1680
+ if (md.evidenceClass) continue;
1681
+ const t = String(r.type ?? r.relationshipType ?? "");
1682
+ const conf = typeof r.confidence === "number" ? r.confidence : 1;
1683
+ if (t === "contains" || t === "exports") {
1684
+ md.evidenceClass = md.relationshipType === "compose_ui_tree" ? "heuristic" : "structural";
1685
+ continue;
1686
+ }
1687
+ if (t === "references") {
1688
+ md.evidenceClass = md.isImport || md.isImportDep ? "import_based" : "reference_qualified";
1689
+ continue;
1690
+ }
1691
+ if (t === "imports") {
1692
+ md.evidenceClass = "import_based";
1693
+ continue;
1694
+ }
1695
+ if (t === "extends" || t === "implements") {
1696
+ md.evidenceClass = conf >= 1 ? "structural" : "inferred";
1697
+ continue;
1698
+ }
1699
+ if (t === "overrides") {
1700
+ md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
1701
+ continue;
1702
+ }
1703
+ if (t === "instantiates") {
1704
+ md.evidenceClass = md.isNew || conf >= 0.9 ? "structural" : "usage_contextual";
1705
+ continue;
1706
+ }
1707
+ md.evidenceClass = "usage_contextual";
1708
+ }
1709
+ }
1710
+ /**
1711
+ * (#825): the `line_number` field for a relationship, taken from the node
1712
+ * that CARRIES THE NAME the edge is about. java and scala settled this rule
1713
+ * (the line belongs to the name, never to the construct that contains it) and
1714
+ * kotlin shares their node shapes, so it is the same contract, ported.
1715
+ *
1716
+ * Returns nothing at all when no node is available — never 0: #820/#821, a
1717
+ * stored 0 is a claim that the relationship occurs on line 0.
1718
+ */
1719
+ lineField(nameNode, fallback) {
1720
+ const n = nameNode ?? fallback;
1721
+ return n ? { line_number: n.startPosition.row + 1 } : {};
1722
+ }
1723
+ /**
1724
+ * Is this `call_expression` a KOTLIN 2.2 CONTEXT-PARAMETER HEADER rather than a
1725
+ * call?
1726
+ *
1727
+ * `context(session: FirSession)` is a DECLARATION FORM with no node of its own
1728
+ * in this grammar, so it arrives as an ordinary call and was published as one:
1729
+ * `calls[Global -> context]` at 0.7 for every header — a real statement
1730
+ * attributed to a symbol no kotlin file declares. Measured on a 400-file
1731
+ * stratified draw of the pinned fixture corpus (kotlin@9b80c8ff): 20 headers in 4
1732
+ * files, 28 of the sample's 524 `Global` call edges (the other 496 are Gradle
1733
+ * `.kts` script statements, which have no owner either).
1734
+ *
1735
+ * The guard tests for an ERROR inside the `call_suffix`, NOT for the name
1736
+ * `context` alone, and that distinction is the whole rule: a `value_argument`
1737
+ * cannot carry an annotation, so the parameter list does not parse (the dumped
1738
+ * tree ends `ERROR ": FirSession"`), while a real `context(session)`
1739
+ * INVOCATION parses cleanly. `context` is a SOFT keyword — `fun context(x: Int)`
1740
+ * is a legal declaration and a call to it is a real call — so a name-only test
1741
+ * would delete live edges, which `kotlin/context-parameters` pins against.
1742
+ */
1743
+ /**
1744
+ * Is this declaration LOCAL — written inside a BODY rather than in a class body
1745
+ * or at file level?
1746
+ *
1747
+ * The test walks up to the nearest enclosing position and answers at the FIRST
1748
+ * of them: a body (`function_body`, `lambda_literal`, `anonymous_function`) is
1749
+ * a local; a declaration position (`class_body`, `enum_class_body`,
1750
+ * `source_file`) is not. `anonymous_initializer` is treated as a declaration
1751
+ * position on purpose — an `init` block's locals are M60 K2's ruling, not this
1752
+ * one, and folding them in here would move a pin that already exists.
1753
+ *
1754
+ * This is deliberately ancestry-only: the same question "is this declaration
1755
+ * only reachable from inside a body" is what `kotlinDeclarationPlacement`
1756
+ * answers for the exports surface, and that helper is line-keyed because it
1757
+ * works on symbol ranges after the tree is gone. Both exist because they run at
1758
+ * different stages; a declaration that is local by ancestry is local by line.
1759
+ */
1760
+ /**
1761
+ * The file's module identity: its first TOP-LEVEL TYPE, or null.
1762
+ *
1763
+ * One predicate, three readers (the `references` source rewrite, the `imports`
1764
+ * source, and the `exports` unit source) so the three cannot disagree about
1765
+ * which name stands for the compilation unit — the failure mode that made the
1766
+ * old file emit one `exports` edge sourced at the package, a second at `(file)`
1767
+ * and a third spelling in the fixtures.
1768
+ *
1769
+ * A top-level type is a class/interface/enum with no owner, which excludes a
1770
+ * nested type, a member, a top-level function and a top-level property. Phantom
1771
+ * symbols (data-class generated members) and anonymous objects are excluded
1772
+ * explicitly: neither can be named by an import.
1773
+ */
1774
+ primaryTopLevelType(symbols) {
1775
+ return symbols.find((s) => {
1776
+ if (s.ownerName) return false;
1777
+ if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) return false;
1778
+ return s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum;
1779
+ }) ?? null;
1780
+ }
1781
+ isKotlinLocalDeclaration(node) {
1782
+ let p = node.parent;
1783
+ while (p) {
1784
+ const t = p.type;
1785
+ if (t === "class_body" || t === "enum_class_body" || t === "source_file" || t === "anonymous_initializer") return false;
1786
+ if (t === "function_body" || t === "lambda_literal" || t === "anonymous_function") return true;
1787
+ p = p.parent;
1788
+ }
1789
+ return false;
1790
+ }
1791
+ isKotlinContextParameterHeader(node) {
1792
+ const nameChild = node.namedChild(0);
1793
+ if (!nameChild || nameChild.type !== "simple_identifier" || nameChild.text !== "context") return false;
1794
+ const suffix = this.findChildByType(node, ["call_suffix"]);
1795
+ return !!suffix && suffix.hasError;
1796
+ }
1797
+ /**
1798
+ * (#825): the node that NAMES a kotlin call.
1799
+ *
1800
+ * Measured from the pinned wasm rather than assumed: a kotlin
1801
+ * `call_expression`'s named children are `navigation_expression` and
1802
+ * `call_suffix`, and the navigation expression SPANS the receiver — for
1803
+ * `s\n .trim()\n .toUpperCase()` the node opens on line 5 while
1804
+ * `.toUpperCase()` (the name, and the `()` that follows it) is written on 7.
1805
+ * The last identifier inside the navigation expression is the callee leaf,
1806
+ * which is where the call is written.
1807
+ */
1808
+ calleeLeafNode(callNode, leafText) {
1809
+ const nav = callNode.namedChild(0);
1810
+ if (!nav) return null;
1811
+ let last = null;
1812
+ let legacyLast = null;
1813
+ let leafHit = null;
1814
+ const visit = (n, recordLeaf) => {
1815
+ if (n.type === "simple_identifier" || n.type === "type_identifier") {
1816
+ last = n;
1817
+ if (recordLeaf && leafText !== void 0 && n.text === leafText) leafHit = n;
1818
+ }
1819
+ for (let i = 0; i < n.namedChildCount; i++) {
1820
+ const c = n.namedChild(i);
1821
+ if (c) visit(c, recordLeaf);
1822
+ }
1823
+ };
1824
+ visit(nav, true);
1825
+ legacyLast = last;
1826
+ if (!leafHit && leafText !== void 0) visit(callNode, true);
1827
+ if (!leafHit && leafText !== void 0) {
1828
+ const navChild = this.findChildByType(callNode, ["navigation_expression"]);
1829
+ if (navChild && navChild.id !== nav.id) visit(navChild, true);
1830
+ }
1831
+ return leafHit ?? legacyLast ?? last ?? nav;
1832
+ }
1833
+ /**
1834
+ * (#825): the node whose start is the DECLARATION's own line.
1835
+ *
1836
+ * An annotation is a modifier — `@Test`/`@Bean` decorate the declaration, they
1837
+ * are not where it is written. Measured on the 40-file kotlin oracle sample:
1838
+ * with the node's own start, `contains` and `overrides` edges landed on the
1839
+ * ANNOTATION line for every annotated member (102 of the sample's 241
1840
+ * inconsistent claims started at an `@Test`/`@Bean` line). java measured the
1841
+ * same defect at 27% of its symbols and scala settled the contract; kotlin
1842
+ * inherits one rule, not a third variant.
1843
+ *
1844
+ * The symbol range and the edges that name it move together, so a symbol and
1845
+ * its `contains`/`overrides` lines can never disagree.
1846
+ */
1847
+ declarationLineNode(node) {
1848
+ const name = this.findNameNode(node);
1849
+ if (name) return name;
1850
+ for (let i = 0; i < node.namedChildCount; i++) {
1851
+ const c = node.namedChild(i);
1852
+ if (!c) continue;
1853
+ if (c.type === "modifiers" || c.type === "annotation") continue;
1854
+ if (c.type === "line_comment" || c.type === "multiline_comment") continue;
1855
+ return c;
1856
+ }
1857
+ return node;
1858
+ }
1859
+ /** The range of a SYMBOL: annotations skipped (see declarationLineNode). */
1860
+ symbolRange(node) {
1861
+ const decl = this.declarationLineNode(node);
1862
+ const base = this.nodeToRange(node);
1863
+ return {
1864
+ ...base,
1865
+ startLine: decl.startPosition.row + 1,
1866
+ startColumn: decl.startPosition.column + 1,
1867
+ startIndex: decl.startIndex
1868
+ };
1869
+ }
1870
+ nodeToRange(node) {
1871
+ return {
1872
+ startLine: node.startPosition.row + 1,
1873
+ startColumn: node.startPosition.column + 1,
1874
+ endLine: node.endPosition.row + 1,
1875
+ endColumn: node.endPosition.column + 1,
1876
+ startIndex: node.startIndex,
1877
+ endIndex: node.endIndex
1878
+ };
1879
+ }
1880
+ // Additional helper methods (stubs for now)...
1881
+ /**
1882
+ * Kotlin Interface Extraction
1883
+ */
1884
+ extractInterface(node, symbols, ctx, parentClass) {
1885
+ const nameNode = this.findNameNode(node);
1886
+ if (!nameNode) return;
1887
+ if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
1888
+ const interfaceName = nameNode.text;
1889
+ const modifiers = this.extractModifiers(node, ctx);
1890
+ const superTypes = this.extractSuperTypes(node, ctx);
1891
+ const superTypeKinds = this.extractSuperTypesWithKinds(node);
1892
+ const typeParameters = this.extractTypeParameters(node, ctx);
1893
+ const annotations = this.extractAnnotations(node, ctx);
1894
+ const symbol = {
1895
+ name: interfaceName,
1896
+ kind: ASTEngine.SymbolKind.Interface,
1897
+ range: this.symbolRange(node),
1898
+ scope: this.enclosingScope(ctx),
1899
+ visibility: this.determineVisibility(modifiers, ctx),
1900
+ isAbstract: true,
1901
+ // All interfaces are abstract
1902
+ ownerName: parentClass,
1903
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1904
+ decorators: annotations,
1905
+ metadata: {
1906
+ modifiers,
1907
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1908
+ superTypes,
1909
+ superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
1910
+ // (#825): parallel line array — one line per supertype, in the
1911
+ // same order. Heritage edges take their line from here, not from the
1912
+ // class header (which names only the first supertype).
1913
+ superTypeLines: superTypeKinds.map((s) => s.line),
1914
+ inheritance: superTypes,
1915
+ typeParameters,
1916
+ methodCount: this.countMethods(node, ctx),
1917
+ isKotlinInterface: true,
1918
+ kotlinSpecific: true
1919
+ }
1920
+ };
1921
+ symbols.push(symbol);
1922
+ }
1923
+ /**
1924
+ * Kotlin Enum Class Extraction
1925
+ */
1926
+ extractEnumClass(node, symbols, ctx, parentClass) {
1927
+ const nameNode = this.findNameNode(node);
1928
+ if (!nameNode) return;
1929
+ if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
1930
+ const enumName = nameNode.text;
1931
+ const modifiers = this.extractModifiers(node, ctx);
1932
+ const annotations = this.extractAnnotations(node, ctx);
1933
+ const enumValues = this.extractKotlinEnumValues(node);
1934
+ const superTypes = this.extractSuperTypes(node, ctx);
1935
+ const superTypeKinds = this.extractSuperTypesWithKinds(node);
1936
+ const symbol = {
1937
+ name: enumName,
1938
+ kind: ASTEngine.SymbolKind.Enum,
1939
+ range: this.symbolRange(node),
1940
+ scope: this.enclosingScope(ctx),
1941
+ visibility: this.determineVisibility(modifiers, ctx),
1942
+ ownerName: parentClass,
1943
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
1944
+ decorators: annotations,
1945
+ metadata: {
1946
+ modifiers,
1947
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
1948
+ superTypes,
1949
+ superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
1950
+ // (#825): parallel line array — one line per supertype.
1951
+ superTypeLines: superTypeKinds.map((s) => s.line),
1952
+ enumValues,
1953
+ valueCount: enumValues.length,
1954
+ hasEnumMethods: this.countMethods(node, ctx) > 0,
1955
+ isKotlinEnum: true,
1956
+ kotlinSpecific: true
1957
+ }
1958
+ };
1959
+ symbols.push(symbol);
1960
+ const enumBody = this.findChildByType(node, ["enum_class_body", "class_body"]);
1961
+ const entryNodes = [];
1962
+ if (enumBody) {
1963
+ for (let i = 0; i < enumBody.namedChildCount; i++) {
1964
+ const c = enumBody.namedChild(i);
1965
+ if (c?.type === "enum_entry") entryNodes.push(c);
1966
+ }
1967
+ }
1968
+ for (let i = 0; i < enumValues.length; i++) {
1969
+ const enumValue = enumValues[i];
1970
+ const entryNode = entryNodes[i] ?? node;
1971
+ symbols.push({
1972
+ name: enumValue,
1973
+ kind: ASTEngine.SymbolKind.EnumMember,
1974
+ range: this.symbolRange(entryNode),
1975
+ scope: this.currentScope(ctx),
1976
+ visibility: "public",
1977
+ ownerName: enumName,
1978
+ ownerKind: ASTEngine.SymbolKind.Enum,
1979
+ metadata: {
1980
+ isEnumValue: true,
1981
+ kotlinSpecific: true
1982
+ }
1983
+ });
1984
+ }
1985
+ }
1986
+ /**
1987
+ * Kotlin Constructor Extraction
1988
+ */
1989
+ extractConstructor(node, symbols, ctx, parentClass) {
1990
+ const parameters = this.extractKotlinParameters(node, ctx);
1991
+ const modifiers = this.extractModifiers(node, ctx);
1992
+ const annotations = this.extractAnnotations(node, ctx);
1993
+ const isPrimaryConstructor = node.type === "primary_constructor";
1994
+ const constructorProperties = [];
1995
+ if (isPrimaryConstructor) {
1996
+ parameters.length = 0;
1997
+ for (let i = 0; i < node.namedChildCount; i++) {
1998
+ const paramNode = node.namedChild(i);
1999
+ if (paramNode && paramNode.type === "class_parameter") {
2000
+ const nameNode = this.findNameNode(paramNode);
2001
+ const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
2002
+ let exists = false;
2003
+ if (nameNode && nameNode.text.length > 0) {
2004
+ for (let j = 0; j < parameters.length; j++) {
2005
+ if (parameters[j].name === nameNode.text) {
2006
+ exists = true;
2007
+ break;
2008
+ }
2009
+ }
2010
+ }
2011
+ if (nameNode && !exists) {
2012
+ parameters.push({
2013
+ name: nameNode.text,
2014
+ type: typeNode ? this.cleanKotlinType(typeNode.text) : "Any"
2015
+ });
2016
+ }
2017
+ const bindingKind = this.findChildByType(paramNode, ["binding_pattern_kind"]);
2018
+ if (bindingKind && (bindingKind.text === "val" || bindingKind.text === "var")) {
2019
+ constructorProperties.push(paramNode);
2020
+ }
2021
+ }
2022
+ }
2023
+ }
2024
+ let hasDefaults = false;
2025
+ for (let i = 0; i < parameters.length; i++) {
2026
+ if (parameters[i].defaultValue !== void 0) {
2027
+ hasDefaults = true;
2028
+ break;
2029
+ }
2030
+ }
2031
+ if (!_RealKotlinAnalyzer.publishableEndpoint(parentClass)) return;
2032
+ const constructorName = parentClass;
2033
+ let ctorDeclNode = node;
2034
+ let ancestor = node.parent;
2035
+ while (ancestor && ancestor.type === "class_body") ancestor = ancestor.parent;
2036
+ if (ancestor && (ancestor.type === "class_declaration" || ancestor.type === "object_declaration")) ctorDeclNode = ancestor;
2037
+ const symbol = {
2038
+ name: constructorName,
2039
+ kind: ASTEngine.SymbolKind.Constructor,
2040
+ range: this.symbolRange(ctorDeclNode),
2041
+ scope: this.currentScope(ctx),
2042
+ visibility: this.determineVisibility(modifiers, ctx),
2043
+ parameters,
2044
+ decorators: annotations,
2045
+ ownerName: parentClass,
2046
+ ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
2047
+ metadata: {
2048
+ modifiers,
2049
+ annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
2050
+ isPrimaryConstructor,
2051
+ parameterCount: parameters.length,
2052
+ hasDefaultParameters: hasDefaults,
2053
+ kotlinSpecific: true
2054
+ }
2055
+ };
2056
+ symbols.push(symbol);
2057
+ if (parentClass) {
2058
+ for (let i = 0; i < constructorProperties.length; i++) {
2059
+ const paramNode = constructorProperties[i];
2060
+ const nameNode = this.findNameNode(paramNode);
2061
+ const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
2062
+ const bindingKind = this.findChildByType(paramNode, ["binding_pattern_kind"]);
2063
+ if (!nameNode || !bindingKind) continue;
2064
+ symbols.push({
2065
+ name: nameNode.text,
2066
+ kind: ASTEngine.SymbolKind.Property,
2067
+ range: this.symbolRange(paramNode),
2068
+ scope: this.currentScope(ctx),
2069
+ visibility: this.determineVisibility(this.extractModifiers(paramNode, ctx), ctx),
2070
+ ownerName: parentClass,
2071
+ ownerKind: ASTEngine.SymbolKind.Class,
2072
+ returnType: typeNode ? this.cleanKotlinType(typeNode.text) : void 0,
2073
+ metadata: {
2074
+ isVal: bindingKind.text === "val",
2075
+ isVar: bindingKind.text === "var",
2076
+ isConstructorProperty: true,
2077
+ kotlinSpecific: true
2078
+ }
2079
+ });
2080
+ }
2081
+ }
2082
+ }
2083
+ /**
2084
+ * Kotlin Package Extraction
2085
+ */
2086
+ extractPackage(node, ctx) {
2087
+ const packageNameNode = this.findChildByType(node, ["identifier", "simple_identifier", "type_identifier"]);
2088
+ if (packageNameNode) {
2089
+ const packageName = packageNameNode.text;
2090
+ ctx.packageStack = packageName.split(".");
2091
+ }
2092
+ }
2093
+ /**
2094
+ * Kotlin Import Extraction
2095
+ */
2096
+ extractImport(node, ctx) {
2097
+ void ctx;
2098
+ }
2099
+ /**
2100
+ * Kotlin Annotation Extraction
2101
+ */
2102
+ extractAnnotation(node, symbols, ctx) {
2103
+ void symbols;
2104
+ void ctx;
2105
+ }
2106
+ /**
2107
+ * Kotlin Modifier Extraction
2108
+ */
2109
+ extractModifiers(node, ctx) {
2110
+ const modifiers = [];
2111
+ const modifiersNode = this.findChildByType(node, ["modifiers"]);
2112
+ if (modifiersNode) {
2113
+ for (let j = 0; j < modifiersNode.namedChildCount; j++) {
2114
+ const modifier = modifiersNode.namedChild(j);
2115
+ if (modifier && modifier.type !== "annotation") {
2116
+ modifiers.push(modifier.text.trim());
2117
+ }
2118
+ }
2119
+ }
2120
+ return modifiers;
2121
+ }
2122
+ /**
2123
+ * Kotlin Return Type Extraction
2124
+ */
2125
+ extractReturnType(node, ctx) {
2126
+ const typeNode = this.findChildByType(node, ["user_type", "nullable_type", "function_type", "type_identifier"]);
2127
+ if (typeNode) {
2128
+ return this.cleanKotlinType(typeNode.text);
2129
+ }
2130
+ const functionBody = this.findChildByType(node, ["function_body"]);
2131
+ if (functionBody && !functionBody.text.startsWith("{")) {
2132
+ return void 0;
2133
+ }
2134
+ return "Unit";
2135
+ }
2136
+ /**
2137
+ * Kotlin Receiver Type Extraction (for extension functions)
2138
+ */
2139
+ extractReceiverType(node, ctx) {
2140
+ const fieldReceiver = node.childForFieldName("receiver");
2141
+ if (fieldReceiver) return this.cleanKotlinType(fieldReceiver.text);
2142
+ for (let i = 0; i < node.namedChildCount; i++) {
2143
+ const c = node.namedChild(i);
2144
+ if (c?.type === "simple_identifier") break;
2145
+ if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
2146
+ return this.cleanKotlinType(c.text);
2147
+ }
2148
+ }
2149
+ return void 0;
2150
+ }
2151
+ /**
2152
+ * Kotlin Function Call Detection
2153
+ * Analyzes Kotlin method calls, extension functions, and Android/Spring patterns
2154
+ */
2155
+ findKotlinFunctionCalls(node, ctx) {
2156
+ const calls = [];
2157
+ const stack = [node];
2158
+ while (stack.length > 0) {
2159
+ const n = stack.pop();
2160
+ if (this.isKotlinContextParameterHeader(n)) continue;
2161
+ if (n.type === "call_expression") {
2162
+ const callName = this.buildKotlinCallExpression(n);
2163
+ if (callName && !this.isKotlinBuiltin(callName)) {
2164
+ calls.push(callName);
2165
+ }
2166
+ }
2167
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
2168
+ const child = n.namedChild(i);
2169
+ if (child) stack.push(child);
2170
+ }
2171
+ }
2172
+ return calls;
2173
+ }
2174
+ /**
2175
+ * Kotlin Annotations Extraction (multiple)
2176
+ */
2177
+ extractAnnotations(node, ctx) {
2178
+ const annotations = [];
2179
+ const modifiers = this.extractModifiers(node, ctx);
2180
+ if (modifiers.includes("expect")) annotations.push("@expect");
2181
+ if (modifiers.includes("actual")) annotations.push("@actual");
2182
+ const collect = (n) => {
2183
+ for (let i = 0; i < n.namedChildCount; i++) {
2184
+ const c = n.namedChild(i);
2185
+ if (c && c.type === "annotation") {
2186
+ annotations.push(c.text.startsWith("@") ? c.text.slice(1) : c.text);
2187
+ }
2188
+ }
2189
+ };
2190
+ if (node) {
2191
+ collect(node);
2192
+ const modifiersNode = this.findChildByType(node, ["modifiers"]);
2193
+ if (modifiersNode) collect(modifiersNode);
2194
+ }
2195
+ return annotations;
2196
+ }
2197
+ /**
2198
+ * Kotlin KDoc Comment Extraction
2199
+ */
2200
+ extractKDocComment(node, ctx) {
2201
+ const parent = node.parent;
2202
+ if (!parent) return void 0;
2203
+ let selfIdx = -1;
2204
+ for (let i = 0; i < parent.namedChildCount; i++) {
2205
+ const c = parent.namedChild(i);
2206
+ if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
2207
+ selfIdx = i;
2208
+ break;
2209
+ }
2210
+ }
2211
+ for (let i = selfIdx - 1; i >= 0; i--) {
2212
+ const sibling = parent.namedChild(i);
2213
+ if (!sibling) continue;
2214
+ if ((sibling.type === "multiline_comment" || sibling.type === "line_comment") && sibling.text.startsWith("/**")) {
2215
+ return sibling.text;
2216
+ }
2217
+ break;
2218
+ }
2219
+ return void 0;
2220
+ }
2221
+ /**
2222
+ * Kotlin Visibility Determination
2223
+ */
2224
+ determineVisibility(modifiers, ctx) {
2225
+ for (let i = 0; i < modifiers.length; i++) {
2226
+ const mod = modifiers[i];
2227
+ if (mod === "private") return "private";
2228
+ if (mod === "protected") return "protected";
2229
+ if (mod === "internal") return "internal";
2230
+ }
2231
+ return "public";
2232
+ }
2233
+ /**
2234
+ * Kotlin Function Complexity Calculation
2235
+ */
2236
+ calculateFunctionComplexity(node, ctx) {
2237
+ let complexity = 1;
2238
+ const stack = [node];
2239
+ while (stack.length > 0) {
2240
+ const n = stack.pop();
2241
+ if (n.type === "if_expression" || n.type === "when_entry" || // per-case, matching java/TS switch_case granularity
2242
+ n.type === "for_statement" || n.type === "while_statement" || n.type === "do_while_statement" || n.type === "catch_block" || n.type === "elvis_expression") {
2243
+ complexity++;
2244
+ } else if (n.type === "conjunction_expression" || n.type === "disjunction_expression") {
2245
+ complexity++;
2246
+ }
2247
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
2248
+ const child = n.namedChild(i);
2249
+ if (child) stack.push(child);
2250
+ }
2251
+ }
2252
+ return complexity;
2253
+ }
2254
+ /**
2255
+ * Kotlin Super Types Extraction
2256
+ */
2257
+ /**
2258
+ * Extract the clean base type name of every supertype. A delegation_specifier
2259
+ * contains either a user_type directly (Repository<User>) or a
2260
+ * constructor_invocation (BaseService<User>()) wrapping one. The base name
2261
+ * strips type arguments and nullability so relationship targets are valid
2262
+ * symbol leaves (pool-worker's noise filter rejects targets containing <>).
2263
+ */
2264
+ extractSuperTypes(node, ctx) {
2265
+ return this.extractSuperTypesWithKinds(node).map((s) => s.name);
2266
+ }
2267
+ /**
2268
+ * Kotlin semantics (Kotlin docs, Inheritance): a supertype written with a
2269
+ * constructor invocation `Base()` is an extended CLASS; a bare type `Repo`
2270
+ * is an IMPLEMENTED interface. Position alone is wrong - a class whose
2271
+ * first (or only) supertype is an interface was labelled extends.
2272
+ *
2273
+ * (#825): each entry also carries the line the SUPERtype is NAMED on.
2274
+ * A kotlin supertype list wraps (`class A :\n B(),\n C`), so the
2275
+ * class header's line names only the first entry. Same rule as java's
2276
+ * `extractJavaInheritance` and scala's `extractSupertypes`.
2277
+ */
2278
+ extractSuperTypesWithKinds(node) {
2279
+ const out = [];
2280
+ for (let i = 0; i < node.namedChildCount; i++) {
2281
+ const c = node.namedChild(i);
2282
+ if (c?.type === "delegation_specifier") {
2283
+ const invocation = this.findChildByType(c, ["constructor_invocation"]);
2284
+ let userType = this.findChildByType(c, ["user_type"]);
2285
+ if (!userType && invocation) userType = this.findChildByType(invocation, ["user_type"]);
2286
+ if (userType) {
2287
+ out.push({
2288
+ name: this.cleanKotlinBaseType(userType.text),
2289
+ isConstructorCall: !!invocation,
2290
+ line: userType.startPosition.row + 1
2291
+ });
2292
+ }
2293
+ }
2294
+ }
2295
+ return out;
2296
+ }
2297
+ /**
2298
+ * Reduce a (possibly generic/nullable) type to its base name:
2299
+ * "BaseService<User>" → "BaseService", "User?" → "User",
2300
+ * "Map<String, List<Int>>" → "Map".
2301
+ */
2302
+ cleanKotlinBaseType(typeText) {
2303
+ let t = typeText;
2304
+ while (t.endsWith("?")) t = t.slice(0, -1);
2305
+ const lt = t.indexOf("<");
2306
+ if (lt !== -1) t = t.slice(0, lt);
2307
+ return t.trim();
2308
+ }
2309
+ /**
2310
+ * Kotlin Type Parameters Extraction
2311
+ */
2312
+ /**
2313
+ * Positional `value_arguments` lookup. The pinned kotlin wasm exposes only
2314
+ * four fields (receiver/alternative/condition/consequence — verified
2315
+ * 2026-09-16), none of which is `value_arguments`, so this helper remains the
2316
+ * mechanism for annotation/route argument extraction (M14.1). The earlier
2317
+ * "fieldCount=0" claim in this comment was stale: the 1852ea1 rebuild added
2318
+ * field info, but only `receiver` among the fields this analyzer would want.
2319
+ */
2320
+ getValueArguments(node) {
2321
+ const stack = [node];
2322
+ while (stack.length > 0) {
2323
+ const n = stack.pop();
2324
+ for (let i = 0; i < n.namedChildCount; i++) {
2325
+ const c = n.namedChild(i);
2326
+ if (!c) continue;
2327
+ if (c.type === "value_arguments") return c;
2328
+ stack.push(c);
2329
+ }
2330
+ }
2331
+ return void 0;
2332
+ }
2333
+ // KAD-MR3 K13 (M49.K1): getCallFunctionName, extractKotlinAnnotationArguments
2334
+ // and extractKotlinAnnotationTarget were dead (grep-proven zero call sites) —
2335
+ // deleted. extractKotlinAnnotationArguments's arg-walk lives on in the
2336
+ // E6 route extractor as a real consumer.
2337
+ extractTypeParameters(node, ctx) {
2338
+ const typeParameters = [];
2339
+ const typeParamList = this.findChildByType(node, ["type_parameters"]);
2340
+ if (typeParamList) {
2341
+ for (let i = 0; i < typeParamList.namedChildCount; i++) {
2342
+ const typeParam = typeParamList.namedChild(i);
2343
+ if (typeParam?.type === "type_parameter") {
2344
+ const paramName = typeParam.namedChildren.find(
2345
+ (c) => c.type === "simple_identifier" || c.type === "type_identifier"
2346
+ )?.text;
2347
+ if (paramName) {
2348
+ typeParameters.push(paramName);
2349
+ }
2350
+ }
2351
+ }
2352
+ }
2353
+ return typeParameters;
2354
+ }
2355
+ /**
2356
+ * Kotlin Method Counting — DIRECT members only.
2357
+ * KAD-MR3 K14 (M49.K1): the subtree-wide count misattributed nested
2358
+ * structure — a nested class's members inflated the OUTER class's counts
2359
+ * (class Outer { class Nested { fun nm() } } reported methodCount=2 for
2360
+ * Outer). Probed containers: class/object bodies are class_body, enum
2361
+ * members live in enum_class_body.
2362
+ */
2363
+ countMethods(node, ctx) {
2364
+ let count = 0;
2365
+ for (const bt of ["class_body", "enum_class_body"]) {
2366
+ const body = this.findChildByType(node, [bt]);
2367
+ if (!body) continue;
2368
+ for (let i = 0; i < body.namedChildCount; i++) {
2369
+ const m = body.namedChild(i);
2370
+ if (m?.type === "function_declaration") count++;
2371
+ }
2372
+ }
2373
+ return count;
2374
+ }
2375
+ /**
2376
+ * Kotlin Property Counting — DIRECT members only (KAD-MR3 K14, see
2377
+ * countMethods). Companion-object properties belong to the companion, not
2378
+ * the outer class.
2379
+ */
2380
+ countProperties(node, ctx) {
2381
+ let count = 0;
2382
+ for (const bt of ["class_body", "enum_class_body"]) {
2383
+ const body = this.findChildByType(node, [bt]);
2384
+ if (!body) continue;
2385
+ for (let i = 0; i < body.namedChildCount; i++) {
2386
+ const m = body.namedChild(i);
2387
+ if (m?.type === "property_declaration") count++;
2388
+ }
2389
+ }
2390
+ return count;
2391
+ }
2392
+ /**
2393
+ * Kotlin Companion Object Detection — DIRECT member of THIS type only
2394
+ * (KAD-MR3 K14: the subtree-wide scan credited a nested class's companion
2395
+ * to the outer class).
2396
+ */
2397
+ hasCompanionObject(node, ctx) {
2398
+ for (const bt of ["class_body", "enum_class_body"]) {
2399
+ const body = this.findChildByType(node, [bt]);
2400
+ if (!body) continue;
2401
+ for (let i = 0; i < body.namedChildCount; i++) {
2402
+ const m = body.namedChild(i);
2403
+ if (m?.type !== "companion_object" && m?.type !== "object_declaration") continue;
2404
+ if (m.type === "companion_object") return true;
2405
+ const modifiers = this.extractModifiers(m, ctx);
2406
+ for (let j = 0; j < modifiers.length; j++) {
2407
+ if (modifiers[j] === "companion") return true;
2408
+ }
2409
+ }
2410
+ }
2411
+ return false;
2412
+ }
2413
+ /**
2414
+ * Kotlin Property Type Extraction
2415
+ */
2416
+ extractPropertyType(node, ctx) {
2417
+ const declarationNode = this.findChildByType(node, ["variable_declaration"]);
2418
+ const typeNode = this.findChildByType(node, ["user_type", "nullable_type", "function_type", "type_identifier"]) || (declarationNode ? this.findChildByType(declarationNode, ["user_type", "nullable_type", "function_type", "type_identifier"]) : null);
2419
+ if (typeNode) {
2420
+ return this.cleanKotlinType(typeNode.text);
2421
+ }
2422
+ const LITERAL_TYPES = ["integer_literal", "real_literal", "string_literal", "character_literal", "boolean_literal", "call_expression"];
2423
+ for (let i = 0; i < node.namedChildCount; i++) {
2424
+ const c = node.namedChild(i);
2425
+ if (c && LITERAL_TYPES.indexOf(c.type) !== -1) {
2426
+ return this.inferKotlinTypeFromInitializer(c);
2427
+ }
2428
+ }
2429
+ return void 0;
2430
+ }
2431
+ /**
2432
+ * Kotlin Val Property Detection
2433
+ */
2434
+ isValProperty(node) {
2435
+ const binding = this.findChildByType(node, ["binding_pattern_kind"]);
2436
+ if (binding) return binding.text === "val";
2437
+ return stringIncludes(node.text, "val ") && !stringIncludes(node.text, "var ");
2438
+ }
2439
+ /**
2440
+ * Kotlin Var Property Detection
2441
+ */
2442
+ isVarProperty(node) {
2443
+ const binding = this.findChildByType(node, ["binding_pattern_kind"]);
2444
+ if (binding) return binding.text === "var";
2445
+ return stringIncludes(node.text, "var ");
2446
+ }
2447
+ /**
2448
+ * Kotlin Property Delegation Detection
2449
+ */
2450
+ hasDelegate(node) {
2451
+ return !!this.findChildByType(node, ["property_delegate"]);
2452
+ }
2453
+ /**
2454
+ * Kotlin Custom Getter Detection
2455
+ */
2456
+ hasCustomGetter(node) {
2457
+ return this.hasPropertyAccessor(node, "getter");
2458
+ }
2459
+ /**
2460
+ * Kotlin accessor run: the pinned grammar emits accessors as SIBLINGS of
2461
+ * property_declaration (class_body > [property_declaration, getter,
2462
+ * setter]) — probe-verified, and the run is CONTIGUOUS (a setter follows
2463
+ * the getter, not the declaration). Text matching fabricated both ("get()")
2464
+ * hit `val t = m.get()` (a plain call named get); "set("+"value") hit
2465
+ * `val u = s.set(1, value)`). Span-locate the declaration among its
2466
+ * parent's children, then walk the contiguous accessor run that follows.
2467
+ */
2468
+ hasPropertyAccessor(node, accessorType) {
2469
+ const parent = node.parent;
2470
+ if (!parent) return false;
2471
+ for (let i = 0; i < parent.namedChildCount; i++) {
2472
+ const c = parent.namedChild(i);
2473
+ if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
2474
+ for (let j = i + 1; j < parent.namedChildCount; j++) {
2475
+ const sib = parent.namedChild(j);
2476
+ if (sib?.type === "getter" || sib?.type === "setter") {
2477
+ if (sib.type === accessorType) return true;
2478
+ continue;
2479
+ }
2480
+ break;
2481
+ }
2482
+ return false;
2483
+ }
2484
+ }
2485
+ return false;
2486
+ }
2487
+ /**
2488
+ * Kotlin Custom Setter Detection
2489
+ */
2490
+ hasCustomSetter(node) {
2491
+ return this.hasPropertyAccessor(node, "setter");
2492
+ }
2493
+ // ======================== ADDITIONAL KOTLIN HELPER METHODS ========================
2494
+ /**
2495
+ * Declared-type reference graph (M14). Emits References edges from the
2496
+ * enclosing function/class to every named type used in a declaration
2497
+ * position. Exact-triple dedupe guards against nested-declaration overlap
2498
+ * (e.g. lambda params inside a property initializer).
2499
+ */
2500
+ extractDeclaredTypeRefs(rootNode, relationships) {
2501
+ const emitted = /* @__PURE__ */ new Set();
2502
+ const collectTypeNames = (n, out) => {
2503
+ if (n.type === "type_identifier") {
2504
+ if (n.text.length === 0) return;
2505
+ if (!out.some((x) => x.name === n.text)) out.push({ name: n.text, line: n.startPosition.row + 1 });
2506
+ return;
2507
+ }
2508
+ for (let i = 0; i < n.namedChildCount; i++) {
2509
+ const c = n.namedChild(i);
2510
+ if (c) collectTypeNames(c, out);
2511
+ }
2512
+ };
2513
+ const resolveSource = (start) => {
2514
+ let p = start;
2515
+ while (p) {
2516
+ if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
2517
+ const name = this.findNameNode(p);
2518
+ if (name) return name.text;
2519
+ }
2520
+ const topLevelProp = this.kotlinTopLevelPropertyName(p);
2521
+ if (topLevelProp) return topLevelProp;
2522
+ p = p.parent;
2523
+ }
2524
+ return "Global";
2525
+ };
2526
+ const emit = (decl, typeRoots, kind, relType = "declared_type", sourceOverride) => {
2527
+ const source = sourceOverride ?? resolveSource(decl);
2528
+ for (const tr of typeRoots) {
2529
+ if (!tr) continue;
2530
+ const names = [];
2531
+ collectTypeNames(tr, names);
2532
+ for (const { name: t, line } of names) {
2533
+ const key = `${source}|${t}|${kind}`;
2534
+ if (emitted.has(key)) continue;
2535
+ emitted.add(key);
2536
+ relationships.push({
2537
+ source,
2538
+ target: t,
2539
+ type: ASTEngine.RelationshipType.References,
2540
+ confidence: 0.95,
2541
+ // (#825): the line the referenced TYPE is named on.
2542
+ line_number: line,
2543
+ metadata: { relationshipType: relType, declaredOn: decl.type }
2544
+ });
2545
+ }
2546
+ }
2547
+ };
2548
+ const walk = (n) => {
2549
+ switch (n.type) {
2550
+ case "class_parameter":
2551
+ case "parameter": {
2552
+ emit(n, [n], n.type);
2553
+ break;
2554
+ }
2555
+ case "variable_declaration": {
2556
+ emit(n, [n], "variable_declaration");
2557
+ break;
2558
+ }
2559
+ case "as_expression":
2560
+ case "check_expression": {
2561
+ if (n.namedChildCount >= 2) {
2562
+ const right = n.namedChild(n.namedChildCount - 1);
2563
+ emit(n, [right], n.type, n.type);
2564
+ }
2565
+ break;
2566
+ }
2567
+ case "callable_reference": {
2568
+ const tId = this.findNameNode(n);
2569
+ if (tId) emit(n, [tId], "callable_reference", "callable_reference");
2570
+ break;
2571
+ }
2572
+ case "type_alias": {
2573
+ const tName = this.findNameNode(n);
2574
+ if (tName) {
2575
+ const rhs = [];
2576
+ for (let i = 0; i < n.namedChildCount; i++) {
2577
+ const c = n.namedChild(i);
2578
+ if (c && c.type !== "type_identifier" && c.type !== "type_parameters") rhs.push(c);
2579
+ }
2580
+ emit(n, rhs, "type_alias", "type_alias", tName.text);
2581
+ }
2582
+ break;
2583
+ }
2584
+ case "object_literal": {
2585
+ const deleg = this.findChildByType(n, ["delegation_specifier"]);
2586
+ if (deleg) emit(n, [deleg], "object_literal", "object_literal");
2587
+ break;
2588
+ }
2589
+ case "function_declaration": {
2590
+ let sawParams = false;
2591
+ let sawName = false;
2592
+ for (let i = 0; i < n.namedChildCount; i++) {
2593
+ const c = n.namedChild(i);
2594
+ if (!c) continue;
2595
+ if (c.type === "function_value_parameters") {
2596
+ sawParams = true;
2597
+ continue;
2598
+ }
2599
+ if (c.type === "simple_identifier" || c.type === "type_identifier") {
2600
+ if (!sawName && !sawParams && !c.text.startsWith("`")) {
2601
+ if (i > 0 && n.namedChild(i - 1)?.type === "user_type") {
2602
+ emit(n, [c], "receiver_type");
2603
+ }
2604
+ sawName = true;
2605
+ continue;
2606
+ }
2607
+ continue;
2608
+ }
2609
+ if (c.type === "receiver_type" || c.type === "user_type" || c.type === "nullable_type" || c.type === "function_type") {
2610
+ if (!sawName && !sawParams) {
2611
+ emit(n, [c], "receiver_type");
2612
+ sawName = true;
2613
+ } else if (sawParams) {
2614
+ emit(n, [c], "return_type", "return_type");
2615
+ break;
2616
+ }
2617
+ }
2618
+ }
2619
+ break;
2620
+ }
2621
+ }
2622
+ for (let i = 0; i < n.namedChildCount; i++) {
2623
+ const c = n.namedChild(i);
2624
+ if (c) walk(c);
2625
+ }
2626
+ };
2627
+ walk(rootNode);
2628
+ }
2629
+ /**
2630
+ * M14 #419 K1/K4: expression-level Uses edges.
2631
+ * - indexing_expression / directly_assignable_expression (with an
2632
+ * indexing_suffix child): `xs[i]` reads and writes USE the indexed
2633
+ * expression — previously only contains edges were emitted, so the array
2634
+ * reference was invisible to the use graph.
2635
+ * - interpolated_identifier: "hi $name" string templates USE the
2636
+ * referenced identifier. Kotlin's grammar produces a real template node
2637
+ * (unlike groovy's opaque GStrings — the groovy S1 ruling), so the read
2638
+ * is visible and must be captured.
2639
+ * - CONV-K-1 (#740): BARE and DOTTED member reads. Property reads are the
2640
+ * most common read form in kotlin and produced NOTHING — `return x` and
2641
+ * `return this.x` both emitted zero uses (probe-confirmed live), while
2642
+ * only the indexing (K1) and template (K4) forms were ever claimed. Gate
2643
+ * A: the name must resolve to a DECLARED MEMBER of the enclosing type.
2644
+ * Gate B (CGD15): a name the enclosing function/lambda binds as a
2645
+ * parameter or local is not a member read — without it `val x = 9` inside
2646
+ * a method fabricates uses(method -> x) on a property it only shadows.
2647
+ * Both gates are the real work; the emission is a line.
2648
+ * Deduped per (source, target) so repeated indexing in one function emits
2649
+ * one honest edge, not a fan of identical edges.
2650
+ */
2651
+ collectExpressionUses(rootNode, relationships, symbols) {
2652
+ const emitted = /* @__PURE__ */ new Set();
2653
+ const membersByOwner = /* @__PURE__ */ new Map();
2654
+ for (const s of symbols) {
2655
+ if (!s.ownerName) continue;
2656
+ if (s.kind === ASTEngine.SymbolKind.Constructor || s.kind === ASTEngine.SymbolKind.Module) continue;
2657
+ let set = membersByOwner.get(s.ownerName);
2658
+ if (!set) {
2659
+ set = /* @__PURE__ */ new Set();
2660
+ membersByOwner.set(s.ownerName, set);
2661
+ }
2662
+ set.add(s.name);
2663
+ }
2664
+ const resolveSource = (start) => {
2665
+ let p = start;
2666
+ while (p) {
2667
+ if (p.type === "secondary_constructor" || p.type === "primary_constructor") {
2668
+ return this.kotlinConstructorOwnerName(p) ?? "";
2669
+ }
2670
+ if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
2671
+ const name = this.findNameNode(p);
2672
+ if (name) return name.text;
2673
+ }
2674
+ const topLevelProp = this.kotlinTopLevelPropertyName(p);
2675
+ if (topLevelProp) return topLevelProp;
2676
+ p = p.parent;
2677
+ }
2678
+ return "Global";
2679
+ };
2680
+ const emitUse = (n, target) => {
2681
+ if (!_RealKotlinAnalyzer.publishableEndpoint(target)) return;
2682
+ const source = resolveSource(n.parent);
2683
+ if (!_RealKotlinAnalyzer.publishableEndpoint(source)) return;
2684
+ const key = `${source}|${target}|uses`;
2685
+ if (emitted.has(key)) return;
2686
+ emitted.add(key);
2687
+ relationships.push({
2688
+ source,
2689
+ target,
2690
+ type: ASTEngine.RelationshipType.Uses,
2691
+ confidence: 0.9,
2692
+ // (#825): a read happens where the name is written. `n` is the
2693
+ // expression the walk found, which is the site that carries it.
2694
+ line_number: n.startPosition.row + 1,
2695
+ metadata: { relationshipType: "expression_use" }
2696
+ });
2697
+ };
2698
+ const resolveOwner = (start) => {
2699
+ let p = start;
2700
+ while (p) {
2701
+ if (p.type === "class_declaration" || p.type === "object_declaration") {
2702
+ const nm = this.findNameNode(p);
2703
+ return nm ? nm.text : null;
2704
+ }
2705
+ p = p.parent;
2706
+ }
2707
+ return null;
2708
+ };
2709
+ const boundCache = /* @__PURE__ */ new Map();
2710
+ const boundOf = (scope) => {
2711
+ const hit = boundCache.get(scope.id);
2712
+ if (hit) return hit;
2713
+ const out = /* @__PURE__ */ new Set();
2714
+ const w = [scope];
2715
+ while (w.length > 0) {
2716
+ const m = w.pop();
2717
+ if (m.type === "simple_identifier") {
2718
+ const pt = m.parent?.type ?? "";
2719
+ if (pt === "parameter" || pt === "class_parameter" || pt === "variable_declaration") {
2720
+ out.add(m.text);
2721
+ }
2722
+ }
2723
+ for (let i = m.namedChildCount - 1; i >= 0; i--) {
2724
+ const c = m.namedChild(i);
2725
+ if (c) w.push(c);
2726
+ }
2727
+ }
2728
+ boundCache.set(scope.id, out);
2729
+ return out;
2730
+ };
2731
+ const isLocallyBound = (n, name) => {
2732
+ let p = n.parent;
2733
+ while (p) {
2734
+ if (p.type === "function_declaration" || p.type === "lambda_literal" || p.type === "primary_constructor" || p.type === "secondary_constructor") {
2735
+ if (boundOf(p).has(name)) return true;
2736
+ }
2737
+ if (p.type === "class_declaration" || p.type === "object_declaration") break;
2738
+ p = p.parent;
2739
+ }
2740
+ return false;
2741
+ };
2742
+ const isWriteLhs = (n) => {
2743
+ let a = n.parent;
2744
+ while (a && (a.type === "directly_assignable_expression" || a.type === "navigation_expression" || a.type === "navigation_suffix")) a = a.parent;
2745
+ if (a && a.type === "assignment") {
2746
+ const lhs = a.namedChild(0);
2747
+ return !!lhs && lhs.startIndex <= n.startIndex && lhs.endIndex >= n.endIndex;
2748
+ }
2749
+ return false;
2750
+ };
2751
+ const NOT_A_READ = /* @__PURE__ */ new Set([
2752
+ "parameter",
2753
+ "class_parameter",
2754
+ "variable_declaration",
2755
+ "type_identifier",
2756
+ "user_type",
2757
+ "nullable_type",
2758
+ "import_header",
2759
+ "package_header",
2760
+ "function_declaration",
2761
+ "class_declaration",
2762
+ "object_declaration",
2763
+ "property_declaration",
2764
+ "value_parameter"
2765
+ ]);
2766
+ const emitMemberRead = (n, target) => {
2767
+ if (!_RealKotlinAnalyzer.publishableEndpoint(target)) return;
2768
+ const source = resolveSource(n.parent);
2769
+ if (!_RealKotlinAnalyzer.publishableEndpoint(source)) return;
2770
+ const key = `${source}|${target}|uses`;
2771
+ if (emitted.has(key)) return;
2772
+ emitted.add(key);
2773
+ relationships.push({
2774
+ source,
2775
+ target,
2776
+ type: ASTEngine.RelationshipType.Uses,
2777
+ confidence: 0.6,
2778
+ // (#825): a bare member read IS its identifier node.
2779
+ line_number: n.startPosition.row + 1,
2780
+ metadata: { isBareRead: true, relationshipType: "expression_use" }
2781
+ });
2782
+ };
2783
+ const walk = (n) => {
2784
+ if (n.type === "indexing_expression" || n.type === "directly_assignable_expression") {
2785
+ const recv = n.namedChild(0);
2786
+ let hasIndexing = false;
2787
+ for (let i = 0; i < n.namedChildCount; i++) {
2788
+ if (n.namedChild(i)?.type === "indexing_suffix") {
2789
+ hasIndexing = true;
2790
+ break;
2791
+ }
2792
+ }
2793
+ const parent = n.parent;
2794
+ const lhs = parent && parent.type === "assignment" ? parent.namedChild(0) : null;
2795
+ const isWrite = !!lhs && lhs.startIndex === n.startIndex && lhs.endIndex === n.endIndex;
2796
+ if (recv && hasIndexing && !isWrite) {
2797
+ const receiverPath = this.kotlinIdentifierPath(recv);
2798
+ if (receiverPath) emitUse(n, receiverPath);
2799
+ else if (this.kotlinNameShapedText(recv.text)) emitUse(n, recv.text);
2800
+ }
2801
+ } else if (n.type === "interpolated_identifier") {
2802
+ emitUse(n, n.text);
2803
+ } else if (n.type === "simple_identifier") {
2804
+ const txt = n.text;
2805
+ const pt = n.parent?.type ?? "";
2806
+ const owner = resolveOwner(n.parent);
2807
+ const isMember = !!owner && (membersByOwner.get(owner)?.has(txt) ?? false);
2808
+ if (!NOT_A_READ.has(pt) && pt !== "call_expression" && !isWriteLhs(n) && !isLocallyBound(n, txt) && isMember) {
2809
+ emitMemberRead(n, txt);
2810
+ }
2811
+ }
2812
+ for (let i = 0; i < n.namedChildCount; i++) {
2813
+ const c = n.namedChild(i);
2814
+ if (c) walk(c);
2815
+ }
2816
+ };
2817
+ walk(rootNode);
2818
+ }
2819
+ /**
2820
+ * Kotlin Call Expression Builder
2821
+ */
2822
+ /**
2823
+ * M60 (#924, kotlin half): canonicalize the PUBLISHED name surface.
2824
+ *
2825
+ * Kotlin has no symbol factory (symbols are pushed inline by the extractors),
2826
+ * so the strip cannot be applied at construction the way swift's createSymbol
2827
+ * does it; it is applied here, at the publish boundary — after every analysis
2828
+ * pass and before exports/imports are derived from these same objects, so all
2829
+ * published spellings agree with each other (a `contains` target still equals
2830
+ * the symbol name it points at) and with what a reference can spell.
2831
+ *
2832
+ * Internal keys are deliberately NOT rewritten past this point: the analysis
2833
+ * resolves references in the source's own spelling (a kotlin reference to a
2834
+ * quoted name is itself quoted), so the quoted form is the correct lookup key
2835
+ * inside the analysis and the stripped form is the correct published name.
2836
+ */
2837
+ canonicalizePublishedNames(symbols, relationships) {
2838
+ for (const s of symbols) {
2839
+ if (typeof s.name === "string") s.name = stripKotlinQuoting(s.name);
2840
+ if (typeof s.ownerName === "string") s.ownerName = stripKotlinQuoting(s.ownerName);
2841
+ if (typeof s.id === "string") s.id = stripKotlinQuoting(s.id);
2842
+ }
2843
+ for (const r of relationships) {
2844
+ if (typeof r.source === "string") r.source = stripKotlinQuoting(r.source);
2845
+ if (typeof r.target === "string") r.target = stripKotlinQuoting(r.target);
2846
+ }
2847
+ }
2848
+ buildKotlinCallExpression(node) {
2849
+ switch (node.type) {
2850
+ case "simple_identifier":
2851
+ case "identifier":
2852
+ return node.text;
2853
+ case "navigation_expression":
2854
+ return this.buildKotlinNavigationExpression(node);
2855
+ case "call_expression": {
2856
+ const nav = this.findChildByType(node, ["navigation_expression"]);
2857
+ if (nav) return this.buildKotlinNavigationExpression(nav);
2858
+ const functionName = this.findChildByType(node, ["simple_identifier", "identifier"]);
2859
+ return functionName ? functionName.text : node.text.split("(")[0];
2860
+ }
2861
+ default:
2862
+ return node.text;
2863
+ }
2864
+ }
2865
+ /**
2866
+ * Kotlin Navigation Expression Builder (for method calls)
2867
+ */
2868
+ /**
2869
+ * M60 (#924, kotlin E half): the NAME of an expression, when it has one.
2870
+ *
2871
+ * The uses walk published an indexing receiver's raw TEXT, so a read of
2872
+ * `dest.selectAll().where { dest.id eq 1 }.single()[dest.id]` was published as
2873
+ * that whole expression — arguments, lambda body and brackets included. Every
2874
+ * one of the 27 such strings the census collected on kotlin is an indexing
2875
+ * receiver (script/m14/tmp/c924-name-surface-probe.ts). A name is the chain of
2876
+ * IDENTIFIERS along the callee/receiver spine; arguments and lambda bodies are
2877
+ * never part of it. Returns undefined when the spine carries no identifier, and
2878
+ * the caller then decides by the old text's SHAPE (see kotlinNameShapedText).
2879
+ */
2880
+ kotlinIdentifierPath(node) {
2881
+ if (!node) return void 0;
2882
+ switch (node.type) {
2883
+ case "simple_identifier":
2884
+ case "identifier":
2885
+ case "type_identifier":
2886
+ return stripKotlinQuoting(node.text);
2887
+ case "navigation_expression": {
2888
+ const receiver = node.namedChild(0);
2889
+ let selector = node.namedChild(node.namedChildCount - 1);
2890
+ if (!receiver || !selector) return void 0;
2891
+ if (selector.type === "navigation_suffix") {
2892
+ const inner = selector.namedChild(selector.namedChildCount - 1);
2893
+ if (!inner) return void 0;
2894
+ selector = inner;
2895
+ }
2896
+ const rt = this.kotlinIdentifierPath(receiver);
2897
+ const st = this.kotlinIdentifierPath(selector);
2898
+ return rt && st ? `${rt}.${st}` : void 0;
2899
+ }
2900
+ case "call_expression":
2901
+ case "indexing_expression":
2902
+ return this.kotlinIdentifierPath(node.namedChild(0));
2903
+ default:
2904
+ return void 0;
2905
+ }
2906
+ }
2907
+ /**
2908
+ * M60 (#924, kotlin E half): is this published TEXT itself name-shaped? A name
2909
+ * carries no argument, lambda, index or separator punctuation — the same rule
2910
+ * the census's E class measures. Used only as a FALLBACK: a receiver whose spine
2911
+ * has no identifier (`this`, a type parameter, a quoted name) keeps its old
2912
+ * spelling rather than losing the edge, because those spellings are names and
2913
+ * the defect being fixed is an EXPRESSION published as one.
2914
+ */
2915
+ kotlinNameShapedText(text) {
2916
+ if (!text) return false;
2917
+ if (text.includes("\n")) return false;
2918
+ for (const ch of ["(", ")", "{", "}", "[", "]", ","]) {
2919
+ if (text.includes(ch)) return false;
2920
+ }
2921
+ return text.trim() === text;
2922
+ }
2923
+ buildKotlinNavigationExpression(node) {
2924
+ const receiver = node.namedChild(0);
2925
+ let selector = node.namedChild(node.namedChildCount - 1);
2926
+ if (!receiver || !selector) return node.text;
2927
+ if (selector.type === "navigation_suffix") {
2928
+ const inner = selector.namedChild(selector.namedChildCount - 1);
2929
+ if (!inner) return node.text;
2930
+ selector = inner;
2931
+ }
2932
+ const receiverText = receiver.type === "navigation_expression" ? this.buildKotlinNavigationExpression(receiver) : receiver.type === "call_expression" ? this.buildKotlinCallExpression(receiver) : receiver.text;
2933
+ return `${receiverText}.${selector.text}`;
2934
+ }
2935
+ /**
2936
+ * Check if function call is Kotlin builtin
2937
+ */
2938
+ /**
2939
+ * Stdlib check for qualified_calls. EXACT matching only - the previous
2940
+ * substring loop classified any user call whose name merely CONTAINED a
2941
+ * builtin word (getUserById -> "get", runMigration -> "run") as stdlib,
2942
+ * silently emptying symbol.qualified_calls (M14.1 fix). Qualified calls are
2943
+ * suppressed only by full-name match or stdlib package prefix; unqualified
2944
+ * calls by exact leaf against the stdlib set + scope-function extras.
2945
+ */
2946
+ isKotlinBuiltin(call) {
2947
+ if (!call) return false;
2948
+ for (const prefix of _RealKotlinAnalyzer.stdlibPackagePrefixes) {
2949
+ if (call.startsWith(prefix)) return true;
2950
+ }
2951
+ if (this.kotlinStdlibExact.has(call)) return true;
2952
+ if (!call.includes(".")) {
2953
+ return _RealKotlinAnalyzer.kotlinBuiltinExtras.has(call);
2954
+ }
2955
+ return false;
2956
+ }
2957
+ static stdlibPackagePrefixes = [
2958
+ "kotlin.",
2959
+ "kotlinx.",
2960
+ "java.",
2961
+ "javax.",
2962
+ "android.",
2963
+ "androidx."
2964
+ ];
2965
+ static kotlinBuiltinExtras = /* @__PURE__ */ new Set([
2966
+ "println",
2967
+ "print",
2968
+ "readLine",
2969
+ "require",
2970
+ "check",
2971
+ "error",
2972
+ "let",
2973
+ "run",
2974
+ "with",
2975
+ "apply",
2976
+ "also",
2977
+ "takeIf",
2978
+ "takeUnless",
2979
+ "toString",
2980
+ "equals",
2981
+ "hashCode",
2982
+ "copy",
2983
+ "componentN",
2984
+ "repeat",
2985
+ "uppercase",
2986
+ "lowercase",
2987
+ "trim",
2988
+ "mutableMapOf",
2989
+ "mapOf",
2990
+ "mutableListOf",
2991
+ "listOf",
2992
+ "setOf",
2993
+ "mutableSetOf",
2994
+ "arrayOf",
2995
+ "emptyList",
2996
+ "emptyMap",
2997
+ "emptySet"
2998
+ ]);
2999
+ /**
3000
+ * Exact stdlib names used by the call-graph filter (leaf-level). Operators
3001
+ * like plus/minus/times are deliberately excluded so in-file operator
3002
+ * overloads produce real Calls edges.
3003
+ */
3004
+ kotlinStdlibExact = /* @__PURE__ */ new Set([
3005
+ "println",
3006
+ "print",
3007
+ "readLine",
3008
+ "require",
3009
+ "check",
3010
+ "error",
3011
+ "let",
3012
+ "run",
3013
+ "with",
3014
+ "apply",
3015
+ "also",
3016
+ "takeIf",
3017
+ "takeUnless",
3018
+ "toString",
3019
+ "equals",
3020
+ "hashCode",
3021
+ "copy",
3022
+ "componentN",
3023
+ "mutableMapOf",
3024
+ "mapOf",
3025
+ "mutableListOf",
3026
+ "listOf",
3027
+ "setOf",
3028
+ "mutableSetOf",
3029
+ "arrayOf",
3030
+ "emptyList",
3031
+ "emptyMap",
3032
+ "emptySet",
3033
+ "repeat",
3034
+ "uppercase",
3035
+ "lowercase",
3036
+ "trim",
3037
+ "startsWith",
3038
+ "endsWith",
3039
+ "contains",
3040
+ "substring",
3041
+ "map",
3042
+ "filter",
3043
+ "reduce",
3044
+ "fold",
3045
+ "forEach",
3046
+ "sorted",
3047
+ "sortedBy",
3048
+ "distinct",
3049
+ "first",
3050
+ "last",
3051
+ "add",
3052
+ "remove",
3053
+ "size",
3054
+ "isEmpty",
3055
+ "isNotEmpty",
3056
+ "put",
3057
+ "get",
3058
+ "getOrElse",
3059
+ "getOrDefault",
3060
+ "any",
3061
+ "all",
3062
+ "none",
3063
+ "count",
3064
+ "sum",
3065
+ "max",
3066
+ "min",
3067
+ "take",
3068
+ "drop"
3069
+ ]);
3070
+ /**
3071
+ * Kotlin Annotation Name Extraction
3072
+ */
3073
+ getKotlinAnnotationName(node) {
3074
+ const direct = this.findNameNode(node);
3075
+ if (direct) return `@${direct.text}`;
3076
+ const userType = this.findChildByType(node, ["user_type"]);
3077
+ if (userType) {
3078
+ let last = null;
3079
+ for (let i = 0; i < userType.namedChildCount; i++) {
3080
+ const c = userType.namedChild(i);
3081
+ if (c && (c.type === "type_identifier" || c.type === "simple_identifier")) last = c;
3082
+ }
3083
+ if (last) return `@${last.text}`;
3084
+ }
3085
+ return void 0;
3086
+ }
3087
+ /**
3088
+ * Reduce raw annotation text to its short name: "@org.x.GetMapping(\"/u\")"
3089
+ * → "GetMapping". Decorators keep the raw source text; anything comparing
3090
+ * annotation NAMES must compare through this normalization.
3091
+ */
3092
+ kotlinShortAnnotationName(raw) {
3093
+ let t = raw.startsWith("@") ? raw.slice(1) : raw;
3094
+ const paren = t.indexOf("(");
3095
+ if (paren !== -1) t = t.slice(0, paren);
3096
+ const dot = t.lastIndexOf(".");
3097
+ return dot === -1 ? t : t.slice(dot + 1);
3098
+ }
3099
+ /**
3100
+ * Kotlin Annotation Arguments Extraction
3101
+ */
3102
+ extractKotlinAnnotationArguments(node) {
3103
+ const args = [];
3104
+ const argsNode = this.getValueArguments(node);
3105
+ if (argsNode) {
3106
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
3107
+ const arg = argsNode.namedChild(i);
3108
+ if (arg) {
3109
+ args.push(arg.text);
3110
+ }
3111
+ }
3112
+ }
3113
+ return args;
3114
+ }
3115
+ /**
3116
+ * Kotlin Annotation Target Extraction
3117
+ */
3118
+ extractKotlinAnnotationTarget(node) {
3119
+ const targetNode = this.findChildByType(node, ["use_site_target"]);
3120
+ if (targetNode) {
3121
+ const text = targetNode.text;
3122
+ return text.endsWith(":") ? text.slice(0, -1) : text;
3123
+ }
3124
+ return void 0;
3125
+ }
3126
+ /**
3127
+ * Kotlin Type Cleaning
3128
+ */
3129
+ cleanKotlinType(typeText) {
3130
+ const parts = typeText.split(" ");
3131
+ const validParts = [];
3132
+ for (let i = 0; i < parts.length; i++) {
3133
+ if (parts[i]) validParts.push(parts[i]);
3134
+ }
3135
+ return validParts.join(" ").replace("??", "?").replace("? ?", "?").trim();
3136
+ }
3137
+ /**
3138
+ * Kotlin Type Inference from Initializer
3139
+ */
3140
+ /**
3141
+ * Infer the type of a property from its initializer using the initializer
3142
+ * NODE TYPE — never by regex-testing its text. Kotlin literal nodes carry
3143
+ * their kind in the grammar: integer_literal, real_literal, float_literal,
3144
+ * string_literal, character_literal, boolean_literal, call_expression.
3145
+ */
3146
+ inferKotlinTypeFromInitializer(initNode) {
3147
+ const text = initNode.text.trim();
3148
+ switch (initNode.type) {
3149
+ case "integer_literal":
3150
+ return text.endsWith("L") ? "Long" : "Int";
3151
+ case "real_literal":
3152
+ return text.endsWith("f") || text.endsWith("F") ? "Float" : "Double";
3153
+ case "float_literal":
3154
+ return "Float";
3155
+ case "string_literal":
3156
+ return "String";
3157
+ case "character_literal":
3158
+ return "Char";
3159
+ case "boolean_literal":
3160
+ return "Boolean";
3161
+ case "call_expression": {
3162
+ const callee = this.buildKotlinCallExpression(initNode);
3163
+ const name = callee.split(".").pop() || callee;
3164
+ if (name === "listOf" || name === "emptyList") return "List<*>";
3165
+ if (name === "mutableListOf") return "MutableList<*>";
3166
+ if (name === "setOf" || name === "emptySet") return "Set<*>";
3167
+ if (name === "mapOf" || name === "emptyMap") return "Map<*, *>";
3168
+ if (name === "arrayOf") return "Array<*>";
3169
+ return name;
3170
+ }
3171
+ default:
3172
+ return "Any";
3173
+ }
3174
+ }
3175
+ /**
3176
+ * Kotlin Enum Values Extraction
3177
+ */
3178
+ extractKotlinEnumValues(node) {
3179
+ const values = [];
3180
+ const enumBody = this.findChildByType(node, ["enum_class_body", "class_body"]);
3181
+ if (!enumBody) return values;
3182
+ for (let i = 0; i < enumBody.namedChildCount; i++) {
3183
+ const child = enumBody.namedChild(i);
3184
+ if (child?.type === "enum_entry") {
3185
+ const nameNode = this.findNameNode(child);
3186
+ if (nameNode) {
3187
+ values.push(nameNode.text);
3188
+ }
3189
+ }
3190
+ }
3191
+ return values;
3192
+ }
3193
+ calculateRealComplexity(symbols, relationships) {
3194
+ return symbols.length + relationships.length;
3195
+ }
3196
+ calculateCyclomaticComplexity(node) {
3197
+ let complexity = 1;
3198
+ const stack = [node];
3199
+ while (stack.length > 0) {
3200
+ const n = stack.pop();
3201
+ switch (n.type) {
3202
+ case "if_expression":
3203
+ // else_clause deliberately NOT counted: an `else` is not an
3204
+ // independent decision (family contract). Counting both if and
3205
+ // else double-counted every if/else.
3206
+ case "for_statement":
3207
+ case "while_statement":
3208
+ case "do_while_statement":
3209
+ case "when_entry":
3210
+ case "catch_block":
3211
+ case "elvis_expression":
3212
+ complexity++;
3213
+ break;
3214
+ case "conjunction_expression":
3215
+ case "disjunction_expression":
3216
+ complexity++;
3217
+ break;
3218
+ case "lambda_literal":
3219
+ break;
3220
+ }
3221
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3222
+ const child = n.namedChild(i);
3223
+ if (child) stack.push(child);
3224
+ }
3225
+ }
3226
+ return complexity;
3227
+ }
3228
+ /**
3229
+ * (#825): `{name, node}` pairs, not bare names.
3230
+ *
3231
+ * The compose containment edges are derived from the CALLS inside a
3232
+ * `@Composable` body, and their target is the called name — so the line must
3233
+ * come from the call node that carries it. Returning the node is the cheapest
3234
+ * way to keep that true as the walk changes; the previous string-only return
3235
+ * left those edges with no line at all (unknown, which is honest but useless).
3236
+ */
3237
+ extractFunctionCalls(node) {
3238
+ const calls = [];
3239
+ const stack = [node];
3240
+ while (stack.length > 0) {
3241
+ const n = stack.pop();
3242
+ if (this.isKotlinContextParameterHeader(n)) continue;
3243
+ if (n.type === "call_expression") {
3244
+ const full = this.buildKotlinCallExpression(n);
3245
+ calls.push({ name: full.split(".").pop() || full, node: this.calleeLeafNode(n) ?? n });
3246
+ }
3247
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3248
+ const child = n.namedChild(i);
3249
+ if (child) stack.push(child);
3250
+ }
3251
+ }
3252
+ return calls;
3253
+ }
3254
+ analyzeRelationships(node, relationships, symbols, ctx) {
3255
+ const stack = [node];
3256
+ while (stack.length > 0) {
3257
+ const n = stack.pop();
3258
+ if (n.type === "function_declaration") {
3259
+ let receiverNode = void 0;
3260
+ for (let i = 0; i < n.namedChildCount; i++) {
3261
+ const c = n.namedChild(i);
3262
+ if (c?.type === "simple_identifier") break;
3263
+ if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
3264
+ receiverNode = c;
3265
+ break;
3266
+ }
3267
+ }
3268
+ const nameNode = this.findNameNode(n);
3269
+ if (receiverNode && nameNode) {
3270
+ const receiverType = receiverNode.text.endsWith("?") ? receiverNode.text.slice(0, -1) : receiverNode.text;
3271
+ relationships.push({
3272
+ source: nameNode.text,
3273
+ target: receiverType,
3274
+ type: ASTEngine.RelationshipType.Uses,
3275
+ confidence: 0.9,
3276
+ // (#825): the receiver type is named by its receiver node.
3277
+ ...this.lineField(receiverNode, n),
3278
+ metadata: {
3279
+ relationshipType: "kotlin_extension_function",
3280
+ isExtension: true
3281
+ }
3282
+ });
3283
+ }
3284
+ const isComposable = this.extractAnnotations(n, ctx).some((a) => this.kotlinShortAnnotationName(a) === "Composable");
3285
+ if (isComposable && nameNode) {
3286
+ const bodyNode = this.findChildByType(n, ["function_body", "class_body"]);
3287
+ if (bodyNode) {
3288
+ const bodyCalls = this.extractFunctionCalls(bodyNode);
3289
+ for (let i = 0; i < bodyCalls.length; i++) {
3290
+ const call = bodyCalls[i].name;
3291
+ if (call.length > 0 && call[0] >= "A" && call[0] <= "Z") {
3292
+ relationships.push({
3293
+ source: nameNode.text,
3294
+ target: call,
3295
+ // (#825): the UI component is named at its call site.
3296
+ line_number: bodyCalls[i].node.startPosition.row + 1,
3297
+ type: ASTEngine.RelationshipType.Contains,
3298
+ confidence: 0.8,
3299
+ metadata: {
3300
+ relationshipType: "compose_ui_tree",
3301
+ isUiComponent: true,
3302
+ // KAD-MR3 K18/K4 (M49.K1): a PascalCase-name heuristic is
3303
+ // NOT verified containment — the tier classifier stamped
3304
+ // every contains edge "structural" (probe: Intent(...)
3305
+ // identical to Text(...)). Honest label: heuristic — same
3306
+ // tier as a 0.5 override guess. The compose edge keeps
3307
+ // its 0.8 (patterns.ts filters >=0.7), but consumers can
3308
+ // now distinguish it from verified containment.
3309
+ evidenceClass: "heuristic"
3310
+ }
3311
+ });
3312
+ }
3313
+ }
3314
+ }
3315
+ }
3316
+ if (isComposable && nameNode) {
3317
+ const renderBody = this.findChildByType(n, ["function_body"]);
3318
+ if (renderBody) {
3319
+ const composableNames = /* @__PURE__ */ new Set();
3320
+ for (let si = 0; si < symbols.length; si++) {
3321
+ const sym = symbols[si];
3322
+ const symAnns = sym.metadata?.annotations;
3323
+ if (!Array.isArray(symAnns) || symAnns.indexOf("Composable") < 0) continue;
3324
+ if (sym.name === nameNode.text) continue;
3325
+ if (sym.kind !== ASTEngine.SymbolKind.Function && sym.kind !== ASTEngine.SymbolKind.Method) continue;
3326
+ composableNames.add(sym.name);
3327
+ }
3328
+ if (composableNames.size > 0) {
3329
+ const renderCalls = this.extractFunctionCalls(renderBody);
3330
+ for (let ri = 0; ri < renderCalls.length; ri++) {
3331
+ if (!composableNames.has(renderCalls[ri].name)) continue;
3332
+ relationships.push({
3333
+ source: nameNode.text,
3334
+ target: renderCalls[ri].name,
3335
+ // The rendered component is named at its call site.
3336
+ line_number: renderCalls[ri].node.startPosition.row + 1,
3337
+ type: ASTEngine.RelationshipType.Renders,
3338
+ confidence: 0.9,
3339
+ metadata: {
3340
+ relationshipType: "compose_render",
3341
+ isComposableMarker: true,
3342
+ isComponentCall: true,
3343
+ // The marker on the source and the target's OWN declaration are both
3344
+ // read from the tree — structural, unlike the PascalCase heuristic above.
3345
+ evidenceClass: "structural"
3346
+ }
3347
+ });
3348
+ }
3349
+ }
3350
+ }
3351
+ }
3352
+ }
3353
+ if (n.type === "call_expression") {
3354
+ const full = this.buildKotlinCallExpression(n);
3355
+ const callName = full.split(".").pop() || "";
3356
+ if (callName === "launch" || callName === "async" || callName === "withContext") {
3357
+ let parentFunc = n.parent;
3358
+ while (parentFunc && parentFunc?.type !== "function_declaration") {
3359
+ parentFunc = parentFunc.parent;
3360
+ }
3361
+ if (parentFunc) {
3362
+ const parentName = this.findNameNode(parentFunc)?.text;
3363
+ if (parentName) {
3364
+ relationships.push({
3365
+ source: parentName,
3366
+ target: callName,
3367
+ type: ASTEngine.RelationshipType.Uses,
3368
+ confidence: 1,
3369
+ // (#825): the dispatcher is named at the call itself.
3370
+ ...this.lineField(this.calleeLeafNode(n, callName), n),
3371
+ metadata: {
3372
+ relationshipType: "coroutine_dispatch",
3373
+ isAsync: true
3374
+ }
3375
+ });
3376
+ }
3377
+ }
3378
+ }
3379
+ }
3380
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3381
+ const child = n.namedChild(i);
3382
+ if (child) stack.push(child);
3383
+ }
3384
+ }
3385
+ for (let i = 0; i < symbols.length; i++) {
3386
+ const cls = symbols[i];
3387
+ if (cls.kind === ASTEngine.SymbolKind.Class || cls.kind === ASTEngine.SymbolKind.Interface || cls.kind === ASTEngine.SymbolKind.Enum) {
3388
+ if (cls.metadata?.isAnonymousObject) continue;
3389
+ if (cls.metadata?.superTypes) {
3390
+ const ctorCalls = cls.metadata?.superTypesAreConstructorCalls;
3391
+ const superLines = cls.metadata?.superTypeLines;
3392
+ for (let j = 0; j < cls.metadata.superTypes.length; j++) {
3393
+ const isExtends = cls.kind === ASTEngine.SymbolKind.Interface || ctorCalls?.[j] === true;
3394
+ if (!_RealKotlinAnalyzer.publishableEndpoint(cls.name)) continue;
3395
+ if (!_RealKotlinAnalyzer.publishableEndpoint(cls.metadata.superTypes[j])) continue;
3396
+ relationships.push({
3397
+ source: cls.name,
3398
+ target: cls.metadata.superTypes[j],
3399
+ type: isExtends ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.Implements,
3400
+ confidence: 1,
3401
+ ...typeof superLines?.[j] === "number" ? { line_number: superLines[j] } : {},
3402
+ metadata: { relationshipType: isExtends ? "kotlin_extends" : "kotlin_implements" }
3403
+ });
3404
+ }
3405
+ }
3406
+ }
3407
+ }
3408
+ }
3409
+ /**
3410
+ * The name of a node that is a TOP-LEVEL property declaration, or undefined.
3411
+ *
3412
+ * ONTOLOGY (kotlin, 2026-10-03) — the `Global` fallback, and why it fires on
3413
+ * ordinary, well-formed kotlin.
3414
+ *
3415
+ * The three attribution resolvers (`resolveCallSource` for `calls` /
3416
+ * `instantiates`, `extractDeclaredTypeRefs.resolveSource` for the declared-type
3417
+ * `references` graph, and the `uses` resolver) all walk up the parent chain
3418
+ * looking for a declaration to own the edge, and all three listed the same
3419
+ * three node types:
3420
+ *
3421
+ * function_declaration | class_declaration | object_declaration
3422
+ *
3423
+ * `property_declaration` was missing from all three. So every edge written
3424
+ * inside a TOP-LEVEL property's own type clause or initializer had no
3425
+ * declaration to stop at, the walk reached `source_file`, and the fallback
3426
+ * returned the literal `"Global"` — a symbol name that appears nowhere in
3427
+ * kotlin and in no file of the corpus.
3428
+ *
3429
+ * MEASURED, by script/m14/kotlin-r7-endpoint-probe.ts on a 1,200-file
3430
+ * every-Nth draw of the pinned fixture corpus (kotlin@9b80c8ff): 1,290 `Global`
3431
+ * sources in files the analyzer reported as CLEAN, i.e. where the grammar had
3432
+ * no trouble at all and the fallback was standing in for nothing. Two real
3433
+ * files, both ordinary shipped `.kt`:
3434
+ *
3435
+ * CommonJsAndWasmArgumentProviders.kt:101
3436
+ * private val testBaseDir: Path = Paths.get("").toAbsolutePath()
3437
+ * ^^^^^^^^^^^^^ call sourced to Global
3438
+ * KaFirSymbol.kt:65
3439
+ * private val FirBasedSymbol<*>.realPsi: PsiElement?
3440
+ * ^^^^^^^^^^^ declared_type ref sourced to Global
3441
+ *
3442
+ * WHY ONLY WHEN `parent.type === "source_file"`, and this is the load-bearing
3443
+ * condition rather than a detail. A property inside a `class_body` is owned by
3444
+ * its CLASS, and the walk reaches the class first, so it never reaches this
3445
+ * branch — which is the existing, fixture-pinned contract ("calls from
3446
+ * class-level contexts (property initializers, init blocks) belong to the
3447
+ * enclosing class", kotlin/init-attribution). Treating every property as a
3448
+ * boundary would move those edges off the class and onto the property, which is
3449
+ * a different and wrong answer. A TOP-LEVEL property has no enclosing class, so
3450
+ * there is nothing else that could own it and it owns itself — and the
3451
+ * analyzer already publishes it as a `Property` symbol with exactly this name,
3452
+ * so the edge's source resolves to a real symbol.
3453
+ *
3454
+ * `type_alias` is deliberately NOT handled here. A top-level typealias declares
3455
+ * no runtime symbol (the specification expands it at compile time), and the
3456
+ * `exports` rule already excludes it for that reason; giving it edges as an
3457
+ * owner would put a name in the graph that no file can navigate to.
3458
+ */
3459
+ kotlinTopLevelPropertyName(node) {
3460
+ if (node.type === "getter" || node.type === "setter") {
3461
+ const parent = node.parent;
3462
+ if (!parent || parent.type !== "source_file") return void 0;
3463
+ let best;
3464
+ for (let i = 0; i < parent.namedChildCount; i++) {
3465
+ const c = parent.namedChild(i);
3466
+ if (!c || c.type !== "property_declaration") continue;
3467
+ if (c.startPosition.row > node.startPosition.row) continue;
3468
+ if (!best || c.startPosition.row > best.startPosition.row) best = c;
3469
+ }
3470
+ return best ? this.kotlinTopLevelPropertyName(best) : void 0;
3471
+ }
3472
+ if (node.type !== "property_declaration") return void 0;
3473
+ if (node.parent?.type !== "source_file") return void 0;
3474
+ const decl = this.findChildByType(node, ["variable_declaration"]);
3475
+ if (!decl) return void 0;
3476
+ return this.findChildByType(decl, ["simple_identifier", "type_identifier"])?.text;
3477
+ }
3478
+ /**
3479
+ * THE IMPORTED-BINDING CLAIM FOR KOTLIN IMPORTS .
3480
+ *
3481
+ * Two halves, both needed because the arbiter verifies one against the other:
3482
+ * 1. EVIDENCE on the imports edge: `metadata.importedBindings = [binding]` + `metadata.module = <the
3483
+ * module path as written>` for EXTERNAL imports only. The gate builds file -> specifier -> bound
3484
+ * names from exactly this pair and verifies claims against it.
3485
+ * 2. CLAIM on the use row: `metadata.importedBinding = { specifier, binding }` on a calls row whose
3486
+ * callee is BARE (no receiver) or a uses row that is a BARE READ (`isBareRead`), whose target IS the
3487
+ * imported binding and whose file does not declare a shadowing name.
3488
+ *
3489
+ * REFUSALS (each a way the claim could become false, fail-closed):
3490
+ * - container root NOT in the EXTERNAL allowlist (only JVM/platform-reserved roots are listed; a
3491
+ * project-local import is project-resolvable and claiming it would launder — that is why the list is
3492
+ * the reserved vocabulary and not "anything that looks like a package");
3493
+ * - an on-demand import (`import a.b.*`) binds no name this file states;
3494
+ * - the FILE declares the name (the declaration shadows the import — the use is not the binding);
3495
+ * - a receiver-carrying call / field access row (its target is a member leaf, cut 2);
3496
+ * - a row that already carries a claim.
3497
+ */
3498
+ attachKotlinImportedBindingClaims(relationships, symbols, imports) {
3499
+ const bindings = /* @__PURE__ */ new Map();
3500
+ const isExternal = (module) => {
3501
+ for (const root of KOTLIN_EXTERNAL_IMPORT_ROOTS) if (module === root || module.startsWith(root)) return true;
3502
+ return false;
3503
+ };
3504
+ for (const imp of imports) {
3505
+ const module = String(imp?.module ?? "");
3506
+ if (module.length === 0) continue;
3507
+ if (imp.isNamespaceImport === true) continue;
3508
+ const dot = module.lastIndexOf(".");
3509
+ const leaf = dot > 0 ? module.slice(dot + 1) : module;
3510
+ const binding = String(imp.metadata?.alias ?? imp.alias ?? "") || leaf;
3511
+ if (!binding || binding === "*") continue;
3512
+ if (!isExternal(module)) continue;
3513
+ bindings.set(binding, module);
3514
+ }
3515
+ if (bindings.size === 0) return;
3516
+ for (const r of relationships) {
3517
+ if (String(r.type ?? "") !== "imports") continue;
3518
+ const specifier = String(r.target ?? "");
3519
+ if (!isExternal(specifier)) continue;
3520
+ const dot = specifier.lastIndexOf(".");
3521
+ const leaf = dot > 0 ? specifier.slice(dot + 1) : specifier;
3522
+ let binding = leaf;
3523
+ for (const [b, spec] of bindings) if (spec === specifier) {
3524
+ binding = b;
3525
+ break;
3526
+ }
3527
+ if (!specifier || !binding || binding === "*") continue;
3528
+ const md = r.metadata = r.metadata ?? {};
3529
+ if (md.importedBindings !== void 0) continue;
3530
+ md.importedBindings = [binding];
3531
+ md.module = specifier;
3532
+ }
3533
+ const fileDeclared = /* @__PURE__ */ new Set();
3534
+ for (const sym of symbols) if (sym?.name) fileDeclared.add(sym.name);
3535
+ for (const r of relationships) {
3536
+ const label = String(r.type ?? "");
3537
+ if (label !== "calls" && label !== "uses") continue;
3538
+ const md = r.metadata ?? {};
3539
+ if (md.importedBinding !== void 0) continue;
3540
+ const target = String(r.target ?? "");
3541
+ if (!target || !bindings.has(target)) continue;
3542
+ if (fileDeclared.has(target)) continue;
3543
+ if (label === "calls") {
3544
+ if (md.kotlinBareCallee !== true) continue;
3545
+ } else if (md.isBareRead !== true) {
3546
+ continue;
3547
+ }
3548
+ r.metadata = { ...md, importedBinding: { specifier: bindings.get(target), binding: target } };
3549
+ }
3550
+ }
3551
+ processImportsExports(node, imports, exports, relationships, symbols, ctx) {
3552
+ const stack = [node];
3553
+ const packageName = ctx.packageStack.join(".");
3554
+ const unitIdentityForImports = this.primaryTopLevelType(symbols);
3555
+ const importSource = unitIdentityForImports?.name || kotlinCompilationUnitName(ctx.currentFile) || packageName || "root";
3556
+ while (stack.length > 0) {
3557
+ const n = stack.pop();
3558
+ if (n.type === "import_header") {
3559
+ const pathNode = this.findChildByType(n, ["identifier"]);
3560
+ if (pathNode && pathNode.text.length > 0) {
3561
+ const importPath = pathNode.text;
3562
+ const aliasNode = this.findChildByType(n, ["import_alias"]);
3563
+ const isWildcard = !!this.findChildByType(n, ["wildcard_import"]);
3564
+ const aliasText = aliasNode ? aliasNode.namedChild(aliasNode.namedChildCount - 1)?.text ?? aliasNode.text : void 0;
3565
+ imports.push({
3566
+ module: importPath,
3567
+ items: isWildcard ? ["*"] : [aliasText ?? (importPath.split(".").pop() || importPath)],
3568
+ isDefaultImport: false,
3569
+ isNamespaceImport: isWildcard,
3570
+ range: this.nodeToRange(n),
3571
+ metadata: {
3572
+ isKotlinImport: true,
3573
+ alias: aliasText
3574
+ }
3575
+ });
3576
+ const importShape = isWildcard ? "imported_package_wildcard" : aliasText ? "imported_declaration_aliased" : "imported_declaration";
3577
+ relationships.push({
3578
+ source: importSource,
3579
+ target: importPath,
3580
+ type: ASTEngine.RelationshipType.Imports,
3581
+ confidence: 0.9,
3582
+ // (#825): the imported path is named on the import statement.
3583
+ ...this.lineField(pathNode, n),
3584
+ metadata: {
3585
+ isKotlinImport: true,
3586
+ // `isImport` is the flag the JVM-family evidence classifier keys on
3587
+ // (see classifyEvidenceTier). Without it an `imports` edge would be
3588
+ // filed as `usage_contextual` and be indistinguishable from a
3589
+ // heuristic in a payload.
3590
+ isImport: true,
3591
+ relationshipType: importShape,
3592
+ importShape,
3593
+ isWildcard,
3594
+ // The same fact under the ontology's own spelling
3595
+ // (kotlin/imports-exports-ontology pins `meta.isWildcardImport`),
3596
+ // beside the lane's `isWildcard`: the wildcard form IS the fact
3597
+ // that separates `import a.b.*` from `import a.b.C`, and a consumer
3598
+ // reading either name must not have to guess which one this lane
3599
+ // chose. Emitted only when true — an absent flag is not a false one.
3600
+ ...isWildcard ? { isWildcardImport: true } : {},
3601
+ ...aliasText ? { alias: aliasText } : {},
3602
+ ...packageName ? {} : { rootPackageFile: true },
3603
+ reasonCode: "declared-import-path"
3604
+ }
3605
+ });
3606
+ }
3607
+ }
3608
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
3609
+ const child = n.namedChild(i);
3610
+ if (child) stack.push(child);
3611
+ }
3612
+ }
3613
+ this.emitKotlinExportRelationships(symbols, relationships, ctx, node);
3614
+ }
3615
+ /**
3616
+ * ONTOLOGY (kotlin, 2026-10-03): the `exports` LABEL — a relationship behind
3617
+ * an export surface that already existed as data.
3618
+ *
3619
+ * BEFORE: the loop in `analyze()` filled `ParseResult.exports` and NONE of its
3620
+ * rows reached a relationship, so the lane published a public-API list and no
3621
+ * graph edge. Measured on 300 tracked files of
3622
+ * the pinned fixture corpus (kotlin@9b80c8ff) before this change: 0 `exports`
3623
+ * edges. On a 1,500-file window the `ExportInfo` list held 2,809 rows and the
3624
+ * relationship list held none.
3625
+ *
3626
+ * THE RULE, and every clause of it is a kotlin language fact:
3627
+ *
3628
+ * 1. TOP-LEVEL ONLY — `!s.ownerName`. A member is reached through its
3629
+ * container, and membership is already published as `contains`; publishing
3630
+ * it twice would make `contains` and `exports` two names for one edge.
3631
+ * Measured on the window: 16,694 nested declarations against 7,554
3632
+ * top-level ones, so this clause governs the majority of the population.
3633
+ * 2. NOT LEXICALLY LOCAL — see `isKotlinLocalDeclaration`. "No ownerName" is
3634
+ * NOT the same predicate as "top level": a class or function declared
3635
+ * inside a function body gets no owner in this analyzer, because
3636
+ * `isMemberPosition` is only true inside a `class_body`. Measured: 148 of
3637
+ * 2,809 ownerless public symbols on the window are local declarations
3638
+ * (`KaFirCompilerFacility.kt:467 function register`, `:584 mapModule`,
3639
+ * `:594 appendDanglingChunk`), and the pre-existing `ExportInfo` loop
3640
+ * published every one of them. A local function is callable from exactly
3641
+ * one lexical scope, so an `exports` edge claiming a module publishes it
3642
+ * would be a fabricated edge in `bb_migrate`.
3643
+ * 3. PUBLIC ONLY — `visibility === "public" || undefined`. Kotlin's default
3644
+ * visibility is public, `private` is file-scoped and `internal` is
3645
+ * module-scoped; neither of the latter is reachable by name from another
3646
+ * module, which is the reachability an `imports` edge asserts from the
3647
+ * other side. Measured on the window: 1,719 `internal` and 657 `private`
3648
+ * top-level declarations excluded.
3649
+ * 4. NOT SYNTHESIZED — `!isPhantom && !isAnonymousObject`. A data class's
3650
+ * `componentN`/`copy`/`equals` are written nowhere; an anonymous object is
3651
+ * named by no import.
3652
+ * 5. NOT A SCRIPT — a `.kts` file's top-level declarations become members of
3653
+ * the script's class and are not importable by any other file, so a script
3654
+ * publishes no API. Measured: 12 script top-level declarations in a
3655
+ * 3,000-file window, all of them also excluded by clause 6.
3656
+ * 6. THE FILE MUST NAME A PACKAGE — the source of the edge IS the package, and
3657
+ * kotlin's root package has no name to publish it under. THIS IS THE ONE
3658
+ * CLAUSE THAT IS NOT A LANGUAGE RULE. Kotlin, unlike java (JLS 7.4.2), does
3659
+ * not forbid importing from the unnamed package, so excluding these claims
3660
+ * a restriction the language does not make; it is a LIMITATION of the edge
3661
+ * shape, declared as one, with its cost measured rather than assumed: 693 of
3662
+ * 2,613 top-level ownerless public declarations on the window sit in the
3663
+ * root package, 3 of them in `.kt` files and the rest in `testData`
3664
+ * fragments (the corpus is 71% `testData/`). On the production subset
3665
+ * 1,045 of 1,056 files carry a package header and 0 of 1,748 top-level
3666
+ * public declarations are excluded by this clause. Publishing them under a
3667
+ * placeholder source instead would put a fabricated symbol name into
3668
+ * `bb_graph` for a quarter of the population, which is the worse of the
3669
+ * two failures and is the reason standing rule R7 forbids it.
3670
+ *
3671
+ * Confidence 1.0: the edge is a modifier read plus a position test with no
3672
+ * inference in it, so it is a fact about the source rather than a heuristic.
3673
+ * `evidenceClass` is left to `classifyEvidenceTier`, which already files
3674
+ * `exports` as `structural` — one place, so the two cannot disagree.
3675
+ */
3676
+ emitKotlinExportRelationships(symbols, relationships, ctx, root) {
3677
+ const packageName = ctx.packageStack.join(".");
3678
+ if (!packageName) return;
3679
+ if (ctx.currentFile.toLowerCase().endsWith(".kts")) return;
3680
+ const unitType = this.primaryTopLevelType(symbols);
3681
+ if (!unitType) return;
3682
+ const placement = this.kotlinDeclarationPlacement(root, ctx.content.split("\n").length);
3683
+ for (let i = 0; i < symbols.length; i++) {
3684
+ const s = symbols[i];
3685
+ if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) continue;
3686
+ if (s.visibility !== "public" && s.visibility !== void 0) continue;
3687
+ if (s.ownerName) continue;
3688
+ if (!s.name) continue;
3689
+ const declLine = s.range?.startLine ?? 0;
3690
+ if (declLine < 1 || declLine >= placement.local.length) continue;
3691
+ if (placement.local[declLine]) continue;
3692
+ if (!placement.top[declLine]) continue;
3693
+ relationships.push({
3694
+ source: unitType.name,
3695
+ target: s.name,
3696
+ type: ASTEngine.RelationshipType.Exports,
3697
+ confidence: 1,
3698
+ // (#825): the exported name is written on the declaration.
3699
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
3700
+ metadata: {
3701
+ isKotlinExport: true,
3702
+ relationshipType: "exported_declaration",
3703
+ // The PUBLISHER of this row is the compilation unit, spelled as the
3704
+ // file's top-level type. `moduleIdentity` says which rule chose it, so
3705
+ // a consumer reading three exports spellings in this lane can tell
3706
+ // which one answers which question without matching on the source text.
3707
+ moduleIdentity: "primary-top-level-type",
3708
+ kind: String(s.kind),
3709
+ // `exportKind` is the same fact under the name the ontology fixtures
3710
+ // pin (kotlin/imports-exports-ontology), kept beside `kind` rather than
3711
+ // replacing it: `kind` is the symbol's own predicate and `exportKind`
3712
+ // is what the surface calls it, and a consumer reading either must not
3713
+ // have to guess which spelling this lane chose.
3714
+ exportKind: String(s.kind),
3715
+ visibility: s.visibility || "public",
3716
+ // The join key with `imports`, stated because it is not obvious: an
3717
+ // `exports` edge is `package -> simpleName`, so `source + "." + target`
3718
+ // is exactly the dotted path an `imports` edge targets.
3719
+ dottedName: `${packageName}.${s.name}`
3720
+ }
3721
+ });
3722
+ }
3723
+ }
3724
+ /**
3725
+ * Which start lines carry a TOP-LEVEL declaration, and which carry one nested
3726
+ * inside a body — a function body, a lambda, an initializer, a class body.
3727
+ *
3728
+ * WHY A SEPARATE PASS RATHER THAN A PER-SYMBOL WALK. The rule "no ownerName
3729
+ * means top level" is wrong, and the way it is wrong is that a declaration
3730
+ * inside a function body carries no owner in this analyzer (`isMemberPosition`
3731
+ * is only true inside a `class_body`). Measured on a 1,500-file window: 148 of
3732
+ * 2,809 ownerless public symbols are such local declarations, and the
3733
+ * pre-existing `ExportInfo` loop published every one of them as an export. A
3734
+ * local function is callable from exactly one lexical scope, so publishing it
3735
+ * as module API would be a fabricated edge in `bb_migrate`.
3736
+ *
3737
+ * A per-symbol walk up from the symbol's own line would be an
3738
+ * O(symbols x tree) scan, and this lane's complexity envelope forbids that in a
3739
+ * hot path. So the tree is walked ONCE, carrying a boolean for "am I inside a
3740
+ * body", and the answer is recorded per declaration start line. Each symbol is
3741
+ * then two Set lookups. The walk is iterative, like the rest of this analyzer:
3742
+ * a kotlin file nests deeper than a worker thread's JS stack should be asked
3743
+ * for.
3744
+ *
3745
+ * Two earlier versions of the measuring probe got this wrong in ways that would
3746
+ * have certified a fabricated rule as measured, and both are recorded because
3747
+ * both produced a confident zero:
3748
+ * - one took the first ancestor whose TEXT contained the symbol's name, which
3749
+ * is `source_file` for every symbol, so it answered "top level" 64 times out
3750
+ * of 64;
3751
+ * - one used tree DEPTH, and `tree-sitter-kotlin` wraps a file in
3752
+ * `source_file`, so a top-level declaration sits at depth 2 and the probe
3753
+ * read 0 top-level declarations on a corpus where every file has some.
3754
+ */
3755
+ kotlinDeclarationPlacement(root, lineCount) {
3756
+ const DECL = /* @__PURE__ */ new Set([
3757
+ "class_declaration",
3758
+ "object_declaration",
3759
+ "function_declaration",
3760
+ "property_declaration",
3761
+ "type_alias"
3762
+ ]);
3763
+ const BODY = /* @__PURE__ */ new Set([
3764
+ "function_body",
3765
+ "lambda_literal",
3766
+ "anonymous_initializer",
3767
+ "secondary_constructor",
3768
+ "class_body",
3769
+ "object_literal",
3770
+ "catch_block",
3771
+ "do_while_statement",
3772
+ "while_statement",
3773
+ "for_statement"
3774
+ ]);
3775
+ const rows = Math.max(1, lineCount) + 2;
3776
+ const local = new Uint8Array(rows);
3777
+ const top = new Uint8Array(rows);
3778
+ const nameRow = (n) => {
3779
+ for (let k = 0; k < n.namedChildCount; k++) {
3780
+ const c = n.namedChild(k);
3781
+ if (!c || c.type === "modifiers") continue;
3782
+ if (c.type === "type_identifier" || c.type === "simple_identifier") return c.startPosition.row + 1;
3783
+ if (c.type === "variable_declaration") {
3784
+ for (let m = 0; m < c.namedChildCount; m++) {
3785
+ const g = c.namedChild(m);
3786
+ if (g && g.type === "simple_identifier") return g.startPosition.row + 1;
3787
+ }
3788
+ }
3789
+ }
3790
+ return null;
3791
+ };
3792
+ const stack = [{ n: root, inBody: false }];
3793
+ while (stack.length > 0) {
3794
+ const { n, inBody } = stack.pop();
3795
+ const here = inBody || BODY.has(n.type);
3796
+ if (DECL.has(n.type)) {
3797
+ const startRow = n.startPosition.row + 1;
3798
+ const named = nameRow(n);
3799
+ const endRow = named && named > startRow ? named : startRow;
3800
+ const into = here ? local : top;
3801
+ for (let r = startRow; r <= endRow && r < rows; r++) into[r] = 1;
3802
+ }
3803
+ for (let k = n.namedChildCount - 1; k >= 0; k--) {
3804
+ const c = n.namedChild(k);
3805
+ if (c) stack.push({ n: c, inBody: here });
3806
+ }
3807
+ }
3808
+ return { local, top };
3809
+ }
3810
+ // KAD-MR3 K6 (M49.K1): performKotlinTypeInference was DEAD MACHINERY — it
3811
+ // gated on SymbolKind.Variable, but the extractor emits Property symbols
3812
+ // and no Variable kind, so every branch (is/has→Boolean,
3813
+ // remember/mutableStateOf→MutableState) was unreachable (probe: kinds
3814
+ // [class, method], zero Variables). Deleted rather than fed — fabricating
3815
+ // return types from NAME PREFIXES ("isValid→Boolean") is a guessing
3816
+ // heuristic masquerading as inference. metadata.hasTypeInference stays
3817
+ // (declared-type extraction IS real inference input).
3818
+ getKotlinFrameworkPatterns(frameworks, symbols) {
3819
+ const patterns = [];
3820
+ for (let i = 0; i < frameworks.length; i++) {
3821
+ const fw = frameworks[i];
3822
+ if (fw === "Jetpack Compose") {
3823
+ let hasUI = false, hasState = false;
3824
+ for (let j = 0; j < symbols.length; j++) {
3825
+ const s = symbols[j];
3826
+ if (s.metadata?.annotations) {
3827
+ for (let k = 0; k < s.metadata.annotations.length; k++) {
3828
+ if (s.metadata.annotations[k] === "Composable") hasUI = true;
3829
+ }
3830
+ }
3831
+ if (s.qualified_calls) {
3832
+ for (let k = 0; k < s.qualified_calls.length; k++) {
3833
+ if (s.qualified_calls[k] === "remember" || s.qualified_calls[k] === "mutableStateOf") hasState = true;
3834
+ }
3835
+ }
3836
+ }
3837
+ const fwPatterns = [];
3838
+ if (hasUI) fwPatterns.push("ui_components");
3839
+ if (hasState) fwPatterns.push("state_management");
3840
+ patterns.push({ framework: fw, patterns: fwPatterns });
3841
+ } else if (fw === "Android SDK") {
3842
+ let hasMvvm = false, hasLifecycle = false;
3843
+ for (let j = 0; j < symbols.length; j++) {
3844
+ const s = symbols[j];
3845
+ if (s.metadata?.superTypes) {
3846
+ for (let k = 0; k < s.metadata.superTypes.length; k++) {
3847
+ if (s.metadata.superTypes[k] === "ViewModel") hasMvvm = true;
3848
+ if (s.metadata.superTypes[k] === "Activity" || s.metadata.superTypes[k] === "Fragment") hasLifecycle = true;
3849
+ }
3850
+ }
3851
+ }
3852
+ const fwPatterns = [];
3853
+ if (hasMvvm) fwPatterns.push("mvvm_architecture");
3854
+ if (hasLifecycle) fwPatterns.push("android_lifecycle");
3855
+ patterns.push({ framework: fw, patterns: fwPatterns });
3856
+ } else if (fw === "Room") {
3857
+ let hasEntities = false, hasDaos = false;
3858
+ for (let j = 0; j < symbols.length; j++) {
3859
+ const s = symbols[j];
3860
+ if (s.metadata?.annotations) {
3861
+ for (let k = 0; k < s.metadata.annotations.length; k++) {
3862
+ if (s.metadata.annotations[k] === "Entity") hasEntities = true;
3863
+ if (s.metadata.annotations[k] === "Dao") hasDaos = true;
3864
+ }
3865
+ }
3866
+ }
3867
+ const fwPatterns = [];
3868
+ if (hasEntities) fwPatterns.push("database_entities");
3869
+ if (hasDaos) fwPatterns.push("data_access_objects");
3870
+ patterns.push({ framework: fw, patterns: fwPatterns });
3871
+ } else if (fw === "Spring Boot") {
3872
+ let hasRest = false, hasService = false;
3873
+ for (let j = 0; j < symbols.length; j++) {
3874
+ const s = symbols[j];
3875
+ if (s.metadata?.annotations) {
3876
+ for (let k = 0; k < s.metadata.annotations.length; k++) {
3877
+ if (s.metadata.annotations[k] === "RestController") hasRest = true;
3878
+ if (s.metadata.annotations[k] === "Service") hasService = true;
3879
+ }
3880
+ }
3881
+ }
3882
+ const fwPatterns = [];
3883
+ if (hasRest) fwPatterns.push("rest_controllers");
3884
+ if (hasService) fwPatterns.push("services");
3885
+ patterns.push({ framework: fw, patterns: fwPatterns });
3886
+ }
3887
+ }
3888
+ return patterns;
3889
+ }
3890
+ generateArchitecturalInsights(symbols, relationships) {
3891
+ const insights = [];
3892
+ let viewModelsCount = 0, composablesCount = 0, suspendCount = 0;
3893
+ for (let i = 0; i < symbols.length; i++) {
3894
+ const s = symbols[i];
3895
+ if (s.metadata?.superTypes) {
3896
+ for (let j = 0; j < s.metadata.superTypes.length; j++) {
3897
+ if (s.metadata.superTypes[j] === "ViewModel") viewModelsCount++;
3898
+ }
3899
+ }
3900
+ if (s.metadata?.annotations) {
3901
+ for (let j = 0; j < s.metadata.annotations.length; j++) {
3902
+ if (s.metadata.annotations[j] === "Composable") composablesCount++;
3903
+ }
3904
+ }
3905
+ if (s.metadata?.modifiers) {
3906
+ for (let j = 0; j < s.metadata.modifiers.length; j++) {
3907
+ if (s.metadata.modifiers[j] === "suspend") suspendCount++;
3908
+ }
3909
+ }
3910
+ }
3911
+ if (viewModelsCount > 0) {
3912
+ insights.push({
3913
+ type: "architecture",
3914
+ category: "android_mvvm",
3915
+ message: `Detected ${viewModelsCount} ViewModels, indicating MVVM architecture.`,
3916
+ severity: "info"
3917
+ });
3918
+ }
3919
+ if (composablesCount > 0) {
3920
+ insights.push({
3921
+ type: "ui_framework",
3922
+ category: "compose",
3923
+ message: `Jetpack Compose UI detected with ${composablesCount} composable functions.`,
3924
+ severity: "info"
3925
+ });
3926
+ }
3927
+ if (suspendCount > 0) {
3928
+ insights.push({
3929
+ type: "concurrency",
3930
+ category: "coroutines",
3931
+ message: `Coroutine usage detected with ${suspendCount} suspend functions.`,
3932
+ severity: "info"
3933
+ });
3934
+ }
3935
+ return insights;
3936
+ }
3937
+ calculateArchitecturalScore(symbols, relationships, patterns) {
3938
+ let score = 80;
3939
+ for (let i = 0; i < patterns.length; i++) {
3940
+ if (patterns[i] === "Jetpack Compose") score += 5;
3941
+ if (patterns[i] === "Coroutines") score += 5;
3942
+ if (patterns[i] === "Data Classes") score += 5;
3943
+ }
3944
+ return Math.min(100, score);
3945
+ }
3946
+ assessKotlinCodeQuality(symbols, relationships, patterns) {
3947
+ const score = this.calculateArchitecturalScore(symbols, relationships, patterns);
3948
+ if (score >= 90) return "excellent";
3949
+ if (score >= 80) return "good";
3950
+ if (score >= 60) return "fair";
3951
+ return "poor";
3952
+ }
3953
+ };
3954
+ export {
3955
+ RealKotlinAnalyzer
3956
+ };