@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,2405 @@
1
+ import {
2
+ fileClaimForTarget
3
+ } from "./chunk-EWMWK7WN.js";
4
+ import {
5
+ ASTEngine
6
+ } from "./chunk-3ZTOHHSW.js";
7
+ import "./chunk-BQ3OVQ3P.js";
8
+ import "./chunk-SK6YVHFZ.js";
9
+ import "./chunk-2BSKD7O3.js";
10
+ import "./chunk-KAET47ZL.js";
11
+ import "./chunk-KCDECRJY.js";
12
+ import "./chunk-MLKGABMK.js";
13
+
14
+ // src/ast/real-xml-analyzer.ts
15
+ import { statSync } from "fs";
16
+ import * as path from "path";
17
+ var RealXmlAnalyzer = class _RealXmlAnalyzer {
18
+ /**
19
+ * Every node type this analyzer acts on, and why. The F8 completeness gate
20
+ * compares this against the analyzer's dispatch sites AND against the node
21
+ * types observed in a real corpus, so a type that appears in the corpus, is
22
+ * not declared here, and is not a pass-through literal is a red gate.
23
+ */
24
+ static DECLARED = {
25
+ document: "walk root; the root element is reached through the walk",
26
+ prolog: "container: XMLDecl, doctypedecl and the misc PIs/comments under it are dispatched from here",
27
+ element: "the document's structural unit: a symbol plus a `contains` edge from its parent element",
28
+ STag: "start tag: carries the element Name and the Attribute list",
29
+ EmptyElemTag: "empty tag: carries the element Name and the Attribute list",
30
+ Name: "the name-bearing leaf for tags, attributes, PIs, DTD declarations and notations",
31
+ Attribute: "a property of its element, and the source of `references` / `imports` edges for its value",
32
+ doctypedecl: "a Type symbol naming the DTD, plus its ExternalID import",
33
+ ExternalID: "a file reference on a doctypedecl, entity or notation \u2014 ONLY when it carries a SystemLiteral",
34
+ SystemLiteral: "the file path inside a SYSTEM ExternalID",
35
+ PubidLiteral: "the public identifier inside a PUBLIC ExternalID; a catalogue name, not a path",
36
+ elementdecl: "a Type symbol for a DTD element declaration; its contentspec is the content model",
37
+ AttlistDecl: "the element whose attributes are declared here",
38
+ AttDef: "a Property symbol plus a `contains` edge from the element it is declared for",
39
+ GEDecl: "a general entity declaration: a Type symbol owned by the DTD, plus its ExternalID import",
40
+ PEDecl: "a parameter entity declaration: a Type symbol owned by the DTD, plus its ExternalID import",
41
+ NotationDecl: "a notation declaration: a Type symbol owned by the DTD, plus its ExternalID import",
42
+ PublicID: "a PUBLIC identifier on a NotationDecl; it names NO file, so it publishes no import",
43
+ NDataDecl: "the notation an unparsed entity points at: a `contains` edge",
44
+ contentspec: "the content model of an element declaration; each Name leaf becomes a `contains` edge",
45
+ NotationType: "the notations an attribute may carry: each Name leaf becomes a `contains` edge",
46
+ PI: "a processing instruction: a Module symbol owned by its enclosing element",
47
+ PITarget: "the name-bearing leaf of a PI",
48
+ StyleSheetPI: "a file reference: the PI spelling the `xml-stylesheet` external token produces",
49
+ XmlModelPI: "a file reference: the PI spelling the `xml-model` external token produces",
50
+ PseudoAtt: "a pseudo-attribute of a StyleSheetPI / XmlModelPI",
51
+ PseudoAttValue: "the value of a pseudo-attribute; `href` is a file path"
52
+ };
53
+ /**
54
+ * Node types the walk deliberately walks THROUGH without acting on them. The
55
+ * F8 gate requires every observed named type to be in `DECLARED` or here; the
56
+ * value is the reason, so a pass-through is a decision on record and not a
57
+ * shrug.
58
+ */
59
+ static PASSTHROUGH_LITERALS = {
60
+ XMLDecl: "`xml` is reserved; version and encoding are not declarations",
61
+ Comment: "not a declaration",
62
+ CharData: "text, not a declaration",
63
+ CData: "text, not a declaration",
64
+ CDSect: "wrapper around CData",
65
+ CDStart: "wrapper for `<![CDATA[`",
66
+ CharRef: "a numeric character reference, not a name",
67
+ EntityRef: "a USE; the declaration is GEDecl / PEDecl",
68
+ PEReference: "inside an internal subset a PEReference is a DeclSep \u2014 a separator, not a use",
69
+ content: "an element's child sequence: walked through so each child element is reached; nothing on it is a name",
70
+ conditionalSect: "an INCLUDE/IGNORE section inside an internal subset; walked through so the declarations it guards are reached",
71
+ VersionNum: "the version pseudo-attribute literal inside an XMLDecl, not a name",
72
+ EncName: "the encoding pseudo-attribute literal inside an XMLDecl, not a name",
73
+ URI: "the aliased body of a SystemLiteral; the edge target is the enclosing literal, and the LITERAL carries the edge line",
74
+ _intSubsetItem: "hidden rule; the repeat that carries a subset's declarations and conditional sections",
75
+ ETag: "the repaired grammar rejects a mismatched end name, so the name adds nothing",
76
+ AttValue: "read by the Attribute branch",
77
+ Mixed: "`(#PCDATA|a|b)*` \u2014 the names are read from the Name leaves, not the wrapper",
78
+ children: "the element content model; its Name leaves are the declarations that matter",
79
+ Nmtoken: "an enumeration member is a token, not a name",
80
+ Enumeration: "wrapper over the Nmtokens of a DTD attribute type",
81
+ StringType: "the literal CDATA, not a name",
82
+ TokenizedType: "the literal ID / IDREF / \u2026 token, not a name",
83
+ DefaultDecl: "the literal #REQUIRED / #IMPLIED / #FIXED value, not a name",
84
+ EntityValue: "an entity's replacement text, not a name"
85
+ };
86
+ /**
87
+ * Namespaces whose members are specification built-ins rather than project
88
+ * symbols. A `type="xsd:string"` reference is real and is emitted, but it is
89
+ * marked `resolvable: "builtin"` and never published as a phantom symbol —
90
+ * recording a `string` symbol that no document in the corpus declares is the
91
+ * M60.R10 "a value where a name belongs" class of lie.
92
+ */
93
+ /**
94
+ * WHERE A DECLARATION'S NAME ACTUALLY LIVES, per vocabulary.
95
+ *
96
+ * In a schema language the DECLARED NAME of a construct is the VALUE of its
97
+ * `name` attribute, not its tag name: `<xs:complexType name="RootType">`
98
+ * declares the type `RootType`. An analyzer that publishes the tag name files
99
+ * every type in a project under one symbol — `xsd:element` alone appears 5,914
100
+ * times in this corpus — and makes the search a developer actually types,
101
+ * `bb_search("RootType")`, return nothing at all.
102
+ *
103
+ * The rule is gated on the element's EFFECTIVE NAMESPACE and on a DECLARED set
104
+ * of local names, and both halves are load-bearing:
105
+ *
106
+ * · tag-only gating is wrong, and measurably so. Outside a declaration
107
+ * vocabulary 29,196 elements carry a `name` attribute in this corpus and
108
+ * nearly all of them are DATA: `<property name="wsdl.dir">`,
109
+ * `<target name="client">`, a POM's `<name>` display label. Renaming those
110
+ * would publish a data value as a graph symbol.
111
+ * · `rng:ref name="…"` is a REFERENCE, not a declaration, so it is
112
+ * deliberately absent from the RelaxNG set; and because the value carries
113
+ * no prefix the QName pass cannot see it either. That gap is DECLARED here
114
+ * rather than left silently open.
115
+ *
116
+ * XSLT's local bindings (`xsl:variable`, `xsl:param`, `xsl:with-param`) are
117
+ * included: they are named, they are scoped to the enclosing template, and the
118
+ * walk already carries that owner.
119
+ */
120
+ static DECLARATION_VOCABULARIES = {
121
+ "http://www.w3.org/2001/XMLSchema": /* @__PURE__ */ new Set([
122
+ "element",
123
+ "attribute",
124
+ "complexType",
125
+ "simpleType",
126
+ "group",
127
+ "attributeGroup",
128
+ "notation",
129
+ "key",
130
+ "keyref",
131
+ "unique"
132
+ ]),
133
+ "http://relaxng.org/ns/structure/1.0": /* @__PURE__ */ new Set([
134
+ "grammar",
135
+ "define",
136
+ "element",
137
+ "attribute",
138
+ "group",
139
+ "data",
140
+ "value",
141
+ "text",
142
+ "list",
143
+ "div",
144
+ "except",
145
+ "name",
146
+ "externalRef"
147
+ ]),
148
+ "http://www.w3.org/1999/XSL/Transform": /* @__PURE__ */ new Set([
149
+ "template",
150
+ "attribute-set",
151
+ "key",
152
+ "decimal-format",
153
+ "character-map",
154
+ "output",
155
+ "strip-space",
156
+ "preserve-space",
157
+ "variable",
158
+ "param",
159
+ "with-param",
160
+ "function"
161
+ ]),
162
+ "http://schemas.xmlsoap.org/wsdl/": /* @__PURE__ */ new Set([
163
+ "definitions",
164
+ "binding",
165
+ "portType",
166
+ "message",
167
+ "service",
168
+ "port",
169
+ "operation"
170
+ ])
171
+ };
172
+ /**
173
+ * ATTRIBUTE NAMES whose unprefixed value names a declaration.
174
+ *
175
+ * A PREFIX is the primary signal and needs no table: a value written `tns:X`
176
+ * names a declaration, and the census over 3,000 corpus files puts 14,479 such
177
+ * occurrences on real naming attributes (`type` 7,770, `ref` 1,903, `base`
178
+ * 1,608, Spring's `serviceName` 1,526 and `endpointName` 1,516, …). A prefixed
179
+ * value is a reference wherever it appears.
180
+ *
181
+ * An UNPREFIXED value is not, and the first version of this rule treated it as
182
+ * one, which is the M60.R10 class in reverse: the census counted 9,391 bare
183
+ * `key="…"` values that are property names, 4,453 `use="optional|required"`,
184
+ * 1,404 `maxOccurs="unbounded"`, 14,023 `value="…"` and 2,157 `w:val="Normal"`,
185
+ * and the analyzer published references to names like `unbounded` and `Normal`
186
+ * that nothing declares as a type. So an unprefixed value is a reference only
187
+ * on an attribute in this set, and the set is the DECLARATION (this is the
188
+ * M60.R10 target-spelling register, per-vocabulary, with the census above as
189
+ * its evidence).
190
+ */
191
+ static REFERENCE_ATTRIBUTES = /* @__PURE__ */ new Set([
192
+ // XSD 1.0 §4.2 / §3.4.3 / §3.4.4 — the four attributes whose value is a type
193
+ // or element reference by definition.
194
+ "type",
195
+ "base",
196
+ "ref",
197
+ "itemType",
198
+ "memberTypes",
199
+ "substitutionGroup",
200
+ "refer",
201
+ // WSDL 1.1 §3.5 / §3.6, and the SOAP binding's own reference attributes.
202
+ "portType",
203
+ "binding",
204
+ // RelaxNG: a `name` on a `ref` names another pattern, not this one. The
205
+ // DECLARING uses of `name` (`rng:element`, `rng:attribute`) are handled by
206
+ // the declared-name rule and must not also become references, which is why
207
+ // `name` is NOT in this set and `rng:ref` is named explicitly below.
208
+ "pattern"
209
+ ]);
210
+ /**
211
+ * XSLT and RelaxNG name ONE construct by an attribute the generic set cannot
212
+ * carry, because the attribute is `name` and `name` is a DECLARATION on every
213
+ * sibling construct. Declared explicitly for that reason, and each is pinned by
214
+ * a fixture with its sibling case pinned the other way.
215
+ *
216
+ * `call-template` WAS here and was removed, and the removal is the M60
217
+ * ontology finding rather than a tidiness change. XSLT 1.0 §6 Named Templates
218
+ * is unambiguous about the construct:
219
+ *
220
+ * "An xsl:call-template element INVOKES A TEMPLATE BY NAME; it has a
221
+ * required name attribute that identifies the template to be invoked."
222
+ *
223
+ * and a named template is the only procedure the XML family has: it takes
224
+ * parameters (§11.6), is invoked by name, and returns a result tree fragment.
225
+ * Labelling that edge `references` says the document MENTIONS a name, when the
226
+ * document says it CALLS one — a `calls` edge existed in the ontology and the
227
+ * vocabulary was withholding it. So `call-template` emits `calls` (see
228
+ * `XSLT_CALL_ELEMENTS`), and `apply-templates` stays a `references` edge
229
+ * because XSLT 1.0 §5.4 gives `xsl:apply-templates` NO `name` attribute at
230
+ * all — §6 says so outright: "the name attribute on an xsl:template element
231
+ * does not affect whether the template is invoked by an xsl:apply-templates
232
+ * element". A dispatch by pattern and mode is not a call.
233
+ */
234
+ static NAMED_REFERENCE_ELEMENTS = {
235
+ "http://www.w3.org/1999/XSL/Transform": /* @__PURE__ */ new Set(["apply-templates"]),
236
+ "http://relaxng.org/ns/structure/1.0": /* @__PURE__ */ new Set(["ref"])
237
+ };
238
+ /**
239
+ * The XSLT instructions whose `name` attribute names a template that is
240
+ * INVOKED, which is a `calls` edge and not a `references` one.
241
+ *
242
+ * `xsl:call-template` is the whole list, and deliberately so. `xsl:apply-imports`
243
+ * (§5.6) invokes the rule a stylesheet overrides, but it names it by nothing —
244
+ * it takes no `name` — so there is no edge to write. `xsl:apply-templates`
245
+ * dispatches by pattern and mode and, in XSLT 1.0, carries no `name`.
246
+ */
247
+ static XSLT_CALL_ELEMENTS = /* @__PURE__ */ new Set(["call-template"]);
248
+ /**
249
+ * XSLT names a template at its definition and calls it by that name, so `name`
250
+ * on these two is a REFERENCE to a declaration elsewhere in the same document.
251
+ * The QName pass cannot see it: the value carries no prefix, so it is not a
252
+ * QName. Declared explicitly, for the same reason.
253
+ */
254
+ static XSLT_NAMED_TEMPLATE_REFERENCES = /* @__PURE__ */ new Set([
255
+ "call-template",
256
+ "apply-templates"
257
+ ]);
258
+ /**
259
+ * The M14 Matrix 1d evidence tier for each edge CLASS, assigned where the edge
260
+ * is written so a tier cannot drift from the rule that produced it.
261
+ *
262
+ * structural the parent/child or declaration/member relation is READ
263
+ * off the tree — the fact needs nothing outside the file
264
+ * import_based the target is a FILE, taken from a path the document
265
+ * itself writes
266
+ * reference_qualified the target is a NAME reached through a declared
267
+ * namespace prefix, or an XSLT named template reached by
268
+ * its declared name
269
+ * usage_contextual a containment the document states through an attribute
270
+ * rather than through nesting — the DTD content model and
271
+ * the NOTATION attribute type, which are the document
272
+ * saying what an element MAY contain
273
+ * inferred the edge is a reading the analyzer supplies and the
274
+ * document does not state outright
275
+ *
276
+ * XML emitted no tier at all before this, so the lane's Matrix 1d cell was
277
+ * silent; the completeness gate's section D now pins four of the five.
278
+ */
279
+ static EVIDENCE_TIER = {
280
+ xml_element: "structural",
281
+ xml_attribute: "structural",
282
+ xml_namespace: "structural",
283
+ dtd_element_declaration: "structural",
284
+ dtd_attribute_declaration: "structural",
285
+ dtd_content_model: "usage_contextual",
286
+ dtd_notation_reference: "usage_contextual",
287
+ // NOT `structural`, and the containment ledger is what moved it. XML 1.0 §4.5
288
+ // writes the clause as "NDATA NotationName", naming a notation that the DTD
289
+ // declares ELSEWHERE — and necessarily EARLIER, so the notation's declaration is
290
+ // never inside the entity's. Measured over the full 11,464-file admitted
291
+ // population: 2 edges, `libxslt/tests/general/bug-21-.xml` (`<!ENTITY test SYSTEM
292
+ // "test.jpg" NDATA JPEG>` with `<!NOTATION JPEG …>` at 96-130 and the entity at
293
+ // 131-174) and `libxslt/tests/general/bug-52.xml`, and in both the target's span lies
294
+ // entirely BEFORE the source's. A `structural` tier is a claim that nesting was
295
+ // observed, and no nesting was; `usage_contextual` already defines this exact shape
296
+ // one row up — "a containment the document states through an attribute rather than
297
+ // through nesting" — and the DTD content model is its precedent. Two edges is not
298
+ // worth a new relationship type, and relabelling them `references` would be worse:
299
+ // `references` resolution has four classes and this construct is not one of them.
300
+ dtd_ndata_notation: "usage_contextual",
301
+ dtd_entity_declaration: "structural",
302
+ dtd_parameter_entity_declaration: "structural",
303
+ dtd_notation_declaration: "structural",
304
+ processing_instruction: "structural",
305
+ pseudo_attribute: "structural",
306
+ doctype_external_id: "import_based",
307
+ entity_external_id: "import_based",
308
+ parameter_entity_external_id: "import_based",
309
+ notation_external_id: "import_based",
310
+ processing_instruction_href: "import_based",
311
+ schema_location: "import_based",
312
+ attribute_qname_reference: "reference_qualified",
313
+ // An XSLT named template INVOKED by name. `reference_qualified` because the
314
+ // document names its target — that is what makes the call resolvable — and
315
+ // not `structural`, because the tree says nothing about which template an
316
+ // instruction invokes until the name is read.
317
+ xslt_named_template_call: "reference_qualified",
318
+ // XSD type derivation, `xs:extension`/`xs:restriction` `base=`. `structural`:
319
+ // the vocabulary element IS the declaration of the relation, so the fact needs
320
+ // nothing outside the file, exactly as a parent/child edge does.
321
+ xsd_derivation_base: "structural",
322
+ // XSLT 1.0 §5.5 competing template rules. `structural`, not `inferred`: both
323
+ // declarations and the three attributes that make them compete are read straight
324
+ // off the tree, and the only thing the spec contributes is WHICH of two colliding
325
+ // declarations wins — a direction, recorded in `metadata.derivation`, not a guess
326
+ // about whether the relation exists. Leaving this role unmapped let `addRelationship`
327
+ // label every one of these edges `inferred`, which is the tier the ontology census
328
+ // counts as heuristic: a deterministic parse fact filed as a guess, and the census
329
+ // row `tier inferred = 0` would have become false without anything changing in the
330
+ // analyzer's behaviour.
331
+ xslt_competing_template_rule: "structural",
332
+ // An entity use is reached through a NAME in the document's own DTD, not
333
+ // through a declared namespace prefix — but it is a reference to a
334
+ // declaration all the same, and `usage_contextual` would say the document is
335
+ // only *using* the entity, which understates it: `&logo;` in content means the
336
+ // element's value IS that entity's replacement text. `reference_qualified` is
337
+ // the same claim an XSLT named-template call makes.
338
+ entity_reference: "reference_qualified",
339
+ xslt_named_template_reference: "reference_qualified",
340
+ // An XInclude directive. `import_based` is the tier for a FILE inclusion
341
+ // directive (the same tier cpp's `#include`, fsharp's `#load` and erlang's
342
+ // `-include` carry): the clause names another FILE, and the fact
343
+ // needs nothing outside the file to be read — but it is not a symbol
344
+ // reference, because the target is a file, not a declaration.
345
+ xinclude_part: "import_based",
346
+ // XSLT's `<xsl:include href>` — the SAME inclusion contract in the stylesheet
347
+ // vocabulary (XSLT 1.0 §2.6.1: the included stylesheet's content is treated as
348
+ // if it were written at the include's position), so the same tier.
349
+ xslt_include_part: "import_based",
350
+ // XSD definition composition: `<xs:group ref="G"/>` / `<xs:attributeGroup ref="G"/>`
351
+ // inside a type composes G's definitions into THIS type with no base and no
352
+ // conformance declared. `reference_qualified` because the document names the
353
+ // composed definition — the same reading a QName reference gets — and the
354
+ // relation the edge asserts (composition into this type) is decided by the
355
+ // vocabulary construct, not inferred.
356
+ xsd_definition_composition: "reference_qualified",
357
+ // XSLT's `<xsl:import href>` — a MODULE import with import precedence
358
+ // (XSLT 1.0 §2.6.2), not a textual splice: the imported definitions are in
359
+ // scope but may be overridden by the importing stylesheet's own. That is the
360
+ // `imports` relation, and it shares `imports`' tier.
361
+ xslt_stylesheet_import: "import_based"
362
+ };
363
+ /**
364
+ * THE XINCLUDE NAMESPACE — the W3C XML Inclusions vocabulary (XInclude 1.0,
365
+ * https://www.w3.org/TR/xinclude/).
366
+ *
367
+ * This is the only XML construct that says "splice the named document's content
368
+ * INTO this one at this point". It is identified by NAMESPACE, never by the
369
+ * written prefix: `xi` is a convention, not syntax, and a document may bind the
370
+ * vocabulary to any prefix or to the default namespace. The analyzer already
371
+ * resolves the element's effective namespace from the document's own `xmlns`
372
+ * declarations (`rememberNamespaces`), so the test is on that URI.
373
+ */
374
+ static XINCLUDE_NAMESPACE = "http://www.w3.org/2001/XInclude";
375
+ static BUILTIN_NAMESPACE_URIS = /* @__PURE__ */ new Set([
376
+ "http://www.w3.org/2001/XMLSchema",
377
+ "http://www.w3.org/XML/1998/namespace"
378
+ ]);
379
+ /**
380
+ * The five entities XML declares in EVERY document (XML 1.0 §4.6, "Entity
381
+ * declarations"), whether or not a DTD mentions them.
382
+ *
383
+ * The same shape of knowledge as `BUILTIN_NAMESPACE_URIS` above and the
384
+ * spec-bound prefixes below it, and needed for the same reason: the grammar
385
+ * reports each of them as an `EntityRef`, and a pass with only "declared or
386
+ * not" answers has to call all of them undefined. A document containing
387
+ * `&lt;` is well-formed in every conforming parser, so `unresolved` on it is
388
+ * not a cautious reading — it is a false claim about the document.
389
+ *
390
+ * Deliberately exactly five. `&nbsp;`, `&mdash;` and the rest of the HTML
391
+ * entity set are NOT predefined; an XML document that uses one without
392
+ * declaring it really does reference something undefined, and that stays
393
+ * `unresolved`.
394
+ */
395
+ static PREDEFINED_ENTITIES = /* @__PURE__ */ new Set([
396
+ "lt",
397
+ "gt",
398
+ "amp",
399
+ "apos",
400
+ "quot"
401
+ ]);
402
+ /**
403
+ * The two prefixes the Namespaces specification binds in EVERY document,
404
+ * whether or not the document says so (XML Namespaces 1.0 §4, and the
405
+ * Namespaces in XML 1.1 §2.2 which makes it a namespace name rather than a
406
+ * reserved prefix). Neither may be declared to anything else: re-binding `xml`
407
+ * is an error, and `xmlns` is bound to the XMLNS namespace.
408
+ *
409
+ * This table exists because a well-formed document CANNOT supply them, so a
410
+ * resolver that only reads declarations can never resolve them. See M60 X8.
411
+ */
412
+ /**
413
+ * The XML Schema built-in DERIVED types, from Part 2's own list. Written out
414
+ * rather than derived, because the analyzer and the corpus oracle each keep
415
+ * their own copy ON PURPOSE: two implementations that share a constant cannot
416
+ * disagree, and a constant that cannot be disagreed with cannot find a defect.
417
+ */
418
+ static XSD_BUILTIN_TYPE_NAMES = /* @__PURE__ */ new Set([
419
+ "anyType",
420
+ "anySimpleType",
421
+ "string",
422
+ "boolean",
423
+ "decimal",
424
+ "float",
425
+ "double",
426
+ "duration",
427
+ "dateTime",
428
+ "time",
429
+ "date",
430
+ "gYearMonth",
431
+ "gYear",
432
+ "gMonthDay",
433
+ "gDay",
434
+ "gMonth",
435
+ "hexBinary",
436
+ "base64Binary",
437
+ "anyURI",
438
+ "QName",
439
+ "NOTATION",
440
+ "normalizedString",
441
+ "token",
442
+ "language",
443
+ "NMTOKEN",
444
+ "NMTOKENS",
445
+ "Name",
446
+ "NCName",
447
+ "ID",
448
+ "IDREF",
449
+ "IDREFS",
450
+ "ENTITY",
451
+ "ENTITIES",
452
+ "integer",
453
+ "nonPositiveInteger",
454
+ "negativeInteger",
455
+ "long",
456
+ "int",
457
+ "short",
458
+ "byte",
459
+ "nonNegativeInteger",
460
+ "unsignedLong",
461
+ "unsignedInt",
462
+ "unsignedShort",
463
+ "unsignedByte",
464
+ "positiveInteger",
465
+ "dateTimeStamp",
466
+ "dayTimeDuration",
467
+ "yearMonthDuration",
468
+ "lang",
469
+ "space",
470
+ "base",
471
+ "id"
472
+ ]);
473
+ static IMPLICIT_PREFIX_BINDINGS = {
474
+ xml: "http://www.w3.org/XML/1998/namespace",
475
+ xmlns: "http://www.w3.org/2000/xmlns/"
476
+ };
477
+ /**
478
+ * THE LANE'S OWN FILE VOCABULARY, as ONE set with two readers — the two halves of the imports
479
+ * file claim must not be able to disagree about what a file of this lane is.
480
+ *
481
+ * The router maps exactly these seven extensions to language `xml`
482
+ * (`src/ast/language-detection.ts:197`, the M60 XML lane comment above it), the incoming gate's
483
+ * lane definition admits exactly these (`script/m14/incoming-gate.ts` LANES_B). The POSITIVE half
484
+ * of the claim passes them to `fileClaimForTarget` as the admitted set; the NEGATIVE half requires
485
+ * a bare target to end in one of them before it will call the target a file. A second spelling of
486
+ * the list in the same file would be a second rule, and the copy that drifted would be the one
487
+ * that mattered.
488
+ */
489
+ static IMPORT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
490
+ "xml",
491
+ "xsd",
492
+ "dtd",
493
+ "svg",
494
+ "xsl",
495
+ "xslt",
496
+ "rng"
497
+ ]);
498
+ /**
499
+ * Transient per-attribute context, set by the element branch before its
500
+ * attributes run. `bareNameReference` — the element is one that NAMES another
501
+ * declaration rather than being one (XSLT `call-template`, RelaxNG `ref`).
502
+ * `bareNameDeclarationElement` — the element DECLARES through `name`
503
+ * (RelaxNG `element`, `attribute`), so its `name` is a declaration and must not
504
+ * also become a reference to itself.
505
+ */
506
+ bareNameReference = false;
507
+ bareNameDeclarationElement = false;
508
+ /**
509
+ * The project's placeholder for an endpoint that could not be determined. Named
510
+ * here rather than inlined so the refusal that uses it and the census that checks
511
+ * it are reading the same string.
512
+ */
513
+ static PLACEHOLDER_LITERAL = "unknown";
514
+ /** `xsl:call-template`: its `name` attribute names a template it INVOKES. */
515
+ xsltCallElement = false;
516
+ /** An XSD `extension`/`restriction`: its `base` attribute names a SUPERtype. */
517
+ xsdDerivationElement = false;
518
+ derivationKind = "extension";
519
+ analyze(rootNode, content, filePath, context) {
520
+ this.bareNameReference = false;
521
+ this.bareNameDeclarationElement = false;
522
+ this.xsltCallElement = false;
523
+ this.xsdDerivationElement = false;
524
+ const symbols = [];
525
+ const relationships = [];
526
+ const imports = [];
527
+ const errors = [];
528
+ const seenRelationships = /* @__PURE__ */ new Map();
529
+ const publishedNames = /* @__PURE__ */ new Set();
530
+ const namespaceBindings = /* @__PURE__ */ new Map();
531
+ const bindingScopes = [];
532
+ const declaredNames = /* @__PURE__ */ new Set();
533
+ const importedNamespaces = /* @__PURE__ */ new Map();
534
+ const referenceCandidates = [];
535
+ const callCandidates = [];
536
+ const derivationCandidates = [];
537
+ const compositionCandidates = [];
538
+ const declaredTemplateNames = /* @__PURE__ */ new Set();
539
+ const competingTemplates = [];
540
+ const entityReferenceCandidates = [];
541
+ const parameterEntityReferences = [];
542
+ const shadowCandidates = [];
543
+ let errorNodes = 0;
544
+ let missingNodes = 0;
545
+ let documentElementName;
546
+ const publish = (symbol, identity) => {
547
+ symbols.push(symbol);
548
+ if (identity) publishedNames.add(identity);
549
+ return symbol;
550
+ };
551
+ const addRelationship = (source, target, type, metadata, targetNode) => {
552
+ if (!source || !target) return;
553
+ if (type === ASTEngine.RelationshipType.References) {
554
+ if (source === target) return;
555
+ }
556
+ const key = `${source}\0${type}\0${target}`;
557
+ const line = targetNode ? targetNode.startPosition.row + 1 : void 0;
558
+ const role = metadata.role;
559
+ const existing = seenRelationships.get(key);
560
+ if (existing) {
561
+ existing.metadata.occurrences = (existing.metadata.occurrences ?? 1) + 1;
562
+ if (role && role !== existing.metadata.role) {
563
+ const also = existing.metadata.alsoRoles ?? [];
564
+ if (!also.includes(role)) also.push(role);
565
+ existing.metadata.alsoRoles = also;
566
+ const tiers = existing.metadata.alsoRoleTiers ?? [];
567
+ if (!tiers.some((t) => t.role === role)) {
568
+ tiers.push({ role, tier: _RealXmlAnalyzer.EVIDENCE_TIER[role] ?? "inferred" });
569
+ }
570
+ existing.metadata.alsoRoleTiers = tiers;
571
+ }
572
+ const lines = existing.metadata.lines ?? (existing.line_number !== void 0 ? [existing.line_number] : []);
573
+ if (line !== void 0) {
574
+ if (!lines.includes(line)) lines.push(line);
575
+ if (existing.line_number === void 0 || line < existing.line_number) {
576
+ existing.line_number = line;
577
+ existing.metadata.line = line;
578
+ }
579
+ }
580
+ lines.sort((a, b) => a - b);
581
+ existing.metadata.lines = lines;
582
+ return;
583
+ }
584
+ const relationship = {
585
+ source,
586
+ target,
587
+ type,
588
+ confidence: 1,
589
+ line_number: line,
590
+ // M14 Matrix 1d: the tier is a property of the RULE that produced the
591
+ // edge, so it is stamped from a table keyed by that rule's role and an
592
+ // unrecognised role is `inferred` — which is the honest default, because
593
+ // a tier nobody can name is a reading the analyzer supplies rather than
594
+ // one the document states.
595
+ metadata: { ...metadata, occurrences: 1, evidenceClass: role && _RealXmlAnalyzer.EVIDENCE_TIER[role] || "inferred" }
596
+ };
597
+ seenRelationships.set(key, relationship);
598
+ relationships.push(relationship);
599
+ };
600
+ const ensureTargetSymbol = (name, ownerName, kind, node, metadata) => {
601
+ if (!name || !node) return name;
602
+ if (declaredNames.has(name)) return name;
603
+ const identity = ownerName ? `${ownerName}\0${name}` : name;
604
+ if (publishedNames.has(identity)) return name;
605
+ publish(
606
+ {
607
+ name,
608
+ kind,
609
+ range: this.nodeToRange(node),
610
+ visibility: "public",
611
+ ownerName,
612
+ ownerKind: ownerName ? ASTEngine.SymbolKind.Type : void 0,
613
+ qualifiedName: ownerName ? `${ownerName}.${name}` : name,
614
+ metadata: { ...metadata, nodeType: node.type }
615
+ },
616
+ identity
617
+ );
618
+ declaredNames.add(name);
619
+ return name;
620
+ };
621
+ const work = [{ node: rootNode }];
622
+ while (work.length > 0) {
623
+ const { node, ownerName, dtdName, bindDepth, nearestNamedDeclaration } = work.pop();
624
+ while (bindingScopes.length > (bindDepth ?? 0)) {
625
+ const scope = bindingScopes.pop();
626
+ namespaceBindings.clear();
627
+ for (const [k, v] of scope.outer) namespaceBindings.set(k, v);
628
+ }
629
+ if (node.isMissing) {
630
+ missingNodes++;
631
+ continue;
632
+ }
633
+ if (node.isError) errorNodes++;
634
+ if (node.type === "element") {
635
+ const tagNode = this.findDirectChild(node, "STag") || this.findDirectChild(node, "EmptyElemTag");
636
+ const nameNode = tagNode ? this.findDirectChild(tagNode, "Name") : null;
637
+ const name = nameNode?.text.trim();
638
+ if (name && tagNode) {
639
+ bindingScopes.push({ outer: new Map(namespaceBindings) });
640
+ this.rememberNamespaces(tagNode, namespaceBindings);
641
+ const colon = name.indexOf(":");
642
+ const prefix = colon >= 0 ? name.slice(0, colon) : "";
643
+ const localName = colon >= 0 ? name.slice(colon + 1) : name;
644
+ const effectiveNamespace = namespaceBindings.get(prefix) ?? (colon < 0 ? namespaceBindings.get("") : void 0);
645
+ const declaration = this.declaredNameOf(effectiveNamespace, localName, tagNode);
646
+ const declaredName = declaration?.name;
647
+ const symbolName = declaredName ?? name;
648
+ if (prefix.length > 0 && namespaceBindings.has(prefix)) {
649
+ addRelationship(symbolName, prefix, ASTEngine.RelationshipType.Uses, {
650
+ role: "xml_namespace_prefix_use",
651
+ namespaceUri: namespaceBindings.get(prefix),
652
+ resolvable: "in-document"
653
+ }, nameNode);
654
+ }
655
+ const isNamedVocabularyDeclaration = effectiveNamespace !== void 0 && _RealXmlAnalyzer.DECLARATION_VOCABULARIES[effectiveNamespace]?.has(localName) === true && declaredName !== void 0;
656
+ const namedDeclarationForChildren = isNamedVocabularyDeclaration ? symbolName : nearestNamedDeclaration;
657
+ const elementSymbol = {
658
+ name: symbolName,
659
+ kind: ASTEngine.SymbolKind.Class,
660
+ // M19 (master review, 2026-09-29): a VOCABULARY DECLARATION is written
661
+ // on its `name=` attribute, not on the tag that opens it, and the two
662
+ // are routinely on DIFFERENT LINES. Measured over the 2,500-file
663
+ // blast radius: 4 of 19,852 declarations reported the tag's line, so
664
+ // a search for the symbol landed on `<xsl:with-param` rather than on
665
+ // `name="content"` a line below. The symbol's range therefore starts
666
+ // at the ATTRIBUTE, while `elementRange` below keeps the structural
667
+ // span so a reader can still find the element itself. Before this the
668
+ // two readings shared one field and one of them had to be wrong.
669
+ //
670
+ // M60 (containment ledger, 2026-10-04): the ANCHOR is right about the
671
+ // line and wrong about the SPAN, and the span is what `contains` is made
672
+ // of. Anchoring the same-line case to the `name=` Attribute put the
673
+ // declaration's range INSIDE its own element, so
674
+ //
675
+ // <define name="boolean"> symbol `boolean` @ name="boolean"
676
+ // <choice> symbol `choice` @ <choice>…</choice>
677
+ //
678
+ // produced the edge `boolean -contains-> choice` — which is TRUE — with a
679
+ // target that is not inside the source, and the attribute symbol `name`
680
+ // occupied the identical byte range as the declaration itself.
681
+ // Measured over 300 corpus files: 194 contains edges unverifiable by span
682
+ // nesting because two names shared one range, and 280 more whose source
683
+ // span did not enclose a child the source really does contain. Both are
684
+ // invisible to a resolution rate, which is 100 % on this type.
685
+ //
686
+ // So the all-on-one-line case anchors to the ELEMENT, and the reported LINE
687
+ // cannot move, because that is exactly the condition under which the
688
+ // element's row and the attribute's row are the same row. The other two
689
+ // shapes keep the Attribute / AttValue anchor and M19 exactly as measured —
690
+ // at the cost of leaving their containment unverifiable, which is 4 of
691
+ // 19,852 declarations and is declared rather than rounded away.
692
+ range: declaration ? this.nodeToRange(declaration.allOnOneLine ? node : declaration.nameNode) : this.nodeToRange(node),
693
+ visibility: "public",
694
+ ownerName,
695
+ ownerKind: ownerName ? ASTEngine.SymbolKind.Class : void 0,
696
+ qualifiedName: ownerName ? `${ownerName}.${symbolName}` : symbolName,
697
+ metadata: {
698
+ nodeType: "element",
699
+ isXmlElement: true,
700
+ tagName: name,
701
+ isSchemaConstruct: this.isSchemaConstruct(name),
702
+ // The DECLARED name, when the vocabulary puts it in the `name`
703
+ // attribute. `tagName` stays the tag, so the structural reading is
704
+ // carried rather than overwritten.
705
+ ...declaration ? { isVocabDeclaration: true, declaredName: declaration.name, declarationKind: localName, declaredByAttribute: declaration.attribute, namespaceUri: effectiveNamespace, elementRange: this.nodeToRange(node) } : {}
706
+ }
707
+ };
708
+ publish(elementSymbol, ownerName ? `${ownerName}\0${symbolName}` : symbolName);
709
+ declaredNames.add(symbolName);
710
+ if (documentElementName === void 0 && ownerName === void 0) documentElementName = symbolName;
711
+ const isXIncludeElement = effectiveNamespace === _RealXmlAnalyzer.XINCLUDE_NAMESPACE && localName === "include";
712
+ const isXsltIncludeElement = effectiveNamespace === "http://www.w3.org/1999/XSL/Transform" && localName === "include";
713
+ const isXsltImportElement = effectiveNamespace === "http://www.w3.org/1999/XSL/Transform" && localName === "import";
714
+ if (isXIncludeElement || isXsltIncludeElement) {
715
+ const hrefAttr = this.findDirectAttribute(tagNode, "href");
716
+ const hrefPath = (this.attributeValueOf(tagNode, "href") ?? "").trim();
717
+ if (hrefAttr && hrefPath.length > 0 && !this.isRemoteUri(hrefPath)) {
718
+ const partSymbol = _RealXmlAnalyzer.includedModuleStem(hrefPath);
719
+ if (partSymbol.length > 0) {
720
+ addRelationship(documentElementName ?? symbolName, partSymbol, ASTEngine.RelationshipType.PartOf, {
721
+ // The VOCABULARY rides on the role, so a consumer can tell the two
722
+ // inclusion channels apart without re-deriving namespace bindings.
723
+ role: isXIncludeElement ? "xinclude_part" : "xslt_include_part",
724
+ // Mandatory, per `src/ast/types.ts` — see the block above.
725
+ partRole: "library",
726
+ // THE PATH IS EVIDENCE, THE STEM IS THE TARGET. Keeping both is
727
+ // what makes the difference checkable instead of inferred.
728
+ partPath: hrefPath,
729
+ partSymbol,
730
+ // XInclude alone can spell a TEXT inclusion; XSLT's include is
731
+ // always a parsed stylesheet, so the field is present only where
732
+ // the vocabulary gives it a value.
733
+ ...isXIncludeElement ? { parseMode: (this.attributeValueOf(tagNode, "parse") ?? "") === "text" ? "text" : "xml" } : {},
734
+ // The written element, so the construct is identified on the row
735
+ // without the consumer having to know the namespace URI.
736
+ elementName: name,
737
+ isFileInclusion: true,
738
+ // A part is NOT an import (the contract keeps them distinct): the
739
+ // included content does not create an import binding, it becomes
740
+ // this document's content.
741
+ isImport: false
742
+ }, this.findDirectChild(hrefAttr, "AttValue"));
743
+ }
744
+ }
745
+ } else if (isXsltImportElement) {
746
+ const hrefAttr = this.findDirectAttribute(tagNode, "href");
747
+ const hrefPath = (this.attributeValueOf(tagNode, "href") ?? "").trim();
748
+ if (hrefAttr && hrefPath.length > 0 && !this.isRemoteUri(hrefPath)) {
749
+ addRelationship(documentElementName ?? symbolName, hrefPath, ASTEngine.RelationshipType.Imports, {
750
+ role: "xslt_stylesheet_import",
751
+ attributeName: "href",
752
+ // A module import, and the row says so rather than leaving a consumer
753
+ // to infer it from the element name.
754
+ importKind: "stylesheet",
755
+ resolvable: this.importTargetClass(hrefPath)
756
+ }, this.findDirectChild(hrefAttr, "AttValue"));
757
+ }
758
+ }
759
+ if (declaration && effectiveNamespace === "http://www.w3.org/1999/XSL/Transform" && localName === "template") {
760
+ declaredTemplateNames.add(symbolName);
761
+ }
762
+ if (effectiveNamespace === "http://www.w3.org/1999/XSL/Transform" && localName === "template") {
763
+ const matchAttr = this.findDirectAttribute(tagNode, "match");
764
+ if (matchAttr) {
765
+ const matchValue = this.attributeValueOf(tagNode, "match") ?? "";
766
+ const modeValue = this.attributeValueOf(tagNode, "mode") ?? "";
767
+ const priorityValue = this.attributeValueOf(tagNode, "priority") ?? "";
768
+ competingTemplates.push({
769
+ key: `${matchValue}|${modeValue}|${priorityValue}`,
770
+ declaredName: declaration ? symbolName : "",
771
+ match: matchValue,
772
+ mode: modeValue,
773
+ priority: priorityValue,
774
+ node,
775
+ anchorNode: matchAttr,
776
+ ownerName
777
+ });
778
+ }
779
+ }
780
+ if (ownerName) {
781
+ addRelationship(ownerName, symbolName, ASTEngine.RelationshipType.Contains, {
782
+ role: "xml_element",
783
+ ...declaration ? { tagName: name, declarationKind: localName } : {}
784
+ // M60 requires the edge's line to be the line the TARGET NAME is
785
+ // written on, and `addRelationship` already takes that node for
786
+ // exactly this reason (M59.A7). This call site was passing the
787
+ // ELEMENT, so a multi-line declaration pointed one line too high.
788
+ }, declaration ? declaration.nameNode : node);
789
+ }
790
+ for (let i = 0; i < tagNode.namedChildCount; i++) {
791
+ const child = tagNode.namedChild(i);
792
+ if (!child || child.type !== "Attribute") continue;
793
+ this.extractAttribute(
794
+ child,
795
+ symbolName,
796
+ symbols,
797
+ addRelationship,
798
+ publishedNames,
799
+ declaredNames,
800
+ namespaceBindings,
801
+ referenceCandidates,
802
+ ensureTargetSymbol,
803
+ localName,
804
+ effectiveNamespace,
805
+ callCandidates,
806
+ derivationCandidates,
807
+ compositionCandidates,
808
+ ownerName,
809
+ nearestNamedDeclaration,
810
+ isNamedVocabularyDeclaration ? symbolName : void 0
811
+ );
812
+ }
813
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
814
+ const child = node.namedChild(i);
815
+ if (child && child !== tagNode) work.push({ node: child, ownerName: symbolName, dtdName, bindDepth: bindingScopes.length, nearestNamedDeclaration: namedDeclarationForChildren });
816
+ }
817
+ continue;
818
+ }
819
+ }
820
+ if (node.type === "doctypedecl") {
821
+ const nameNode = this.findDirectChild(node, "Name");
822
+ const positionedName = this.isPositionedDeclarationName(node, nameNode) ? nameNode.text.trim() : void 0;
823
+ const currentDtdName = positionedName || dtdName;
824
+ if (positionedName) {
825
+ publish(
826
+ {
827
+ name: positionedName,
828
+ kind: ASTEngine.SymbolKind.Type,
829
+ range: this.nodeToRange(node),
830
+ visibility: "public",
831
+ metadata: { nodeType: node.type, isDoctype: true, dtdName: positionedName }
832
+ },
833
+ positionedName
834
+ );
835
+ declaredNames.add(positionedName);
836
+ }
837
+ const externalId = this.findDirectChild(node, "ExternalID");
838
+ if (externalId && currentDtdName) {
839
+ this.extractExternalId(externalId, currentDtdName, "doctype_external_id", addRelationship, imports);
840
+ }
841
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
842
+ const child = node.namedChild(i);
843
+ if (child && child !== nameNode && child !== externalId) work.push({ node: child, ownerName, dtdName: currentDtdName });
844
+ }
845
+ continue;
846
+ }
847
+ if (node.type === "elementdecl") {
848
+ const nameNode = this.findDirectChild(node, "Name");
849
+ const name = nameNode?.text.trim();
850
+ if (name) {
851
+ publish(
852
+ {
853
+ name,
854
+ kind: ASTEngine.SymbolKind.Type,
855
+ range: this.nodeToRange(node),
856
+ visibility: "public",
857
+ ownerName: dtdName,
858
+ ownerKind: dtdName ? ASTEngine.SymbolKind.Type : void 0,
859
+ metadata: { nodeType: node.type, isDtdElementDeclaration: true, dtdName }
860
+ },
861
+ dtdName ? `${dtdName}\0${name}` : name
862
+ );
863
+ declaredNames.add(name);
864
+ if (dtdName) addRelationship(dtdName, name, ASTEngine.RelationshipType.Contains, { role: "dtd_element_declaration" }, nameNode);
865
+ const contentSpec = this.findDirectChild(node, "contentspec");
866
+ if (contentSpec) {
867
+ for (const member of this.descendantsOfType(contentSpec, "Name")) {
868
+ const memberName = member.text.trim();
869
+ if (!memberName) continue;
870
+ shadowCandidates.push({
871
+ name: memberName,
872
+ ownerName: dtdName,
873
+ node: member,
874
+ metadata: { isDtdContentModelMember: true, isDoctype: true, dtdName },
875
+ edge: { source: name, role: "dtd_content_model", dtdName }
876
+ });
877
+ }
878
+ }
879
+ }
880
+ }
881
+ if (node.type === "AttlistDecl") {
882
+ const elementName = this.findDirectChild(node, "Name")?.text.trim();
883
+ if (elementName) {
884
+ for (let i = 0; i < node.namedChildCount; i++) {
885
+ const attDef = node.namedChild(i);
886
+ if (!attDef || attDef.type !== "AttDef") continue;
887
+ const attrNameNode = this.findDirectChild(attDef, "Name");
888
+ const attrName = attrNameNode?.text.trim();
889
+ if (!attrName) continue;
890
+ const defaultDecl = this.findDirectChild(attDef, "DefaultDecl");
891
+ const attrType = attDef.namedChildCount > 0 ? this.findFirstNamedChild(attDef, /* @__PURE__ */ new Set(["StringType", "TokenizedType", "Enumeration", "NotationType"])) : null;
892
+ publish(
893
+ {
894
+ name: attrName,
895
+ kind: ASTEngine.SymbolKind.Property,
896
+ range: this.nodeToRange(attDef),
897
+ visibility: "public",
898
+ ownerName: elementName,
899
+ ownerKind: ASTEngine.SymbolKind.Type,
900
+ metadata: {
901
+ nodeType: "AttDef",
902
+ isDtdAttributeDeclaration: true,
903
+ dtdElement: elementName,
904
+ attributeType: attrType?.text,
905
+ defaultDeclaration: defaultDecl?.text
906
+ }
907
+ },
908
+ `${elementName}\0${attrName}`
909
+ );
910
+ addRelationship(elementName, attrName, ASTEngine.RelationshipType.Contains, { role: "dtd_attribute_declaration" }, attrNameNode);
911
+ if (defaultDecl) {
912
+ const stack = [defaultDecl];
913
+ while (stack.length > 0) {
914
+ const n = stack.pop();
915
+ if (n.type === "EntityRef") {
916
+ const text = n.text.trim();
917
+ if (text.length > 2 && text.startsWith("&") && text.endsWith(";")) {
918
+ const ref = text.slice(1, -1);
919
+ if (ref.length > 0) {
920
+ entityReferenceCandidates.push({ ownerName: attrName, name: ref, node: n, textNode: n });
921
+ }
922
+ }
923
+ continue;
924
+ }
925
+ for (let i2 = n.namedChildCount - 1; i2 >= 0; i2--) {
926
+ const c = n.namedChild(i2);
927
+ if (c) stack.push(c);
928
+ }
929
+ }
930
+ }
931
+ for (const notation of this.descendantsOfType(attDef, "NotationType")) {
932
+ for (const notationName of this.descendantsOfType(notation, "Name")) {
933
+ const target = notationName.text.trim();
934
+ if (!target) continue;
935
+ shadowCandidates.push({
936
+ name: target,
937
+ ownerName: dtdName,
938
+ node: notationName,
939
+ metadata: { isDtdNotation: true, dtdName },
940
+ edge: { source: elementName, role: "dtd_notation_reference", dtdName }
941
+ });
942
+ }
943
+ }
944
+ }
945
+ }
946
+ }
947
+ if (node.type === "GEDecl" || node.type === "PEDecl" || node.type === "NotationDecl") {
948
+ const nameNode = this.findDirectChild(node, "Name");
949
+ const name = nameNode?.text.trim();
950
+ const isParameter = node.type === "PEDecl";
951
+ const isNotation = node.type === "NotationDecl";
952
+ if (name) {
953
+ {
954
+ publish(
955
+ {
956
+ name,
957
+ kind: ASTEngine.SymbolKind.Type,
958
+ range: this.nodeToRange(node),
959
+ visibility: "public",
960
+ ownerName: dtdName,
961
+ ownerKind: dtdName ? ASTEngine.SymbolKind.Type : void 0,
962
+ metadata: {
963
+ nodeType: node.type,
964
+ isDtdEntityDeclaration: true,
965
+ isDtdParameterEntity: isParameter,
966
+ isDtdNotationDeclaration: isNotation,
967
+ dtdName
968
+ }
969
+ },
970
+ dtdName ? `${dtdName}\0${name}` : name
971
+ );
972
+ }
973
+ declaredNames.add(name);
974
+ if (dtdName) {
975
+ addRelationship(dtdName, name, ASTEngine.RelationshipType.Contains, {
976
+ role: isParameter ? "dtd_parameter_entity_declaration" : isNotation ? "dtd_notation_declaration" : "dtd_entity_declaration",
977
+ dtdName
978
+ }, nameNode);
979
+ }
980
+ const externalId = this.findDirectChild(node, "ExternalID");
981
+ if (externalId) {
982
+ this.extractExternalId(
983
+ externalId,
984
+ name,
985
+ isParameter ? "parameter_entity_external_id" : isNotation ? "notation_external_id" : "entity_external_id",
986
+ addRelationship,
987
+ imports
988
+ );
989
+ }
990
+ const ndata = this.findDirectChild(node, "NDataDecl");
991
+ const ndataName = ndata ? this.findDirectChild(ndata, "Name") : null;
992
+ if (ndataName) {
993
+ const target = ndataName.text.trim();
994
+ if (target) {
995
+ shadowCandidates.push({
996
+ name: target,
997
+ ownerName: dtdName,
998
+ node: ndataName,
999
+ metadata: { isDtdNotation: true, dtdName },
1000
+ edge: { source: name, role: "dtd_ndata_notation", dtdName }
1001
+ });
1002
+ }
1003
+ }
1004
+ {
1005
+ const value = this.findDirectChild(node, "EntityValue");
1006
+ if (value) {
1007
+ const stack = [value];
1008
+ while (stack.length > 0) {
1009
+ const n = stack.pop();
1010
+ if (n.type === "EntityRef") {
1011
+ const text = n.text.trim();
1012
+ if (text.length > 2 && text.startsWith("&") && text.endsWith(";")) {
1013
+ const ref = text.slice(1, -1);
1014
+ if (ref.length > 0) {
1015
+ entityReferenceCandidates.push({ ownerName: name, name: ref, node: n, textNode: n });
1016
+ }
1017
+ }
1018
+ continue;
1019
+ }
1020
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1021
+ const c = n.namedChild(i);
1022
+ if (c) stack.push(c);
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ if (node.type === "PI") {
1030
+ const targetNode = this.findDirectChild(node, "PITarget");
1031
+ const target = targetNode?.text.trim();
1032
+ if (target && target.toLowerCase() !== "xml") {
1033
+ publish(
1034
+ {
1035
+ name: target,
1036
+ kind: ASTEngine.SymbolKind.Module,
1037
+ range: this.nodeToRange(node),
1038
+ visibility: "public",
1039
+ ownerName,
1040
+ ownerKind: ownerName ? ASTEngine.SymbolKind.Class : void 0,
1041
+ metadata: { nodeType: node.type, isProcessingInstruction: true, target }
1042
+ },
1043
+ ownerName ? `${ownerName}\0${target}` : target
1044
+ );
1045
+ if (ownerName) addRelationship(ownerName, target, ASTEngine.RelationshipType.Contains, { role: "processing_instruction" }, targetNode);
1046
+ }
1047
+ }
1048
+ if (node.type === "StyleSheetPI" || node.type === "XmlModelPI") {
1049
+ const label = node.type === "StyleSheetPI" ? "xml-stylesheet" : "xml-model";
1050
+ publish(
1051
+ {
1052
+ name: label,
1053
+ kind: ASTEngine.SymbolKind.Module,
1054
+ range: this.nodeToRange(node),
1055
+ visibility: "public",
1056
+ metadata: { nodeType: node.type, isProcessingInstruction: true, target: label, pseudoInstruction: true }
1057
+ },
1058
+ label
1059
+ );
1060
+ for (let i = 0; i < node.namedChildCount; i++) {
1061
+ const pseudo = node.namedChild(i);
1062
+ if (!pseudo || pseudo.type !== "PseudoAtt") continue;
1063
+ const pseudoNameNode = this.findDirectChild(pseudo, "Name");
1064
+ const valueNode = this.findDirectChild(pseudo, "PseudoAttValue");
1065
+ const attributeName = pseudoNameNode?.text.trim();
1066
+ if (!attributeName) continue;
1067
+ const value = valueNode ? this.unquote(valueNode.text).trim() : void 0;
1068
+ if (attributeName === "href" && value) {
1069
+ const targetClass = this.importTargetClass(value);
1070
+ addRelationship(label, value, ASTEngine.RelationshipType.Imports, {
1071
+ role: "processing_instruction_href",
1072
+ attributeName,
1073
+ resolvable: targetClass,
1074
+ ...targetClass === "external" ? { isExternalTarget: true } : {}
1075
+ }, valueNode);
1076
+ imports.push({
1077
+ module: value,
1078
+ path: value,
1079
+ importType: "xml-stylesheet",
1080
+ range: this.nodeToRange(valueNode ?? pseudo),
1081
+ metadata: { instruction: label, attributeName }
1082
+ });
1083
+ } else {
1084
+ publish(
1085
+ {
1086
+ name: attributeName,
1087
+ kind: ASTEngine.SymbolKind.Property,
1088
+ range: this.nodeToRange(pseudo),
1089
+ visibility: "public",
1090
+ ownerName: label,
1091
+ ownerKind: ASTEngine.SymbolKind.Module,
1092
+ metadata: { nodeType: pseudo.type, isPseudoAttribute: true, value }
1093
+ },
1094
+ `${label}\0${attributeName}`
1095
+ );
1096
+ addRelationship(label, attributeName, ASTEngine.RelationshipType.Contains, { role: "pseudo_attribute" }, pseudoNameNode);
1097
+ }
1098
+ }
1099
+ }
1100
+ if (node.type === "EntityRef" && ownerName) {
1101
+ const text = node.text.trim();
1102
+ if (text.length > 2 && text.startsWith("&") && text.endsWith(";")) {
1103
+ const name = text.slice(1, -1);
1104
+ if (name.length > 0) entityReferenceCandidates.push({ ownerName: nearestNamedDeclaration ?? ownerName, name, node, textNode: node });
1105
+ }
1106
+ }
1107
+ if (node.type === "PEReference") {
1108
+ const text = node.text.trim();
1109
+ if (text.length > 1 && text.startsWith("%")) {
1110
+ const semi = text.indexOf(";");
1111
+ if (semi > 1) {
1112
+ const ref = text.slice(1, semi).trim();
1113
+ const isValueForm = ref.length === 0 || _RealXmlAnalyzer.containsEcmaWhitespace(ref);
1114
+ const inEntityValue = this.withinEntityValue(node);
1115
+ if (!isValueForm && ref.length > 0) {
1116
+ const ownerForRef = inEntityValue ? this.enclosingDeclaredName(node) ?? dtdName ?? "" : dtdName ?? "";
1117
+ if (ownerForRef.length > 0) {
1118
+ parameterEntityReferences.push({
1119
+ ownerName: ownerForRef,
1120
+ name: ref,
1121
+ node,
1122
+ textNode: node,
1123
+ recovery: !!this.withinErrorRegion(node)
1124
+ });
1125
+ }
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+ const childDepth = bindDepth ?? 0;
1131
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1132
+ const child = node.namedChild(i);
1133
+ if (child) work.push({ node: child, ownerName, dtdName, bindDepth: childDepth, nearestNamedDeclaration });
1134
+ }
1135
+ }
1136
+ for (const candidate of shadowCandidates) {
1137
+ ensureTargetSymbol(candidate.name, candidate.ownerName, ASTEngine.SymbolKind.Type, candidate.node, candidate.metadata);
1138
+ addRelationship(candidate.edge.source, candidate.name, ASTEngine.RelationshipType.Contains, {
1139
+ role: candidate.edge.role,
1140
+ dtdName: candidate.edge.dtdName
1141
+ }, candidate.node);
1142
+ }
1143
+ {
1144
+ const IMPORT_ELEMENTS = /* @__PURE__ */ new Set(["import", "include", "import-schema"]);
1145
+ const stack = [rootNode];
1146
+ while (stack.length) {
1147
+ const n = stack.pop();
1148
+ for (let i = 0; i < n.childCount; i++) {
1149
+ const c = n.child(i);
1150
+ if (c) stack.push(c);
1151
+ }
1152
+ if (n.type !== "element") continue;
1153
+ const tag = n.children.find((c) => c && (c.type === "STag" || c.type === "EmptyElemTag"));
1154
+ if (!tag) continue;
1155
+ const local = this.findDirectChild(tag, "Name")?.text.trim().split(":").pop() ?? "";
1156
+ if (!IMPORT_ELEMENTS.has(local)) continue;
1157
+ let nsUri;
1158
+ let path2;
1159
+ for (let i = 0; i < tag.namedChildCount; i++) {
1160
+ const attr = tag.namedChild(i);
1161
+ if (attr?.type !== "Attribute") continue;
1162
+ const an = this.findDirectChild(attr, "Name")?.text.trim();
1163
+ const av = this.findDirectChild(attr, "AttValue");
1164
+ const value = av ? this.unquote(av.text).trim() : void 0;
1165
+ if (!an || value === void 0) continue;
1166
+ if (an === "namespace") nsUri = value;
1167
+ else if (an === "schemaLocation") {
1168
+ const parts = _RealXmlAnalyzer.splitOnWhitespace(value);
1169
+ path2 = parts.length % 2 === 0 ? parts[parts.length - 1] : parts[0];
1170
+ } else if (an === "href") path2 = value;
1171
+ }
1172
+ if (nsUri) importedNamespaces.set(nsUri, { path: path2, element: local });
1173
+ }
1174
+ }
1175
+ for (const candidate of referenceCandidates) {
1176
+ if (candidate.bareNameReference) {
1177
+ if (!declaredNames.has(candidate.value)) continue;
1178
+ addRelationship(candidate.ownerName, candidate.value, ASTEngine.RelationshipType.References, {
1179
+ role: "xslt_named_template_reference",
1180
+ attributeName: candidate.attributeName,
1181
+ resolvable: "in-document"
1182
+ }, candidate.valueNode);
1183
+ continue;
1184
+ }
1185
+ const qname = this.asQName(candidate.value);
1186
+ if (!qname) continue;
1187
+ const resolvableInDocument = declaredNames.has(qname.local);
1188
+ const isBuiltin = candidate.binding !== void 0 && _RealXmlAnalyzer.BUILTIN_NAMESPACE_URIS.has(candidate.binding);
1189
+ const imported = !resolvableInDocument && !isBuiltin && candidate.binding !== void 0 ? importedNamespaces.get(candidate.binding) : void 0;
1190
+ const ownerIsVocabulary = candidate.ownerNs !== void 0 && _RealXmlAnalyzer.DECLARATION_VOCABULARIES[candidate.ownerNs] !== void 0;
1191
+ const isBareValue = qname.prefix.length === 0;
1192
+ const unresolvedBareReference = !resolvableInDocument && !isBuiltin && !imported && isBareValue && ownerIsVocabulary;
1193
+ if (!resolvableInDocument && !isBuiltin && !imported && !unresolvedBareReference) continue;
1194
+ const ambiguousWithBuiltin = unresolvedBareReference && _RealXmlAnalyzer.XSD_BUILTIN_TYPE_NAMES.has(qname.local);
1195
+ if (resolvableInDocument) {
1196
+ ensureTargetSymbol(qname.local, void 0, ASTEngine.SymbolKind.Type, candidate.nameNode, { isReferencedDeclaration: true });
1197
+ }
1198
+ const claimsExternalTarget = isBuiltin || !resolvableInDocument && !unresolvedBareReference;
1199
+ addRelationship(candidate.ownerName, qname.local, ASTEngine.RelationshipType.References, {
1200
+ role: "attribute_qname_reference",
1201
+ attributeName: candidate.attributeName,
1202
+ qname: candidate.value,
1203
+ prefix: qname.prefix,
1204
+ namespaceUri: candidate.binding,
1205
+ resolvable: isBuiltin ? "builtin" : resolvableInDocument ? "in-document" : unresolvedBareReference ? "unresolved" : "external",
1206
+ ...claimsExternalTarget ? { isExternalTarget: true } : {},
1207
+ // X13: the name is also a built-in, and this file cannot say whether the
1208
+ // document means the built-in or a same-named type an `xs:include` brought
1209
+ // in. Recorded so a consumer can see the ambiguity instead of inheriting
1210
+ // whichever reading the graph happened to take.
1211
+ ...ambiguousWithBuiltin ? { ambiguousWithBuiltin: true } : {},
1212
+ // where the name actually lives, so a consumer can follow it rather than
1213
+ // having to guess from the namespace alone
1214
+ ...imported ? { importedFrom: imported.path, importedBy: imported.element } : {}
1215
+ }, candidate.valueNode);
1216
+ }
1217
+ for (const cand of callCandidates) {
1218
+ if (!declaredTemplateNames.has(cand.templateName)) continue;
1219
+ addRelationship(cand.ownerName, cand.templateName, ASTEngine.RelationshipType.Calls, {
1220
+ role: "xslt_named_template_call",
1221
+ attributeName: "name",
1222
+ templateName: cand.templateName,
1223
+ // A name resolved against the stylesheet's own declarations, which is what
1224
+ // makes this edge different from a mention. XSLT 2.0 §6.1.3 adds
1225
+ // `xsl:use-when`/`use-unless` to conditional dispatch, and neither can change
1226
+ // WHICH template is invoked.
1227
+ resolvable: "in-document"
1228
+ }, cand.textNode);
1229
+ }
1230
+ {
1231
+ const byKey = /* @__PURE__ */ new Map();
1232
+ for (const t of competingTemplates) {
1233
+ const arr = byKey.get(t.key) ?? [];
1234
+ arr.push(t);
1235
+ byKey.set(t.key, arr);
1236
+ }
1237
+ for (const group of byKey.values()) {
1238
+ if (group.length < 2) continue;
1239
+ const declared = group.map((t) => t.declaredName);
1240
+ const useDeclaredNames = declared.every((d) => d.length > 0) && new Set(declared).size === declared.length;
1241
+ const identityOf = (t, index) => {
1242
+ if (useDeclaredNames) return t.declaredName;
1243
+ const derived = `xsl:template[match=${t.match}|mode=${t.mode}|priority=${t.priority}]#${index}`;
1244
+ ensureTargetSymbol(derived, t.ownerName, ASTEngine.SymbolKind.Class, t.anchorNode, {
1245
+ nodeType: "element",
1246
+ tagName: "xsl:template",
1247
+ isXmlElement: true,
1248
+ // Marked, so a consumer can tell a derived identity from a declared one
1249
+ // instead of inferring it from the punctuation.
1250
+ derivedTemplateIdentity: true,
1251
+ // The `name` attribute is carried here because a colliding pair usually
1252
+ // shares one, or has none: dropping it would lose the only name the
1253
+ // document actually wrote.
1254
+ ...t.declaredName.length > 0 ? { declaredName: t.declaredName } : {},
1255
+ templateMatch: t.match,
1256
+ templateMode: t.mode,
1257
+ templatePriority: t.priority,
1258
+ templateOccurrenceInGroup: index
1259
+ });
1260
+ return derived;
1261
+ };
1262
+ const identities = group.map(identityOf);
1263
+ for (let i = 1; i < group.length; i++) {
1264
+ const winner = group[i];
1265
+ const source = identities[i];
1266
+ const target = identities[i - 1];
1267
+ if (source.length === 0 || target.length === 0 || source === target) continue;
1268
+ addRelationship(source, target, ASTEngine.RelationshipType.Overrides, {
1269
+ role: "xslt_competing_template_rule",
1270
+ // The three attributes §5.5 compares, carried so a consumer can re-derive
1271
+ // the collision instead of trusting the edge.
1272
+ templateMatch: winner.match,
1273
+ templateMode: winner.mode,
1274
+ templatePriority: winner.priority,
1275
+ derivation: "xslt-5.5-later-template-replaces-earlier",
1276
+ resolvable: "in-document",
1277
+ ...winner.declaredName.length > 0 ? { declaredName: winner.declaredName } : {}
1278
+ }, winner.anchorNode);
1279
+ }
1280
+ }
1281
+ }
1282
+ for (const cand of derivationCandidates) {
1283
+ const qname = this.asQName(cand.value);
1284
+ if (!qname) continue;
1285
+ const resolvableInDocument = declaredNames.has(qname.local);
1286
+ const isBuiltin = cand.binding !== void 0 && _RealXmlAnalyzer.BUILTIN_NAMESPACE_URIS.has(cand.binding);
1287
+ const imported = !resolvableInDocument && !isBuiltin && cand.binding !== void 0 ? importedNamespaces.get(cand.binding) : void 0;
1288
+ const ownerIsVocabulary = cand.ownerNs !== void 0 && _RealXmlAnalyzer.DECLARATION_VOCABULARIES[cand.ownerNs] !== void 0;
1289
+ const isBareValue = qname.prefix.length === 0;
1290
+ const unresolvedBare = !resolvableInDocument && !isBuiltin && !imported && isBareValue && ownerIsVocabulary;
1291
+ if (!resolvableInDocument && !isBuiltin && !imported && !unresolvedBare) continue;
1292
+ if (resolvableInDocument) {
1293
+ ensureTargetSymbol(qname.local, void 0, ASTEngine.SymbolKind.Type, cand.nameNode, { isReferencedDeclaration: true });
1294
+ }
1295
+ const claimsExternalTarget = isBuiltin || !resolvableInDocument && !unresolvedBare;
1296
+ addRelationship(cand.ownerName, qname.local, ASTEngine.RelationshipType.Extends, {
1297
+ role: "xsd_derivation_base",
1298
+ attributeName: "base",
1299
+ qname: cand.value,
1300
+ prefix: qname.prefix,
1301
+ namespaceUri: cand.binding,
1302
+ // WHICH derivation, because XSD has two and they narrow in opposite
1303
+ // directions: `extension` keeps everything the base admits and adds to it,
1304
+ // `restriction` admits a subset. The edge is the same subtype relation in
1305
+ // both cases and this is what tells a consumer which way.
1306
+ derivation: cand.kind,
1307
+ resolvable: isBuiltin ? "builtin" : resolvableInDocument ? "in-document" : unresolvedBare ? "unresolved" : "external",
1308
+ ...claimsExternalTarget ? { isExternalTarget: true } : {},
1309
+ ...unresolvedBare && _RealXmlAnalyzer.XSD_BUILTIN_TYPE_NAMES.has(qname.local) ? { ambiguousWithBuiltin: true } : {},
1310
+ ...imported ? { importedFrom: imported.path, importedBy: imported.element } : {}
1311
+ }, cand.valueNode);
1312
+ }
1313
+ for (const cand of compositionCandidates) {
1314
+ const qname = this.asQName(cand.value);
1315
+ if (!qname) continue;
1316
+ const resolvableInDocument = declaredNames.has(qname.local);
1317
+ const isBuiltin = cand.binding !== void 0 && _RealXmlAnalyzer.BUILTIN_NAMESPACE_URIS.has(cand.binding);
1318
+ const imported = !resolvableInDocument && !isBuiltin && cand.binding !== void 0 ? importedNamespaces.get(cand.binding) : void 0;
1319
+ if (!resolvableInDocument && !isBuiltin && !imported) continue;
1320
+ if (resolvableInDocument) {
1321
+ ensureTargetSymbol(qname.local, void 0, ASTEngine.SymbolKind.Type, cand.nameNode, { isReferencedDeclaration: true });
1322
+ }
1323
+ addRelationship(cand.ownerName, qname.local, ASTEngine.RelationshipType.MixesIn, {
1324
+ role: "xsd_definition_composition",
1325
+ attributeName: "ref",
1326
+ qname: cand.value,
1327
+ prefix: qname.prefix,
1328
+ namespaceUri: cand.binding,
1329
+ // WHICH carrier, because the two constructs compose different things: a group
1330
+ // composes a content model, an attributeGroup composes attribute declarations.
1331
+ // A consumer filtering on the kind does not have to re-derive it from the tag.
1332
+ composedKind: cand.kind,
1333
+ resolvable: isBuiltin ? "builtin" : resolvableInDocument ? "in-document" : "external",
1334
+ ...isBuiltin || !resolvableInDocument ? { isExternalTarget: true } : {},
1335
+ ...imported ? { importedFrom: imported.path, importedBy: imported.element } : {}
1336
+ }, cand.valueNode);
1337
+ }
1338
+ for (const cand of entityReferenceCandidates) {
1339
+ const declared = declaredNames.has(cand.name);
1340
+ const builtin = _RealXmlAnalyzer.PREDEFINED_ENTITIES.has(cand.name);
1341
+ addRelationship(cand.ownerName, cand.name, ASTEngine.RelationshipType.References, {
1342
+ role: "entity_reference",
1343
+ entityName: cand.name,
1344
+ resolvable: declared ? "in-document" : builtin ? "builtin" : "unresolved",
1345
+ ...declared || builtin ? {} : { danglingEntityReference: true },
1346
+ // `builtin` IS the external claim here: the five predefined entities are not declarations
1347
+ // of this population (or of any document), and a bare identifier is verified by
1348
+ // `verifyExternalClaim` exactly when the key space does not declare it. `unresolved`
1349
+ // (a dangling entity reference) is deliberately NOT marked — the analyzer has not
1350
+ // measured that it is outside the population, only that this file does not declare it.
1351
+ ...builtin && !declared ? { isExternalTarget: true } : {}
1352
+ }, cand.textNode);
1353
+ }
1354
+ for (const cand of parameterEntityReferences) {
1355
+ const declared = declaredNames.has(cand.name);
1356
+ addRelationship(cand.ownerName, cand.name, ASTEngine.RelationshipType.References, {
1357
+ role: "parameter_entity_reference",
1358
+ entityName: cand.name,
1359
+ isParameterEntity: true,
1360
+ resolvable: declared ? "in-document" : "unresolved",
1361
+ // Kept and LABELLED rather than dropped, which is the recovery-labelling
1362
+ // contract `xml/recovery-labelling` already pins for symbols and edges out
1363
+ // of a damaged file. libxml2's `test/errors/*` is a corpus of deliberately
1364
+ // malformed documents; a parameter reference recovered out of one is a claim
1365
+ // the bytes make that no processor would honour, and saying so is more
1366
+ // useful than silence.
1367
+ ...cand.recovery ? { recovery: true, reasonCode: "parameter-entity-reference-in-damaged-region" } : {},
1368
+ ...declared ? {} : { danglingEntityReference: true }
1369
+ }, cand.textNode);
1370
+ }
1371
+ const hasSyntaxErrors = errorNodes > 0 || missingNodes > 0;
1372
+ if (hasSyntaxErrors) {
1373
+ for (const symbol of symbols) symbol.metadata.syntaxErrors = true;
1374
+ for (const relationship of relationships) relationship.metadata.syntaxErrors = true;
1375
+ }
1376
+ {
1377
+ const isPlaceholder = (name) => name === _RealXmlAnalyzer.PLACEHOLDER_LITERAL;
1378
+ const keptSymbols = symbols.filter((s) => !isPlaceholder(String(s.name ?? "")));
1379
+ const keptRelationships = relationships.filter((x) => !isPlaceholder(String(x.source ?? "")) && !isPlaceholder(String(x.target ?? "")));
1380
+ const droppedSymbols = symbols.length - keptSymbols.length;
1381
+ const droppedRelationships = relationships.length - keptRelationships.length;
1382
+ if (droppedSymbols > 0 || droppedRelationships > 0) {
1383
+ this.qualifySameFileTier(keptSymbols, keptRelationships);
1384
+ this.attachFileClaims(keptRelationships, filePath, context);
1385
+ return {
1386
+ language: "xml",
1387
+ symbols: keptSymbols,
1388
+ relationships: keptRelationships,
1389
+ imports,
1390
+ exports: [],
1391
+ errors,
1392
+ metadata: {
1393
+ complexity: keptSymbols.length + keptRelationships.length,
1394
+ linesOfCode: content.split("\n").length,
1395
+ cyclomaticComplexity: 1,
1396
+ patterns: [],
1397
+ framework: "XML",
1398
+ errorNodes,
1399
+ missingNodes,
1400
+ syntaxErrors: hasSyntaxErrors,
1401
+ placeholderLiteralRefusals: droppedSymbols + droppedRelationships
1402
+ }
1403
+ };
1404
+ }
1405
+ }
1406
+ this.qualifySameFileTier(symbols, relationships);
1407
+ this.attachFileClaims(relationships, filePath, context);
1408
+ return {
1409
+ language: "xml",
1410
+ symbols,
1411
+ relationships,
1412
+ imports,
1413
+ exports: [],
1414
+ errors,
1415
+ metadata: {
1416
+ complexity: symbols.length + relationships.length,
1417
+ linesOfCode: content.split("\n").length,
1418
+ cyclomaticComplexity: 1,
1419
+ patterns: [],
1420
+ framework: "XML",
1421
+ errorNodes,
1422
+ missingNodes,
1423
+ syntaxErrors: hasSyntaxErrors
1424
+ }
1425
+ };
1426
+ }
1427
+ /**
1428
+ * `<!DOCTYPE a PUBLIC "-//X//DTD A//EN" "a.dtd">` spans lines: `PUBLIC` and the
1429
+ * public id are on one, the SYSTEM path on another. The edge target is the
1430
+ * path, so the edge's line is the LITERAL's line (M59.A7 / #825) — passing the
1431
+ * `ExternalID` node instead points the edge at the `SYSTEM` keyword's line,
1432
+ * which is the line of no name in the edge.
1433
+ *
1434
+ * A `SystemLiteral` is REQUIRED and a `PubidLiteral` is not a substitute. The two are
1435
+ * different things and only one of them is a file: `SYSTEM "graphic.gif"` names a path
1436
+ * a parser opens, `PUBLIC "GIF"` names a symbolic identifier a parser matches against a
1437
+ * catalogue. This read was `systemLiteral || pubidLiteral`, so the second stood in for
1438
+ * the first and a public identifier became an edge target — the fabricated
1439
+ * `GIF -imports-> GIF` in `libxslt/tests/general/bug-52.xml`, at `confidence: 1`, on a
1440
+ * tier table that maps `notation_external_id` to `import_based`.
1441
+ *
1442
+ * The invariant lives HERE rather than only at the one caller that broke it, because
1443
+ * this is the single point where the target string is created: any future caller that
1444
+ * hands this a `PublicID` now gets nothing instead of an edge, which is the correct
1445
+ * outcome for a node that carries no file. Measured by
1446
+ * `validation/m14/fixtures/xml/public-id-census.mts` — 298 system-literal nodes and 298
1447
+ * `externalIdKind: "system"` edges agree exactly, so nothing legitimate is lost.
1448
+ */
1449
+ extractExternalId(externalId, owner, role, addRelationship, imports) {
1450
+ const literal = this.findDirectChild(externalId, "SystemLiteral");
1451
+ if (!literal) return;
1452
+ const path2 = this.unquote(literal.text);
1453
+ if (!path2) return;
1454
+ imports.push({
1455
+ module: path2,
1456
+ path: path2,
1457
+ importType: "xml-doctype",
1458
+ range: this.nodeToRange(literal),
1459
+ metadata: { isDtdExternalId: true, role, systemId: true }
1460
+ });
1461
+ const targetClass = this.importTargetClass(path2);
1462
+ addRelationship(owner, path2, ASTEngine.RelationshipType.Imports, {
1463
+ role,
1464
+ externalIdKind: "system",
1465
+ resolvable: targetClass,
1466
+ // A DOCTYPE whose SYSTEM identifier is a URI names a DTD outside this project — the largest
1467
+ // single group of this lane's unresolved edges is exactly this shape
1468
+ // (`http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd`). Path-like, so the owner check
1469
+ // returns externalUnverified and can never refute; a relative `file` path is left unmarked.
1470
+ ...targetClass === "external" ? { isExternalTarget: true } : {}
1471
+ }, literal);
1472
+ }
1473
+ /**
1474
+ * A correctly-parsed declaration's name is its FIRST named child. If it is
1475
+ * not, the node is a recovery shape and any `Name` beneath it may be a
1476
+ * fragment of a construct the parser did not understand — publishing it would
1477
+ * put a name in the graph that is not a declaration. The pre-repair grammar
1478
+ * produced exactly this for a conditional section: the DTD's name came out as
1479
+ * the literal text `INCLUDE`.
1480
+ */
1481
+ isPositionedDeclarationName(node, nameNode) {
1482
+ if (!nameNode) return false;
1483
+ const firstNamed = node.namedChild(0);
1484
+ return Boolean(firstNamed) && firstNamed.id === nameNode.id;
1485
+ }
1486
+ /** Reads the `xmlns` / `xmlns:prefix` attributes of a tag into the binding map. */
1487
+ rememberNamespaces(tagNode, bindings) {
1488
+ for (let i = 0; i < tagNode.namedChildCount; i++) {
1489
+ const child = tagNode.namedChild(i);
1490
+ if (!child || child.type !== "Attribute") continue;
1491
+ const attributeName = this.findDirectChild(child, "Name")?.text.trim();
1492
+ if (attributeName !== "xmlns" && !attributeName?.startsWith("xmlns:")) continue;
1493
+ const valueNode = this.findDirectChild(child, "AttValue");
1494
+ if (!valueNode) continue;
1495
+ const prefix = attributeName === "xmlns" ? "" : attributeName.slice(6);
1496
+ if (attributeName !== "xmlns" && prefix.length === 0) continue;
1497
+ bindings.set(prefix, this.unquote(valueNode.text));
1498
+ }
1499
+ }
1500
+ extractAttribute(node, ownerName, symbols, addRelationship, publishedNames, declaredNames, namespaceBindings, referenceCandidates, ensureTargetSymbol, ownerLocalName, ownerNamespace, callCandidates, derivationCandidates, compositionCandidates, elementOwnerName, nearestNamedDeclaration, selfNamedDeclaration) {
1501
+ const ownerNs = ownerNamespace;
1502
+ const namedRefs = ownerNamespace ? _RealXmlAnalyzer.NAMED_REFERENCE_ELEMENTS[ownerNamespace] : void 0;
1503
+ this.bareNameReference = namedRefs ? namedRefs.has(ownerLocalName) : false;
1504
+ this.xsltCallElement = ownerNamespace === "http://www.w3.org/1999/XSL/Transform" && _RealXmlAnalyzer.XSLT_CALL_ELEMENTS.has(ownerLocalName);
1505
+ this.xsdDerivationElement = ownerNamespace === "http://www.w3.org/2001/XMLSchema" && (ownerLocalName === "extension" || ownerLocalName === "restriction");
1506
+ this.derivationKind = ownerLocalName === "extension" ? "extension" : "restriction";
1507
+ this.bareNameDeclarationElement = ownerNamespace === "http://relaxng.org/ns/structure/1.0" && (ownerLocalName === "element" || ownerLocalName === "attribute" || ownerLocalName === "define");
1508
+ const nameNode = this.findDirectChild(node, "Name");
1509
+ const valueNode = this.findDirectChild(node, "AttValue");
1510
+ const attributeName = nameNode?.text.trim();
1511
+ if (!attributeName) return;
1512
+ const value = valueNode ? this.unquote(valueNode.text) : void 0;
1513
+ const isNamespace = attributeName === "xmlns" || attributeName.startsWith("xmlns:");
1514
+ const namespacePrefix = attributeName === "xmlns" ? "default" : attributeName.slice("xmlns:".length);
1515
+ if (!attributeName) return;
1516
+ if (isNamespace && namespacePrefix.length === 0) return;
1517
+ const colon = isNamespace ? -1 : attributeName.indexOf(":");
1518
+ const attributePrefix = colon > 0 ? attributeName.slice(0, colon) : void 0;
1519
+ const attributeLocalName = colon > 0 ? attributeName.slice(colon + 1) : attributeName;
1520
+ if (colon > 0 && attributeLocalName.length === 0) return;
1521
+ const name = isNamespace ? namespacePrefix : attributeLocalName;
1522
+ const kind = isNamespace ? ASTEngine.SymbolKind.Namespace : ASTEngine.SymbolKind.Property;
1523
+ if (attributePrefix && namespaceBindings.has(attributePrefix)) {
1524
+ addRelationship(ownerName, attributePrefix, ASTEngine.RelationshipType.Uses, {
1525
+ role: "xml_namespace_prefix_use",
1526
+ namespaceUri: namespaceBindings.get(attributePrefix),
1527
+ resolvable: "in-document"
1528
+ }, nameNode);
1529
+ }
1530
+ symbols.push({
1531
+ name,
1532
+ kind,
1533
+ range: this.nodeToRange(node),
1534
+ visibility: "public",
1535
+ ownerName,
1536
+ ownerKind: ASTEngine.SymbolKind.Class,
1537
+ // M60: an attribute is a declaration OWNED BY the element it is written on,
1538
+ // and `pool-worker.ts:930` defaults `qualifiedName` to the bare name when the
1539
+ // analyzer leaves it unset. So `svg/@fill` and `path/@fill` were both
1540
+ // published under the single key `fill` and the store could not tell two
1541
+ // declarations of two elements apart (`parallel-indexer.ts:3038` writes
1542
+ // `symbol.qualifiedName || symbol.name`). Measured on the 600-file draw
1543
+ // `099e7a868f7b73ee`: 83,454 `property` symbols and 1,002 `namespace` symbols
1544
+ // carried a qualified name equal to their bare name.
1545
+ //
1546
+ // The same expression already used for elements at `:790` and `:920`, so this
1547
+ // is one convention in one analyzer rather than a second identity scheme.
1548
+ //
1549
+ // WHAT IT DOES NOT FIX, stated rather than implied: two instances of the SAME
1550
+ // tag (`<path fill>` twice) still publish `path.fill` twice, because a
1551
+ // qualified name is unique within a scope and the distinguishing information
1552
+ // — ancestry and position — is not in the published name. Measured residual
1553
+ // for this edit: 2,195 of 14,081 ambiguous edges repaired, 11,886 left
1554
+ // (`validation/m14/measurements/xml/qname-population-after-d1.json`).
1555
+ qualifiedName: ownerName ? `${ownerName}.${name}` : name,
1556
+ metadata: {
1557
+ nodeType: node.type,
1558
+ isXmlAttribute: !isNamespace,
1559
+ isXmlNamespace: isNamespace,
1560
+ // The name AS WRITTEN, prefix included. The symbol's `name` is the local
1561
+ // part, so this is the only place the prefix survives in the analyzer's
1562
+ // output — see the comment on `attributeLocalName` above.
1563
+ attributeName,
1564
+ attributeLocalName,
1565
+ value,
1566
+ namespacePrefix: isNamespace ? namespacePrefix : attributePrefix,
1567
+ namespaceUri: isNamespace ? value : void 0
1568
+ }
1569
+ });
1570
+ if (!publishedNames.has(`${ownerName}\0${name}`)) publishedNames.add(`${ownerName}\0${name}`);
1571
+ addRelationship(ownerName, name, ASTEngine.RelationshipType.Contains, {
1572
+ role: isNamespace ? "xml_namespace" : "xml_attribute",
1573
+ value,
1574
+ // M59 (#825): the name as WRITTEN. For `xmlns="…"` the edge's target is the
1575
+ // synthesised element name `default` (the default namespace has no written
1576
+ // prefix), which appears nowhere in the source — so a source check on the
1577
+ // target is unfixable by construction. Carrying `xmlns` (or `xmlns:pfx`)
1578
+ // lets the claim be checked against what is actually on the line.
1579
+ attributeName
1580
+ }, nameNode);
1581
+ if (isNamespace || value === void 0) return;
1582
+ if (attributeName === "schemaLocation") {
1583
+ const parts = _RealXmlAnalyzer.splitOnWhitespace(value);
1584
+ const paired = parts.length % 2 === 0 && parts.length >= 2;
1585
+ let emitted = 0;
1586
+ for (let i = paired ? 1 : 0; i < parts.length; i += paired ? 2 : 1) {
1587
+ const target = parts[i];
1588
+ if (!target || this.isRemoteUri(target)) continue;
1589
+ addRelationship(ownerName, target, ASTEngine.RelationshipType.Imports, {
1590
+ role: "schema_location",
1591
+ attributeName,
1592
+ namespace: paired ? parts[i - 1] : void 0,
1593
+ resolvable: this.importTargetClass(target)
1594
+ }, valueNode);
1595
+ emitted++;
1596
+ }
1597
+ if (emitted > 0) {
1598
+ const last = symbols[symbols.length - 1];
1599
+ if (last && last.name === attributeName && last.ownerName === ownerName) {
1600
+ last.metadata.schemaLocationTargets = parts.filter((_x, i) => paired ? i % 2 === 1 : true);
1601
+ }
1602
+ return;
1603
+ }
1604
+ }
1605
+ if (this.bareNameReference) {
1606
+ if (!nameNode || !valueNode) return;
1607
+ referenceCandidates.push({
1608
+ ownerName,
1609
+ attributeName,
1610
+ value,
1611
+ nameNode,
1612
+ valueNode,
1613
+ binding: void 0,
1614
+ ownerNs,
1615
+ bareNameReference: true
1616
+ });
1617
+ return;
1618
+ }
1619
+ if (this.xsltCallElement && attributeName === "name" && valueNode) {
1620
+ const colon2 = value.indexOf(":");
1621
+ const local = colon2 >= 0 ? value.slice(colon2 + 1) : value;
1622
+ if (local.length > 0 && local.length <= 256 && nearestNamedDeclaration) {
1623
+ callCandidates.push({ ownerName: nearestNamedDeclaration, templateName: local, valueNode: node, textNode: valueNode });
1624
+ }
1625
+ return;
1626
+ }
1627
+ const collapsed = _RealXmlAnalyzer.collapseWhitespace(value);
1628
+ const qname = this.asQName(collapsed);
1629
+ if (!qname || !nameNode || !valueNode) return;
1630
+ if (!qname.prefix && !_RealXmlAnalyzer.REFERENCE_ATTRIBUTES.has(attributeName)) return;
1631
+ if (!qname.prefix && this.bareNameDeclarationElement) return;
1632
+ const declared = namespaceBindings.get(qname.prefix);
1633
+ const binding = declared ?? _RealXmlAnalyzer.IMPLICIT_PREFIX_BINDINGS[qname.prefix];
1634
+ if (this.xsdDerivationElement && attributeName === "base") {
1635
+ derivationCandidates.push({
1636
+ // `nearestNamedDeclaration`, NOT `ownerName`. `ownerName` is the derivation
1637
+ // ELEMENT's own symbol, which for `<xs:restriction>` is the tag text — measured at
1638
+ // 438 of 438 corpus edges naming a vocabulary constructor instead of a declared
1639
+ // type. See the NEAREST NAMED SCHEMA DECLARATION block in the walk above.
1640
+ ownerName: nearestNamedDeclaration ?? "",
1641
+ value: collapsed,
1642
+ binding,
1643
+ ownerNs,
1644
+ kind: this.derivationKind,
1645
+ valueNode,
1646
+ nameNode
1647
+ });
1648
+ return;
1649
+ }
1650
+ if (attributeName === "ref" && ownerNs === "http://www.w3.org/2001/XMLSchema" && (ownerLocalName === "group" || ownerLocalName === "attributeGroup")) {
1651
+ compositionCandidates.push({
1652
+ // Same rule as derivation: the composed-INTO type is the nearest named
1653
+ // declaration above the carrier — the carrier itself is `xs:group`, a vocabulary
1654
+ // tag no document declares. `selfNamedDeclaration` is undefined here in practice
1655
+ // (a composing group/attributeGroup has no `name=` of its own), and the nearest
1656
+ // named ancestor is the type or nesting group the definitions become part of.
1657
+ ownerName: selfNamedDeclaration ?? nearestNamedDeclaration ?? ownerName,
1658
+ value: collapsed,
1659
+ binding,
1660
+ kind: ownerLocalName,
1661
+ valueNode,
1662
+ nameNode
1663
+ });
1664
+ return;
1665
+ }
1666
+ referenceCandidates.push({
1667
+ // THE SOURCE OF A REFERENCE EDGE. This is the third edge type to need it, and the
1668
+ // defect is the same one the other two had: `ownerName` is the element's OWN symbol,
1669
+ // which is its declared name when the vocabulary writes one on `name=` and its TAG
1670
+ // TEXT when it does not. A tag text is not an entity a document declares.
1671
+ //
1672
+ // From FSharp.Data/docs/data/po.xsd, verbatim:
1673
+ //
1674
+ // "xs:element" -> "comment" (role=attribute_qname_reference)
1675
+ //
1676
+ // `<xs:element ref="comment" minOccurs="0"/>` sits inside
1677
+ // `<xs:complexType name="PurchaseOrderType">`. The reference is made BY that type's
1678
+ // content model, so the source is `PurchaseOrderType`; the analyzer published
1679
+ // `xs:element`, which is the schema vocabulary's word for a type CONSTRUCTOR and a
1680
+ // symbol no document declares. Measured at 1,066 of 10,411 `references` edges on the
1681
+ // gate's own sample — see `docs/analyzers/xml.md`.
1682
+ //
1683
+ // The rule, and it is deliberately two lines rather than one:
1684
+ //
1685
+ // self declared the carrier HAS a name of its own, so the reference belongs
1686
+ // to that declaration — `<xs:element name="e" type="T"/>` is
1687
+ // `e -> T`, and this branch leaves those edges exactly as they
1688
+ // were. It is also why `nearestNamedDeclaration` must not be
1689
+ // consulted here: for that element the enclosing scope is not
1690
+ // the declarer.
1691
+ // nearest, else tag the carrier has NO name, so the declarer is the nearest
1692
+ // named ancestor, and the tag is kept when there is none. That
1693
+ // fallback is the difference between this fix and the
1694
+ // `extends` fix, which has no fallback at all and refuses
1695
+ // instead: `references` is emitted for carriers in EVERY
1696
+ // vocabulary — Spring, SVG, WSDL, DocBook and application XML —
1697
+ // and most of them have no named schema ancestor, so refusing
1698
+ // would delete real edges rather than re-base them. It cannot
1699
+ // fabricate, because it is the value that was there before.
1700
+ ownerName: selfNamedDeclaration ?? nearestNamedDeclaration ?? ownerName,
1701
+ attributeName,
1702
+ // The COLLAPSED value, not the raw one. The deferred pass re-parses
1703
+ // `candidate.value` with `asQName`, so storing the raw text meant the
1704
+ // facet was applied once for the push-site check and then thrown away —
1705
+ // the candidate passed the collapsed test here and failed the raw one
1706
+ // there, and the edge was refused exactly as before. One transformation,
1707
+ // at the point the value enters the queue, and both readers see the same
1708
+ // string. It also makes `metadata.qname` the reading the spec defines
1709
+ // rather than the bytes a human typed.
1710
+ value: collapsed,
1711
+ nameNode,
1712
+ valueNode,
1713
+ binding,
1714
+ ownerNs
1715
+ });
1716
+ }
1717
+ /**
1718
+ * A QName is `NCName` or `NCName ':' NCName` with NOTHING else in the value.
1719
+ * Written as a character scan rather than a regular expression: the lane's rule
1720
+ * is that no regular expression does syntax work, and this is a syntax
1721
+ * decision about an attribute value.
1722
+ */
1723
+ asQName(value) {
1724
+ if (!value || value.length > 256) return null;
1725
+ let colon = -1;
1726
+ for (let i = 0; i < value.length; i++) {
1727
+ if (value[i] === ":") {
1728
+ if (colon >= 0) return null;
1729
+ colon = i;
1730
+ }
1731
+ }
1732
+ const prefix = colon < 0 ? "" : value.slice(0, colon);
1733
+ const local = colon < 0 ? value : value.slice(colon + 1);
1734
+ if (!this.isNcName(local)) return null;
1735
+ if (colon >= 0 && !this.isNcName(prefix)) return null;
1736
+ return { prefix, local };
1737
+ }
1738
+ /** XML `NCName` — the character set the grammar's own `Name` rule accepts. */
1739
+ isNcName(text) {
1740
+ if (text.length === 0) return false;
1741
+ for (let i = 0; i < text.length; i++) {
1742
+ const c = text[i];
1743
+ const alpha = c >= "A" && c <= "Z" || c >= "a" && c <= "z";
1744
+ const ok = i === 0 ? alpha || c === "_" : alpha || c >= "0" && c <= "9" || c === "_" || c === "." || c === "-" || c === ":";
1745
+ if (!ok) return false;
1746
+ }
1747
+ return true;
1748
+ }
1749
+ /** A URI with a scheme or an authority is not a repository-relative file path. */
1750
+ isRemoteUri(target) {
1751
+ for (let i = 0; i < target.length; i++) {
1752
+ const c = target[i];
1753
+ if (c === "/" || c === "\\" || c === ".") return false;
1754
+ if (c === ":") return true;
1755
+ }
1756
+ return false;
1757
+ }
1758
+ /**
1759
+ * WHAT KIND OF THING an `imports` target is, so the edge carries a class a consumer
1760
+ * can count instead of reading as a dangling name.
1761
+ *
1762
+ * `imports` is the only relationship type this analyzer emits whose target is not a
1763
+ * symbol, and until this existed the type reported 0 % resolution with nothing on the
1764
+ * edge to say why — which is the state the round brief's cause C calls "silently
1765
+ * unresolved". Measured on the census draw `3bd539410d1a9189` by
1766
+ * `validation/m14/fixtures/xml/imports-target-classes.mts`: 263 edges, 84 distinct
1767
+ * targets, **0 of 263 carrying any class**. The same probe splits them 153 path-shaped,
1768
+ * 107 scheme-bearing, 3 in-document fragments.
1769
+ *
1770
+ * Three classes, and each is a claim this function can actually keep:
1771
+ *
1772
+ * `in-document` — the target starts with `#`, so it names a location in THIS file.
1773
+ * The one `imports` shape whose target can resolve to a symbol here.
1774
+ * `external` — `isRemoteUri`, the SAME test `schemaLocation` already applies, so
1775
+ * this file does not grow a second notion of "remote" that disagrees with the
1776
+ * first. Outside the corpus by construction.
1777
+ * `file` — a path. **Not a claim that the file exists.** The analyzer is a
1778
+ * per-file pure unit and does no filesystem access, so existence is a corpus
1779
+ * question it cannot answer; the probe answers it for one draw and the honest
1780
+ * split of the 153 is 130 admitting as xml, 21 present nowhere, 2 present but not
1781
+ * admitted. A consumer that needs that split must ask the corpus, not the edge.
1782
+ *
1783
+ * `file` is a new member of the vocabulary `references` and `extends` already use
1784
+ * (`in-document` / `builtin` / `external` / `unresolved`); nothing outside this file
1785
+ * switches on it, which was checked rather than assumed.
1786
+ */
1787
+ importTargetClass(target) {
1788
+ if (target.startsWith("#")) return "in-document";
1789
+ return this.isRemoteUri(target) ? "external" : "file";
1790
+ }
1791
+ /**
1792
+ * WHICH ATTRIBUTE carries a declaration's name, where `name` is not it.
1793
+ *
1794
+ * XSLT 1.0 §7.1 gives `xsl:output` no `name` at all: the output method IS its
1795
+ * name, written `method="html"`. That is the only such case measured across
1796
+ * the four vocabularies, and it is a table rather than a branch so that the
1797
+ * next one is a DECLARATION in the same place as the first, and so the
1798
+ * completeness gate can see that the attribute vocabulary is not silently
1799
+ * assumed to be `{name}`.
1800
+ */
1801
+ /**
1802
+ * Declared EMPTY, and that emptiness is the finding.
1803
+ *
1804
+ * This used to read `{ output: "method" }`, on the theory that XSLT
1805
+ * `xsl:output` names something. It does not: `method` names the output METHOD
1806
+ * (`xml`, `html`, `text`) and nothing else. So `<xsl:output method="xml"/>`
1807
+ * was published as a symbol called **`xml`**, marked `isVocabDeclaration`,
1808
+ * with `method`/`encoding`/`indent` hung off it as its attributes — and the
1809
+ * element's real name, `xsl:output`, appeared nowhere in the graph.
1810
+ *
1811
+ * That is the M60.R10 defect class in MIRROR IMAGE. R10 is publishing a VALUE
1812
+ * where a NAME belongs, into a `contains` target; this was taking a value
1813
+ * where a NAME belongs and calling the element IT. The comment on
1814
+ * REFERENCE_ATTRIBUTES above spends two paragraphs refusing to publish
1815
+ * `unbounded` and `Normal` as names, and then the declared-name table handed
1816
+ * `xml` to the graph in the very next screen. The corpus paid for it in
1817
+ * bug-128.xml and bug-92.xml, where `contains xsl:stylesheet -> xsl:output`
1818
+ * was missing and the element had been renamed to its own configuration.
1819
+ *
1820
+ * XSLT 2.0 does give `xsl:output` a `name`, for stylesheets that declare
1821
+ * several outputs — and `name` is already this function's default, so the
1822
+ * correct table is the empty one. `xsl:output` in XSLT 1.0, which is what
1823
+ * these files are, has no name at all and is correctly published under its
1824
+ * written name: it is an instruction, not a declaration.
1825
+ */
1826
+ static DECLARATION_NAME_ATTRIBUTE = {};
1827
+ /**
1828
+ * The DECLARED NAME of an element, when its vocabulary puts the name in an
1829
+ * attribute — and ONLY then.
1830
+ *
1831
+ * Returns null for every other element, which is the common case: a document's
1832
+ * elements are named by their tag, and an attribute that happens to be spelled
1833
+ * `name` is data. An ANONYMOUS inline declaration (`<xs:complexType>` with no
1834
+ * `name` inside an `<xs:element>`) is also null, and correctly so: an inline
1835
+ * type has no name, and publishing the tag for it is the honest reading.
1836
+ *
1837
+ * `nameNode` is the node the NAME ITSELF is written on, which is the AttValue
1838
+ * when the value is on a different line from the attribute keyword and the
1839
+ * whole Attribute otherwise. It is not the same node in both cases, and
1840
+ * picking the Attribute unconditionally was the whole of M19:
1841
+ *
1842
+ * <xsd:element name=
1843
+ * "InvalidModificationFault"
1844
+ *
1845
+ * (cxf/wsn/wsdl/rp-2.xsd) — the Attribute starts on 233, where the line reads
1846
+ * `<xsd:element name=` and carries no part of the name; the AttValue is on
1847
+ * 234. Anchoring to the Attribute reported 233, so a search for the name
1848
+ * landed one line above the string that spells it.
1849
+ *
1850
+ * `allOnOneLine` says which of the shapes this declaration is, because the shapes
1851
+ * want DIFFERENT spans even where two of them agree on the line. See the call site:
1852
+ * when all three share a line the SYMBOL anchors to the whole element, and the
1853
+ * difference is containment rather than cosmetics.
1854
+ */
1855
+ declaredNameOf(namespaceUri, localName, tagNode) {
1856
+ if (!namespaceUri) return null;
1857
+ const declaring = _RealXmlAnalyzer.DECLARATION_VOCABULARIES[namespaceUri];
1858
+ if (!declaring || !declaring.has(localName)) return null;
1859
+ const attribute = _RealXmlAnalyzer.DECLARATION_NAME_ATTRIBUTE[localName] ?? "name";
1860
+ for (let i = 0; i < tagNode.namedChildCount; i++) {
1861
+ const node = tagNode.namedChild(i);
1862
+ if (node?.type !== "Attribute") continue;
1863
+ const attrNameNode = this.findDirectChild(node, "Name");
1864
+ if (attrNameNode?.text.trim() !== attribute) continue;
1865
+ const valueNode = this.findDirectChild(node, "AttValue");
1866
+ if (!valueNode) return null;
1867
+ const value = this.unquote(valueNode.text).trim();
1868
+ if (value.length === 0 || value.length > 256) return null;
1869
+ const anchor = valueNode.startPosition.row !== node.startPosition.row ? valueNode : node;
1870
+ return {
1871
+ name: value,
1872
+ attribute,
1873
+ nameNode: anchor,
1874
+ // The switch the call site needs is "are the TAG, the ATTRIBUTE and the VALUE
1875
+ // all on ONE line". Either half alone is wrong, and the M19 fixture
1876
+ // `xml/xsd-declared-name-lines` fails on each of them in turn:
1877
+ //
1878
+ // tag 27 / attribute 28 / value 28 `<xsd:attribute` + `name="X"` on 28
1879
+ // — attribute and value share a line, the tag does not. Testing the
1880
+ // value's row alone anchors to the element and reports 27.
1881
+ // tag 23 / attribute 23 / value 24 `<xsd:element name=` + `"X"` on 24
1882
+ // — tag and attribute share a line, the value does not. Testing the
1883
+ // attribute's row alone anchors to the element and reports 23.
1884
+ //
1885
+ // All three on one line is the only shape where the element's row and the
1886
+ // attribute's row are the same row, which is the whole reason the element
1887
+ // anchor is line-safe.
1888
+ allOnOneLine: node.startPosition.row === tagNode.startPosition.row && valueNode.startPosition.row === node.startPosition.row
1889
+ };
1890
+ }
1891
+ return null;
1892
+ }
1893
+ /**
1894
+ * Splits on runs of whitespace as a CHAR WALK, never a regex.
1895
+ *
1896
+ * `schemaLocation` is the one attribute whose value is a token list rather than
1897
+ * a single name, and tokenising it is the only place this analyzer needs to cut
1898
+ * a value into words. `\s` was doing that job and it was the analyzer's last
1899
+ * regex — which made this doc's "zero regex usages" claim false and left the
1900
+ * machine census carrying that line as `needs_review` with an empty
1901
+ * justification. The census and the doc disagreed and the census was right.
1902
+ *
1903
+ * The character set is spelled out rather than borrowed from `\s` so the
1904
+ * behaviour is pinned to a stated set — space, tab, LF, CR, the XML `S`
1905
+ * production — instead of to a regex dialect.
1906
+ */
1907
+ /**
1908
+ * XML Schema's `collapse` whiteSpace facet, as a char walk.
1909
+ *
1910
+ * Every run of space/tab/LF/CR becomes ONE space, and the ends are trimmed.
1911
+ * This is the transformation the `collapse` facet defines, and it is NOT the
1912
+ * same as trimming: `"a b"` collapses to `"a b"` rather than `"ab"`. Applied to
1913
+ * a QName-typed attribute value before the value is read as a name — see the
1914
+ * call site in the reference pass for why that matters and why an interior
1915
+ * space must still be refused afterwards.
1916
+ */
1917
+ static collapseWhitespace(text) {
1918
+ const out = [];
1919
+ let pendingSpace = false;
1920
+ let sawAny = false;
1921
+ for (let i = 0; i < text.length; i++) {
1922
+ const c = text.charCodeAt(i);
1923
+ if (c === 32 || c === 9 || c === 10 || c === 13) {
1924
+ if (sawAny) pendingSpace = true;
1925
+ continue;
1926
+ }
1927
+ if (pendingSpace) {
1928
+ out.push(" ");
1929
+ pendingSpace = false;
1930
+ }
1931
+ out.push(text[i]);
1932
+ sawAny = true;
1933
+ }
1934
+ return out.join("");
1935
+ }
1936
+ static splitOnWhitespace(text) {
1937
+ const parts = [];
1938
+ let start = -1;
1939
+ for (let i = 0; i <= text.length; i++) {
1940
+ const c = i < text.length ? text.charCodeAt(i) : 32;
1941
+ if (c === 32 || c === 9 || c === 10 || c === 13) {
1942
+ if (start >= 0) {
1943
+ parts.push(text.slice(start, i));
1944
+ start = -1;
1945
+ }
1946
+ } else if (start < 0) {
1947
+ start = i;
1948
+ }
1949
+ }
1950
+ return parts;
1951
+ }
1952
+ /**
1953
+ * Does `text` contain any code point JavaScript's `\s` matches?
1954
+ *
1955
+ * This is the analyzer's LAST regular expression, deleted. It stood at the
1956
+ * `PEReference` branch as `/\s/.test(ref)` — the predicate separating a parameter
1957
+ * entity REFERENCE (`%name;`) from the VALUE form (`% name '...'`) — and the doc
1958
+ * already claimed zero regex usages while the machine census carried that line as
1959
+ * unremoved. The census and the doc disagreed and the census was right.
1960
+ *
1961
+ * ## Why the class is spelled out and NOT narrowed to XML's own `S`
1962
+ *
1963
+ * The obvious replacement is {@link splitOnWhitespace}, since XML 1.0 §2.3 `S` is
1964
+ * `[ \t\r\n]` and that is the only whitespace a document may use BETWEEN tokens.
1965
+ * Narrowing to it is a behaviour change, and it was measured rather than assumed:
1966
+ * `validation/m14/fixtures/xml/pereference-shape-probe.mts` runs the deleted
1967
+ * predicate (`\s`), the narrowed one (`S`) and a `Name`-child lookup over every
1968
+ * `PEReference` node obtainable from the corpus, the lane's fixtures and eight
1969
+ * synthetic shapes.
1970
+ *
1971
+ * - corpus: 0 nodes. The branch is not exercised by the fixture corpus
1972
+ * at all, so no corpus number can decide this. The 22 admitted files whose
1973
+ * text contains `<!ENTITY %` are bare `.dtd` fragments, which this grammar
1974
+ * cannot root (no `document`), and the one admitted `.xml` — libxslt's
1975
+ * `tests/xmlspec/REC-xml-20001006.xml` — carries its `<!ENTITY %` inside
1976
+ * `CDSect`/`CDATA`, i.e. documentation ABOUT parameter entities.
1977
+ * - fixtures: 4 nodes, `\s` and `S` agree on 4 of 4.
1978
+ * - synthetic: 7 nodes, `\s` and `S` agree on 6 of 7. The one disagreement is
1979
+ * `%p<U+00A0>e;`, where `\s` drops it and `S` publishes a symbol named
1980
+ * `p<U+00A0>e` — a name no XML parser would accept, since `Name` is
1981
+ * `[a-zA-Z_][a-zA-Z0-9_:.·-]*`. On that single node the wider class is the
1982
+ * better answer, so this keeps it.
1983
+ *
1984
+ * ## Why the code points are listed rather than inherited
1985
+ *
1986
+ * The set is ECMAScript's `WhiteSpace` and `LineTerminator` productions, written
1987
+ * out so the behaviour is pinned to a stated list rather than to a dialect. It is
1988
+ * 25 code points and `isEcmaWhitespace` is total over them, which is what makes
1989
+ * this a replacement rather than an approximation: for any string, the answer is
1990
+ * identical to the deleted expression's, because both ask the same per-character
1991
+ * question and a character class is a per-character question.
1992
+ *
1993
+ * A narrower class WOULD be defensible here on XML grounds, and is recorded as a
1994
+ * limitation rather than shipped, because shipping it would change one measured
1995
+ * verdict on a shape the corpus cannot produce, which is the trade this lane has
1996
+ * no evidence to make.
1997
+ */
1998
+ static containsEcmaWhitespace(text) {
1999
+ for (let i = 0; i < text.length; i++) {
2000
+ if (_RealXmlAnalyzer.isEcmaWhitespace(text.charCodeAt(i))) return true;
2001
+ }
2002
+ return false;
2003
+ }
2004
+ /** ECMAScript `\s`: `WhiteSpace` + `LineTerminator`. 25 code points, total. */
2005
+ static isEcmaWhitespace(c) {
2006
+ if (c === 32 || c === 9 || c === 10 || c === 13) return true;
2007
+ if (c === 11 || c === 12) return true;
2008
+ if (c === 160) return true;
2009
+ if (c === 5760) return true;
2010
+ if (c >= 8192 && c <= 8202) return true;
2011
+ if (c === 8232 || c === 8233) return true;
2012
+ if (c === 8239 || c === 8287) return true;
2013
+ if (c === 12288) return true;
2014
+ if (c === 65279) return true;
2015
+ return false;
2016
+ }
2017
+ isSchemaConstruct(name) {
2018
+ const separator = name.lastIndexOf(":");
2019
+ const localName = separator >= 0 ? name.slice(separator + 1) : name;
2020
+ switch (localName) {
2021
+ case "schema":
2022
+ case "element":
2023
+ case "attribute":
2024
+ case "complexType":
2025
+ case "simpleType":
2026
+ case "sequence":
2027
+ case "choice":
2028
+ case "all":
2029
+ case "group":
2030
+ case "attributeGroup":
2031
+ case "notation":
2032
+ case "import":
2033
+ case "include":
2034
+ case "redefine":
2035
+ case "annotation":
2036
+ case "documentation":
2037
+ case "appinfo":
2038
+ case "any":
2039
+ case "anyAttribute":
2040
+ return true;
2041
+ default:
2042
+ return false;
2043
+ }
2044
+ }
2045
+ /**
2046
+ * The DECLARATION a node sits inside, for a reference inside replacement text.
2047
+ *
2048
+ * `<!ENTITY % shared "… %other; …">` makes `shared` depend on `other`, and the
2049
+ * owner of that use is `shared` — not the DOCTYPE, and not an element. Same
2050
+ * reasoning as the `GEDecl` EntityValue case, reached by walking up to the
2051
+ * nearest declaration rather than by descending into it, because the reference
2052
+ * may be nested inside arbitrary markup in the replacement text.
2053
+ */
2054
+ enclosingDeclaredName(node) {
2055
+ let cur = node.parent;
2056
+ let depth = 0;
2057
+ while (cur && depth < 12) {
2058
+ if (cur.type === "GEDecl" || cur.type === "PEDecl" || cur.type === "NotationDecl") {
2059
+ const name = this.findDirectChild(cur, "Name")?.text.trim();
2060
+ return name && name.length > 0 ? name : null;
2061
+ }
2062
+ cur = cur.parent;
2063
+ depth++;
2064
+ }
2065
+ return null;
2066
+ }
2067
+ /**
2068
+ * Is this node inside a region the PARSER could not read?
2069
+ *
2070
+ * Walks up rather than asking the file: `errorNodes` is a whole-file count and
2071
+ * would label every edge in a partly-damaged document, which is the opposite of
2072
+ * what `recovery` is for. libxml2's `test/errors/*` is a corpus of
2073
+ * deliberately malformed documents whose only purpose is to be malformed, and
2074
+ * their parameter references are assertions about bytes no processor would accept.
2075
+ */
2076
+ /**
2077
+ * Is this node inside an entity's replacement text?
2078
+ *
2079
+ * Walks UP, deliberately: the question is about a node's POSITION, and asking
2080
+ * the node itself for an `EntityValue` child cannot answer it. See the call site
2081
+ * for what the first version got wrong.
2082
+ */
2083
+ withinEntityValue(node) {
2084
+ let cur = node.parent;
2085
+ let depth = 0;
2086
+ while (cur && depth < 12) {
2087
+ if (cur.type === "EntityValue") return true;
2088
+ if (cur.type === "GEDecl" || cur.type === "PEDecl" || cur.type === "NotationDecl") return false;
2089
+ cur = cur.parent;
2090
+ depth++;
2091
+ }
2092
+ return false;
2093
+ }
2094
+ withinErrorRegion(node) {
2095
+ let cur = node;
2096
+ let depth = 0;
2097
+ while (cur && depth < 24) {
2098
+ if (cur.type === "ERROR") return true;
2099
+ if (cur.isMissing) return true;
2100
+ cur = cur.parent;
2101
+ depth++;
2102
+ }
2103
+ return false;
2104
+ }
2105
+ findDirectChild(node, type) {
2106
+ for (let i = 0; i < node.namedChildCount; i++) {
2107
+ const child = node.namedChild(i);
2108
+ if (child?.type === type) return child;
2109
+ }
2110
+ return null;
2111
+ }
2112
+ /**
2113
+ * THE INCLUDED DOCUMENT'S MODULE STEM — the `part_of` target, for BOTH inclusion
2114
+ * vocabularies this analyzer reads: XInclude's `<xi:include href>` and XSLT's
2115
+ * `<xsl:include href>`.
2116
+ *
2117
+ * `part_of`'s contract allows "the part's file OR its module symbol", and this
2118
+ * analyzer emits the SYMBOL because that is the only half the arbiter can bind: a
2119
+ * PATH target resolved 0 of 4,612 `part_of` edges (100% unresolved) in cpp's
2120
+ * measurement, because targets are resolved against SYMBOL NAMES. The path is kept
2121
+ * on the row as `metadata.partPath`; this is the symbol-shaped spelling of the same
2122
+ * fact.
2123
+ *
2124
+ * An href is a URI reference (RFC 3986): a `#fragment` selects a part of the
2125
+ * target, and a `?query` is not part of a file name, so both are cut BEFORE the
2126
+ * basename is taken — otherwise `intro.xml#sec2` would yield `xml#sec2`. The
2127
+ * directory segments are dropped and the extension is dropped, so
2128
+ * `sections/intro.xml` and `../shared/intro.xml` both name the module `intro`,
2129
+ * which is what a consumer sees. A dot at position 0 (`.hidden`) is not an
2130
+ * extension separator, so that name is kept whole.
2131
+ *
2132
+ * PURE CHARACTER SCAN, never a pattern: the analyzer's no-regex rule is a
2133
+ * committed gate, and this is string arithmetic on a path, not a shape search.
2134
+ */
2135
+ static includedModuleStem(href) {
2136
+ let end = href.length;
2137
+ const hash = href.indexOf("#");
2138
+ if (hash >= 0 && hash < end) end = hash;
2139
+ const query = href.indexOf("?");
2140
+ if (query >= 0 && query < end) end = query;
2141
+ const pathOnly = href.slice(0, end);
2142
+ const slash = pathOnly.lastIndexOf("/");
2143
+ const base = slash >= 0 ? pathOnly.slice(slash + 1) : pathOnly;
2144
+ const dot = base.lastIndexOf(".");
2145
+ const stem = dot > 0 ? base.slice(0, dot) : base;
2146
+ return stem.trim();
2147
+ }
2148
+ /**
2149
+ * A named `Attribute` child of a tag node, by attribute name.
2150
+ *
2151
+ * Read off the TREE rather than off the tag's text: an attribute name is a `Name`
2152
+ * child inside an `Attribute` node, and three sibling attributes on one tag differ
2153
+ * only in which of their own `Name` children says `match`. Splitting the tag's text
2154
+ * would have to re-tokenise, and would match an attribute NAME that appears inside
2155
+ * another attribute's VALUE.
2156
+ */
2157
+ findDirectAttribute(tagNode, attrName) {
2158
+ for (let i = 0; i < tagNode.namedChildCount; i++) {
2159
+ const attr = tagNode.namedChild(i);
2160
+ if (attr?.type !== "Attribute") continue;
2161
+ const nameNode = this.findDirectChild(attr, "Name");
2162
+ const text = nameNode?.text.trim();
2163
+ if (text === attrName) return attr;
2164
+ }
2165
+ return null;
2166
+ }
2167
+ /**
2168
+ * An attribute's value, unquoted and with the XML Schema `collapse` facet applied.
2169
+ *
2170
+ * `unquote` rather than a pattern, for the reason the rest of this analyzer spells
2171
+ * out: an `AttValue` is `"…"` or `'…'` in the tree, and only the quote characters
2172
+ * need removing. Returns `null` for an attribute written with no value, which is a
2173
+ * different thing from an empty value and must not collapse to `""`.
2174
+ */
2175
+ attributeValueOf(tagNode, attrName) {
2176
+ const attr = this.findDirectAttribute(tagNode, attrName);
2177
+ if (!attr) return null;
2178
+ const valueNode = this.findDirectChild(attr, "AttValue");
2179
+ if (!valueNode) return null;
2180
+ return _RealXmlAnalyzer.collapseWhitespace(this.unquote(valueNode.text.trim()));
2181
+ }
2182
+ findFirstNamedChild(node, types) {
2183
+ for (let i = 0; i < node.namedChildCount; i++) {
2184
+ const child = node.namedChild(i);
2185
+ if (child && types.has(child.type)) return child;
2186
+ }
2187
+ return null;
2188
+ }
2189
+ descendantsOfType(node, type) {
2190
+ const out = [];
2191
+ const stack = [node];
2192
+ while (stack.length > 0) {
2193
+ const current = stack.pop();
2194
+ for (let i = 0; i < current.namedChildCount; i++) {
2195
+ const child = current.namedChild(i);
2196
+ if (!child) continue;
2197
+ if (child.type === type) out.push(child);
2198
+ stack.push(child);
2199
+ }
2200
+ }
2201
+ return out;
2202
+ }
2203
+ /**
2204
+ * THE SAME-FILE TIER — a BOUNDED fix for 366 edges of this lane's ~9,131 residual, and not a fix for
2205
+ * the lane. Sized from a POPULATION-scope run of the lane's own `ambiguity-split.mts`
2206
+ * (`validation/m14/fixtures/xml/measurements/ambiguity-split-population.txt`): the same-file bucket (B1)
2207
+ * totals 1,798 across every type — references 189 · extends 173 · calls 4 · overrides 0, plus 1,432
2208
+ * `contains` which production ALREADY binds through `containsLike` and which is therefore not part of
2209
+ * the residual. So the tier reaches 366, and the commit that lands it says exactly that, because the
2210
+ * next reader would otherwise inherit a 600-file draw's 279 as the lane's shape — which is how the
2211
+ * assignment that produced this file's measurement was written in the first place.
2212
+ *
2213
+ * THE RULE, and why it cannot be wrong: the lane ALREADY computes a two-part identity
2214
+ * (`qualifiedName = ownerName + "." + name`, `:780` and `:934`) and then hands the graph the BARE name,
2215
+ * because `addRelationship` takes two strings. `src/ast/parallel-indexer.ts:3435-3437` already looks up
2216
+ * `name === target` and then `qualifiedName === target`. So emitting the qualified form is readable to
2217
+ * the store WITHOUT changing the store. An edge is qualified only when the file declares its target
2218
+ * under EXACTLY ONE owner — `set.size === 1` — so there is no choice to make and no preference to
2219
+ * invent, which is the difference between a tier and a guess. Zero owners leaves it bare (nothing to
2220
+ * name) and two or more leaves it bare (still ambiguous, and the tier does not pretend otherwise).
2221
+ *
2222
+ * The BARE name is preserved as `metadata.bareTarget`, so nothing is lost and a consumer that matched
2223
+ * the old spelling can still see it.
2224
+ */
2225
+ /**
2226
+ * THE FILE CLAIM — the ~466-edge lever, taken through infrastructure that ALREADY EXISTS.
2227
+ *
2228
+ * The lane's own note above the DOCTYPE emission says a file claim "REFUTES when no candidate is an
2229
+ * admitted file, and `extFAIL` must stay 0" — and that is true of an UNGUARDED claim. It is not true of
2230
+ * `fileClaimForTarget` (`src/ast/file-claim.ts:137`), whose whole contract is the opposite: it returns
2231
+ * the project-relative path ONLY when a candidate is CONFIRMED, and null otherwise, "because an
2232
+ * unconfirmed claim is what REFUTES". Two lanes already use it in production — `real-clean-analyzer.ts`
2233
+ * and `real-graphql-analyzer.ts` — and `resolveImportTargets` (`src/ast/import-resolution.ts:382`) is
2234
+ * wired into the indexer at `parallel-indexer.ts:5015` and `:5561`. So this is a USE of a channel rather
2235
+ * than a file model to build.
2236
+ *
2237
+ * WHAT IT DOES TO THE COLUMNS: the arbiter's `projectFile` branch is the one claim it can CHECK against
2238
+ * the project's own file list (`verifyFileClaim`), so a confirmed claim moves an edge out of
2239
+ * `unresolved` and into `extOK` — a claim the project VERIFIES, which is what clause 1 asks for and is
2240
+ * a RESOLUTION in the owner's standard rather than a ruling. An edge whose path cannot be confirmed is
2241
+ * left exactly as it was: no claim, no refutation, extFAIL stays zero.
2242
+ *
2243
+ * ── THE NEGATIVE HALF OF THE SAME CLAIM, WHICH WAS THE UNSTATED HOLE ────────────────────────────
2244
+ *
2245
+ * The file claim above can only ever say "the file IS here". When the file is not here the edge said
2246
+ * NOTHING checkable, and the incoming gate counted it as `unresolved` — this lane's largest unresolved
2247
+ * class: **60 of its 72 whole-population unresolved edges (83.3 %), all of them `imports`** (39
2248
+ * `schemaLocation` paths + 21 external-ID `SYSTEM` paths; measured at HEAD `fb93f6f18`, recorded in
2249
+ * `validation/m14/verification/XML-FIX.md` §2.2). The analyzer already computes the reason —
2250
+ * `importTargetClass` returns `resolvable: "file"` for exactly this shape (`:2780`) — and then
2251
+ * publishes it on a key the gate does not read. That is the fleet-wide cause (a) of
2252
+ * `RESIDUAL-CLASSIFICATION-41-LANES.md:20-34`, present on this lane.
2253
+ *
2254
+ * So the negative half is published as `sourcePathClaim`, the claim kind the arbiter ALREADY verifies
2255
+ * (`SOURCE_PATH_CLAIM_KEY`, `script/m14/incoming-rule.ts:321-337`, `classifyEdge`'s inverted-polarity
2256
+ * branch): *nothing the project ADMITS resolves to this written source string*. No new channel, no new
2257
+ * marker spelling, no claim the project cannot check.
2258
+ *
2259
+ * WHY THIS CAN NEVER REFUTE, which is the property `extFAIL` 0 depends on: the gate's verdict for this
2260
+ * claim is a pure function of `facts.files` (the admitted-file list), and `facts.files` is a SUBSET of
2261
+ * the files that exist on disk. Before attaching, every spelling the gate's `sourcePathCandidates`
2262
+ * tests — plus separator-normalised variants of them — is checked against the filesystem under the
2263
+ * project root (`isFileAbsentFromProject`). If ANY candidate exists as a regular file, the claim is NOT
2264
+ * attached and the edge is left exactly as it was. So "claim attached" implies "no candidate can be in
2265
+ * `facts.files`" implies "the gate cannot refute it" — by construction, not by measurement luck.
2266
+ *
2267
+ * AND THE SHAPE GUARD IS NOT THE BARE-NAME LAUNDERING HOLE THE CLAIM'S OWN RULE WARNS ABOUT: the claim
2268
+ * is attached only to an `imports` target the LANE classified `resolvable: "file"`, and only when it is
2269
+ * spelled as a path (a separator) or ends in one of THIS LANE's seven admitted file extensions.
2270
+ * `imports` targets are created from `SystemLiteral`/`schemaLocation` attribute text — a file reference
2271
+ * by construction, never a declaration name — and a target ending in `.dtd`/`.xsd`/… cannot be a
2272
+ * published symbol in this lane's key space in any ordinary document. What is refused is the bare word
2273
+ * with no file spelling (`SYSTEM "foo"`), which is exactly where the "could also name a declaration"
2274
+ * risk the claim's contract names actually lives.
2275
+ */
2276
+ attachFileClaims(relationships, filePath, context) {
2277
+ const projectRoot = typeof context?.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
2278
+ if (projectRoot === null) return;
2279
+ for (const rel of relationships) {
2280
+ if (String(rel.type ?? "") !== ASTEngine.RelationshipType.Imports) continue;
2281
+ const md = rel.metadata ?? {};
2282
+ if (typeof md.importProjectFile === "string") continue;
2283
+ const written = String(rel.target ?? "");
2284
+ if (written.length === 0) continue;
2285
+ const claim = fileClaimForTarget({
2286
+ projectRoot,
2287
+ filePath,
2288
+ written,
2289
+ admittedExtensions: [..._RealXmlAnalyzer.IMPORT_FILE_EXTENSIONS]
2290
+ });
2291
+ if (claim !== null) {
2292
+ md.importProjectFile = claim;
2293
+ rel.metadata = md;
2294
+ continue;
2295
+ }
2296
+ if (md.resolvable !== "file") continue;
2297
+ if (!this.isPathShapedImportTarget(written)) continue;
2298
+ if (!this.isFileAbsentFromProject(projectRoot, written)) continue;
2299
+ md.sourcePathClaim = [written];
2300
+ rel.metadata = md;
2301
+ }
2302
+ }
2303
+ /**
2304
+ * IS THIS `imports` TARGET SPELLED AS A FILE? — the guard for the negative file claim.
2305
+ *
2306
+ * A target with a path separator is a path. A target with no separator is a FILE only when it
2307
+ * carries a file EXTENSION this lane admits — the vocabulary comes from `IMPORT_FILE_EXTENSIONS`,
2308
+ * the same set the positive file claim is checked against, so the two halves of the claim cannot
2309
+ * disagree about what a file of this lane is. A bare word with no file spelling (`SYSTEM "foo"`)
2310
+ * is refused, because that is the one shape that could also be a declaration name.
2311
+ */
2312
+ isPathShapedImportTarget(written) {
2313
+ if (written.includes("/") || written.includes("\\")) return true;
2314
+ const dot = written.lastIndexOf(".");
2315
+ if (dot <= 0 || dot === written.length - 1) return false;
2316
+ return _RealXmlAnalyzer.IMPORT_FILE_EXTENSIONS.has(written.slice(dot + 1).toLowerCase());
2317
+ }
2318
+ /**
2319
+ * NO SPELLING OF THIS TARGET IS A FILE UNDER THE PROJECT ROOT — the proof that the negative file
2320
+ * claim cannot be refuted by the arbiter.
2321
+ *
2322
+ * The candidate arithmetic MIRRORS the gate's `sourcePathCandidates` (`incoming-rule.ts:344-353`:
2323
+ * the written string, `./`-stripped, `../`-stripped, the dot-segment-joined path, the basename)
2324
+ * and is a SUPERSET of it (separator normalisation is added), so a spelling the gate would test can
2325
+ * never be missing here. A superset is the safe direction: a candidate this guard sees but the gate
2326
+ * does not can only make the guard refuse a claim (the edge stays unresolved, never refuted).
2327
+ *
2328
+ * It is a REFUSAL, never a resolution: it returns true only when nothing was found, and its only
2329
+ * effect is to suppress a claim.
2330
+ *
2331
+ * PURE CHARACTER SCANS, no patterns. Each of the three normalisations below is a shape test on a
2332
+ * FIXED PREFIX or a single character, which is what a pattern would be doing too — only a pattern
2333
+ * reaches back into the source text to do it, and this analyzer's no-regex rule is a committed
2334
+ * gate (script/m14/no-regex-gate.sh). The three replacements are behaviour-identical to the
2335
+ * patterns they replace, stated here so the equivalence is checkable: every backslash becomes a
2336
+ * slash; a leading `./` is dropped; a leading `../` is dropped (and `./../x` therefore becomes
2337
+ * `../x`, exactly as `.replace(/^\.\//, "")` did).
2338
+ */
2339
+ isFileAbsentFromProject(projectRoot, written) {
2340
+ const trimmed = written.trim();
2341
+ let normalised = "";
2342
+ for (const ch of trimmed) normalised += ch === "\\" ? "/" : ch;
2343
+ const segments = normalised.split("/").filter((s) => s.length > 0 && s !== "." && s !== "..");
2344
+ const candidates = /* @__PURE__ */ new Set([
2345
+ trimmed,
2346
+ normalised,
2347
+ normalised.startsWith("./") ? normalised.slice(2) : normalised,
2348
+ normalised.startsWith("../") ? normalised.slice(3) : normalised,
2349
+ segments.join("/"),
2350
+ segments.length > 0 ? segments[segments.length - 1] : ""
2351
+ ]);
2352
+ for (const candidate of candidates) {
2353
+ if (candidate.length === 0 || candidate.startsWith("/")) continue;
2354
+ if (candidate === ".." || candidate.startsWith("../")) continue;
2355
+ try {
2356
+ if (statSync(path.join(projectRoot, candidate)).isFile()) return false;
2357
+ } catch {
2358
+ }
2359
+ }
2360
+ return true;
2361
+ }
2362
+ qualifySameFileTier(symbols, relationships) {
2363
+ const byName = /* @__PURE__ */ new Map();
2364
+ for (const sym of symbols) {
2365
+ const n = String(sym.name ?? "");
2366
+ const q = String(sym.qualifiedName ?? "");
2367
+ if (n.length === 0 || q.length === 0 || q === n) continue;
2368
+ let set = byName.get(n);
2369
+ if (!set) byName.set(n, set = /* @__PURE__ */ new Set());
2370
+ set.add(q);
2371
+ }
2372
+ const TIER_TYPES = /* @__PURE__ */ new Set(["references", "extends", "calls", "uses", "overrides"]);
2373
+ for (const rel of relationships) {
2374
+ if (!TIER_TYPES.has(String(rel.type ?? ""))) continue;
2375
+ const target = String(rel.target ?? "");
2376
+ const qs = byName.get(target);
2377
+ if (qs === void 0 || qs.size !== 1) continue;
2378
+ const md = rel.metadata ?? {};
2379
+ md.bareTarget = target;
2380
+ rel.metadata = md;
2381
+ rel.target = [...qs][0];
2382
+ }
2383
+ }
2384
+ unquote(text) {
2385
+ if (text.length >= 2) {
2386
+ const first = text[0];
2387
+ const last = text[text.length - 1];
2388
+ if ((first === '"' || first === "'") && first === last) return text.slice(1, -1);
2389
+ }
2390
+ return text;
2391
+ }
2392
+ nodeToRange(node) {
2393
+ return {
2394
+ startLine: node.startPosition.row + 1,
2395
+ startColumn: node.startPosition.column + 1,
2396
+ endLine: node.endPosition.row + 1,
2397
+ endColumn: node.endPosition.column + 1,
2398
+ startIndex: node.startIndex,
2399
+ endIndex: node.endIndex
2400
+ };
2401
+ }
2402
+ };
2403
+ export {
2404
+ RealXmlAnalyzer
2405
+ };