@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,1759 @@
1
+ import {
2
+ ASTEngine
3
+ } from "./chunk-V44PGPA6.js";
4
+ import "./chunk-BQ3OVQ3P.js";
5
+ import "./chunk-SK6YVHFZ.js";
6
+ import "./chunk-2BSKD7O3.js";
7
+ import "./chunk-KAET47ZL.js";
8
+ import {
9
+ EXTENSION_LANGUAGE
10
+ } from "./chunk-KCDECRJY.js";
11
+ import "./chunk-MLKGABMK.js";
12
+
13
+ // src/ast/real-html-analyzer.ts
14
+ import * as path from "path";
15
+ import { existsSync, statSync } from "fs";
16
+ import { createRequire } from "module";
17
+ var require2 = createRequire(import.meta.url);
18
+ function isHtmlSpace(code) {
19
+ return code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
20
+ }
21
+ function asciiLower(text) {
22
+ for (let i = 0; i < text.length; i++) {
23
+ const c = text.charCodeAt(i);
24
+ if (c >= 65 && c <= 90) return text.slice(0, i) + text.slice(i).toLowerCase();
25
+ }
26
+ return text;
27
+ }
28
+ var NAMED_CHARACTER_REFERENCES = /* @__PURE__ */ new Map([
29
+ ["amp", "&"],
30
+ ["AMP", "&"],
31
+ ["lt", "<"],
32
+ ["LT", "<"],
33
+ ["gt", ">"],
34
+ ["GT", ">"],
35
+ ["quot", '"'],
36
+ ["QUOT", '"'],
37
+ ["apos", "'"],
38
+ ["nbsp", "\xA0"],
39
+ ["copy", "\xA9"],
40
+ ["reg", "\xAE"],
41
+ ["trade", "\u2122"],
42
+ ["hellip", "\u2026"],
43
+ ["mdash", "\u2014"],
44
+ ["ndash", "\u2013"],
45
+ ["lsquo", "\u2018"],
46
+ ["rsquo", "\u2019"],
47
+ ["ldquo", "\u201C"],
48
+ ["rdquo", "\u201D"],
49
+ ["bull", "\u2022"],
50
+ ["middot", "\xB7"],
51
+ ["deg", "\xB0"],
52
+ ["plusmn", "\xB1"],
53
+ ["times", "\xD7"],
54
+ ["divide", "\xF7"],
55
+ ["laquo", "\xAB"],
56
+ ["raquo", "\xBB"],
57
+ ["sect", "\xA7"],
58
+ ["para", "\xB6"],
59
+ ["euro", "\u20AC"],
60
+ ["pound", "\xA3"],
61
+ ["yen", "\xA5"],
62
+ ["cent", "\xA2"],
63
+ ["dagger", "\u2020"],
64
+ ["Dagger", "\u2021"],
65
+ ["permil", "\u2030"],
66
+ ["larr", "\u2190"],
67
+ ["rarr", "\u2192"],
68
+ ["harr", "\u2194"],
69
+ ["darr", "\u2193"],
70
+ ["uarr", "\u2191"]
71
+ ]);
72
+ function isReferenceDigits(text, hex) {
73
+ if (text.length === 0) return false;
74
+ for (let i = 0; i < text.length; i++) {
75
+ const c = text.charCodeAt(i);
76
+ if (c >= 48 && c <= 57) continue;
77
+ if (hex && (c >= 97 && c <= 102 || c >= 65 && c <= 70)) continue;
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ function decodeCharacterReferences(text) {
83
+ if (text.indexOf("&") === -1) return text;
84
+ let out = "";
85
+ let i = 0;
86
+ while (i < text.length) {
87
+ const amp = text.indexOf("&", i);
88
+ if (amp === -1) {
89
+ out += text.slice(i);
90
+ break;
91
+ }
92
+ out += text.slice(i, amp);
93
+ const semi = text.indexOf(";", amp + 1);
94
+ if (semi === -1 || semi - amp > 33) {
95
+ out += "&";
96
+ i = amp + 1;
97
+ continue;
98
+ }
99
+ const body = text.slice(amp + 1, semi);
100
+ if (body.length === 0) {
101
+ out += "&";
102
+ i = amp + 1;
103
+ continue;
104
+ }
105
+ if (body.charCodeAt(0) === 35) {
106
+ const hex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88 || body.charCodeAt(1) === 35 && (body.charCodeAt(2) === 120 || body.charCodeAt(2) === 88);
107
+ const digits = hex ? body.charCodeAt(1) === 35 ? body.slice(3) : body.slice(2) : body.slice(1);
108
+ if (digits.length > 0 && digits.length <= 8 && isReferenceDigits(digits, hex)) {
109
+ const code = hex ? Number.parseInt(digits, 16) : Number.parseInt(digits, 10);
110
+ if (code >= 0 && code <= 1114111 && !(code >= 55296 && code <= 57343)) {
111
+ out += String.fromCodePoint(code);
112
+ i = semi + 1;
113
+ continue;
114
+ }
115
+ }
116
+ out += "&";
117
+ i = amp + 1;
118
+ continue;
119
+ }
120
+ const named = NAMED_CHARACTER_REFERENCES.get(body);
121
+ if (named !== void 0) {
122
+ out += named;
123
+ i = semi + 1;
124
+ continue;
125
+ }
126
+ out += "&";
127
+ i = amp + 1;
128
+ }
129
+ return out;
130
+ }
131
+ function unquote(text) {
132
+ const n = text.length;
133
+ if (n >= 2) {
134
+ const first = text.charCodeAt(0);
135
+ const last = text.charCodeAt(n - 1);
136
+ if ((first === 34 || first === 39) && first === last) return text.slice(1, -1);
137
+ }
138
+ return text;
139
+ }
140
+ function urlScheme(value) {
141
+ const n = value.length;
142
+ for (let i = 0; i < n; i++) {
143
+ const c = value.charCodeAt(i);
144
+ if (c === 58) {
145
+ if (i === 0) return "";
146
+ const f = value.charCodeAt(0);
147
+ const alpha2 = f >= 65 && f <= 90 || f >= 97 && f <= 122;
148
+ return alpha2 ? asciiLower(value.slice(0, i)) : "";
149
+ }
150
+ const alpha = c >= 65 && c <= 90 || c >= 97 && c <= 122;
151
+ const digit = c >= 48 && c <= 57;
152
+ if (!alpha && !digit && c !== 43 && c !== 45 && c !== 46) return "";
153
+ }
154
+ return "";
155
+ }
156
+ function isNetworkScheme(value) {
157
+ const s = urlScheme(value);
158
+ return s === "http" || s === "https" || s === "ftp" || s === "ftps";
159
+ }
160
+ function isNonResourceScheme(value) {
161
+ const s = urlScheme(value);
162
+ if (s === "") return false;
163
+ return !isNetworkScheme(value);
164
+ }
165
+ function isProtocolRelative(value) {
166
+ return value.length >= 2 && value.charCodeAt(0) === 47 && value.charCodeAt(1) === 47;
167
+ }
168
+ function fragmentTarget(value) {
169
+ return value.length > 1 && value.charCodeAt(0) === 35 ? value.slice(1) : "";
170
+ }
171
+ function srcsetCandidates(value) {
172
+ const out = [];
173
+ const n = value.length;
174
+ let i = 0;
175
+ while (i < n) {
176
+ while (i < n && (isHtmlSpace(value.charCodeAt(i)) || value.charCodeAt(i) === 44)) i++;
177
+ if (i >= n) break;
178
+ const urlStart = i;
179
+ while (i < n && !isHtmlSpace(value.charCodeAt(i))) i++;
180
+ const url = value.slice(urlStart, i);
181
+ let trimmed = url;
182
+ while (trimmed.length > 0 && trimmed.charCodeAt(trimmed.length - 1) === 44) {
183
+ trimmed = trimmed.slice(0, -1);
184
+ i++;
185
+ }
186
+ if (trimmed.length > 0) out.push(trimmed);
187
+ let depth = 0;
188
+ while (i < n) {
189
+ const c = value.charCodeAt(i);
190
+ if (c === 40) depth++;
191
+ else if (c === 41 && depth > 0) depth--;
192
+ else if (c === 44 && depth === 0) {
193
+ i++;
194
+ break;
195
+ }
196
+ i++;
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+ function firstToken(text) {
202
+ const n = text.length;
203
+ let start = 0;
204
+ while (start < n && isHtmlSpace(text.charCodeAt(start))) start++;
205
+ if (start >= n) return "";
206
+ let end = start;
207
+ while (end < n && !isHtmlSpace(text.charCodeAt(end))) end++;
208
+ return text.slice(start, end);
209
+ }
210
+ function table(entries) {
211
+ return Object.assign(/* @__PURE__ */ Object.create(null), entries);
212
+ }
213
+ function map() {
214
+ return /* @__PURE__ */ Object.create(null);
215
+ }
216
+ var SEMANTIC_ELEMENTS = table({
217
+ "header": true,
218
+ "nav": true,
219
+ "main": true,
220
+ "section": true,
221
+ "article": true,
222
+ "aside": true,
223
+ "footer": true,
224
+ "h1": true,
225
+ "h2": true,
226
+ "h3": true,
227
+ "h4": true,
228
+ "h5": true,
229
+ "h6": true,
230
+ "figure": true,
231
+ "figcaption": true,
232
+ "time": true,
233
+ "data": true,
234
+ "mark": true,
235
+ "details": true,
236
+ "summary": true,
237
+ "dialog": true,
238
+ "meter": true,
239
+ "progress": true,
240
+ "search": true,
241
+ "address": true,
242
+ "hgroup": true
243
+ });
244
+ var INTERACTIVE_ELEMENTS = table({
245
+ "button": true,
246
+ "input": true,
247
+ "select": true,
248
+ "textarea": true,
249
+ "a": true,
250
+ "details": true,
251
+ "summary": true,
252
+ "dialog": true,
253
+ "menu": true,
254
+ "option": true,
255
+ "optgroup": true,
256
+ "label": true,
257
+ "fieldset": true,
258
+ "output": true,
259
+ "area": true
260
+ });
261
+ var FORM_ELEMENTS = table({
262
+ "form": true,
263
+ "input": true,
264
+ "select": true,
265
+ "textarea": true,
266
+ "button": true,
267
+ "fieldset": true,
268
+ "legend": true,
269
+ "label": true,
270
+ "datalist": true,
271
+ "optgroup": true,
272
+ "option": true,
273
+ "output": true,
274
+ "meter": true,
275
+ "progress": true
276
+ });
277
+ var TEMPLATE_BARRIERS = table({ "template": true, "slot": true });
278
+ var LINK_FETCHABLE_RELS = table({
279
+ "stylesheet": true,
280
+ "icon": true,
281
+ "shortcut icon": true,
282
+ "apple-touch-icon": true,
283
+ "apple-touch-icon-precomposed": true,
284
+ "mask-icon": true,
285
+ "manifest": true,
286
+ "preload": true,
287
+ "prefetch": true,
288
+ "preload-stylesheet": true,
289
+ "modulepreload": true,
290
+ "import": true,
291
+ "subresource": true,
292
+ "alternate": true,
293
+ "canonical": true
294
+ });
295
+ var LINK_RESOURCE_TYPES = table({
296
+ "stylesheet": "stylesheet",
297
+ "icon": "icon",
298
+ "shortcut icon": "icon",
299
+ "apple-touch-icon": "icon",
300
+ "apple-touch-icon-precomposed": "icon",
301
+ "mask-icon": "icon",
302
+ "manifest": "manifest",
303
+ "preload": "preload",
304
+ "modulepreload": "preload",
305
+ "prefetch": "prefetch",
306
+ "import": "import",
307
+ "alternate": "alternate",
308
+ "canonical": "canonical"
309
+ });
310
+ var PARAM_RESOURCE_NAMES = table({
311
+ "movie": true,
312
+ "src": true,
313
+ "url": true,
314
+ "codebase": true,
315
+ "archive": true
316
+ });
317
+ var CONDITIONAL_DIRECTIVE_SUFFIXES = ["if", "else", "elseif", "when", "unless", "show", "test"];
318
+ var REPEAT_DIRECTIVE_SUFFIXES = ["for", "each", "repeat"];
319
+ function spaceSeparatedUrls(value) {
320
+ const out = [];
321
+ let start = 0;
322
+ for (let i = 0; i <= value.length; i++) {
323
+ if (i !== value.length && !isHtmlSpace(value.charCodeAt(i))) continue;
324
+ if (i > start) out.push(value.slice(start, i));
325
+ start = i + 1;
326
+ }
327
+ return out;
328
+ }
329
+ function refreshContentUrl(content) {
330
+ const semi = content.indexOf(";");
331
+ if (semi === -1) return "";
332
+ if (!asciiLower(content.slice(semi + 1).trim()).startsWith("url=")) return "";
333
+ const inner = content.slice(semi + 1).trim().slice(4).trim();
334
+ if (inner.length < 2) return inner;
335
+ const first = inner[0];
336
+ const last = inner[inner.length - 1];
337
+ if ((first === '"' || first === "'") && first === last) return inner.slice(1, -1).trim();
338
+ return inner;
339
+ }
340
+ var RESOURCE_RULES = table({
341
+ script: [
342
+ { attr: "src", resourceType: "script", import: true },
343
+ // SVG2 spells a script's source `href`, not `src`. Inside `<svg>` that is the
344
+ // ONLY spelling there is, so a table with `src` alone dropped every inline SVG
345
+ // script's dependency. The row is keyed on the tag, not the namespace, which
346
+ // is safe because an HTML `<script>` has no `href` to read — the lookup finds
347
+ // no attribute and emits nothing, rather than needing a namespace test the
348
+ // table has no way to express.
349
+ { attr: "href", resourceType: "svg-script", import: true }
350
+ ],
351
+ link: [
352
+ // `rel` decides both WHETHER the href is a document the page fetches and
353
+ // WHICH kind it is: `rel=stylesheet` is a stylesheet, `rel=icon` an icon,
354
+ // and `rel=preconnect`/`dns-prefetch` name a host for a connection warm-up
355
+ // rather than a subresource — the pre-fix analyzer mirrored those into
356
+ // `imports`, so the index claimed a dependency on a URL nothing serves.
357
+ { attr: "href", resourceType: "link", import: true, relFilter: LINK_FETCHABLE_RELS }
358
+ ],
359
+ img: [
360
+ { attr: "src", resourceType: "img", import: true },
361
+ { attr: "srcset", resourceType: "img-srcset", candidates: "srcset", import: true },
362
+ { attr: "longdesc", resourceType: "img-longdesc", import: true }
363
+ ],
364
+ // `<source>` carries BOTH a single `src` and a `srcset` candidate list, and
365
+ // the pre-fix analyzer read only `src` and only when the IMMEDIATE parent was
366
+ // picture/audio/video — so `<video><div><source src=…></div></video>` lost the
367
+ // resource. A candidate's own `media` attribute is the context test, and the
368
+ // ancestor test is now a bounded walk (see `isMediaContext`).
369
+ source: [
370
+ { attr: "src", resourceType: "media-source", import: true },
371
+ { attr: "srcset", resourceType: "img-srcset", candidates: "srcset", import: true }
372
+ ],
373
+ video: [
374
+ { attr: "src", resourceType: "video", import: true },
375
+ { attr: "poster", resourceType: "video-poster", import: true }
376
+ ],
377
+ audio: [{ attr: "src", resourceType: "audio", import: true }],
378
+ track: [{ attr: "src", resourceType: "track", import: true }],
379
+ embed: [{ attr: "src", resourceType: "embed", import: true }],
380
+ // `<object data>` names the resource itself; `<object archive>` names the
381
+ // ADDITIONAL resources needed to instantiate it, and the HTML specification
382
+ // defines it as a SPACE-separated list of URIs — the same shape as `<a ping>`
383
+ // and for the same reason (several per attribute, no descriptor, no comma).
384
+ //
385
+ // It was missing, and the carrier census found it rather than the ontology gate:
386
+ // `script/m14/verify-carrier-census.ts` reports `object@archive` as UNREVIEWED at
387
+ // 4 occurrences in 2 documents, and a carrier nobody has ruled is a carrier
388
+ // nobody has decided about. Measured on the tree over 1,699 corpus documents
389
+ // (aspnetcore/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/
390
+ // RazorWebSite.UrlResolution.Index.html and its .Encoded.html twin):
391
+ //
392
+ // object@archive 4 occurrences, 2 files — 3 real URLs, 1 Razor artifact
393
+ //
394
+ // That is a small population and the verdict does not pretend otherwise; the
395
+ // construct is real, it occurs, and the rule for it is one table row. It is a
396
+ // FETCH like `data`, so it takes the same shape and the same `import` mirror.
397
+ object: [
398
+ { attr: "data", resourceType: "object", import: true },
399
+ { attr: "archive", resourceType: "object-archive", import: true, valueList: "space" }
400
+ ],
401
+ input: [{ attr: "src", resourceType: "input-image", import: true }],
402
+ iframe: [{ attr: "src", resourceType: "iframe", import: true }],
403
+ frame: [{ attr: "src", resourceType: "frame", import: true }],
404
+ // SVG reference carriers. `xlink:href` is the legacy spelling; both are read
405
+ // and the attribute map is already lowercased, so `XLINK:HREF` lands here too.
406
+ use: [
407
+ // `href="#icon"` is the ordinary SVG form and is a same-document reference;
408
+ // `href="sprite.svg#icon"` is an external file and a fetch. Both spellings of
409
+ // the attribute are read, and the attribute map is lowercased, so the legacy
410
+ // `XLINK:HREF` lands on the same rule.
411
+ { attr: "href", resourceType: "svg-use", import: true, fragmentRef: true },
412
+ { attr: "xlink:href", resourceType: "svg-use", import: true, fragmentRef: true }
413
+ ],
414
+ image: [
415
+ { attr: "href", resourceType: "svg-image", import: true, fragmentRef: true },
416
+ { attr: "xlink:href", resourceType: "svg-image", import: true, fragmentRef: true }
417
+ ],
418
+ param: [{ attr: "value", resourceType: "param", import: true, paramNameFilter: PARAM_RESOURCE_NAMES }],
419
+ a: [
420
+ { attr: "href", resourceType: "navigation", link: true, navigation: true, fragmentRef: true },
421
+ // `ping` is a whitespace-separated list of URLs FETCHED when the link is
422
+ // activated. It is a fetch, not a navigation — the browser requests it; the
423
+ // user never goes there — so it takes the resource path and the `import`
424
+ // mirror, and the scheme contract filters an external endpoint.
425
+ { attr: "ping", resourceType: "ping", import: true, valueList: "space" }
426
+ ],
427
+ area: [{ attr: "href", resourceType: "navigation", link: true, navigation: true, fragmentRef: true }],
428
+ form: [{ attr: "action", resourceType: "form-action", link: true }],
429
+ button: [{ attr: "formaction", resourceType: "form-action", link: true }],
430
+ html: [{ attr: "manifest", resourceType: "manifest", import: true }],
431
+ meta: [
432
+ // The refresh PRAGMA. `content` is a URL only under `http-equiv=refresh`,
433
+ // and the gate is what keeps `charset`/`description`/viewport text from
434
+ // becoming an edge to a sentence. 40 occurrences across 26 files in one
435
+ // corpus, and entirely absent from the table until a carrier census looked
436
+ // for it rather than for what the table already knew.
437
+ { attr: "content", resourceType: "meta-refresh", link: true, navigation: true, httpEquiv: "refresh", contentUrl: true }
438
+ ],
439
+ // SVG filter reference. `feImage` pulls an image to paint INTO the filter
440
+ // result, which is a fetch like any other; the tag is lowercased by the
441
+ // walker, so the key is `feimage`.
442
+ feimage: [
443
+ { attr: "href", resourceType: "svg-filter-image", import: true },
444
+ { attr: "xlink:href", resourceType: "svg-filter-image", import: true }
445
+ ],
446
+ // `cite` names where a quotation or a change came FROM. Nothing fetches it and
447
+ // nothing navigates to it, but it IS a written reference out of the document,
448
+ // so it is an edge — recorded as a usage-contextual reference rather than as
449
+ // an import, because grading a citation as a structural fetch overstates it.
450
+ // Four elements carry it, and all four are in the table rather than one, since
451
+ // the ruling is about the attribute's meaning and not about any single tag.
452
+ q: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
453
+ blockquote: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
454
+ del: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
455
+ ins: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
456
+ // Legacy presentational background images. Deprecated in HTML5 and still
457
+ // fetched by every shipping browser, so the dependency is real; the
458
+ // `legacy-` resourceType says so at the edge, which is the difference between
459
+ // a recorded fact and an implied one.
460
+ //
461
+ // `td`/`th` ARE THE SAME ATTRIBUTE ON THE SAME SPEC LIST, AND THIS TABLE WAS
462
+ // MISSING THEM (carrier census, 2026-10-28). The legacy `background`
463
+ // attribute is defined on `body`, `table`, `td` and `th`; the two cell
464
+ // elements are not a variant this analyzer invented, they are the other half
465
+ // of the same construct, so `body` and `table` without them described two of
466
+ // four owners. Measured consequence: the census found `td@background`
467
+ // carrying a real image path
468
+ // (`FSharp.Data/tests/FSharp.Data.Tests/Data/MarketDepth.htm`,
469
+ // `/bwx_drop_shadow_900_300.gif`) with no table claiming it, so an edge the
470
+ // browser really fetches was absent from the graph. `th` is added in the same
471
+ // commit rather than after a census reports it: the two cells differ in
472
+ // nothing the attribute is concerned with, and a ruling that names one and
473
+ // not the other is a ruling about the corpus that happened to run.
474
+ body: [{ attr: "background", resourceType: "legacy-background", import: true }],
475
+ table: [{ attr: "background", resourceType: "legacy-background", import: true }],
476
+ td: [{ attr: "background", resourceType: "legacy-background", import: true }],
477
+ th: [{ attr: "background", resourceType: "legacy-background", import: true }]
478
+ });
479
+ var FORM_ACTION_INPUT_TYPES = table({ submit: true, image: true });
480
+ var INPUT_FORMACTION_RULE = { attr: "formaction", resourceType: "form-action", link: true };
481
+ var HTML_DOCUMENT_EXTS = new Set(
482
+ Object.keys(EXTENSION_LANGUAGE).filter((ext) => EXTENSION_LANGUAGE[ext] === "html")
483
+ );
484
+ function resourceExtension(value) {
485
+ const clean = value.split("#")[0].split("?")[0];
486
+ const base = clean.slice(clean.lastIndexOf("/") + 1);
487
+ const dot = base.lastIndexOf(".");
488
+ return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
489
+ }
490
+ function rebaseToProject(documentPath, projectRoot, value) {
491
+ const written = value.split("#")[0].split("?")[0];
492
+ if (written.length === 0) return null;
493
+ const abs = written.startsWith("/") ? path.resolve(projectRoot, written.slice(1)) : path.resolve(path.dirname(documentPath), written);
494
+ const rel = path.relative(projectRoot, abs);
495
+ if (rel.length === 0) return null;
496
+ const posix2 = rel.split(path.sep).join("/");
497
+ if (posix2.startsWith("..") || path.posix.isAbsolute(posix2)) return null;
498
+ return posix2;
499
+ }
500
+ var IDREF_RULES = [
501
+ { tag: "", attr: "aria-labelledby", kind: "aria-labelledby", list: true },
502
+ { tag: "", attr: "aria-describedby", kind: "aria-describedby", list: true },
503
+ { tag: "", attr: "aria-details", kind: "aria-details", list: true },
504
+ { tag: "", attr: "aria-errormessage", kind: "aria-errormessage", list: false },
505
+ { tag: "", attr: "aria-controls", kind: "aria-controls", list: true },
506
+ { tag: "", attr: "aria-owns", kind: "aria-owns", list: true },
507
+ { tag: "", attr: "aria-flowto", kind: "aria-flowto", list: true },
508
+ { tag: "", attr: "aria-activedescendant", kind: "aria-activedescendant", list: false },
509
+ { tag: "td", attr: "headers", kind: "table-header", list: true },
510
+ { tag: "th", attr: "headers", kind: "table-header", list: true },
511
+ // The form owner and the datalist an input offers. Both name an element in the
512
+ // same document through its `id`, which is why they are here and not in the
513
+ // resource table: nothing is fetched and nothing is navigated to. Both are
514
+ // SINGLE references, which is why they carry no `idrefList`.
515
+ { tag: "", attr: "form", kind: "form-owner", list: false },
516
+ { tag: "", attr: "list", kind: "datalist", list: false },
517
+ // `<output for>` names the controls whose value the output reports — a
518
+ // space-separated token list, like `headers`.
519
+ { tag: "output", attr: "for", kind: "output-for", list: true }
520
+ ];
521
+ var HEADING_ELEMENTS = table({ h1: true, h2: true, h3: true, h4: true, h5: true, h6: true });
522
+ var DELEGATE_FILE_ACTOR = "(file)";
523
+ var RealHtmlAnalyzer = class {
524
+ /**
525
+ * The embedded-block languages whose delegate analyzer could not be loaded in
526
+ * this process, populated only by `mergeEmbeddedBlocks` and published as
527
+ * `metadata.unavailableEmbeddedLangs`. Non-empty means: this document HAS inline
528
+ * blocks and they were NOT analysed. A field rather than a log line, because a
529
+ * consumer that asks "does this file contain code?" needs to be able to tell
530
+ * "no" from "not measured", and only a published field can say that.
531
+ */
532
+ unavailableEmbeddedLangs = [];
533
+ /**
534
+ * THE DOCUMENT AND ITS PROJECT ROOT, for the two claim keys a resource edge can carry.
535
+ *
536
+ * Fields rather than two more parameters on `emitResource`, whose six call sites already
537
+ * thread eight arguments each: the values are properties OF THE ANALYSIS (`filePath` and
538
+ * `context.projectRoot`), they are identical for every emission in one document, and
539
+ * `analyze` is synchronous, so the pair is set once at the top of the walk and read by the
540
+ * helper below. Empty `claimProjectRoot` means the caller did not name a project root — the
541
+ * fixture runner does not — and a project-relative candidate is then NOT stated: an
542
+ * unrooted path would be a claim about a file list the caller never supplied.
543
+ */
544
+ claimDocumentPath = "";
545
+ claimProjectRoot = "";
546
+ /**
547
+ * THE CLAIM A RESOURCE TARGET CAN CARRY — the reason the edge names something this run cannot
548
+ * resolve to a declaration, published on a key the ARBITER reads.
549
+ *
550
+ * THE DEFECT THIS CLOSES, in this lane's own measurement. `incoming-rule.ts` recognises a fixed
551
+ * list of keys (`EXTERNAL_KEYS`, `:56-80`) plus the file/source-path claim kinds, and the lane
552
+ * published `isExternalResource` on its network-URL fetches — a truthful marker on a spelling
553
+ * that is not in that list, so the arbiter read the edge as an unstated hole. `causeOK` was 0
554
+ * fleet-wide for exactly this reason. The fix is not to invent a new marker but to publish the
555
+ * claim in the vocabulary the arbiter already verifies:
556
+ *
557
+ * A NETWORK URL (`https:`, `http:`, `//cdn…`) is outside every admitted file list BY
558
+ * CONSTRUCTION, so the edge carries `sourcePathClaim`. Its polarity is inverted on purpose
559
+ * (`incoming-rule.ts:317-339`): the project REFUTES the claim the moment a candidate IS an
560
+ * admitted file, which is what stops this from becoming a laundering channel.
561
+ *
562
+ * AN HTML DOCUMENT that exists beside this one — `../index.html`, `page.html`, `/guide.html`
563
+ * — carries `importProjectFile`, the FILE claim, and it is the honest one: the project's own
564
+ * admitted list then CONFIRMS the file. MEASURED over the lane's bounded population: of
565
+ * 20,470 such targets, 20,295 name a document that is on disk AND admitted, and the other
566
+ * 175 are DEAD LINKS — nothing on disk — which is why existence is probed before the claim is
567
+ * published. Without that probe the claim would be published on 175 targets that name no file,
568
+ * and the arbiter would count 175 refutations against the lane: a false statement, not a
569
+ * better-looking table.
570
+ *
571
+ * ANYTHING ELSE PATH-SHAPED (`../../styles/main.css`, `/assets/x.js`) carries the same
572
+ * `sourcePathClaim`: the project admits no file answering the written string, which for an
573
+ * extension this lane cannot admit is true by construction.
574
+ *
575
+ * A BARE NAME GETS NOTHING (`details.html` with no file beside it, `main`, `getItem`). The
576
+ * source-path claim is defined only for a target containing `/` or a scheme
577
+ * (`incoming-rule.ts:335-337`), and a bare name is exactly the spelling that can also name a
578
+ * DECLARATION — so publishing a claim on it is the one move that could hide a resolution.
579
+ *
580
+ * EVERY KEY HERE IS ADDITIVE. The target is untouched, no existing metadata key is renamed or
581
+ * removed, and a claim is read by the arbiter only where the target names no declaration
582
+ * (`classifyEdge`), so no edge's resolution can move because of this function.
583
+ */
584
+ resourceClaimMetadata(value, network) {
585
+ if (network) return { sourcePathClaim: [value] };
586
+ if (this.claimProjectRoot.length > 0 && HTML_DOCUMENT_EXTS.has(resourceExtension(value))) {
587
+ const rebased = rebaseToProject(this.claimDocumentPath, this.claimProjectRoot, value);
588
+ const absCandidate = rebased !== null ? path.join(this.claimProjectRoot, rebased) : null;
589
+ if (absCandidate !== null && existsSync(absCandidate)) {
590
+ const size = statSync(absCandidate).size;
591
+ if (size > 0 && size <= 1048576) return { importProjectFile: [rebased] };
592
+ }
593
+ }
594
+ if (urlScheme(value).length > 0) return { sourcePathClaim: [value] };
595
+ return {};
596
+ }
597
+ analyze(rootNode, content, filePath, context) {
598
+ const symbols = [];
599
+ const relationships = [];
600
+ const imports = [];
601
+ const exports = [];
602
+ const errors = [];
603
+ this.claimDocumentPath = filePath;
604
+ this.claimProjectRoot = context?.projectRoot ?? "";
605
+ const ERROR_REPORT_CAP = 50;
606
+ if (rootNode.hasError) {
607
+ const errStack = [rootNode];
608
+ while (errStack.length > 0) {
609
+ const n = errStack.pop();
610
+ if ((n.type === "ERROR" || n.isMissing) && errors.length < ERROR_REPORT_CAP) {
611
+ errors.push({
612
+ message: `Syntax ${n.type} detected. The Brain Engine is bypassing this broken node.`,
613
+ range: this.nodeToRange(n),
614
+ severity: "warning"
615
+ });
616
+ }
617
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
618
+ const child = n.namedChild(i);
619
+ if (child) errStack.push(child);
620
+ }
621
+ }
622
+ }
623
+ const moduleName = path.basename(filePath, path.extname(filePath)) || "document";
624
+ const documentRange = this.nodeToRange(rootNode);
625
+ const usedNames = map();
626
+ usedNames[moduleName] = 1;
627
+ symbols.push({
628
+ name: moduleName,
629
+ kind: ASTEngine.SymbolKind.Module,
630
+ range: documentRange,
631
+ scope: "",
632
+ visibility: "public",
633
+ complexity: 0,
634
+ // filled with the addressable-descendant count below
635
+ metadata: {
636
+ htmlSpecific: true,
637
+ isDocument: true,
638
+ doctype: this.findDoctype(rootNode),
639
+ lang: this.documentLanguage(rootNode)
640
+ }
641
+ });
642
+ const handleToSymbol = map();
643
+ const ambiguousHandles = map();
644
+ const deferred = [];
645
+ let totalElements = 0;
646
+ let semanticCount = 0;
647
+ let interactiveCount = 0;
648
+ let formElementCount = 0;
649
+ let ariaCount = 0;
650
+ let headingCount = 0;
651
+ let headingLevelSkips = 0;
652
+ let imgCount = 0;
653
+ let imgWithAltCount = 0;
654
+ let iframeWithoutLoadingCount = 0;
655
+ const counters = { externalLinkCount: 0, internalLinkCount: 0, labelWithoutFor: 0, inputWithoutLabel: 0 };
656
+ let baseHrefCount = 0;
657
+ let branchCount = 0;
658
+ let maxDepth = 0;
659
+ let addressableCount = 0;
660
+ let hasForm = false;
661
+ let hasExternals = false;
662
+ let customElementCount = 0;
663
+ let templateFragmentCount = 0;
664
+ let lastHeadingLevel = 0;
665
+ const frameworks = table({ HTML5: true });
666
+ const insights = map();
667
+ const stack = [{ node: rootNode, nearestNamed: moduleName, depth: 0 }];
668
+ while (stack.length > 0) {
669
+ const frame = stack.pop();
670
+ const node = frame.node;
671
+ if (node.type === "element" || node.type === "script_element" || node.type === "style_element") {
672
+ totalElements++;
673
+ if (frame.depth > maxDepth) maxDepth = frame.depth;
674
+ const tagNameNode = this.getElementTagName(node);
675
+ if (tagNameNode) {
676
+ const tagName = asciiLower(tagNameNode.text);
677
+ const { attrs, lines } = this.extractElementAttributes(node);
678
+ const startTag = this.getStartTag(node);
679
+ const tagLine = startTag ? startTag.startPosition.row + 1 : node.startPosition.row + 1;
680
+ const lineOf = (attr) => lines[attr] ?? tagLine;
681
+ const isSemantic = !!SEMANTIC_ELEMENTS[tagName];
682
+ const isInteractive = !!INTERACTIVE_ELEMENTS[tagName];
683
+ const isForm = !!FORM_ELEMENTS[tagName];
684
+ const isBarrier = !!TEMPLATE_BARRIERS[tagName];
685
+ if (isSemantic) semanticCount++;
686
+ if (isInteractive) interactiveCount++;
687
+ if (isForm) {
688
+ hasForm = true;
689
+ formElementCount++;
690
+ }
691
+ if (attrs["aria-label"] || attrs["role"] || attrs["aria-labelledby"] || attrs["aria-describedby"]) ariaCount++;
692
+ if (HEADING_ELEMENTS[tagName]) {
693
+ headingCount++;
694
+ const level = Number.parseInt(tagName.slice(1), 10);
695
+ if (lastHeadingLevel !== 0 && level > lastHeadingLevel + 1) headingLevelSkips += level - lastHeadingLevel - 1;
696
+ lastHeadingLevel = level;
697
+ }
698
+ if (tagName.indexOf("-") !== -1 && tagName !== "annotation-xml") {
699
+ customElementCount++;
700
+ insights["Custom-element-shaped tags present (registration is a cross-file fact this analyzer cannot see)"] = true;
701
+ }
702
+ if (this.hasDirective(attrs, CONDITIONAL_DIRECTIVE_SUFFIXES) || this.hasDirective(attrs, REPEAT_DIRECTIVE_SUFFIXES)) branchCount++;
703
+ if (isBarrier) branchCount++;
704
+ if (attrs["media"]) branchCount++;
705
+ if (tagName === "picture") branchCount++;
706
+ const rawId = attrs["id"] ?? "";
707
+ const rawName = rawId.length > 0 ? "" : attrs["name"] ?? "";
708
+ const handle = rawId.length > 0 ? rawId : rawName;
709
+ let currentId = frame.nearestNamed;
710
+ if (handle.length > 0) {
711
+ addressableCount++;
712
+ let symbolName = handle;
713
+ const seen = usedNames[handle];
714
+ if (seen === void 0) {
715
+ usedNames[handle] = 1;
716
+ } else {
717
+ let n = seen;
718
+ do {
719
+ n++;
720
+ symbolName = `${handle}#${n}`;
721
+ } while (usedNames[symbolName] !== void 0);
722
+ usedNames[symbolName] = 1;
723
+ usedNames[handle] = n;
724
+ }
725
+ if (handleToSymbol[handle] === void 0) {
726
+ handleToSymbol[handle] = symbolName;
727
+ } else {
728
+ ambiguousHandles[handle] = true;
729
+ }
730
+ currentId = symbolName;
731
+ symbols.push({
732
+ name: symbolName,
733
+ kind: ASTEngine.SymbolKind.Class,
734
+ range: this.nodeToRange(node),
735
+ scope: "",
736
+ // HTML has one scope: a document. There is no private/package/
737
+ // protected, so `public` is the only truthful value here and it is
738
+ // pinned by fixture rather than left as an unexamined default.
739
+ visibility: "public",
740
+ ownerName: frame.nearestNamed,
741
+ ownerKind: frame.nearestNamed === moduleName ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
742
+ complexity: 1,
743
+ metadata: {
744
+ tagName,
745
+ attributes: attrs,
746
+ htmlSpecific: true,
747
+ isAddressable: true,
748
+ identityAttribute: rawId.length > 0 ? "id" : "name",
749
+ isSemantic,
750
+ isInteractive,
751
+ isForm,
752
+ ...rawId.length === 0 && rawName.length > 0 ? { alsoNamed: rawName } : {},
753
+ ...symbolName !== handle ? { duplicateId: true, duplicateOf: handle } : {},
754
+ ...frame.inBarrier ? { inTemplateBarrier: true } : {},
755
+ ...attrs["is"] ? { isCustomElement: attrs["is"] } : {}
756
+ }
757
+ });
758
+ if (frame.nearestNamed !== symbolName) {
759
+ relationships.push({
760
+ source: frame.nearestNamed,
761
+ target: symbolName,
762
+ type: ASTEngine.RelationshipType.Contains,
763
+ confidence: 1,
764
+ line_number: this.nodeToRange(node).startLine,
765
+ metadata: {
766
+ htmlSpecific: true,
767
+ isDOMHierarchy: true,
768
+ evidenceClass: "structural",
769
+ ...frame.nearestNamed === moduleName ? { isDocumentRoot: true } : {}
770
+ }
771
+ });
772
+ }
773
+ }
774
+ const rules = RESOURCE_RULES[tagName];
775
+ if (rules) {
776
+ for (let r = 0; r < rules.length; r++) {
777
+ const rule = rules[r];
778
+ const raw = attrs[rule.attr];
779
+ if (!raw || raw.length === 0) continue;
780
+ let effectiveType = rule.resourceType;
781
+ if (rule.relFilter) {
782
+ const rel = this.fetchableRelToken(attrs["rel"], rule.relFilter);
783
+ if (rel === null) continue;
784
+ effectiveType = LINK_RESOURCE_TYPES[rel] ?? rule.resourceType;
785
+ }
786
+ if (rule.paramNameFilter && !rule.paramNameFilter[asciiLower(attrs["name"] ?? "")]) continue;
787
+ if (rule.httpEquiv !== void 0 && asciiLower(attrs["http-equiv"] ?? "") !== rule.httpEquiv) continue;
788
+ const line = lineOf(rule.attr);
789
+ const owner = currentId;
790
+ const attrRange = this.attributeRange(startTag, rule.attr, node, line);
791
+ if (rule.contentUrl) {
792
+ const inner = refreshContentUrl(decodeCharacterReferences(raw).trim());
793
+ if (inner.length === 0) continue;
794
+ this.countOutcome(
795
+ this.emitResource(inner, owner, rule, line, attrRange, relationships, imports, deferred, true),
796
+ counters
797
+ );
798
+ continue;
799
+ }
800
+ if (rule.valueList === "space") {
801
+ const urls = spaceSeparatedUrls(decodeCharacterReferences(raw));
802
+ for (let u = 0; u < urls.length; u++) {
803
+ this.countOutcome(
804
+ this.emitResource(urls[u], owner, rule, line, attrRange, relationships, imports, deferred),
805
+ counters
806
+ );
807
+ }
808
+ continue;
809
+ }
810
+ if (rule.candidates === "srcset") {
811
+ const candidates = srcsetCandidates(raw);
812
+ for (let c = 0; c < candidates.length; c++) {
813
+ this.countOutcome(
814
+ this.emitResource(candidates[c], owner, rule, line, attrRange, relationships, imports, deferred),
815
+ counters
816
+ );
817
+ }
818
+ continue;
819
+ }
820
+ this.countOutcome(
821
+ this.emitResource(raw, owner, { ...rule, resourceType: effectiveType }, line, attrRange, relationships, imports, deferred),
822
+ counters
823
+ );
824
+ }
825
+ }
826
+ if (tagName === "input" && FORM_ACTION_INPUT_TYPES[asciiLower(attrs["type"] ?? "")]) {
827
+ const fa = attrs["formaction"];
828
+ if (fa && fa.length > 0) {
829
+ this.countOutcome(
830
+ this.emitResource(
831
+ fa,
832
+ currentId,
833
+ INPUT_FORMACTION_RULE,
834
+ lineOf("formaction"),
835
+ this.attributeRange(startTag, "formaction", node, lineOf("formaction")),
836
+ relationships,
837
+ imports,
838
+ deferred
839
+ ),
840
+ counters
841
+ );
842
+ }
843
+ }
844
+ if (tagName === "label") {
845
+ const forAttr = attrs["for"];
846
+ if (forAttr && forAttr.length > 0) {
847
+ deferred.push({
848
+ // THE ELEMENT THAT WRITES THE REFERENCE, not the nearest named
849
+ // ancestor — `currentId` is the label's OWN handle when it is
850
+ // addressable (an `id`/`name`) and the ancestor only when it is not.
851
+ //
852
+ // This branch used `frame.nearestNamed` unconditionally, and it was
853
+ // the ONLY one of the four in-document reference emitters that did:
854
+ // the resource carriers pass `currentId` (`emitResource`'s caller),
855
+ // the IDREF class passes `currentId` (`owner`), the fragment href in
856
+ // `emitResource` passes its caller's `source`, and the lane's own F5
857
+ // oracle `deriveHtmlEdges` passes `next` — which is the element's own
858
+ // symbol name whenever it has one. MEASURED, both documents through
859
+ // `deriveHtmlEdges`:
860
+ //
861
+ // scope-barriers-and-references.html (labels `lbl-later`,
862
+ // `lbl-above`, `lbl-twin`, each with an id):
863
+ // oracle `uses lbl-later -> declared-below`; the analyzer said
864
+ // `uses app -> declared-below` — 3 rows, 100% of that file's
865
+ // label associations, sourced at the wrong actor.
866
+ // idref-carriers.html (`<label id="lbl" for="named-target">`):
867
+ // oracle `uses lbl -> named-target`; analyzer `uses app -> ...`.
868
+ //
869
+ // A label WITHOUT identity is unaffected: `currentId` is then the
870
+ // nearest named ancestor, which is what identity-addressable's
871
+ // `<label for="query">` (no id, inside `#search-form`) pins and what
872
+ // the oracle also derives (`uses search-form -> query`). So this
873
+ // changes the actor only where an actor was available to name, and it
874
+ // is what makes the analyzer agree with its own truth rather than
875
+ // pinning a divergence in two fixtures.
876
+ source: currentId,
877
+ handle: forAttr,
878
+ kind: "label",
879
+ rel: "uses",
880
+ line: lineOf("for")
881
+ });
882
+ } else {
883
+ counters.labelWithoutFor++;
884
+ }
885
+ }
886
+ for (let ir = 0; ir < IDREF_RULES.length; ir++) {
887
+ const rule = IDREF_RULES[ir];
888
+ if (rule.tag.length > 0 && rule.tag !== tagName) continue;
889
+ const raw = attrs[rule.attr];
890
+ if (raw === void 0 || raw.length === 0) continue;
891
+ const line = lineOf(rule.attr);
892
+ const owner = currentId;
893
+ let start = 0;
894
+ for (let i = 0; i <= raw.length; i++) {
895
+ const atEnd = i === raw.length;
896
+ const c = atEnd ? 32 : raw.charCodeAt(i);
897
+ if (!(c === 32 || c === 9 || c === 10 || c === 13 || c === 12)) continue;
898
+ if (i > start) {
899
+ deferred.push({
900
+ source: owner,
901
+ handle: raw.slice(start, i),
902
+ kind: rule.kind,
903
+ rel: "references",
904
+ list: rule.list,
905
+ line
906
+ });
907
+ }
908
+ start = i + 1;
909
+ }
910
+ }
911
+ if (tagName === "input") {
912
+ const type = asciiLower(attrs["type"] ?? "text");
913
+ if (type !== "hidden" && type !== "submit" && type !== "button" && type !== "reset" && type !== "image") {
914
+ counters.inputWithoutLabel++;
915
+ }
916
+ }
917
+ if (tagName === "base" && attrs["href"]) baseHrefCount++;
918
+ if (tagName === "iframe" && !attrs["loading"]) iframeWithoutLoadingCount++;
919
+ if (tagName === "img") {
920
+ imgCount++;
921
+ if (attrs["alt"] !== void 0) imgWithAltCount++;
922
+ }
923
+ const nextBarrier = isBarrier;
924
+ const nextNamed = currentId;
925
+ if (isBarrier) {
926
+ templateFragmentCount++;
927
+ const children3 = node.namedChildren;
928
+ for (let i = children3.length - 1; i >= 0; i--) {
929
+ const child = children3[i];
930
+ if (child) {
931
+ stack.push({
932
+ node: child,
933
+ nearestNamed: nextNamed,
934
+ depth: frame.depth + 1,
935
+ inBarrier: true
936
+ });
937
+ }
938
+ }
939
+ continue;
940
+ }
941
+ const children2 = node.namedChildren;
942
+ for (let i = children2.length - 1; i >= 0; i--) {
943
+ const child = children2[i];
944
+ if (child) {
945
+ stack.push({ node: child, nearestNamed: nextNamed, depth: frame.depth + 1, inBarrier: frame.inBarrier });
946
+ }
947
+ }
948
+ continue;
949
+ }
950
+ }
951
+ const children = node.namedChildren;
952
+ for (let i = children.length - 1; i >= 0; i--) {
953
+ const child = children[i];
954
+ if (child) {
955
+ stack.push({
956
+ node: child,
957
+ nearestNamed: frame.nearestNamed,
958
+ depth: frame.depth,
959
+ inBarrier: frame.inBarrier
960
+ });
961
+ }
962
+ }
963
+ }
964
+ for (let i = 0; i < deferred.length; i++) {
965
+ const ref = deferred[i];
966
+ const target = handleToSymbol[ref.handle];
967
+ if (target === void 0) continue;
968
+ if (ref.rel === "references") {
969
+ relationships.push({
970
+ source: ref.source,
971
+ target,
972
+ type: ASTEngine.RelationshipType.References,
973
+ confidence: 1,
974
+ line_number: ref.line,
975
+ metadata: {
976
+ htmlSpecific: true,
977
+ isIdref: true,
978
+ idrefKind: ref.kind,
979
+ // Published for the LIST carriers only, never as `idrefList: false`: a
980
+ // single-valued reference and a one-token list must not read alike, and
981
+ // the absent/present test is the only one that survives a consumer's
982
+ // truthiness check (`false` and `undefined` are both falsy).
983
+ ...ref.list ? { idrefList: true } : {},
984
+ evidenceClass: "structural",
985
+ ...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
986
+ }
987
+ });
988
+ continue;
989
+ }
990
+ if (ref.kind === "label") {
991
+ relationships.push({
992
+ source: ref.source,
993
+ target,
994
+ type: ASTEngine.RelationshipType.Uses,
995
+ confidence: 1,
996
+ line_number: ref.line,
997
+ metadata: {
998
+ htmlSpecific: true,
999
+ isLabelAssociation: true,
1000
+ evidenceClass: "structural",
1001
+ ...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
1002
+ }
1003
+ });
1004
+ } else {
1005
+ relationships.push({
1006
+ source: ref.source,
1007
+ target,
1008
+ type: ASTEngine.RelationshipType.Uses,
1009
+ confidence: 1,
1010
+ line_number: ref.line,
1011
+ metadata: {
1012
+ htmlSpecific: true,
1013
+ isInternalAnchor: true,
1014
+ evidenceClass: "structural",
1015
+ ...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
1016
+ }
1017
+ });
1018
+ }
1019
+ }
1020
+ this.accumulateSubtreeSizes(symbols);
1021
+ const structural = this.computeScores({
1022
+ totalElements,
1023
+ semanticCount,
1024
+ interactiveCount,
1025
+ formElementCount,
1026
+ ariaCount,
1027
+ headingCount,
1028
+ headingLevelSkips,
1029
+ imgCount,
1030
+ imgWithAltCount,
1031
+ iframeWithoutLoadingCount,
1032
+ labelWithoutForCount: counters.labelWithoutFor,
1033
+ inputWithoutLabelCount: counters.inputWithoutLabel,
1034
+ branchCount,
1035
+ maxDepth,
1036
+ addressableCount,
1037
+ customElementCount,
1038
+ templateFragmentCount
1039
+ });
1040
+ if (addressableCount > 0) insights["Addressable DOM nodes identified by id/name"] = true;
1041
+ if (semanticCount > 0) insights["Well-structured semantic document"] = true;
1042
+ if (ariaCount > 0) insights["Commitment to accessibility detected"] = true;
1043
+ if (headingLevelSkips > 0) insights["Heading levels skip a level"] = true;
1044
+ if (hasExternals) {
1045
+ insights["External resources linked"] = true;
1046
+ let hasExternalScript = false;
1047
+ let hasExternalStyle = false;
1048
+ for (let i = 0; i < imports.length; i++) {
1049
+ if (imports[i].metadata?.isScript) hasExternalScript = true;
1050
+ if (imports[i].metadata?.isStylesheet) hasExternalStyle = true;
1051
+ }
1052
+ if (hasExternalScript) insights["External interactivity linked"] = true;
1053
+ if (hasExternalStyle) insights["External styling linked"] = true;
1054
+ }
1055
+ if (hasForm) insights["Contains user input forms"] = true;
1056
+ if (customElementCount > 0) insights["Custom-element-shaped tags present; registration not observable from this document"] = true;
1057
+ if (templateFragmentCount > 0) insights["Deferred template fragments present"] = true;
1058
+ if (baseHrefCount > 0) insights["Base URL rewrites relative resource resolution"] = true;
1059
+ if (counters.externalLinkCount > 0) insights["Outbound cross-origin navigation (recorded, not graphed)"] = true;
1060
+ const patterns = [];
1061
+ if (hasForm) patterns.push("Form Controls Usage");
1062
+ if (semanticCount > 0) patterns.push("Semantic HTML5 Structure");
1063
+ if (ariaCount > 0) patterns.push("ARIA Accessibility Enhancements");
1064
+ if (addressableCount > 0) patterns.push("Addressable DOM Identity");
1065
+ if (customElementCount > 0) patterns.push("Custom-element-shaped tag usage");
1066
+ const detectedFrameworks = [];
1067
+ for (const f in frameworks) detectedFrameworks.push(f);
1068
+ const architecturalInsights = [];
1069
+ for (const insight in insights) architecturalInsights.push(insight);
1070
+ if (context && Array.isArray(context.htmlEmbeddedTrees) && context.htmlEmbeddedTrees.length > 0) {
1071
+ const unavailable = this.mergeEmbeddedBlocks(context, moduleName, symbols, relationships, imports, exports, errors);
1072
+ if (unavailable.length > 0) this.unavailableEmbeddedLangs = [...new Set(unavailable)].sort();
1073
+ }
1074
+ return {
1075
+ language: "html",
1076
+ symbols,
1077
+ relationships,
1078
+ imports,
1079
+ exports,
1080
+ errors,
1081
+ metadata: {
1082
+ // Structural depth, not a symbol count: `symbols.length` was the pre-fix
1083
+ // value and it is not a complexity measure of anything.
1084
+ complexity: maxDepth + 1,
1085
+ linesOfCode: content.split("\n").length,
1086
+ cyclomaticComplexity: 1 + branchCount,
1087
+ elementCount: totalElements,
1088
+ addressableNodeCount: addressableCount,
1089
+ maxElementDepth: maxDepth,
1090
+ framework: "HTML5",
1091
+ patterns,
1092
+ detectedFrameworks,
1093
+ frameworkPatterns: [],
1094
+ architecturalInsights,
1095
+ architecturalScore: structural.architecturalScore,
1096
+ codeQuality: structural.architecturalScore > 80 ? "Excellent" : structural.architecturalScore > 60 ? "Good" : "Fair",
1097
+ hasTypeInference: false,
1098
+ hasFrameworkIntelligence: true,
1099
+ // The three scores are published under engine-forwarded keys AND as
1100
+ // metadata.score, so the numbers survive the engine's metadata literal
1101
+ // instead of being computed and dropped.
1102
+ accessibilityScore: structural.accessibility,
1103
+ semanticScore: structural.semantic,
1104
+ performanceScore: structural.performance,
1105
+ score: structural,
1106
+ analysisVersion: "m14-html-identity-v2",
1107
+ ...this.unavailableEmbeddedLangs.length > 0 ? { unavailableEmbeddedLangs: [...this.unavailableEmbeddedLangs] } : {},
1108
+ documentMetrics: {
1109
+ totalElements,
1110
+ semanticCount,
1111
+ interactiveCount,
1112
+ formElementCount,
1113
+ ariaCount,
1114
+ headingCount,
1115
+ headingLevelSkips,
1116
+ imgCount,
1117
+ imgWithAltCount,
1118
+ iframeWithoutLoadingCount,
1119
+ labelWithoutForCount: counters.labelWithoutFor,
1120
+ inputWithoutLabelCount: counters.inputWithoutLabel,
1121
+ customElementCount,
1122
+ templateFragmentCount,
1123
+ baseHrefCount,
1124
+ // A hyperlink is not a code dependency, so outbound navigation is a
1125
+ // DOCUMENT METRIC rather than 200-odd `uses` edges into a graph whose
1126
+ // every edge is scored for architectural criticality.
1127
+ externalLinkCount: counters.externalLinkCount,
1128
+ internalLinkCount: counters.internalLinkCount
1129
+ }
1130
+ }
1131
+ };
1132
+ }
1133
+ // ==========================================================================
1134
+ // RESOURCE EMISSION
1135
+ // ==========================================================================
1136
+ /**
1137
+ * One fetch/inline-resource edge, or one hyperlink, or nothing.
1138
+ *
1139
+ * Returns what it decided so the caller's document metrics stay exact without
1140
+ * this method needing a reference to them:
1141
+ * "fetch" a `uses` edge was emitted for a resource the document loads
1142
+ * "fragment" a same-document reference; the caller already deferred it
1143
+ * "navigation" an in-project hyperlink/endpoint, emitted as `uses`
1144
+ * "external" a cross-origin or non-resource-scheme URL: deliberately
1145
+ * NOT graphed (see the header) — the caller counts it
1146
+ * "script-url" a `javascript:` URL: a hazard, not a dependency
1147
+ * "none" nothing to say
1148
+ *
1149
+ * The decision table:
1150
+ * - a same-document fragment is NOT a fetch: it is a reference into the
1151
+ * document's own address space, resolved after the walk (deferred).
1152
+ * - a non-resource scheme (`mailto:`, `tel:`, `javascript:`, `data:`, …) is
1153
+ * not a dependency. Decoded BEFORE the scheme test (see the header), so
1154
+ * `&#106;avascript:` and `&#0000106;avascript:` cannot slip past — the
1155
+ * pre-fix filter was a bare `startsWith("javascript:")` and missed both,
1156
+ * AND existed only in the `<a>` branch, so `<form action="javascript:…">`
1157
+ * emitted a `calls` edge and an `imports` row for one.
1158
+ * - a network URL is a fetch, but its target is not a project module, so the
1159
+ * edge is `usage_contextual` and is NOT mirrored into `imports` (mirroring
1160
+ * it put `https://cdn.example.com/x.js` in brain_import, where every other
1161
+ * lane's import is a module the index can resolve).
1162
+ * - anything else is a project-relative module: `import_based`, and mirrored.
1163
+ */
1164
+ emitResource(rawValue, source, rule, line, range, relationships, imports, deferred, alreadyDecoded = false) {
1165
+ const value = (alreadyDecoded ? rawValue : decodeCharacterReferences(rawValue)).trim();
1166
+ if (value.length === 0) return "none";
1167
+ const fragment = fragmentTarget(value);
1168
+ if (value === "#") return "none";
1169
+ if (fragment.length > 0) {
1170
+ if (rule.fragmentRef) {
1171
+ deferred.push({ source, handle: fragment, kind: "anchor", rel: "uses", line });
1172
+ return "fragment";
1173
+ }
1174
+ relationships.push({
1175
+ source,
1176
+ target: value,
1177
+ type: ASTEngine.RelationshipType.Uses,
1178
+ confidence: 1,
1179
+ line_number: line,
1180
+ metadata: {
1181
+ htmlSpecific: true,
1182
+ resourceType: rule.resourceType,
1183
+ isInternalAnchor: true,
1184
+ isUnresolvedFragment: true,
1185
+ evidenceClass: "structural"
1186
+ }
1187
+ });
1188
+ return "fragment";
1189
+ }
1190
+ if (isNonResourceScheme(value)) {
1191
+ if (urlScheme(value) === "javascript") {
1192
+ relationships.push({
1193
+ source,
1194
+ target: "javascript:",
1195
+ type: ASTEngine.RelationshipType.Uses,
1196
+ confidence: 1,
1197
+ line_number: line,
1198
+ metadata: {
1199
+ htmlSpecific: true,
1200
+ resourceType: "javascript-url",
1201
+ isScriptUrl: true,
1202
+ evidenceClass: "usage_contextual"
1203
+ }
1204
+ });
1205
+ return "script-url";
1206
+ }
1207
+ return "external";
1208
+ }
1209
+ const network = isProtocolRelative(value) || isNetworkScheme(value);
1210
+ if (rule.link) {
1211
+ if (network) return "external";
1212
+ relationships.push({
1213
+ source,
1214
+ target: value,
1215
+ type: ASTEngine.RelationshipType.Uses,
1216
+ // 0.9, not 1.0: an observed hyperlink is not a derived fact. The tier
1217
+ // classifier in every other gated lane reads confidence, and a 1.0 here
1218
+ // would claim `structural` for a link that was merely written down.
1219
+ confidence: 0.9,
1220
+ line_number: line,
1221
+ // Honesty contract §4.1: a relationship below 1.0 must carry a
1222
+ // MACHINE-READABLE reason code, and the only justification this tier had
1223
+ // was the comment above. Measured over this lane's corpus (1,185
1224
+ // git-tracked documents, `validation/m14/probes/html/ontology-census.mts`):
1225
+ // 60,285 html-native edges sit at this tier — 59,953 `navigation` and 332
1226
+ // `form-action` — and not one carried a field a consumer or an audit could
1227
+ // read. The code makes the tier machine-checkable without re-reading the
1228
+ // analyzer: a consumer filtering "observed, not derived" reads a field.
1229
+ metadata: {
1230
+ htmlSpecific: true,
1231
+ resourceType: rule.resourceType,
1232
+ isNavigation: rule.navigation === true,
1233
+ // A form endpoint is a dependency on a backend route, which is a
1234
+ // materially different fact from "the user can click away".
1235
+ isFormEndpoint: rule.navigation !== true,
1236
+ evidenceClass: "usage_contextual",
1237
+ confidenceReason: "observed-hyperlink-not-derived",
1238
+ // THE CLAIM, on the channel the arbiter reads. A hyperlink is the largest
1239
+ // unresolved class this lane has (22,260 of 32,656 in the bounded
1240
+ // population) and it was emitted as a bare relative spelling, so the
1241
+ // document that wrote it was the only thing that could answer it.
1242
+ ...this.resourceClaimMetadata(value, false)
1243
+ }
1244
+ });
1245
+ return "navigation";
1246
+ }
1247
+ relationships.push({
1248
+ source,
1249
+ target: value,
1250
+ type: ASTEngine.RelationshipType.Uses,
1251
+ confidence: 1,
1252
+ line_number: line,
1253
+ metadata: {
1254
+ htmlSpecific: true,
1255
+ resourceType: rule.resourceType,
1256
+ isExternalResource: network,
1257
+ evidenceClass: rule.evidenceClass ?? (network ? "usage_contextual" : "import_based"),
1258
+ // Same claim, same keys: a fetch names a FILE (`../styles/main.css`) or a
1259
+ // network URL, and both were published verbatim with no reason attached.
1260
+ ...this.resourceClaimMetadata(value, network)
1261
+ }
1262
+ });
1263
+ if (rule.import && !network) {
1264
+ imports.push({
1265
+ module: value,
1266
+ items: ["*"],
1267
+ isDefaultImport: true,
1268
+ isNamespaceImport: false,
1269
+ range,
1270
+ metadata: { isHtmlResource: true, resourceType: rule.resourceType }
1271
+ });
1272
+ }
1273
+ return "fetch";
1274
+ }
1275
+ /**
1276
+ * Merge the sub-analysed inline blocks into the document's own payload.
1277
+ *
1278
+ * Each block is analysed by the JS/TS/CSS analyzer for its own grammar and the
1279
+ * result is re-based onto the document:
1280
+ * - a `line_number` and every metadata key whose NAME is a line move by
1281
+ * `startLine - 1` (the block's first line, 1-based, to file coordinates);
1282
+ * - a `range`'s startLine/endLine move by the same amount and its
1283
+ * startIndex/endIndex by the byte offset, because an index-based consumer
1284
+ * (bb_edit) is misaligned by lines alone;
1285
+ * - a multi-line block is indented, so the FIRST line needs no column shift
1286
+ * and the rest need none either — the block's text is copied verbatim from
1287
+ * the source, so its columns are already the document's;
1288
+ * - the owner of a delegate symbol with no owner is the DOCUMENT, because a
1289
+ * top-level `function` in an inline block is a document-scope declaration,
1290
+ * and `contains` then carries the same line the symbol does.
1291
+ *
1292
+ * Every shifted row is tagged `htmlEmbedded: true` with the block's language
1293
+ * and the offset applied. That tag is what makes the seam falsifiable: a
1294
+ * fixture can assert that an inline script's symbol is on the line it is
1295
+ * written on, and a mutant that drops the shift fails that pin instead of
1296
+ * silently shipping off-by-N lines.
1297
+ */
1298
+ mergeEmbeddedBlocks(context, moduleName, symbols, relationships, imports, exports, errors) {
1299
+ const unavailable = [];
1300
+ const trees = context.htmlEmbeddedTrees ?? [];
1301
+ const langs = context.htmlEmbeddedLangs ?? [];
1302
+ const startLines = context.htmlEmbeddedStartLines ?? [];
1303
+ const startColumns = context.htmlEmbeddedStartColumns ?? [];
1304
+ const startIndices = context.htmlEmbeddedStartIndices ?? [];
1305
+ const delegates = map();
1306
+ const delegateFor = (lang) => {
1307
+ if (delegates[lang] !== void 0) return delegates[lang];
1308
+ let instance = null;
1309
+ try {
1310
+ const mod = lang === "css" ? require2("./real-css-analyzer") : lang === "ts" || lang === "tsx" ? require2("./real-typescript-analyzer") : require2("./real-javascript-analyzer");
1311
+ const cls = lang === "css" ? mod.RealCssAnalyzer : lang === "ts" || lang === "tsx" ? mod.RealTypeScriptAnalyzer : mod.RealJavaScriptAnalyzer;
1312
+ instance = typeof cls === "function" ? new cls() : null;
1313
+ } catch {
1314
+ try {
1315
+ const nodeRequire = createRequire(import.meta.url);
1316
+ const mod = lang === "css" ? nodeRequire("./real-css-analyzer") : lang === "ts" || lang === "tsx" ? nodeRequire("./real-typescript-analyzer") : nodeRequire("./real-javascript-analyzer");
1317
+ const cls = lang === "css" ? mod.RealCssAnalyzer : lang === "ts" || lang === "tsx" ? mod.RealTypeScriptAnalyzer : mod.RealJavaScriptAnalyzer;
1318
+ instance = typeof cls === "function" ? new cls() : null;
1319
+ } catch {
1320
+ instance = null;
1321
+ }
1322
+ }
1323
+ if (instance === null) unavailable.push(lang);
1324
+ delegates[lang] = instance;
1325
+ return instance;
1326
+ };
1327
+ for (let i = 0; i < trees.length; i++) {
1328
+ const tree = trees[i];
1329
+ if (!tree) continue;
1330
+ const lang = langs[i] ?? "js";
1331
+ const lineOffset = (startLines[i] ?? 1) - 1;
1332
+ const byteOffset = startIndices[i] ?? 0;
1333
+ const delegate = delegateFor(lang);
1334
+ if (delegate === null) continue;
1335
+ let sub;
1336
+ try {
1337
+ sub = delegate.analyze(tree.rootNode, "", moduleName);
1338
+ } catch {
1339
+ continue;
1340
+ }
1341
+ for (const s of sub.symbols ?? []) {
1342
+ if (s.range) {
1343
+ s.range = {
1344
+ ...s.range,
1345
+ startLine: s.range.startLine + lineOffset,
1346
+ endLine: s.range.endLine + lineOffset,
1347
+ startIndex: typeof s.range.startIndex === "number" ? s.range.startIndex + byteOffset : s.range.startIndex,
1348
+ endIndex: typeof s.range.endIndex === "number" ? s.range.endIndex + byteOffset : s.range.endIndex
1349
+ };
1350
+ }
1351
+ if (!s.ownerName) {
1352
+ s.ownerName = moduleName;
1353
+ s.ownerKind = ASTEngine.SymbolKind.Module;
1354
+ s.scope = moduleName;
1355
+ }
1356
+ s.metadata = {
1357
+ ...s.metadata ?? {},
1358
+ htmlEmbedded: true,
1359
+ embeddedLang: lang,
1360
+ embeddedLineOffset: lineOffset
1361
+ };
1362
+ symbols.push(s);
1363
+ if (s.ownerName && s.ownerName !== s.name && s.range?.startLine) {
1364
+ relationships.push({
1365
+ source: s.ownerName,
1366
+ target: s.name,
1367
+ type: ASTEngine.RelationshipType.Contains,
1368
+ confidence: 1,
1369
+ line_number: s.range.startLine,
1370
+ metadata: {
1371
+ htmlSpecific: true,
1372
+ isEmbeddedCode: true,
1373
+ // ORIGIN DISCLOSURE (2026-10-05, measured). This row was the ONE
1374
+ // merged row that carried no `htmlEmbedded` tag and no
1375
+ // `embeddedLineOffset`, while the symbols at :1585, the delegate
1376
+ // relationships at :1633 and the imports at :1652 all carried
1377
+ // both. Measured on vuejs through the production path: of 921
1378
+ // delegate rows, 676 carried `htmlEmbedded === true` and 245 did
1379
+ // not — and 245 is the seam-created `contains` class.
1380
+ //
1381
+ // The consequence is not cosmetic. The header of this method
1382
+ // claims "Every shifted row is tagged htmlEmbedded: true"; an
1383
+ // auditor who takes that as the origin test concludes these rows
1384
+ // are html's OWN, which is exactly the delegation question this
1385
+ // lane exists to answer honestly, and gets it backwards for a
1386
+ // quarter of the merged graph. html-origin-census.ts answers the
1387
+ // same question WITHOUT this tag — by differencing the
1388
+ // production path against the seam switched off — and the two
1389
+ // numbers have to agree or one of them is wrong.
1390
+ //
1391
+ // `htmlSpecific: true` on a delegate-created edge is also a lie of
1392
+ // the same shape: the row is not a fact about HTML's grammar, it
1393
+ // is a fact about the javascript inside a `<script>`. It stays,
1394
+ // because it is what tells a consumer the edge arrived through the
1395
+ // html module — but `htmlEmbedded` is what tells them WHERE it was
1396
+ // written, and that was missing.
1397
+ htmlEmbedded: true,
1398
+ embeddedLang: lang,
1399
+ embeddedLineOffset: lineOffset,
1400
+ evidenceClass: "structural"
1401
+ }
1402
+ });
1403
+ }
1404
+ }
1405
+ const delegateBases = /* @__PURE__ */ new Map();
1406
+ for (const r of sub.relationships ?? []) {
1407
+ if (r?.type !== ASTEngine.RelationshipType.Extends) continue;
1408
+ if (typeof r.source === "string" && r.source !== "" && typeof r.target === "string" && r.target !== "") {
1409
+ delegateBases.set(r.source, r.target);
1410
+ }
1411
+ }
1412
+ for (const r of sub.relationships ?? []) {
1413
+ if (r.type === ASTEngine.RelationshipType.Imports) {
1414
+ const mod = r.metadata?.module;
1415
+ if (typeof mod === "string" && mod !== "" && typeof r.target === "string" && r.target !== "" && !r.target.includes(":") && !r.target.includes("/")) {
1416
+ r.target = `${mod}:${r.target}`;
1417
+ }
1418
+ }
1419
+ if (r.type === ASTEngine.RelationshipType.Overrides) {
1420
+ if (typeof r.source === "string" && typeof r.target === "string" && r.target !== "" && !r.target.includes(".")) {
1421
+ const owner = r.source.slice(0, r.source.lastIndexOf("."));
1422
+ const base = owner === "" ? void 0 : delegateBases.get(owner);
1423
+ if (base) r.target = `${base}.${r.target}`;
1424
+ }
1425
+ }
1426
+ if (typeof r.line_number === "number") r.line_number += lineOffset;
1427
+ if (r.metadata) {
1428
+ for (const k of Object.keys(r.metadata)) {
1429
+ if (!asciiLower(k).includes("line")) continue;
1430
+ const v = r.metadata[k];
1431
+ if (typeof v === "number") r.metadata[k] = v + lineOffset;
1432
+ else if (Array.isArray(v) && v.some((x) => typeof x === "number")) {
1433
+ ;
1434
+ r.metadata[k] = v.map((x) => typeof x === "number" ? x + lineOffset : x);
1435
+ }
1436
+ }
1437
+ }
1438
+ if (r.source === DELEGATE_FILE_ACTOR) r.source = moduleName;
1439
+ r.metadata = {
1440
+ ...r.metadata ?? {},
1441
+ htmlEmbedded: true,
1442
+ embeddedLang: lang,
1443
+ embeddedLineOffset: lineOffset
1444
+ };
1445
+ relationships.push(r);
1446
+ }
1447
+ for (const imp of sub.imports ?? []) {
1448
+ if (imp.range) {
1449
+ imp.range = {
1450
+ ...imp.range,
1451
+ startLine: imp.range.startLine + lineOffset,
1452
+ endLine: imp.range.endLine + lineOffset,
1453
+ startIndex: typeof imp.range.startIndex === "number" ? imp.range.startIndex + byteOffset : imp.range.startIndex,
1454
+ endIndex: typeof imp.range.endIndex === "number" ? imp.range.endIndex + byteOffset : imp.range.endIndex
1455
+ };
1456
+ }
1457
+ imp.metadata = { ...imp.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang };
1458
+ imports.push(imp);
1459
+ }
1460
+ for (const e of sub.exports ?? []) {
1461
+ if (e.range) e.range = { ...e.range, startLine: e.range.startLine + lineOffset, endLine: e.range.endLine + lineOffset };
1462
+ if (e.range) {
1463
+ e.range = {
1464
+ ...e.range,
1465
+ startLine: e.range.startLine + lineOffset,
1466
+ endLine: e.range.endLine + lineOffset,
1467
+ startIndex: typeof e.range.startIndex === "number" ? e.range.startIndex + byteOffset : e.range.startIndex,
1468
+ endIndex: typeof e.range.endIndex === "number" ? e.range.endIndex + byteOffset : e.range.endIndex
1469
+ };
1470
+ }
1471
+ e.metadata = { ...e.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang, embeddedLineOffset: lineOffset };
1472
+ exports.push(e);
1473
+ }
1474
+ for (const er of sub.errors ?? []) {
1475
+ if (er.range) {
1476
+ er.range = {
1477
+ ...er.range,
1478
+ startLine: er.range.startLine + lineOffset,
1479
+ endLine: er.range.endLine + lineOffset,
1480
+ startIndex: typeof er.range.startIndex === "number" ? er.range.startIndex + byteOffset : er.range.startIndex,
1481
+ endIndex: typeof er.range.endIndex === "number" ? er.range.endIndex + byteOffset : er.range.endIndex
1482
+ };
1483
+ }
1484
+ er.metadata = { ...er.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang, embeddedLineOffset: lineOffset };
1485
+ errors.push(er);
1486
+ }
1487
+ }
1488
+ return unavailable;
1489
+ }
1490
+ /**
1491
+ * Fold one emission decision into the document metrics. O(1).
1492
+ *
1493
+ * A hyperlink that left the project is COUNTED and not graphed: the pre-fix
1494
+ * analyzer emitted a `uses` edge for every one, so a page linking to fifty
1495
+ * external sites published fifty graph edges that the enrichment pass then
1496
+ * scored for architectural criticality, and the differential could not tell a
1497
+ * CDN dependency from a hyperlink. The count survives in
1498
+ * `documentMetrics.externalLinkCount`, so the information is not lost — it is
1499
+ * where it belongs.
1500
+ */
1501
+ countOutcome(outcome, counters) {
1502
+ if (outcome === "external") counters.externalLinkCount++;
1503
+ else if (outcome === "navigation") counters.internalLinkCount++;
1504
+ }
1505
+ // ==========================================================================
1506
+ // NODE / ATTRIBUTE ACCESS — index cursors, no array mapping, no regex
1507
+ // ==========================================================================
1508
+ /** The element's start_tag or self_closing_tag, or null. */
1509
+ getStartTag(node) {
1510
+ for (let i = 0; i < node.childCount; i++) {
1511
+ const child = node.child(i);
1512
+ if (child?.type === "start_tag" || child?.type === "self_closing_tag") return child;
1513
+ }
1514
+ return null;
1515
+ }
1516
+ getElementTagName(node) {
1517
+ const startTag = this.getStartTag(node);
1518
+ if (!startTag) return null;
1519
+ for (let j = 0; j < startTag.childCount; j++) {
1520
+ const grandChild = startTag.child(j);
1521
+ if (grandChild?.type === "tag_name") return grandChild;
1522
+ }
1523
+ return null;
1524
+ }
1525
+ /**
1526
+ * Attribute map, LOWERCASED KEYS, plus the line each attribute is written on.
1527
+ *
1528
+ * The lowercasing is the whole point and it is the one-line defect that cost
1529
+ * this analyzer its entire graph on any document written in mixed case: the tag
1530
+ * name was lowercased and the attribute name was not, so `<IMG SRC=…>` matched
1531
+ * no rule and produced nothing. HTML attribute names are ASCII
1532
+ * case-INSENSITIVE; the grammar hands them over verbatim.
1533
+ */
1534
+ extractElementAttributes(node) {
1535
+ const attrs = map();
1536
+ const lines = map();
1537
+ const startTag = this.getStartTag(node);
1538
+ if (!startTag) return { attrs, lines };
1539
+ for (let j = 0; j < startTag.childCount; j++) {
1540
+ const attribute = startTag.child(j);
1541
+ if (attribute?.type !== "attribute") continue;
1542
+ let attrName = "";
1543
+ let attrValue = "";
1544
+ let attrLine = attribute.startPosition.row + 1;
1545
+ for (let k = 0; k < attribute.childCount; k++) {
1546
+ const attrChild = attribute.child(k);
1547
+ const t = attrChild?.type;
1548
+ if (t === "attribute_name") {
1549
+ attrName = attrChild.text;
1550
+ attrLine = attrChild.startPosition.row + 1;
1551
+ } else if (t === "quoted_attribute_value" || t === "attribute_value") {
1552
+ const inner = this.findChildByType(attrChild, "attribute_value");
1553
+ attrValue = inner ? inner.text : unquote(attrChild.text);
1554
+ }
1555
+ }
1556
+ if (!attrName) continue;
1557
+ const key = asciiLower(attrName);
1558
+ if (attrs[key] === void 0) {
1559
+ attrs[key] = attrValue;
1560
+ lines[key] = attrLine;
1561
+ }
1562
+ }
1563
+ return { attrs, lines };
1564
+ }
1565
+ /**
1566
+ * The range of ONE attribute of a start_tag, falling back to the element's own
1567
+ * range when the grammar gave us no attribute node for it. O(A) over the
1568
+ * start_tag's children, A = the tag's attribute count.
1569
+ */
1570
+ attributeRange(startTag, attrName, element, line) {
1571
+ if (startTag) {
1572
+ for (let j = 0; j < startTag.childCount; j++) {
1573
+ const attribute = startTag.child(j);
1574
+ if (attribute?.type !== "attribute") continue;
1575
+ for (let k = 0; k < attribute.childCount; k++) {
1576
+ const an = attribute.child(k);
1577
+ if (an?.type === "attribute_name" && asciiLower(an.text) === attrName) {
1578
+ return {
1579
+ startLine: line,
1580
+ startColumn: an.startPosition.column + 1,
1581
+ endLine: an.endPosition.row + 1,
1582
+ endColumn: an.endPosition.column + 1,
1583
+ startIndex: attribute.startIndex,
1584
+ endIndex: attribute.endIndex
1585
+ };
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ const r = this.nodeToRange(element);
1591
+ r.startLine = line;
1592
+ return r;
1593
+ }
1594
+ findChildByType(node, type) {
1595
+ for (let i = 0; i < node.namedChildCount; i++) {
1596
+ const c = node.namedChild(i);
1597
+ if (c && c.type === type) return c;
1598
+ }
1599
+ return null;
1600
+ }
1601
+ /** A directive attribute whose NAME ends with one of `suffixes` — the shape
1602
+ * `v-if` / `ng-repeat` / `:show` / `x-if` all take. Pure-String, O(1) per attr. */
1603
+ hasDirective(attrs, suffixes) {
1604
+ for (const key in attrs) {
1605
+ for (let s = 0; s < suffixes.length; s++) {
1606
+ const suffix = suffixes[s];
1607
+ if (key === suffix) return true;
1608
+ if (key.length > suffix.length + 1 && key.endsWith(suffix) && key.charCodeAt(key.length - suffix.length - 1) === 45) {
1609
+ return true;
1610
+ }
1611
+ }
1612
+ }
1613
+ return false;
1614
+ }
1615
+ /**
1616
+ * The first FETCHABLE token of a `<link rel>` list, or null.
1617
+ *
1618
+ * `rel` is a space-separated token list, not one value: the ubiquitous
1619
+ * `rel="shortcut icon"` is two tokens, and `rel="preconnect stylesheet"` is a
1620
+ * hint plus a real document. Returning the token (rather than a boolean) is
1621
+ * what lets the edge say WHICH kind of link it is.
1622
+ */
1623
+ fetchableRelToken(rel, filter) {
1624
+ if (rel === void 0 || rel.length === 0) return null;
1625
+ const lowered = asciiLower(rel);
1626
+ const n = lowered.length;
1627
+ let start = 0;
1628
+ while (start < n) {
1629
+ while (start < n && isHtmlSpace(lowered.charCodeAt(start))) start++;
1630
+ let end = start;
1631
+ while (end < n && !isHtmlSpace(lowered.charCodeAt(end))) end++;
1632
+ if (end > start) {
1633
+ const token = lowered.slice(start, end);
1634
+ if (filter[token]) return token;
1635
+ }
1636
+ start = end;
1637
+ }
1638
+ return null;
1639
+ }
1640
+ findDoctype(node) {
1641
+ for (let i = 0; i < node.namedChildCount; i++) {
1642
+ const c = node.namedChild(i);
1643
+ if (c && c.type === "doctype") {
1644
+ for (let j = 0; j < c.childCount; j++) {
1645
+ const d = c.child(j);
1646
+ if (d && d.type !== "<!" && d.type !== ">" && d.text.trim().length > 0) {
1647
+ return firstToken(d.text);
1648
+ }
1649
+ }
1650
+ return "html";
1651
+ }
1652
+ }
1653
+ return null;
1654
+ }
1655
+ documentLanguage(node) {
1656
+ for (let i = 0; i < node.namedChildCount; i++) {
1657
+ const c = node.namedChild(i);
1658
+ if (c && c.type === "element") {
1659
+ for (let j = 0; j < c.childCount; j++) {
1660
+ const startTag = c.child(j);
1661
+ if (startTag && (startTag.type === "start_tag" || startTag.type === "self_closing_tag")) {
1662
+ for (let k = 0; k < startTag.childCount; k++) {
1663
+ const a = startTag.child(k);
1664
+ if (a?.type === "attribute") {
1665
+ for (let m = 0; m < a.childCount; m++) {
1666
+ const an = a.child(m);
1667
+ if (an?.type === "attribute_name" && asciiLower(an.text) === "lang") {
1668
+ const inner = this.findChildByType(a, "attribute_value");
1669
+ return inner ? inner.text : unquote(a.text);
1670
+ }
1671
+ }
1672
+ }
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ return null;
1679
+ }
1680
+ /**
1681
+ * Accumulate each addressable element's addressable-subtree size into its
1682
+ * `complexity`, bottom-up, without an O(depth) walk per node.
1683
+ *
1684
+ * One reverse pass over the emitted symbols: a child's count is added to its
1685
+ * owner's accumulator. O(s) time, O(1) extra space beyond the accumulator map
1686
+ * that the symbol list itself already implies. Symbols whose owner is not in
1687
+ * this file (never, for html — every owner is either the module or another
1688
+ * emitted symbol) simply do not accumulate further.
1689
+ */
1690
+ accumulateSubtreeSizes(symbols) {
1691
+ const totals = map();
1692
+ for (let i = 0; i < symbols.length; i++) {
1693
+ const s = symbols[i];
1694
+ totals[s.name] = typeof s.complexity === "number" ? s.complexity : 1;
1695
+ }
1696
+ for (let i = symbols.length - 1; i >= 1; i--) {
1697
+ const s = symbols[i];
1698
+ const owner = s.ownerName;
1699
+ if (owner === void 0 || totals[owner] === void 0) continue;
1700
+ totals[owner] += totals[s.name];
1701
+ }
1702
+ for (let i = 0; i < symbols.length; i++) {
1703
+ const s = symbols[i];
1704
+ s.complexity = totals[s.name] ?? 1;
1705
+ }
1706
+ }
1707
+ // ==========================================================================
1708
+ // SCORING — bounded, monotone, and every input is a MEASURED count
1709
+ // ==========================================================================
1710
+ computeScores(m) {
1711
+ const countable = m.totalElements > 0 ? Math.max(1, m.totalElements - 4) : 1;
1712
+ const semanticRatio = m.semanticCount / countable;
1713
+ let semantic = 50;
1714
+ if (semanticRatio > 0.05) semantic += 20;
1715
+ if (semanticRatio > 0.15) semantic += 20;
1716
+ if (m.headingCount === 0 && m.totalElements > 20) semantic -= 20;
1717
+ let accessibility = 50;
1718
+ if (m.imgCount > 0) {
1719
+ accessibility += Math.round(m.imgWithAltCount / m.imgCount * 30);
1720
+ } else {
1721
+ accessibility += 20;
1722
+ }
1723
+ if (m.ariaCount > 0) accessibility += 10;
1724
+ if (m.headingCount > 0) accessibility += 10;
1725
+ accessibility -= m.headingLevelSkips * 5;
1726
+ accessibility -= Math.min(20, m.labelWithoutForCount * 5);
1727
+ accessibility -= Math.min(20, m.inputWithoutLabelCount);
1728
+ accessibility -= Math.min(15, m.iframeWithoutLoadingCount * 5);
1729
+ let performance = 80;
1730
+ performance -= Math.min(40, m.iframeWithoutLoadingCount * 10);
1731
+ if (m.maxDepth > 30) performance -= 10;
1732
+ performance -= Math.min(10, (m.templateFragmentCount ?? 0) * 2);
1733
+ const architecturalScore = Math.round(
1734
+ (clamp(semantic, 0, 100) + clamp(accessibility, 0, 100) + clamp(performance, 0, 100)) / 3
1735
+ );
1736
+ return {
1737
+ accessibility: clamp(accessibility, 0, 100),
1738
+ semantic: clamp(semantic, 0, 100),
1739
+ performance: clamp(performance, 0, 100),
1740
+ architecturalScore
1741
+ };
1742
+ }
1743
+ nodeToRange(node) {
1744
+ return {
1745
+ startLine: node.startPosition.row + 1,
1746
+ startColumn: node.startPosition.column + 1,
1747
+ endLine: node.endPosition.row + 1,
1748
+ endColumn: node.endPosition.column + 1,
1749
+ startIndex: node.startIndex,
1750
+ endIndex: node.endIndex
1751
+ };
1752
+ }
1753
+ };
1754
+ function clamp(value, lo, hi) {
1755
+ return value < lo ? lo : value > hi ? hi : value;
1756
+ }
1757
+ export {
1758
+ RealHtmlAnalyzer
1759
+ };