@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,1515 @@
1
+ import {
2
+ FILE_CLAIM_MAX_BYTES,
3
+ dirOf,
4
+ fileClaimForTarget,
5
+ foldProjectPath
6
+ } from "./chunk-EWMWK7WN.js";
7
+ import {
8
+ RealCssAnalyzer,
9
+ cssFileScopeOf
10
+ } from "./chunk-IFZNR5UJ.js";
11
+ import {
12
+ ASTEngine
13
+ } from "./chunk-V44PGPA6.js";
14
+ import "./chunk-BQ3OVQ3P.js";
15
+ import "./chunk-SK6YVHFZ.js";
16
+ import "./chunk-2BSKD7O3.js";
17
+ import "./chunk-KAET47ZL.js";
18
+ import "./chunk-KCDECRJY.js";
19
+ import "./chunk-MLKGABMK.js";
20
+
21
+ // src/ast/real-scss-analyzer.ts
22
+ import * as path from "path";
23
+ import { existsSync } from "fs";
24
+ function stripAllQuotes(text) {
25
+ let out = "";
26
+ for (let i = 0; i < text.length; i++) {
27
+ const c = text[i];
28
+ if (c === '"' || c === "'") {
29
+ const close = text.indexOf(c, i + 1);
30
+ if (close < 0) break;
31
+ out += text.slice(i + 1, close);
32
+ i = close;
33
+ } else {
34
+ out += c;
35
+ }
36
+ }
37
+ return out.trim();
38
+ }
39
+ var JS_WHITESPACE = /* @__PURE__ */ new Set([
40
+ " ",
41
+ " ",
42
+ "\n",
43
+ "\v",
44
+ "\f",
45
+ "\r",
46
+ "\xA0",
47
+ "\u1680",
48
+ "\u2000",
49
+ "\u2001",
50
+ "\u2002",
51
+ "\u2003",
52
+ "\u2004",
53
+ "\u2005",
54
+ "\u2006",
55
+ "\u2007",
56
+ "\u2008",
57
+ "\u2009",
58
+ "\u200A",
59
+ "\u2028",
60
+ "\u2029",
61
+ "\u202F",
62
+ "\u205F",
63
+ "\u3000",
64
+ "\uFEFF"
65
+ ]);
66
+ function isWordCharCode(code) {
67
+ return code >= 97 && code <= 122 || // a-z
68
+ code >= 65 && code <= 90 || // A-Z
69
+ code >= 48 && code <= 57 || // 0-9
70
+ code === 95;
71
+ }
72
+ function collapseWhitespace(text) {
73
+ let out = "";
74
+ let pendingSpace = false;
75
+ for (const ch of text) {
76
+ if (JS_WHITESPACE.has(ch)) {
77
+ if (out.length > 0) pendingSpace = true;
78
+ continue;
79
+ }
80
+ if (pendingSpace) {
81
+ out += " ";
82
+ pendingSpace = false;
83
+ }
84
+ out += ch;
85
+ }
86
+ return out;
87
+ }
88
+ function isReadablePlaceholder(name) {
89
+ if (name.length < 2) return false;
90
+ if (name.charCodeAt(0) !== 37) return false;
91
+ const last = name.charCodeAt(name.length - 1);
92
+ if (!isWordCharCode(last)) return false;
93
+ for (let i = 1; i < name.length - 1; i++) {
94
+ const code = name.charCodeAt(i);
95
+ if (code === 45 || isWordCharCode(code)) continue;
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ function containsStatementDelimiter(name) {
101
+ for (let i = 0; i < name.length; i++) {
102
+ const code = name.charCodeAt(i);
103
+ if (code === 59 || code === 123 || code === 125) return true;
104
+ }
105
+ return false;
106
+ }
107
+ function mixinName(name) {
108
+ return `@mixin ${name}`;
109
+ }
110
+ function functionName(name) {
111
+ return `@function ${name}`;
112
+ }
113
+ var RECOVERY_CHILD_CAP = 1e4;
114
+ var SCOPE_NODES = /* @__PURE__ */ new Set([
115
+ "rule_set",
116
+ "mixin_statement",
117
+ "function_statement",
118
+ "if_statement",
119
+ "each_statement",
120
+ "for_statement",
121
+ "while_statement"
122
+ ]);
123
+ function hasScopeAncestor(node) {
124
+ for (let p = node.parent; p; p = p.parent) if (SCOPE_NODES.has(p.type)) return true;
125
+ return false;
126
+ }
127
+ function isPrivateSassName(published) {
128
+ const bare = published.startsWith("$") ? published.slice(1) : published.startsWith("@mixin ") ? published.slice("@mixin ".length) : published.startsWith("@function ") ? published.slice("@function ".length) : published;
129
+ return bare.startsWith("-") || bare.startsWith("_");
130
+ }
131
+ function hasSchemeHead(specifier) {
132
+ const colon = specifier.indexOf(":");
133
+ if (colon <= 0) return false;
134
+ const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
135
+ const isDigit = (c) => c >= 48 && c <= 57;
136
+ if (!isAlpha(specifier.charCodeAt(0))) return false;
137
+ for (let i = 1; i < colon; i++) {
138
+ const c = specifier.charCodeAt(i);
139
+ if (!isAlpha(c) && !isDigit(c) && c !== 43 && c !== 46 && c !== 45) return false;
140
+ }
141
+ return true;
142
+ }
143
+ function hasNodeModulesSegment(specifier) {
144
+ return specifier.startsWith("node_modules/") || specifier.includes("/node_modules/");
145
+ }
146
+ function isScopedPackageSpecifier(specifier) {
147
+ if (!specifier.startsWith("@")) return false;
148
+ const slash = specifier.indexOf("/", 1);
149
+ if (slash < 2) return false;
150
+ const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
151
+ const isDigit = (c) => c >= 48 && c <= 57;
152
+ for (let i = 1; i < slash; i++) {
153
+ const c = specifier.charCodeAt(i);
154
+ if (!isAlpha(c) && !isDigit(c) && c !== 46 && c !== 95 && c !== 45) return false;
155
+ }
156
+ return true;
157
+ }
158
+ function hasCssFileExtension(specifier) {
159
+ const lower = specifier.toLowerCase();
160
+ let at = lower.indexOf(".css");
161
+ while (at >= 0) {
162
+ const next = lower.charAt(at + 4);
163
+ if (next === "" || next === "?" || next === "#") return true;
164
+ at = lower.indexOf(".css", at + 4);
165
+ }
166
+ return false;
167
+ }
168
+ function includeStatementIsComplete(statementText) {
169
+ const trimmed = statementText.trimEnd();
170
+ if (trimmed.endsWith(";")) return true;
171
+ if (!trimmed.endsWith("}")) return false;
172
+ return trimmed.indexOf("{") >= 0;
173
+ }
174
+ function normalizeModuleSpelling(spelling) {
175
+ const segments = spelling.split("\\").join("/").split("/");
176
+ let leaf = segments.length > 0 ? segments[segments.length - 1] ?? spelling : spelling;
177
+ const lower = leaf.toLowerCase();
178
+ for (const ext of [".scss", ".sass", ".css"]) {
179
+ if (lower.endsWith(ext)) {
180
+ leaf = leaf.slice(0, leaf.length - ext.length);
181
+ break;
182
+ }
183
+ }
184
+ if (leaf.startsWith("_")) leaf = leaf.slice(1);
185
+ return leaf;
186
+ }
187
+ function isExternalImportSpecifier(written) {
188
+ if (hasSchemeHead(written) || written.startsWith("//")) return true;
189
+ if (hasNodeModulesSegment(written)) return true;
190
+ if (written.startsWith("~")) return true;
191
+ if (isScopedPackageSpecifier(written)) return true;
192
+ return false;
193
+ }
194
+ function sourcePathClaimIsUnrefutable(projectRoot, written) {
195
+ const candidates = [];
196
+ const push = (v) => {
197
+ const t = v.trim();
198
+ if (t.length > 0 && !candidates.includes(t)) candidates.push(t);
199
+ };
200
+ push(written);
201
+ push(written.startsWith("./") ? written.slice(2) : written);
202
+ push(written.startsWith("../") ? written.slice(3) : written);
203
+ const segs = written.split("/").filter((x) => x.length > 0 && x !== "." && x !== "..");
204
+ if (segs.length > 0) {
205
+ push(segs.join("/"));
206
+ push(segs[segs.length - 1]);
207
+ }
208
+ for (const candidate of candidates) {
209
+ try {
210
+ if (existsSync(path.resolve(projectRoot, candidate))) return false;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+ return true;
216
+ }
217
+ var RealScssAnalyzer = class {
218
+ analyze(rootNode, content, filePath, context) {
219
+ const base = new RealCssAnalyzer().analyze(rootNode, content, filePath);
220
+ const ctx = {
221
+ symbols: base.symbols ?? [],
222
+ relationships: base.relationships ?? [],
223
+ imports: base.imports ?? [],
224
+ exports: base.exports ?? [],
225
+ mixinNames: {},
226
+ functionNames: {},
227
+ variableNames: {},
228
+ namespaces: {},
229
+ sassPartImports: /* @__PURE__ */ new Set(),
230
+ published: {},
231
+ publishedOwner: {},
232
+ moduleName: this.moduleNameOf(filePath),
233
+ topLevelVarLines: {},
234
+ sawDiagnostic: false,
235
+ sawContent: false,
236
+ sawAtRoot: false,
237
+ projectIndex: context?.projectIndex
238
+ };
239
+ const baseModule = ctx.symbols.find(
240
+ (s) => s.kind === ASTEngine.SymbolKind.Module && s.name === ctx.moduleName
241
+ );
242
+ if (baseModule) {
243
+ baseModule.metadata = {
244
+ ...baseModule.metadata ?? {},
245
+ isScssModule: true,
246
+ scssModuleName: ctx.moduleName,
247
+ scssModulePath: filePath
248
+ };
249
+ } else {
250
+ ctx.symbols.unshift({
251
+ name: ctx.moduleName,
252
+ kind: ASTEngine.SymbolKind.Module,
253
+ range: this.nodeToRange(rootNode),
254
+ scope: "",
255
+ visibility: "public",
256
+ complexity: 1,
257
+ metadata: { cssSpecific: true, isScssModule: true, scssModuleName: ctx.moduleName, scssModulePath: filePath }
258
+ });
259
+ }
260
+ for (const s of ctx.symbols) ctx.published[s.name] = true;
261
+ this.collectDeclarations(rootNode, ctx);
262
+ this.walk(rootNode, ctx);
263
+ if (ctx.recoveryCap && ctx.relationships.length > 0) {
264
+ ctx.relationships[0].metadata = { ...ctx.relationships[0].metadata ?? {}, scssRecoveryCap: ctx.recoveryCap };
265
+ }
266
+ this.emitModuleExports(ctx);
267
+ const claimRoot = typeof context?.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
268
+ if (claimRoot !== null) {
269
+ const fileRel = path.relative(path.resolve(claimRoot), filePath).split(path.sep).join("/");
270
+ const baseDir = dirOf(fileRel);
271
+ for (const r of ctx.relationships) {
272
+ if (r.type !== ASTEngine.RelationshipType.Imports) continue;
273
+ const md = r.metadata;
274
+ if (!md || typeof md.importProjectFile === "string") continue;
275
+ const written = String(r.target ?? "").trim();
276
+ if (written.length === 0) continue;
277
+ const cut = written.lastIndexOf("/");
278
+ const dirPart = cut < 0 ? "" : written.slice(0, cut + 1);
279
+ const leaf = cut < 0 ? written : written.slice(cut + 1);
280
+ const spellings = /* @__PURE__ */ new Set([`${written}.scss`, `${written}/_index.scss`, `${written}/index.scss`]);
281
+ if (leaf.length > 0 && !leaf.startsWith("_")) {
282
+ spellings.add(`${dirPart}_${leaf}.scss`);
283
+ spellings.add(`${dirPart}_${leaf}/_index.scss`);
284
+ }
285
+ const extra = [];
286
+ for (const sp of spellings) {
287
+ for (const cand of [foldProjectPath(baseDir, sp), foldProjectPath("", sp)]) {
288
+ if (cand !== null && !extra.includes(cand)) extra.push(cand);
289
+ }
290
+ }
291
+ const claim = fileClaimForTarget({
292
+ projectRoot: claimRoot,
293
+ filePath,
294
+ written,
295
+ admittedExtensions: ["scss"],
296
+ // THE POPULATION'S SIZE CAP, passed because this lane HAS over-cap files (4 for graphql,
297
+ // 2 for scss on the gate's own population line). A claim about one of them would be
298
+ // REFUTED, and nothing in the row would say why.
299
+ maxFileSizeBytes: FILE_CLAIM_MAX_BYTES,
300
+ extraCandidates: extra
301
+ });
302
+ if (claim !== null) {
303
+ md.importProjectFile = claim;
304
+ continue;
305
+ }
306
+ if (isExternalImportSpecifier(written) && sourcePathClaimIsUnrefutable(claimRoot, written)) {
307
+ md.sourcePathClaim = [written];
308
+ }
309
+ }
310
+ }
311
+ this.relabelSassPartImports(ctx);
312
+ this.publishScssFileScopedIdentities(ctx, filePath);
313
+ return {
314
+ ...base,
315
+ symbols: ctx.symbols,
316
+ relationships: ctx.relationships,
317
+ imports: ctx.imports,
318
+ exports: ctx.exports,
319
+ metadata: {
320
+ ...base.metadata ?? {},
321
+ scssDialect: true,
322
+ scssMixins: Object.keys(ctx.mixinNames).length,
323
+ scssFunctions: Object.keys(ctx.functionNames).length,
324
+ scssVariables: Object.keys(ctx.variableNames).length,
325
+ scssModuleName: ctx.moduleName,
326
+ scssFrameworks: ["SCSS/SASS"],
327
+ ...ctx.sawDiagnostic ? { scssHasDiagnostics: true } : {},
328
+ ...ctx.sawContent ? { scssForwardsContent: true } : {},
329
+ ...ctx.sawAtRoot ? { scssEscapesScope: true } : {}
330
+ }
331
+ };
332
+ }
333
+ /** The stylesheet's own module name: its basename, extension included. */
334
+ moduleNameOf(filePath) {
335
+ const normalized = filePath.split("\\").join("/");
336
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
337
+ return base.length > 0 ? base : "module.scss";
338
+ }
339
+ /**
340
+ * Give the symbols THIS analyzer added — and only those — a project-unique `qualifiedName`,
341
+ * then respell the edges that name one of them from the file that declares it.
342
+ *
343
+ * WHY ONLY THE ONES THIS ANALYZER ADDED. The delegate already ran
344
+ * `publishFileScopedIdentities` over its own output, so every symbol it published already
345
+ * carries a `qualifiedName` and every edge it could respell already has. A target that is
346
+ * STILL bare after the delegate declined it (no local declaration, or several and no line to
347
+ * choose between them) is a decision the delegate made deliberately, and re-deciding it here
348
+ * would be a second implementation of a rule this lane is supposed to inherit. So the pass
349
+ * touches a symbol only when `qualifiedName` is ABSENT, which is exactly the set added here:
350
+ * `$variables`, `@mixin`/`@function` symbols, the module's namespaces, the control-flow
351
+ * `interface` symbols, the imported-member symbols, and the module row itself.
352
+ *
353
+ * WHY REFERENCES/EXPORTS/CALLS/USES/OVERRIDES. `Exports` and `Calls` are the two the delegate
354
+ * has no analogue for (CSS has neither a module surface nor a call), and they are the two
355
+ * carrying this lane's measured collisions; `References` and `Uses` are here because the SCSS
356
+ * reads (`$var`, `@include`) are pushed AFTER the delegate's pass and so were never seen by
357
+ * it. `Imports` is deliberately ABSENT for the delegate's own stated reason: an `imports`
358
+ * edge names ANOTHER file's module, and this file's module symbol happens to share its
359
+ * spelling with that file's basename — respelling it would point the import at the importing
360
+ * file itself.
361
+ */
362
+ publishScssFileScopedIdentities(ctx, filePath) {
363
+ const scope = cssFileScopeOf(filePath);
364
+ if (scope === "") return;
365
+ const byName = /* @__PURE__ */ new Map();
366
+ for (const sym of ctx.symbols) {
367
+ const name = String(sym.name ?? "");
368
+ if (name.length === 0) continue;
369
+ if (sym.qualifiedName !== void 0 && sym.qualifiedName !== "") continue;
370
+ sym.qualifiedName = `${scope}::${name}`;
371
+ if (!byName.has(name)) byName.set(name, String(sym.qualifiedName));
372
+ }
373
+ if (byName.size === 0) return;
374
+ const REWRITABLE = /* @__PURE__ */ new Set([
375
+ ASTEngine.RelationshipType.References,
376
+ ASTEngine.RelationshipType.Exports,
377
+ ASTEngine.RelationshipType.Calls,
378
+ ASTEngine.RelationshipType.Uses,
379
+ ASTEngine.RelationshipType.Overrides,
380
+ // `mixes_in` targets a `@mixin <name>` symbol THIS FILE may declare, so the same file-scoped
381
+ // respell applies — and it must: a target published bare for a mixin the file declares is the
382
+ // collision shape this pass exists to remove. `part_of` is deliberately ABSENT for the reason
383
+ // `Imports` is: its target names ANOTHER file's module, and respelling it to this file's
384
+ // scope would point the inclusion at the including file itself.
385
+ ASTEngine.RelationshipType.MixesIn
386
+ ]);
387
+ for (const rel of ctx.relationships) {
388
+ if (!REWRITABLE.has(String(rel.type ?? ""))) continue;
389
+ const target = String(rel.target ?? "");
390
+ if (target.length === 0 || target.includes("::")) continue;
391
+ const qname = byName.get(target);
392
+ if (qname !== void 0) rel.target = qname;
393
+ }
394
+ }
395
+ // ── helpers ────────────────────────────────────────────────────────────────
396
+ lineOf(node) {
397
+ return node ? node.startPosition.row + 1 : void 0;
398
+ }
399
+ isErrorNode(node) {
400
+ return node.type === "ERROR" || node.isMissing === true;
401
+ }
402
+ findChild(node, predicate) {
403
+ for (let i = 0; i < node.namedChildCount; i++) {
404
+ const c = node.namedChild(i);
405
+ if (c && predicate(c)) return c;
406
+ }
407
+ return null;
408
+ }
409
+ findDescendant(node, predicate) {
410
+ const queue = [node];
411
+ while (queue.length > 0) {
412
+ const n = queue.shift();
413
+ if (predicate(n)) return n;
414
+ for (let i = 0; i < n.namedChildCount; i++) {
415
+ const c = n.namedChild(i);
416
+ if (c) queue.push(c);
417
+ }
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * The byte offset where an `@extend` statement's own selector stops.
423
+ *
424
+ * That is the first `;` at or after the statement starts, because `;` terminates the
425
+ * declaration the `@extend` belongs to and everything past it is the next declaration. A
426
+ * statement with no `;` is bounded by its own end.
427
+ *
428
+ * Returned as an OFFSET rather than used as a string test on the target, so a legitimate
429
+ * `@extend` whose target happens to contain `;`-adjacent punctuation is judged by position
430
+ * in the source and not by a shape.
431
+ */
432
+ extendTargetBound(node) {
433
+ const semi = String(node.text ?? "").indexOf(";");
434
+ return semi < 0 ? node.endIndex : node.startIndex + semi;
435
+ }
436
+ /**
437
+ * Every strict descendant matching `predicate`, in tree order.
438
+ *
439
+ * Needed where `findDescendant`'s breadth-first FIRST match is the wrong node: on a
440
+ * broken `@extend` the first match is a `class_selector` spanning the rest of the region
441
+ * and the real referent is a `placeholder` nested inside it (see `extendedSelectorName`).
442
+ * Depth-first, so the innermost usable node is preferred over a shallower one.
443
+ */
444
+ descendants(node, predicate) {
445
+ const out = [];
446
+ const stack = [node];
447
+ while (stack.length > 0) {
448
+ const n = stack.pop();
449
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
450
+ const c = n.namedChild(i);
451
+ if (!c) continue;
452
+ if (predicate(c)) out.push(c);
453
+ stack.push(c);
454
+ }
455
+ }
456
+ return out;
457
+ }
458
+ nodeToRange(node) {
459
+ return {
460
+ startLine: node.startPosition.row + 1,
461
+ startColumn: node.startPosition.column,
462
+ endLine: node.endPosition.row + 1,
463
+ endColumn: node.endPosition.column,
464
+ startIndex: node.startIndex,
465
+ endIndex: node.endIndex
466
+ };
467
+ }
468
+ childForField(node, field) {
469
+ return node.childForFieldName(field);
470
+ }
471
+ /**
472
+ * The innermost PUBLISHED symbol whose range contains `node` — i.e. the rule,
473
+ * mixin or function this statement sits inside.
474
+ *
475
+ * WHY A RANGE LOOKUP AND NOT A TRACKED VARIABLE. The first version threaded an
476
+ * `owner` argument through the walk, the way the CSS analyzer threads
477
+ * `currentSelector`, and it produced three fabrications in one file:
478
+ *
479
+ * uses button -> @mixin button (source was the mixin NAME)
480
+ * uses .btn -> .btn (source was its own target)
481
+ * references color: $brand; -> $brand (source was raw declaration text)
482
+ *
483
+ * All three have a source that is not an entity. The reason is that a tracked
484
+ * variable only advances for the node types the walker recognises, and a `rule_set`
485
+ * carrying an `@include` is not one of them — so inside any ordinary rule the owner
486
+ * was empty and each edge fell back to something adjacent.
487
+ *
488
+ * Looking the owner up by RANGE fixes all three at once and, more importantly, makes
489
+ * the edge name the SAME symbol the CSS lane published for that rule. Recomputing a
490
+ * selector name here would be a second implementation of the CSS lane's naming —
491
+ * including its `&`-chain resolution — and two implementations of a naming rule is
492
+ * how a graph ends up with `.card` on one side of an edge and `div .card` on the
493
+ * other. Innermost wins, so a declaration inside a rule inside a mixin is owned by the
494
+ * rule, and a statement directly in a mixin body is owned by the mixin.
495
+ */
496
+ ownerForNode(node, ctx) {
497
+ const start = node.startIndex;
498
+ const idx = this.ownerIndexOf(ctx);
499
+ const pick = {
500
+ entry: null,
501
+ span: Number.POSITIVE_INFINITY,
502
+ orig: Number.POSITIVE_INFINITY
503
+ };
504
+ const consider = (e) => {
505
+ if (start < e.start || start >= e.end) return;
506
+ const span = e.end - e.start;
507
+ if (span < pick.span || span === pick.span && e.orig < pick.orig) {
508
+ pick.span = span;
509
+ pick.entry = e;
510
+ pick.orig = e.orig;
511
+ }
512
+ };
513
+ let lo = 0;
514
+ let hi = idx.sorted.length - 1;
515
+ let j = -1;
516
+ while (lo <= hi) {
517
+ const mid = lo + hi >> 1;
518
+ if (idx.sorted[mid].start <= start) {
519
+ j = mid;
520
+ lo = mid + 1;
521
+ } else hi = mid - 1;
522
+ }
523
+ for (let i = j; i >= 0; i--) {
524
+ if (idx.maxEnd[i] <= start) break;
525
+ consider(idx.sorted[i]);
526
+ }
527
+ for (const e of idx.tail) consider(e);
528
+ return pick.entry ? pick.entry.sym.name : "";
529
+ }
530
+ /**
531
+ * Build (or grow) the interval index `ownerForNode` reads. Rebuilds happen when the un-indexed
532
+ * tail grows past 32 entries, and whenever the identity guard sees the array reordered — so the
533
+ * index can only ever be a faster spelling of the scan it replaced.
534
+ */
535
+ ownerIndexOf(ctx) {
536
+ const symbols = ctx.symbols;
537
+ let idx = ctx.ownerIndex;
538
+ if (!idx || idx.len > 0 && (idx.len > symbols.length || symbols[idx.len - 1] !== idx.lastSeen)) {
539
+ idx = ctx.ownerIndex = { len: 0, lastSeen: void 0, sorted: [], maxEnd: [], tail: [] };
540
+ }
541
+ if (symbols.length === idx.len) return idx;
542
+ for (let i = idx.len; i < symbols.length; i++) {
543
+ const sym = symbols[i];
544
+ if (sym.metadata?.isScssModule) continue;
545
+ const r = sym.range;
546
+ if (!r || typeof r.startIndex !== "number" || typeof r.endIndex !== "number") continue;
547
+ idx.tail.push({ sym, orig: i, start: r.startIndex, end: r.endIndex });
548
+ }
549
+ idx.len = symbols.length;
550
+ idx.lastSeen = symbols[symbols.length - 1];
551
+ if (idx.tail.length > 32) {
552
+ idx.sorted = idx.sorted.concat(idx.tail).sort((a, b) => a.start - b.start || a.orig - b.orig);
553
+ idx.tail = [];
554
+ idx.maxEnd = new Array(idx.sorted.length);
555
+ let m = Number.NEGATIVE_INFINITY;
556
+ for (let i = 0; i < idx.sorted.length; i++) {
557
+ if (idx.sorted[i].end > m) m = idx.sorted[i].end;
558
+ idx.maxEnd[i] = m;
559
+ }
560
+ }
561
+ return idx;
562
+ }
563
+ /** The old linear scan, kept so the equivalence harness can compare against it (never used in the
564
+ * analysis path). Any difference between this and `ownerForNode` is a regression. */
565
+ ownerForNodeLinear(node, ctx) {
566
+ const start = node.startIndex;
567
+ let best = null;
568
+ let bestSpan = Number.POSITIVE_INFINITY;
569
+ for (const sym of ctx.symbols) {
570
+ if (sym.metadata?.isScssModule) continue;
571
+ const r = sym.range;
572
+ if (!r || typeof r.startIndex !== "number" || typeof r.endIndex !== "number") continue;
573
+ if (start < r.startIndex || start >= r.endIndex) continue;
574
+ const span = r.endIndex - r.startIndex;
575
+ if (span < bestSpan) {
576
+ bestSpan = span;
577
+ best = sym;
578
+ }
579
+ }
580
+ return best ? best.name : "";
581
+ }
582
+ /**
583
+ * A declaration is published ONCE per name. `$brand: #f00` in three places is one
584
+ * variable that is assigned three times, and the CSS lane's identity ruling —
585
+ * `(kind, resolved name)`, scope retained as provenance — applies unchanged: a
586
+ * symbol that exists twice with one name is the identity collision this project has
587
+ * been removing all along, and `.scss` is full of re-assignments.
588
+ */
589
+ publishOnce(ctx, symbol, owner = "") {
590
+ const key = `${symbol.kind} ${symbol.name}`;
591
+ if (!ctx.published[key]) {
592
+ ctx.published[key] = true;
593
+ ctx.publishedOwner[key] = owner;
594
+ ctx.symbols.push(symbol);
595
+ return true;
596
+ }
597
+ if (ctx.publishedOwner[key] === owner) return false;
598
+ let n = 2;
599
+ while (ctx.published[`${key}#${n}`]) n++;
600
+ const disambiguated = { ...symbol, name: `${symbol.name}#${n}` };
601
+ ctx.published[`${key}#${n}`] = true;
602
+ ctx.publishedOwner[`${key}#${n}`] = owner;
603
+ ctx.symbols.push(disambiguated);
604
+ return true;
605
+ }
606
+ // ── the module's public surface: `exports` ─────────────────────────────────
607
+ /**
608
+ * A Sass module exports its top-level variables, mixins and functions, and nothing
609
+ * else — that is precisely what `@use 'this-file'` binds. An assignment inside a rule,
610
+ * a mixin, a function or a control-flow block is LOCAL and is not part of the surface.
611
+ *
612
+ * Measured over 4,394 admitted corpus files: 3,315 top-level `$` assignments naming
613
+ * 2,501 distinct variables, 30 of them private by the `-`/`_` rule, plus 308 mixin and
614
+ * 77 function declarations — so this emits roughly one edge per distinct public member
615
+ * of every stylesheet, which is the module-system pair with `imports`.
616
+ *
617
+ * BOTH ends resolve by construction: the source is this file's own module symbol and
618
+ * the target is a symbol this file publishes. That is the whole reason to model a
619
+ * module's surface as EDGES rather than as a convention: `@use 'a', 'b'` binds
620
+ * members from two files and nothing else in the graph can say which file offered a
621
+ * name.
622
+ */
623
+ emitModuleExports(ctx) {
624
+ const published = /* @__PURE__ */ new Set();
625
+ for (const sym of ctx.symbols) {
626
+ if (sym.metadata?.isScssModule) continue;
627
+ if (sym.metadata?.scssImportedFrom) continue;
628
+ if (sym.kind !== ASTEngine.SymbolKind.Variable && sym.kind !== ASTEngine.SymbolKind.Function) continue;
629
+ const isCallable = sym.kind === ASTEngine.SymbolKind.Function;
630
+ if (isCallable ? !sym.metadata?.scssCallable : !sym.metadata?.isScssVariable) continue;
631
+ if (sym.scope) continue;
632
+ const name = sym.name;
633
+ if (published.has(name)) continue;
634
+ if (isPrivateSassName(name)) {
635
+ published.add(name);
636
+ continue;
637
+ }
638
+ published.add(name);
639
+ ctx.relationships.push({
640
+ source: ctx.moduleName,
641
+ target: name,
642
+ type: ASTEngine.RelationshipType.Exports,
643
+ line_number: sym.range?.startLine ?? 1,
644
+ confidence: 1,
645
+ metadata: {
646
+ evidenceClass: "structural",
647
+ cssSpecific: true,
648
+ isScssModuleMember: true,
649
+ scssModule: ctx.moduleName
650
+ }
651
+ });
652
+ ctx.exports.push({
653
+ name,
654
+ kind: sym.kind,
655
+ range: sym.range,
656
+ visibility: "public",
657
+ metadata: { via: "sass-module-top-level", scssModule: ctx.moduleName }
658
+ });
659
+ }
660
+ }
661
+ // ── `overrides`: the one reassignment Sass itself calls an override ─────────
662
+ /**
663
+ * `$x: … !global` where `$x` was already assigned at this file's top level.
664
+ *
665
+ * This is the narrowest honest `overrides` in the language and every part of it is
666
+ * measured, because the wide readings are wrong:
667
+ *
668
+ * * A SECOND `@mixin x` / `@function x`: 0 occurrences across 4,394 admitted files.
669
+ * The branch exists and cannot fire on this corpus, which is a fact about the
670
+ * corpus and not a claim that the construct does not exist.
671
+ * * A plain same-scope reassignment: 203 occurrences, of which 171 READ the variable
672
+ * they assign (`$_args: append($_args, …)` in bootstrap's `_functions.scss` is the
673
+ * shape) — an accumulator mutating one binding, which is not the replacement of a
674
+ * declaration and would be a fabricated edge. The other 32 are a `$x: 1; … $x: 2`
675
+ * pair, which the lane's own identity ruling already collapses into ONE symbol, so
676
+ * an edge between the two assignments would be a self-edge.
677
+ * * `!global`: 7 occurrences, and it is the only one of the three that means what
678
+ * `overrides` means. `!global` is the flag that reaches PAST the enclosing scope
679
+ * and reassigns the binding outside it, so the prior declaration is replaced and
680
+ * the two are different things. bootstrap's `_box-shadow.test.scss` is the model:
681
+ * `$enable-shadows: false !global;` … `$enable-shadows: true !global;` … and a
682
+ * final restore, three overrides of one module-level variable.
683
+ *
684
+ * The prior assignment must be a TOP-LEVEL one. `!global` beside a name that only
685
+ * exists inside another mixin CREATES a global rather than replacing one, and the 14
686
+ * `!global` assignments in element-plus that fail this test are exactly that.
687
+ *
688
+ * The edge runs from the scope that performs the assignment to the variable it
689
+ * replaces, with `overridesLine` carrying the replaced declaration's line, so a
690
+ * consumer does not have to infer it. `source` falls back to the module symbol for an
691
+ * assignment at the file's top level, which is the only case with no enclosing scope.
692
+ */
693
+ emitGlobalOverride(node, name, ctx) {
694
+ if (!this.hasVisibilityFlag(node, "global")) return;
695
+ const line = this.lineOf(node) ?? 0;
696
+ const prior = ctx.topLevelVarLines[name];
697
+ if (prior === void 0 || prior >= line) return;
698
+ const owner = this.ownerForNode(node, ctx) || ctx.moduleName;
699
+ if (!owner) return;
700
+ ctx.relationships.push({
701
+ source: owner,
702
+ target: name,
703
+ type: ASTEngine.RelationshipType.Overrides,
704
+ line_number: line,
705
+ confidence: 1,
706
+ metadata: {
707
+ evidenceClass: "structural",
708
+ cssSpecific: true,
709
+ isScssGlobalOverride: true,
710
+ overridesLine: prior,
711
+ scssFlagGlobal: true
712
+ }
713
+ });
714
+ }
715
+ // ── pass 1: declarations, so a use may precede its declaration ─────────────
716
+ collectDeclarations(root, ctx) {
717
+ const stack = [root];
718
+ while (stack.length > 0) {
719
+ const n = stack.pop();
720
+ if (this.isErrorNode(n)) continue;
721
+ if (n.type === "mixin_statement") {
722
+ const name = this.childForField(n, "name") ?? this.findChild(n, (c) => c.type === "identifier");
723
+ if (name) ctx.mixinNames[stripAllQuotes(name.text)] = mixinName(stripAllQuotes(name.text));
724
+ } else if (n.type === "function_statement") {
725
+ const name = this.childForField(n, "name") ?? this.findChild(n, (c) => c.type === "identifier");
726
+ if (name) ctx.functionNames[stripAllQuotes(name.text)] = functionName(stripAllQuotes(name.text));
727
+ } else if (n.type === "property_name" && n.text.startsWith("$") && n.parent?.type === "declaration") {
728
+ ctx.variableNames[n.text] = n.text;
729
+ } else if (n.type === "use_statement") {
730
+ const module = this.findChild(n, (c) => c.type === "string_value");
731
+ const ns = this.findChild(n, (c) => c.type === "plain_value" || c.type === "identifier");
732
+ if (module && ns) ctx.namespaces[stripAllQuotes(ns.text)] = stripAllQuotes(module.text);
733
+ }
734
+ if (n.type === "declaration" && !hasScopeAncestor(n)) {
735
+ const prop = this.findChild(n, (c) => c.type === "property_name");
736
+ const name = prop ? prop.text : "";
737
+ if (name.startsWith("$")) {
738
+ const line = n.startPosition.row + 1;
739
+ const seen = ctx.topLevelVarLines[name];
740
+ if (seen === void 0 || line < seen) ctx.topLevelVarLines[name] = line;
741
+ }
742
+ }
743
+ for (let i = 0; i < n.namedChildCount; i++) {
744
+ const c = n.namedChild(i);
745
+ if (c) stack.push(c);
746
+ }
747
+ }
748
+ }
749
+ // ── pass 2: the SCSS statements ────────────────────────────────────────────
750
+ /**
751
+ * @param recovered TRUE when this node sits inside a region the grammar could not read.
752
+ * Threaded rather than recomputed because the read/call scan keeps its OWN notion of
753
+ * recovery — it descends into errors on its own — and the two must agree. A claim the
754
+ * outer walk reached by descending into a broken region and then handed to the inner
755
+ * scan is recovered even though the inner scan arrived by a clean path, so the two are
756
+ * OR'd, and that is what makes `scssRecoveredFromError` trustworthy rather than
757
+ * merely present.
758
+ */
759
+ walk(node, ctx, recovered = false, limit = Number.POSITIVE_INFINITY) {
760
+ if (limit === Number.POSITIVE_INFINITY && node.namedChildCount > RECOVERY_CHILD_CAP) {
761
+ const tally = ctx.recoveryCap ??= { cap: RECOVERY_CHILD_CAP, errorNodes: 0, errorChildren: 0, visited: 0, skipped: 0 };
762
+ tally.errorNodes++;
763
+ tally.errorChildren += node.namedChildCount;
764
+ tally.visited += RECOVERY_CHILD_CAP;
765
+ tally.skipped += node.namedChildCount - RECOVERY_CHILD_CAP;
766
+ limit = RECOVERY_CHILD_CAP;
767
+ }
768
+ for (let i = 0; i < node.namedChildCount && i < limit; i++) {
769
+ const child = node.namedChild(i);
770
+ if (!child) continue;
771
+ if (this.isErrorNode(child)) {
772
+ this.processRecoveredExtend(child, ctx);
773
+ this.walk(child, ctx, true);
774
+ continue;
775
+ }
776
+ if (child.type === "at_rule") {
777
+ const kw = this.findChild(child, (c) => c.type === "at_keyword");
778
+ const keyword = kw ? kw.text.trim().toLowerCase() : "";
779
+ if (keyword === "@content") ctx.sawContent = true;
780
+ if (keyword === "@at-root") ctx.sawAtRoot = true;
781
+ }
782
+ switch (child.type) {
783
+ case "use_statement":
784
+ case "forward_statement":
785
+ this.processModuleStatement(child, ctx);
786
+ break;
787
+ case "mixin_statement":
788
+ case "function_statement":
789
+ this.processCallable(child, ctx);
790
+ break;
791
+ case "extend_statement":
792
+ this.processExtend(child, ctx);
793
+ break;
794
+ // The CSS delegate already published this statement's `imports` row. This case only
795
+ // RECORDS the specifiers whose shape is the Sass FILE-INCLUSION form, for the relabel
796
+ // that runs after the delegate's file-claim pass (see `relabelSassPartImports`).
797
+ case "import_statement":
798
+ this.processSassPartImport(child, ctx);
799
+ break;
800
+ case "at_root_statement":
801
+ case "debug_statement":
802
+ case "warn_statement":
803
+ case "error_statement":
804
+ if (child.type === "at_root_statement") ctx.sawAtRoot = true;
805
+ else ctx.sawDiagnostic = true;
806
+ break;
807
+ case "declaration":
808
+ this.processScssDeclaration(child, ctx);
809
+ this.emitVariableReadEdges(child, ctx, recovered);
810
+ break;
811
+ // A `$var` is also read in an `@include`'s ARGUMENT list and in a control
812
+ // flow's condition or iterable, and those are reads like any other:
813
+ // `@include button($brand, 8px)` reads `$brand`. The first version only
814
+ // looked inside `declaration`, so an argument read produced no edge and the
815
+ // variable looked write-only in the graph.
816
+ case "if_statement":
817
+ case "each_statement":
818
+ case "for_statement":
819
+ case "while_statement":
820
+ this.processControlFlow(child, ctx, this.ownerForNode(child, ctx));
821
+ this.emitVariableReadEdges(child, ctx, recovered);
822
+ break;
823
+ case "include_statement":
824
+ this.processInclude(child, ctx);
825
+ this.emitVariableReadEdges(child, ctx, recovered);
826
+ break;
827
+ // `@return` and an `@use … with (…)` configuration are the two remaining
828
+ // places a `$var` can be READ outside a declaration's value, and both were
829
+ // silent. The `return_statement` case was removed when the self-edge was
830
+ // fixed — the edge went, the read scan went with it because the two shared a
831
+ // case — and nothing replaced it.
832
+ //
833
+ // Measured: `element-plus/packages/theme-chalk/src/color/index.scss` reads
834
+ // `$red`, `$green` and `$blue` in a `@return rgb2hex(rgb($red, $green, $blue))`
835
+ // at L19, and the differential reported them MISSING. A function's own body
836
+ // variables were unreadable in the graph: the function published them, and
837
+ // nothing recorded that it consumed them.
838
+ case "return_statement":
839
+ case "use_statement":
840
+ case "forward_statement":
841
+ this.emitVariableReadEdges(child, ctx, recovered);
842
+ break;
843
+ default:
844
+ break;
845
+ }
846
+ this.walk(child, ctx, recovered);
847
+ }
848
+ }
849
+ ownerNameOf(node) {
850
+ const name = this.childForField(node, "name") ?? this.findChild(node, (c) => c.type === "identifier");
851
+ if (!name) return "";
852
+ const raw = stripAllQuotes(name.text);
853
+ return node.type === "function_statement" ? functionName(raw) : mixinName(raw);
854
+ }
855
+ // ── `@use` / `@forward` ─────────────────────────────────────────────────────
856
+ /**
857
+ * An `imports` edge, which is the ontology's type for exactly this — the same one
858
+ * the CSS lane uses for `@import` and for Sass `@use`/`@forward`.
859
+ *
860
+ * The `as` namespace is recorded on the ImportInfo because it is the part a
861
+ * consumer cannot recover from the module path: `@use 'sass:map' as map` and
862
+ * `@use 'sass:map' as m` are the same import and different graphs.
863
+ */
864
+ processModuleStatement(node, ctx) {
865
+ const moduleNode = this.findChild(node, (c) => c.type === "string_value");
866
+ if (!moduleNode) return;
867
+ const module = stripAllQuotes(moduleNode.text);
868
+ const nsNode = this.findChild(node, (c) => c.type === "plain_value" || c.type === "identifier");
869
+ const namespace = nsNode ? stripAllQuotes(nsNode.text) : void 0;
870
+ if (namespace) {
871
+ ctx.symbols.push({
872
+ name: namespace,
873
+ kind: ASTEngine.SymbolKind.Namespace,
874
+ range: this.nodeToRange(node),
875
+ scope: "",
876
+ visibility: "public",
877
+ complexity: 1,
878
+ metadata: { isAtRule: true, isScssModuleNamespace: true, scssModule: module, atKeyword: `@${node.type.replace("_statement", "")}` }
879
+ });
880
+ }
881
+ ctx.imports.push({
882
+ module,
883
+ isNamespaceImport: true,
884
+ alias: namespace,
885
+ path: module,
886
+ range: this.nodeToRange(node),
887
+ metadata: {
888
+ scssModuleType: node.type,
889
+ ...namespace ? { scssNamespace: namespace } : {}
890
+ }
891
+ });
892
+ if (ctx.projectIndex && namespace) {
893
+ this.resolveModuleImports(ctx, namespace, module, this.nodeToRange(node).startLine);
894
+ }
895
+ }
896
+ /**
897
+ * Resolve an imported module's public members from the project index and create
898
+ * exports edges from this file's module to those members.
899
+ * This allows `@use 'other' as ns` to make `ns.$var`, `ns.mixin()`, `ns.function()` resolvable.
900
+ */
901
+ resolveModuleImports(ctx, namespace, modulePath, importLine) {
902
+ if (!ctx.projectIndex) return;
903
+ const index = ctx.projectIndex;
904
+ if (modulePath.startsWith("sass:")) return;
905
+ const norm = (spelling) => normalizeModuleSpelling(spelling);
906
+ const want = norm(modulePath);
907
+ let targetTypeName;
908
+ if (want !== "") {
909
+ for (const typeName of index.typeMembers.keys()) {
910
+ if (norm(typeName) === want) {
911
+ targetTypeName = typeName;
912
+ break;
913
+ }
914
+ }
915
+ }
916
+ if (!targetTypeName) return;
917
+ const declared = index.typeMembers.get(targetTypeName);
918
+ const closedMembers = index.__closedMembers?.get(targetTypeName) ?? declared;
919
+ if (!closedMembers || closedMembers.size === 0) return;
920
+ let scopePrefix = "";
921
+ for (const sym of ctx.symbols) {
922
+ const q = String(sym.qualifiedName ?? "");
923
+ const sep2 = q.indexOf("::");
924
+ if (sep2 > 0) {
925
+ scopePrefix = q.slice(0, sep2);
926
+ break;
927
+ }
928
+ }
929
+ if (scopePrefix === "") {
930
+ const moduleSym = ctx.symbols.find((sym) => sym.metadata?.isScssModule === true);
931
+ const modulePath2 = String(moduleSym?.metadata?.scssModulePath ?? "");
932
+ if (modulePath2 !== "") scopePrefix = cssFileScopeOf(modulePath2);
933
+ }
934
+ for (const memberName of closedMembers) {
935
+ const qualifiedName = `${namespace}.${memberName}`;
936
+ const scopedName = scopePrefix === "" ? qualifiedName : `${scopePrefix}::${qualifiedName}`;
937
+ ctx.relationships.push({
938
+ source: ctx.moduleName,
939
+ target: scopedName,
940
+ type: ASTEngine.RelationshipType.Exports,
941
+ // THE EDGE'S OWN LINE, not a fabricated 0: the import statement's line is what the edge
942
+ // describes, and `0` is the sentinel this programme has refused everywhere else.
943
+ line_number: importLine ?? 0,
944
+ confidence: 1,
945
+ metadata: {
946
+ evidenceClass: "structural",
947
+ cssSpecific: true,
948
+ isScssModuleMember: true,
949
+ scssModule: ctx.moduleName,
950
+ scssNamespace: namespace,
951
+ scssImportedFrom: modulePath
952
+ }
953
+ });
954
+ ctx.symbols.push({
955
+ name: qualifiedName,
956
+ // the discriminator rides the PREFIX, never the name — the delegate's own rule
957
+ qualifiedName: scopedName,
958
+ kind: ASTEngine.SymbolKind.Variable,
959
+ // conservative; actual kind varies
960
+ range: { startLine: 1, startColumn: 0, endLine: 1, endColumn: 0, startIndex: 0, endIndex: 0 },
961
+ scope: "",
962
+ visibility: "public",
963
+ complexity: 1,
964
+ metadata: {
965
+ isScssVariable: true,
966
+ scssVariableName: qualifiedName,
967
+ scssNamespace: namespace,
968
+ scssImportedFrom: modulePath
969
+ }
970
+ });
971
+ }
972
+ }
973
+ // ── `@mixin` / `@function` ──────────────────────────────────────────────────
974
+ processCallable(node, ctx) {
975
+ const raw = node.type === "function_statement" ? this.childForField(node, "name") : this.childForField(node, "name");
976
+ const nameNode = raw ?? this.findChild(node, (c) => c.type === "identifier");
977
+ if (!nameNode) return;
978
+ const bare = stripAllQuotes(nameNode.text);
979
+ const published = node.type === "function_statement" ? functionName(bare) : mixinName(bare);
980
+ const parameters = this.readParameters(node);
981
+ this.publishOnce(ctx, {
982
+ name: published,
983
+ kind: ASTEngine.SymbolKind.Function,
984
+ range: this.nodeToRange(node),
985
+ scope: "",
986
+ visibility: "public",
987
+ // one step for the declaration, one per parameter, one for the body
988
+ complexity: 1 + parameters.length + (this.findChild(node, (c) => c.type === "block") ? 1 : 0),
989
+ ...parameters.length > 0 ? { parameters } : {},
990
+ metadata: {
991
+ isAtRule: true,
992
+ atKeyword: node.type === "function_statement" ? "@function" : "@mixin",
993
+ scssCallable: true,
994
+ ...node.type === "function_statement" ? { isScssFunction: true } : { isScssMixin: true },
995
+ parameterCount: parameters.length,
996
+ // whether the body returns a value — see the `@return` note above for why
997
+ // this is a flag and not an edge
998
+ ...this.findDescendant(node, (n) => n.type === "return_statement") ? { scssReturns: true } : {}
999
+ }
1000
+ });
1001
+ }
1002
+ /**
1003
+ * A mixin's or function's parameters, read from the `parameter` nodes.
1004
+ *
1005
+ * `defaultValue` is taken from the `default` FIELD rather than by slicing text, so
1006
+ * a parameter whose default is itself an expression keeps the expression rather than
1007
+ * a truncation of it.
1008
+ */
1009
+ // typed NON-optional on purpose: `UniversalSymbol["parameters"]` is
1010
+ // `Parameter[] | undefined`, so returning it made every `.length` below a
1011
+ // possible-undefined error and the fix would have been three `!` assertions. The
1012
+ // empty case is a real empty list, not an absent one.
1013
+ readParameters(node) {
1014
+ const paramsNode = this.findChild(node, (c) => c.type === "parameters");
1015
+ if (!paramsNode) return [];
1016
+ const out = [];
1017
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
1018
+ const p = paramsNode.namedChild(i);
1019
+ if (!p || p.type !== "parameter") continue;
1020
+ const varNode = this.findChild(p, (c) => c.type === "variable");
1021
+ if (!varNode) continue;
1022
+ const dflt = this.childForField(p, "default");
1023
+ out.push({
1024
+ name: stripAllQuotes(varNode.text),
1025
+ ...dflt ? { defaultValue: stripAllQuotes(dflt.text) } : {}
1026
+ });
1027
+ }
1028
+ return out;
1029
+ }
1030
+ // ── `@include` ──────────────────────────────────────────────────────────────
1031
+ /**
1032
+ * `@include` — A MIXIN COMPOSITION, PUBLISHED AS `mixes_in`.
1033
+ *
1034
+ * THE CONSTRUCT IS THE VOCABULARY'S MIXIN APPLICATION: a rule (or the module, for a top-level
1035
+ * include) composes a named mixin's body into itself. It is NOT `extends` (there is no
1036
+ * inheritance clause), NOT `implements` (no conformance is declared) and NOT `calls` (the
1037
+ * ontology's own measured population for this type is a composition row — ruby relabelled its
1038
+ * include/extend/prepend from `calls` to `mixes_in` for exactly this reason, and php's trait-use
1039
+ * rows are the second precedent). `@extend` is this lane's `extends`; conflict rules keep the
1040
+ * two apart.
1041
+ *
1042
+ * THE TARGET IS THE WRITTEN MIXIN NAME IN THE LANE'S PUBLISHED SPELLING, `@mixin <name>`, for
1043
+ * EVERY clean include — not only the ones this file declares. The old version published a `calls`
1044
+ * row only when the callee resolved IN THE FILE, which measured 48 of 132 clean includes on a
1045
+ * 247-file screen: an include of a mixin that `@use`/`@import` brought in produced NOTHING, and
1046
+ * the relation was invisible for 64% of the construct's sites. A target that names another
1047
+ * file's mixin is UNRESOLVED rather than absent — the arbiter resolves it through the project
1048
+ * index the way it resolves an `imports` specifier, and the same-file case is respelled to
1049
+ * `<scope>::@mixin <name>` by `publishScssFileScopedIdentities`.
1050
+ *
1051
+ * THE NAMESPACED FORM IS A MEASURED WALL, NOT AN EDGE WE CAN NAME. The served build splits
1052
+ * `@include ns.mixin` at the dot — the `include_statement` closes with a MISSING `;` and the
1053
+ * dotted name lands in an ERROR/selector sibling — so the MIXIN name is not a node this walk can
1054
+ * read, and publishing `@mixin <namespace>` would be a FABRICATED target. This case returns and
1055
+ * the site is COUNTED by `script/m14/scss-extended-census.ts` (`includeUnparsable`), with R2 (a
1056
+ * grammar patch to accept a dotted callable path) named as the available remedy and NOT taken
1057
+ * this round.
1058
+ */
1059
+ processInclude(node, ctx) {
1060
+ const includeText = String(node.text ?? "");
1061
+ const complete = includeStatementIsComplete(includeText);
1062
+ if (!complete) return;
1063
+ const nameNode = this.findChild(node, (c) => c.type === "identifier" || c.type === "plain_value");
1064
+ if (!nameNode) return;
1065
+ const bare = stripAllQuotes(nameNode.text).trim();
1066
+ if (bare.length === 0 || bare.includes(".")) return;
1067
+ const owner = this.ownerForNode(node, ctx) || ctx.moduleName;
1068
+ if (!owner) return;
1069
+ if (!ctx.mixinNames[bare] && ctx.functionNames[bare]) {
1070
+ ctx.relationships.push({
1071
+ source: owner,
1072
+ target: ctx.functionNames[bare],
1073
+ type: ASTEngine.RelationshipType.Calls,
1074
+ line_number: this.lineOf(nameNode),
1075
+ metadata: { evidenceClass: "usage_contextual", cssSpecific: true, isScssInclude: true }
1076
+ });
1077
+ return;
1078
+ }
1079
+ const sameFileMixin = ctx.mixinNames[bare] !== void 0;
1080
+ ctx.relationships.push({
1081
+ source: owner,
1082
+ target: mixinName(bare),
1083
+ type: ASTEngine.RelationshipType.MixesIn,
1084
+ line_number: this.lineOf(nameNode),
1085
+ // `structural`: the relation is a syntactic declaration-header construct (the keyword plus
1086
+ // the written name), not a name-resolution guess. The tier follows the CONSTRUCT, the same
1087
+ // rule php recorded when its trait-use rows moved off `usage_contextual`.
1088
+ metadata: {
1089
+ evidenceClass: "structural",
1090
+ cssSpecific: true,
1091
+ isScssInclude: true,
1092
+ mixinComposition: true,
1093
+ ...sameFileMixin ? {} : { mixinDeclaredInFile: false }
1094
+ }
1095
+ });
1096
+ }
1097
+ /**
1098
+ * RECORD the specifiers whose `@import` shape is the Sass FILE-INCLUSION form.
1099
+ *
1100
+ * THE SHAPE IS THE WHOLE DISCRIMINATOR, and it is a tree fact rather than a string guess:
1101
+ * `@import "x";` → import_statement with EXACTLY ONE named child, a `string_value`
1102
+ * `@import url("x.css")` → the child is a `call_expression` (stylesheet load)
1103
+ * `@import "x" screen` → a second named child (the media query) (stylesheet load)
1104
+ * `@import "x.css";` → one string_value whose TEXT is a CSS file (stylesheet load)
1105
+ * so the Sass form is: one string_value child AND a specifier that is not a `.css` file, not a
1106
+ * protocol/`//` URL, not `sass:` (built-in module) and not `data:`. The relabel itself runs after
1107
+ * the delegate's file-claim pass; this method only collects the set.
1108
+ */
1109
+ processSassPartImport(node, ctx) {
1110
+ const named = [];
1111
+ for (let i = 0; i < node.namedChildCount; i++) {
1112
+ const c = node.namedChild(i);
1113
+ if (c) named.push(c);
1114
+ }
1115
+ if (named.length !== 1 || named[0].type !== "string_value") return;
1116
+ const spec = stripAllQuotes(named[0].text).trim();
1117
+ if (spec.length === 0) return;
1118
+ if (hasCssFileExtension(spec)) return;
1119
+ const lower = spec.toLowerCase();
1120
+ if (lower.startsWith("//") || lower.startsWith("http://") || lower.startsWith("https://")) return;
1121
+ if (lower.startsWith("sass:") || lower.startsWith("data:")) return;
1122
+ ctx.sassPartImports.add(spec);
1123
+ }
1124
+ /**
1125
+ * MOVE the Sass-form `imports` rows to `part_of`. A LABEL MOVE, NOT A GAIN OR A LOSS: the row's
1126
+ * source, target, line and the delegate's `metadata.importProjectFile` claim are unchanged; only
1127
+ * the type moves, plus the mandatory `partRole` and the structural evidence class. The fixture
1128
+ * suite pins the move in both directions and the census counts the sites, so a reader can check
1129
+ * that nothing vanished.
1130
+ */
1131
+ relabelSassPartImports(ctx) {
1132
+ if (ctx.sassPartImports.size === 0) return;
1133
+ for (const r of ctx.relationships) {
1134
+ if (r.type !== ASTEngine.RelationshipType.Imports) continue;
1135
+ const target = String(r.target ?? "");
1136
+ if (!ctx.sassPartImports.has(target)) continue;
1137
+ r.type = ASTEngine.RelationshipType.PartOf;
1138
+ r.metadata = {
1139
+ ...r.metadata ?? {},
1140
+ // MANDATORY ON EVERY part_of ROW (the vocabulary contract): this file is the LIBRARY —
1141
+ // it carries the directive and names the part it includes.
1142
+ partRole: "library",
1143
+ isSassPartImport: true,
1144
+ evidenceClass: "structural"
1145
+ };
1146
+ }
1147
+ }
1148
+ // ── `@return`: a FACT ABOUT A FUNCTION, NOT AN EDGE ─────────────────────────
1149
+ //
1150
+ // There is deliberately no `@return` handling here. The first version emitted
1151
+ // `contains` from the function to ITSELF, which is a self-edge: it makes a
1152
+ // relationship count look like evidence while connecting nothing, and `contains`
1153
+ // between a thing and itself is not a claim any consumer can use.
1154
+ //
1155
+ // A `@return` names no entity. It says the function produces a value, which is a
1156
+ // property of the function, so it is recorded as one — see `scssReturns` on the
1157
+ // function symbol in `processCallable`, which is where it belongs.
1158
+ // ── `@extend` ───────────────────────────────────────────────────────────────
1159
+ processExtend(node, ctx) {
1160
+ this.emitExtend(node, node, node, ctx, false);
1161
+ }
1162
+ /**
1163
+ * The `@extend` INSIDE A RULE BODY, which is 63 of the 99 `@extend` statements in the
1164
+ * corpus and every one of them was invisible.
1165
+ *
1166
+ * This is a grammar fact and it is worth stating exactly, because it is not a defect
1167
+ * in the analyzer's walk. The `extend_statement` RULE is written for a top-level
1168
+ * `@extend`; nested inside a `block` the same text parses as
1169
+ *
1170
+ * block > ERROR > "@extend", placeholder, ";", ERROR("or", ":", …)
1171
+ *
1172
+ * — so the statement's own node type never appears, the keyword survives only as an
1173
+ * ANONYMOUS token, and the walk (which visits named children) never sees either.
1174
+ * Measured: 125 `@extend` tokens across 4,394 admitted files and only 36
1175
+ * `extend_statement` nodes, so 89 of them — 71% — are in the recovered shape and 63 of
1176
+ * the 99 real `@extend`s (`.card { @extend %badge; }`, which is where a BEM codebase
1177
+ * puts nearly all of them) were producing no edge at all.
1178
+ *
1179
+ * The recognition is by the KEYWORD, not by shape: the first non-whitespace child is
1180
+ * the anonymous token `@extend`. Checking for "an ERROR with a selector in it" would
1181
+ * fire on every broken declaration, and checking node types would check for the type
1182
+ * that is absent.
1183
+ *
1184
+ * The edge is marked `scssRecoveredFromError` because it is read out of a region the
1185
+ * grammar gave up on, and the rest of this lane marks recovered claims the same way
1186
+ * (see `emitVariableReadEdges`). Both ends still resolve: the owner is a rule this
1187
+ * file publishes and the target is a selector this file publishes.
1188
+ */
1189
+ processRecoveredExtend(node, ctx) {
1190
+ if (!this.isErrorNode(node)) return;
1191
+ let keywordSeen = false;
1192
+ for (let i = 0; i < node.childCount; i++) {
1193
+ const c = node.child(i);
1194
+ if (!c) continue;
1195
+ if (c.type === "@extend") {
1196
+ keywordSeen = true;
1197
+ break;
1198
+ }
1199
+ if (!c.isNamed || c.type === "comment") continue;
1200
+ return;
1201
+ }
1202
+ if (!keywordSeen) return;
1203
+ let ruleAncestor = null;
1204
+ for (let p = node.parent; p; p = p.parent) {
1205
+ if (p.type === "rule_set") {
1206
+ ruleAncestor = p;
1207
+ break;
1208
+ }
1209
+ }
1210
+ const ownerNode = ruleAncestor ?? node;
1211
+ const lineNode = node;
1212
+ this.emitExtend(node, ownerNode, lineNode, ctx, true);
1213
+ }
1214
+ emitExtend(targetNode, ownerNode, lineNode, ctx, recovered) {
1215
+ const found = this.extendedSelectorName(targetNode);
1216
+ if (!found) return;
1217
+ const target = found.name;
1218
+ const owner = this.ownerForNode(ownerNode, ctx);
1219
+ if (!owner) return;
1220
+ ctx.relationships.push({
1221
+ source: owner,
1222
+ target,
1223
+ type: ASTEngine.RelationshipType.Extends,
1224
+ line_number: this.lineOf(lineNode),
1225
+ metadata: {
1226
+ evidenceClass: "structural",
1227
+ cssSpecific: true,
1228
+ isScssExtend: true,
1229
+ ...recovered ? { scssRecoveredFromError: true, scssExtendShape: "recovered" } : {},
1230
+ // The TARGET was read out of a node the grammar built across a damaged region, not
1231
+ // off the selector the statement names. That is a different claim from the statement
1232
+ // having been recovered, and the lane already marks the statement, so it marks this
1233
+ // too rather than letting a consumer read a repaired edge as a clean one. It is not
1234
+ // a confidence reduction: the referent is a `placeholder` node the parser produced,
1235
+ // which is a parse fact either way, and the contract asks for a confidence consistent
1236
+ // with how the edge was DERIVED, not with how much text was damaged around it.
1237
+ //
1238
+ // It also makes the pre-fix defect reproducible from the RESULT, which is what lets
1239
+ // `scss/extend-target-carries-statement-delimiter` be a faithful mutant. Without the
1240
+ // mark, a mutant keyed on "target carries a delimiter" can never fire — the fixed
1241
+ // analyzer emits none — and a mutant that cannot fire is a green that means nothing.
1242
+ ...found.recoveredTarget ? { scssExtendTargetRecoveredFromBrokenNode: true } : {}
1243
+ }
1244
+ });
1245
+ }
1246
+ /**
1247
+ * The selector an `@extend` names, in the spelling the lane publishes selectors.
1248
+ *
1249
+ * Built by SLICING the node's own text rather than by assembling it from node types,
1250
+ * for the reason the CSS lane gives for at-rule preludes: a type list has to be
1251
+ * maintained against the grammar by hand and goes stale the moment the grammar moves.
1252
+ * `.btn` comes out as `.btn`, which is the class symbol the same file publishes.
1253
+ */
1254
+ extendedSelectorName(node) {
1255
+ const candidates = this.descendants(
1256
+ node,
1257
+ (n) => n.type === "class_selector" || n.type === "id_selector" || n.type === "plain_value" || n.type === "placeholder"
1258
+ );
1259
+ if (candidates.length === 0) return void 0;
1260
+ const sel = candidates[candidates.length - 1];
1261
+ const bound = sel.endIndex;
1262
+ const name = collapseWhitespace(sel.text);
1263
+ let recoveredTarget = false;
1264
+ if (containsStatementDelimiter(name)) {
1265
+ const inner = this.descendants(
1266
+ sel,
1267
+ (n) => n.startIndex < bound && n.type === "placeholder" && isReadablePlaceholder(collapseWhitespace(n.text))
1268
+ );
1269
+ if (inner.length > 0) return { name: collapseWhitespace(inner[0].text), recoveredTarget: true };
1270
+ return void 0;
1271
+ }
1272
+ if (name.startsWith("%") && !isReadablePlaceholder(name)) return void 0;
1273
+ const hasPlaceholder = candidates.some((c) => c.type === "placeholder");
1274
+ const hasClassSelector = candidates.some((c) => c.type === "class_selector" || c.type === "id_selector" || c.type === "plain_value");
1275
+ if (!hasPlaceholder && hasClassSelector && (sel.type === "class_selector" || sel.type === "id_selector" || sel.type === "plain_value")) {
1276
+ const extendKeywordEnd = node.text?.indexOf("@extend") + 7;
1277
+ if (extendKeywordEnd > 0 && sel.startIndex > node.startIndex + extendKeywordEnd + 2) {
1278
+ return void 0;
1279
+ }
1280
+ }
1281
+ if (sel.type === "plain_value" || sel.type === "variable") {
1282
+ for (let p = sel.parent; p && p !== node; p = p.parent) {
1283
+ if (p.type === "interpolation") {
1284
+ return void 0;
1285
+ }
1286
+ }
1287
+ }
1288
+ if (candidates.length > 1) {
1289
+ let shallowest = candidates[0];
1290
+ let minDepth = 0;
1291
+ for (const c of candidates) {
1292
+ let depth = 0;
1293
+ for (let p = c.parent; p && p !== node; p = p.parent) depth++;
1294
+ if (depth < minDepth || minDepth === 0) {
1295
+ minDepth = depth;
1296
+ shallowest = c;
1297
+ }
1298
+ }
1299
+ const shallowName = collapseWhitespace(shallowest.text);
1300
+ if (containsStatementDelimiter(shallowName)) recoveredTarget = true;
1301
+ }
1302
+ return name.length > 0 ? { name, recoveredTarget } : void 0;
1303
+ }
1304
+ // ── `@if` / `@each` / `@for` / `@while` ──────────────────────────────────────
1305
+ /**
1306
+ * A control-flow construct is an `interface` — the same shape the CSS lane gives
1307
+ * `@supports`, and for the same reason: it is a CONDITIONAL SCOPE that publishes
1308
+ * symbols and attributes rules to them, not a declaration of a callable.
1309
+ *
1310
+ * The scope is named from the construct's own text, collapsed to one line so a
1311
+ * multi-line `@each` still yields a stable symbol name (M60 R10 — a target must be
1312
+ * spelled the way the lane spells its own symbols, deterministically).
1313
+ */
1314
+ processControlFlow(node, ctx, owner) {
1315
+ const keyword = node.type.replace("_statement", "");
1316
+ const raw = collapseWhitespace(node.text.split("{")[0] ?? node.text);
1317
+ const atKeyword = `@${keyword}`;
1318
+ const prelude = raw.startsWith(atKeyword) ? raw.slice(atKeyword.length).trim() : raw;
1319
+ const name = `${atKeyword} ${prelude}`;
1320
+ if (name.length <= `@${keyword} `.length) return;
1321
+ this.publishOnce(
1322
+ ctx,
1323
+ {
1324
+ name,
1325
+ kind: ASTEngine.SymbolKind.Interface,
1326
+ range: this.nodeToRange(node),
1327
+ scope: "",
1328
+ visibility: "public",
1329
+ // one step for the condition, one for each loop/branch variable, one for the body
1330
+ complexity: 1 + this.countDescendantTypes(node, /* @__PURE__ */ new Set(["variable", "block"])),
1331
+ metadata: {
1332
+ isAtRule: true,
1333
+ atKeyword: `@${keyword}`,
1334
+ isScssControlFlow: true,
1335
+ // the loop variables are recorded by name: `@each $k, $v in $map` is the one
1336
+ // place a stylesheet says what it is iterating, and dropping it loses the only
1337
+ // statement of intent the construct makes
1338
+ ...node.type === "each_statement" ? {
1339
+ scssEachKey: this.fieldText(node, "key"),
1340
+ scssEachValue: this.fieldText(node, "value")
1341
+ } : {},
1342
+ ...node.type === "for_statement" ? { scssForFrom: this.fieldText(node, "from"), scssForThrough: this.fieldText(node, "through") } : {},
1343
+ ...node.type === "if_statement" ? { scssCondition: this.fieldText(node, "condition") } : {},
1344
+ ...owner ? { scssOwner: owner } : {}
1345
+ }
1346
+ },
1347
+ // owner AND line: see the note on `publishOnce`
1348
+ `${owner}@${this.lineOf(node) ?? 0}`
1349
+ );
1350
+ }
1351
+ fieldText(node, field) {
1352
+ const c = this.childForField(node, field);
1353
+ return c ? stripAllQuotes(c.text) : void 0;
1354
+ }
1355
+ countDescendantTypes(node, types) {
1356
+ let n = 0;
1357
+ const stack = [node];
1358
+ while (stack.length > 0) {
1359
+ const cur = stack.pop();
1360
+ if (types.has(cur.type)) n++;
1361
+ for (let i = 0; i < cur.namedChildCount; i++) {
1362
+ const c = cur.namedChild(i);
1363
+ if (c) stack.push(c);
1364
+ }
1365
+ }
1366
+ return n;
1367
+ }
1368
+ // ── declarations: `$var: value` and `$var` reads ─────────────────────────────
1369
+ /**
1370
+ * A `$var: value` declaration publishes a `variable` symbol, deduplicated by name.
1371
+ *
1372
+ * The CSS lane's identity ruling applies unchanged, and `.scss` is where it matters
1373
+ * most: a Sass variable is assigned in several places by design — a `!default` at the
1374
+ * top and an override inside a theme — and publishing one symbol per assignment is
1375
+ * the identity collision this project has spent the wave removing.
1376
+ *
1377
+ * The FIRST occurrence's range wins, so the symbol points at where the variable is
1378
+ * introduced rather than at its last override.
1379
+ */
1380
+ processScssDeclaration(node, ctx) {
1381
+ const prop = this.findChild(node, (c) => c.type === "property_name");
1382
+ if (!prop) return;
1383
+ const name = prop.text;
1384
+ if (!name.startsWith("$")) return;
1385
+ if (!ctx.variableNames[name]) ctx.variableNames[name] = name;
1386
+ this.emitGlobalOverride(node, name, ctx);
1387
+ const owner = this.ownerForNode(node, ctx);
1388
+ this.publishOnce(ctx, {
1389
+ name,
1390
+ kind: ASTEngine.SymbolKind.Variable,
1391
+ range: this.nodeToRange(node),
1392
+ scope: owner,
1393
+ visibility: "public",
1394
+ complexity: 1 + this.countDescendantTypes(node, /* @__PURE__ */ new Set(["binary_expression", "call_expression", "map", "list"])),
1395
+ metadata: {
1396
+ isScssVariable: true,
1397
+ scssVariableName: name,
1398
+ ...owner ? { scssOwner: owner } : {},
1399
+ // `!default` and `!global` are the two flags that change what an assignment
1400
+ // MEANS — a default is overridable, a global assignment leaks out of its
1401
+ // scope — so they are recorded rather than parsed away.
1402
+ ...this.hasVisibilityFlag(node, "default") ? { scssFlagDefault: true } : {},
1403
+ ...this.hasVisibilityFlag(node, "global") ? { scssFlagGlobal: true } : {}
1404
+ }
1405
+ });
1406
+ }
1407
+ /**
1408
+ * Whether a declaration carries `!default` or `!global`.
1409
+ *
1410
+ * The walk uses `childCount`/`child()`, NOT `namedChildCount`/`namedChild()`,
1411
+ * because the flag is TWO ANONYMOUS NODES — the tree is
1412
+ * `declaration > integer_value, "!", "default", ";"` with `!` and `default` both
1413
+ * unnamed. The first version walked named children only, so it could never see
1414
+ * them, the flag was silently always false, and 210 `!default` assignments in the
1415
+ * corpus were recorded as carrying no flag at all.
1416
+ *
1417
+ * Found by `variables-flags-and-identity`, which is the whole argument for writing
1418
+ * a fixture's expectations from the CONTRACT: the analyzer agreed with itself the
1419
+ * entire time, because nothing ever asked.
1420
+ */
1421
+ hasVisibilityFlag(node, flag) {
1422
+ const stack = [node];
1423
+ while (stack.length > 0) {
1424
+ const n = stack.pop();
1425
+ if (!n.isNamed && n.type === flag) return true;
1426
+ for (let i = 0; i < n.childCount; i++) {
1427
+ const c = n.child(i);
1428
+ if (c) stack.push(c);
1429
+ }
1430
+ }
1431
+ return false;
1432
+ }
1433
+ /**
1434
+ * A `$var` READ becomes a `references` edge — the same class the CSS lane uses for
1435
+ * `var(--x)`, and the same discipline: only variables THIS FILE declares get an
1436
+ * edge, because a `$var` may arrive as a mixin parameter and inventing an endpoint
1437
+ * for it is the phantom this lane removed once.
1438
+ *
1439
+ * The read is found by DESCENDANT walk from each declaration, so a read inside a
1440
+ * nested value, a `calc()`, or an interpolated selector is found too.
1441
+ */
1442
+ emitVariableReadEdges(node, ctx, outerRecovered = false) {
1443
+ const NESTED_STATEMENT = /* @__PURE__ */ new Set([
1444
+ "declaration",
1445
+ "block",
1446
+ "rule_set",
1447
+ "mixin_statement",
1448
+ "function_statement",
1449
+ "if_statement",
1450
+ "each_statement",
1451
+ "for_statement",
1452
+ "while_statement",
1453
+ "include_statement",
1454
+ "use_statement",
1455
+ "forward_statement",
1456
+ "return_statement",
1457
+ "extend_statement"
1458
+ ]);
1459
+ const stack = [{ node, recovered: outerRecovered }];
1460
+ while (stack.length > 0) {
1461
+ const { node: n, recovered } = stack.pop();
1462
+ if (this.isErrorNode(n)) {
1463
+ for (let i = 0; i < n.namedChildCount; i++) {
1464
+ const c = n.namedChild(i);
1465
+ if (c && !NESTED_STATEMENT.has(c.type)) stack.push({ node: c, recovered: true });
1466
+ }
1467
+ continue;
1468
+ }
1469
+ if (n.type === "variable" && ctx.variableNames[n.text]) {
1470
+ const owner = this.ownerForNode(n, ctx);
1471
+ if (!owner) continue;
1472
+ ctx.relationships.push({
1473
+ source: owner,
1474
+ target: n.text,
1475
+ type: ASTEngine.RelationshipType.References,
1476
+ line_number: this.lineOf(n),
1477
+ metadata: {
1478
+ evidenceClass: "usage_contextual",
1479
+ cssSpecific: true,
1480
+ isScssVarRef: true,
1481
+ ...recovered ? { scssRecoveredFromError: true } : {}
1482
+ }
1483
+ });
1484
+ }
1485
+ if (n.type === "call_expression") {
1486
+ const fn = this.findChild(n, (c) => c.type === "function_name");
1487
+ const declared = fn ? ctx.functionNames[stripAllQuotes(fn.text)] : void 0;
1488
+ if (fn && declared) {
1489
+ const owner = this.ownerForNode(n, ctx);
1490
+ if (owner) {
1491
+ ctx.relationships.push({
1492
+ source: owner,
1493
+ target: declared,
1494
+ type: ASTEngine.RelationshipType.Calls,
1495
+ line_number: this.lineOf(n),
1496
+ metadata: {
1497
+ evidenceClass: "usage_contextual",
1498
+ cssSpecific: true,
1499
+ isScssFunctionCall: true,
1500
+ ...recovered ? { scssRecoveredFromError: true } : {}
1501
+ }
1502
+ });
1503
+ }
1504
+ }
1505
+ }
1506
+ for (let i = 0; i < n.namedChildCount; i++) {
1507
+ const c = n.namedChild(i);
1508
+ if (c && !NESTED_STATEMENT.has(c.type)) stack.push({ node: c, recovered });
1509
+ }
1510
+ }
1511
+ }
1512
+ };
1513
+ export {
1514
+ RealScssAnalyzer
1515
+ };