@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,1640 @@
1
+ import {
2
+ applyDocumentScope,
3
+ documentDirScope,
4
+ projectRelativePath
5
+ } from "./chunk-2WXYNLBA.js";
6
+ import {
7
+ ASTEngine
8
+ } from "./chunk-3ZTOHHSW.js";
9
+ import "./chunk-BQ3OVQ3P.js";
10
+ import "./chunk-SK6YVHFZ.js";
11
+ import "./chunk-2BSKD7O3.js";
12
+ import "./chunk-KAET47ZL.js";
13
+ import "./chunk-KCDECRJY.js";
14
+ import "./chunk-MLKGABMK.js";
15
+
16
+ // src/ast/real-json-analyzer.ts
17
+ import { statSync } from "fs";
18
+ import { join } from "path";
19
+ var VALIDATION_KEYS = /* @__PURE__ */ new Set([
20
+ "minLength",
21
+ "maxLength",
22
+ "minimum",
23
+ "maximum",
24
+ "exclusiveMinimum",
25
+ "exclusiveMaximum",
26
+ "pattern",
27
+ "format",
28
+ "enum",
29
+ "const",
30
+ "multipleOf",
31
+ "minItems",
32
+ "maxItems",
33
+ "uniqueItems",
34
+ "minProperties",
35
+ "maxProperties",
36
+ "additionalProperties"
37
+ ]);
38
+ var API_METHOD_KEYS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "trace"]);
39
+ var LSP_INHERITANCE_KEYS = ["extends", "base", "superclass", "superClass", "implements", "mixins"];
40
+ function isDependencyContainerPath(path) {
41
+ const dot = path.lastIndexOf(".");
42
+ const segment = dot >= 0 ? path.slice(dot + 1) : path;
43
+ return segment === "dependencies" || segment === "devDependencies" || segment === "peerDependencies" || segment === "optionalDependencies";
44
+ }
45
+ function isPathReference(value) {
46
+ return value.startsWith("#/") || value.startsWith("#/definitions/") || value.startsWith("#/components/");
47
+ }
48
+ var RealJsonAnalyzer = class {
49
+ analyze(rootNode, content, filePath, context) {
50
+ const rootName = this.documentName(filePath, context?.projectRoot);
51
+ const ctx = {
52
+ filePath,
53
+ projectRoot: typeof context?.projectRoot === "string" ? context.projectRoot : void 0,
54
+ content,
55
+ rootName,
56
+ symbols: [],
57
+ symbolMap: /* @__PURE__ */ new Map(),
58
+ relationships: [],
59
+ imports: [],
60
+ exports: [],
61
+ errors: [],
62
+ requiredByObject: /* @__PURE__ */ new Map(),
63
+ schemaObjects: /* @__PURE__ */ new Set(),
64
+ referenceSymbols: [],
65
+ validationSymbols: [],
66
+ apiEndpointSymbols: [],
67
+ dependencySymbols: [],
68
+ dependencyVersions: /* @__PURE__ */ new Map(),
69
+ overrideCandidates: [],
70
+ emittedPaths: /* @__PURE__ */ new Set(),
71
+ usesReferenceLiterals: false,
72
+ usesClassDeclarations: false,
73
+ typeDeclarations: /* @__PURE__ */ new Map(),
74
+ documentIds: /* @__PURE__ */ new Map(),
75
+ documentAnchors: /* @__PURE__ */ new Map(),
76
+ inheritanceCandidates: [],
77
+ objectCount: 0,
78
+ propertyCount: 0,
79
+ rootNode,
80
+ arrayCount: 0,
81
+ maxDepth: 0,
82
+ hasNullValues: false,
83
+ hasBooleanValues: false,
84
+ hasArrays: false
85
+ };
86
+ this.collectErrors(rootNode, ctx);
87
+ this.collectSchemaFacts(rootNode, ctx);
88
+ this.extractSymbols(rootNode, ctx);
89
+ this.extractRelationships(ctx);
90
+ this.extractManifestExports(ctx);
91
+ this.extractSchemaComposition(ctx);
92
+ this.extractDeclaredInheritance(ctx);
93
+ this.extractPackageDependencies(ctx);
94
+ this.extractNpmScriptCalls(ctx);
95
+ this.extractDuplicateKeyOverrides(ctx);
96
+ const rootSymbol = {
97
+ name: rootName,
98
+ kind: ASTEngine.SymbolKind.Module,
99
+ qualifiedName: rootName,
100
+ range: this.nodeToRange(rootNode),
101
+ scope: "",
102
+ visibility: "public",
103
+ metadata: {
104
+ jsonSpecific: true,
105
+ documentType: this.documentType(ctx),
106
+ hasSchema: ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema"),
107
+ isApiSpec: this.isApiSpec(ctx),
108
+ objectCount: ctx.objectCount,
109
+ propertyCount: ctx.propertyCount,
110
+ arrayCount: ctx.arrayCount,
111
+ maxDepth: ctx.maxDepth,
112
+ hasNullValues: ctx.hasNullValues,
113
+ hasBooleanValues: ctx.hasBooleanValues
114
+ }
115
+ };
116
+ ctx.symbols.push(rootSymbol);
117
+ ctx.exports.push({
118
+ name: rootName,
119
+ kind: ASTEngine.SymbolKind.Module,
120
+ range: rootSymbol.range,
121
+ visibility: "public"
122
+ });
123
+ const frameworks = this.detectFrameworks(ctx);
124
+ const patterns = this.detectPatterns(ctx);
125
+ const structural = this.structuralScores(ctx);
126
+ return {
127
+ symbols: ctx.symbols,
128
+ relationships: ctx.relationships,
129
+ imports: ctx.imports,
130
+ exports: ctx.exports,
131
+ errors: ctx.errors,
132
+ metadata: {
133
+ complexity: ctx.symbols.length + ctx.relationships.length,
134
+ linesOfCode: content.split("\n").length,
135
+ cyclomaticComplexity: 1,
136
+ framework: frameworks[0],
137
+ patterns,
138
+ detectedFrameworks: frameworks,
139
+ frameworkPatterns: [],
140
+ architecturalInsights: this.insights(ctx),
141
+ architecturalScore: structural.dataModelingScore,
142
+ codeQuality: structural.dataModelingScore >= 90 ? "Excellent" : structural.dataModelingScore >= 70 ? "Good" : "Fair",
143
+ hasTypeInference: false,
144
+ hasFrameworkIntelligence: true,
145
+ dataModelingScore: structural.dataModelingScore,
146
+ schemaValidationScore: structural.schemaValidationScore,
147
+ apiDesignScore: this.isApiSpec(ctx) ? 100 : 80,
148
+ analysisVersion: "4.0.0-pure-json-ast"
149
+ }
150
+ };
151
+ }
152
+ collectErrors(rootNode, ctx) {
153
+ const stack = [rootNode];
154
+ while (stack.length > 0) {
155
+ const node = stack.pop();
156
+ if (node.type === "ERROR" || node.isMissing) {
157
+ ctx.errors.push({
158
+ message: `Syntax ${node.type} detected in JSON AST.`,
159
+ range: this.nodeToRange(node),
160
+ severity: "warning"
161
+ });
162
+ }
163
+ const children = node.namedChildren ?? [];
164
+ for (let i = children.length - 1; i >= 0; i--) {
165
+ const child = children[i];
166
+ if (child) stack.push(child);
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * Two DOCUMENT-level preconditions, read in the pass that already visits every pair.
172
+ *
173
+ * Both exist because the pass-2 corpus probe found inheritance declared as ordinary
174
+ * object keys, and both are deliberately narrow. `implements` had been RULED on the
175
+ * ground that the grammar cannot express a supertype clause, and the corpus says
176
+ * otherwise: `dart-sdk`'s `lsp_meta_model.json` states 87 interface mix-ins and 66 class
177
+ * supertypes, and duckdb's serialization schemas state 200-odd superclasses. A grammar
178
+ * has no node type for a KEYWORD, which is the same unsound question this lane has now
179
+ * had to retract six times.
180
+ *
181
+ * What keeps this from firing everywhere is that both shapes are announced by the
182
+ * document before any key is interpreted:
183
+ *
184
+ * - LSP's meta-model uses `{"kind":"reference","name":X}`, a discriminated union for
185
+ * "this is a named type". Two documents in the admitted population contain one, and
186
+ * only one of them states inheritance. So the `name` key of an arbitrary JSON object
187
+ * is never read as a type declaration.
188
+ * - duckdb's schemas declare a C++ class with a `class` string. Twenty-one documents,
189
+ * all duckdb.
190
+ *
191
+ * The measurement that would overturn this: a corpus document that states a supertype or
192
+ * an interface mix-in in a shape carrying neither marker. 0 occurrences over 11,713
193
+ * files, which is what `validation/m14/fixtures/json/inheritance-probe.ts` reports.
194
+ *
195
+ * It is folded into `collectSchemaFacts` rather than given its own walk, and the reason is
196
+ * measured. `otp`'s adversarial `json_SUITE_data/n_structure_open_array_object.json` is 50,000
197
+ * unclosed `[` — the grammar recovers it as an ERROR root with 200,000 children — and every
198
+ * extra full-tree walk over that shape costs ~100 s on top of a 249 s baseline, because
199
+ * `namedChild(i)` is not O(1) in this binding. Two walks became one.
200
+ */
201
+ noteDocumentConventions(key, value, ctx) {
202
+ if (value?.type !== "string") return;
203
+ if (key === "kind") {
204
+ if (this.stripQuotes(value.text) === "reference") ctx.usesReferenceLiterals = true;
205
+ return;
206
+ }
207
+ if (key !== "class" || ctx.usesClassDeclarations) return;
208
+ const declared = this.stripQuotes(value.text);
209
+ if (this.isIdentifierText(declared)) ctx.usesClassDeclarations = true;
210
+ }
211
+ isIdentifierText(text) {
212
+ const first = text[0];
213
+ if (!(first >= "a" && first <= "z" || first >= "A" && first <= "Z" || first === "_")) return false;
214
+ for (let i = 1; i < text.length; i++) {
215
+ const c = text[i];
216
+ const ok = c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_";
217
+ if (!ok) return false;
218
+ }
219
+ return true;
220
+ }
221
+ /**
222
+ * Collect schema metadata before symbol extraction so `required` is based on
223
+ * the actual sibling array, not on a key-name convention. Each object is
224
+ * visited once, so this pass is O(number of AST nodes).
225
+ */
226
+ collectSchemaFacts(rootNode, ctx) {
227
+ const stack = [{ node: rootNode, parentPath: ctx.rootName, parentName: ctx.rootName }];
228
+ while (stack.length > 0) {
229
+ const { node, parentPath } = stack.pop();
230
+ if (node.type === "document") {
231
+ this.pushTopLevelValues(stack, node, parentPath, ctx.rootName);
232
+ continue;
233
+ }
234
+ if (node.type === "object") {
235
+ const schemaKeys = /* @__PURE__ */ new Set();
236
+ let requiredNames;
237
+ for (const pair of node.namedChildren ?? []) {
238
+ if (pair?.type !== "pair") continue;
239
+ const key = this.keyText(pair);
240
+ const value = pair.childForFieldName("value");
241
+ if (!key) continue;
242
+ this.noteDocumentConventions(key, value, ctx);
243
+ if (value?.type === "string" && (key === "$id" || key === "$anchor")) {
244
+ const declared = this.stripQuotes(String(value.text ?? ""));
245
+ if (declared.length > 0) {
246
+ if (key === "$id") {
247
+ const base = this.withoutEmptyFragment(declared);
248
+ if (!ctx.documentIds.has(base)) ctx.documentIds.set(base, parentPath);
249
+ } else {
250
+ const paths = ctx.documentAnchors.get(declared);
251
+ if (paths) paths.push(parentPath);
252
+ else ctx.documentAnchors.set(declared, [parentPath]);
253
+ }
254
+ }
255
+ }
256
+ schemaKeys.add(key);
257
+ if (key === "required" && value?.type === "array") {
258
+ const required = /* @__PURE__ */ new Set();
259
+ for (const item of value.namedChildren ?? []) {
260
+ if (item?.type === "string") required.add(this.stripQuotes(item.text));
261
+ }
262
+ if (required.size > 0) requiredNames = required;
263
+ }
264
+ if (value?.type === "object") stack.push({ node: value, parentPath: this.childPath(parentPath, key), parentName: key });
265
+ else if (value?.type === "array") stack.push({ node: value, parentPath: this.childPath(parentPath, key), parentName: key });
266
+ }
267
+ let schemaMatches = 0;
268
+ for (const key of schemaKeys) {
269
+ if (key === "$schema" || key === "type" || key === "properties" || key === "required" || key === "items" || key === "definitions" || key === "$defs" || key === "additionalProperties") schemaMatches++;
270
+ }
271
+ if (requiredNames) {
272
+ const requiredOwner = schemaKeys.has("properties") ? this.childPath(parentPath, "properties") : parentPath;
273
+ ctx.requiredByObject.set(requiredOwner, requiredNames);
274
+ }
275
+ if (schemaMatches >= 2) ctx.schemaObjects.add(parentPath);
276
+ continue;
277
+ }
278
+ if (node.type === "array") this.pushArrayChildren(stack, node, parentPath, ctx.rootName, ctx.rootName);
279
+ }
280
+ }
281
+ extractSymbols(rootNode, ctx) {
282
+ const stack = [{ node: rootNode, parentPath: ctx.rootName, parentName: ctx.rootName }];
283
+ while (stack.length > 0) {
284
+ const frame = stack.pop();
285
+ const { node, parentPath, parentName } = frame;
286
+ if (node.type === "document") {
287
+ this.pushTopLevelValues(stack, node, parentPath, parentName);
288
+ continue;
289
+ }
290
+ if (node.type === "object") {
291
+ this.updateDepth(ctx, parentPath);
292
+ if (frame.emitObject) this.addArrayObjectSymbol(ctx, node, parentPath, parentName);
293
+ this.recordTypeDeclaration(node, parentPath, ctx);
294
+ this.pushNamedChildren(stack, node, parentPath, parentName, frame.duplicateKeys ?? /* @__PURE__ */ new Map());
295
+ continue;
296
+ }
297
+ if (node.type === "array") {
298
+ ctx.arrayCount++;
299
+ ctx.hasArrays = true;
300
+ if (frame.emitObject) this.addArrayElementSymbol(ctx, node, parentPath, parentName);
301
+ this.pushArrayChildren(stack, node, parentPath, parentName, ctx.rootName);
302
+ continue;
303
+ }
304
+ if (node.type !== "pair") continue;
305
+ const key = this.keyText(node);
306
+ const value = node.childForFieldName("value");
307
+ if (!key || !value) continue;
308
+ if (frame.duplicateKeys && key.length > 0) {
309
+ const line = node.startPosition.row + 1;
310
+ const prior = frame.duplicateKeys.get(key);
311
+ if (prior === void 0) frame.duplicateKeys.set(key, line);
312
+ else
313
+ ctx.overrideCandidates.push({
314
+ ownerName: parentName,
315
+ ownerPath: parentPath,
316
+ key,
317
+ targetPath: this.childPath(parentPath, key),
318
+ line,
319
+ overridesLine: prior
320
+ });
321
+ }
322
+ const path = this.childPath(parentPath, key);
323
+ const valueType = this.valueType(value);
324
+ const kind = valueType === "object" ? ASTEngine.SymbolKind.Class : ASTEngine.SymbolKind.Property;
325
+ const symbol = {
326
+ name: key,
327
+ kind,
328
+ qualifiedName: path,
329
+ range: this.nodeToRange(node),
330
+ ownerName: parentName,
331
+ ownerKind: parentPath === ctx.rootName ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
332
+ scope: parentPath,
333
+ visibility: "public",
334
+ returnType: valueType,
335
+ metadata: {
336
+ jsonSpecific: true,
337
+ nodeType: node.type,
338
+ valueType,
339
+ fullPath: path,
340
+ isArray: valueType === "array",
341
+ isObject: valueType === "object",
342
+ isRequired: this.isRequired(ctx, parentPath, key),
343
+ hasValidation: this.hasValidation(ctx, key, value),
344
+ isReference: this.isReference(key, value),
345
+ refPath: this.referencePath(key, value),
346
+ isApiEndpoint: this.isApiEndpoint(key, value),
347
+ depth: this.pathDepth(path, ctx.rootName)
348
+ }
349
+ };
350
+ if (ctx.emittedPaths.has(path)) continue;
351
+ ctx.emittedPaths.add(path);
352
+ this.addSymbol(ctx, symbol);
353
+ if (valueType === "null") ctx.hasNullValues = true;
354
+ if (valueType === "boolean") ctx.hasBooleanValues = true;
355
+ if (symbol.metadata.isReference) ctx.referenceSymbols.push(symbol);
356
+ if (symbol.metadata.hasValidation) ctx.validationSymbols.push(symbol);
357
+ if (symbol.metadata.isApiEndpoint) ctx.apiEndpointSymbols.push(symbol);
358
+ if (isDependencyContainerPath(parentPath)) {
359
+ ctx.dependencySymbols.push(symbol);
360
+ if (value.type === "string") {
361
+ const range = this.stripQuotes(String(value.text ?? ""));
362
+ if (range.length > 0) ctx.dependencyVersions.set(path, range);
363
+ }
364
+ }
365
+ if (value.type === "object" || value.type === "array") stack.push({ node: value, parentPath: path, parentName: key });
366
+ }
367
+ }
368
+ /**
369
+ * INHERITANCE DECLARED AS ORDINARY OBJECT KEYS — the corpus measurement that retracted
370
+ * this lane's `implements` ruling and found `extends` incomplete.
371
+ *
372
+ * `implements` was ruled on a grammar inventory ("the grammar produced 20 distinct node
373
+ * types across the corpus and NONE expresses it") and the corpus says otherwise. Two
374
+ * documents in 11,713 declare types and state their supertypes:
375
+ *
376
+ * dart-sdk/…/lsp_meta_model.json — the Language Server Protocol's own serialisation of
377
+ * its type declarations. 66 structures carry
378
+ * "extends": [{"kind":"reference","name":"TextDocumentPositionParams"}]
379
+ * and 87 carry
380
+ * "mixins": [{"kind":"reference","name":"WorkDoneProgressParams"}, …]
381
+ * `mixins` names the TypeScript INTERFACES a class mixes in, which is `implements`
382
+ * and nothing else: in TypeScript an interface carries no implementation, so there is
383
+ * nothing else for the word to mean.
384
+ *
385
+ * duckdb/src/include/duckdb/storage/serialization/*.json — serialisation schemas for
386
+ * C++ classes. `{ "class": "NotNullConstraint", "base": "Constraint", … }`: `class`
387
+ * declares the type, `base` names its superclass, and every superclass named is
388
+ * declared in the same file.
389
+ *
390
+ * Two shapes are declined, and the corpus says why:
391
+ *
392
+ * - A member descriptor. duckdb writes `{ "id": 200, "name": "info", "type":
393
+ * "AlterInfo*", "base": "ParseInfo" }` — a FIELD whose C++ class derives from
394
+ * `ParseInfo`. It is not a type declaration, and 16 such statements name a target the
395
+ * document does not declare. The rule requires a `class` marker, which a member
396
+ * descriptor does not carry, so all 16 are declined by the marker rather than by a
397
+ * name list.
398
+ * - `"parent"` and a non-string `base`. vega's data files carry `"parent": <number>` on
399
+ * 251 pairs; a number is not a type. The value must be a STRING.
400
+ *
401
+ * Confidence is 1.0 for `extends` and 0.9 for `implements`, and the difference is the
402
+ * honest one: a declared supertype is read structurally in both cases, but the mapping of
403
+ * LSP's word `mixins` onto the ontology's `implements` is a NAMING decision, not a parse
404
+ * fact. It therefore carries `reasonCode`, which is what the honesty contract §4 asks of
405
+ * anything below the deterministic threshold.
406
+ */
407
+ recordTypeDeclaration(node, path, ctx) {
408
+ const index = this.objectKeyIndex(node);
409
+ if (this.referenceLiteralName(index) !== void 0) return;
410
+ const className = ctx.usesClassDeclarations ? this.stringPairValue(index, "class") : void 0;
411
+ const named = className ?? (ctx.usesReferenceLiterals ? this.lspDeclaredName(index) : void 0);
412
+ if (named !== void 0 && named.length > 0 && !ctx.typeDeclarations.has(named)) ctx.typeDeclarations.set(named, path);
413
+ if (named === void 0 || named.length === 0) return;
414
+ if (className !== void 0) {
415
+ const base = this.stringPairValue(index, "base");
416
+ if (base !== void 0 && base.length > 0) {
417
+ ctx.inheritanceCandidates.push({
418
+ source: path,
419
+ target: base,
420
+ key: "base",
421
+ form: "duckdb_base_class_name",
422
+ label: "extends",
423
+ line: this.pairLine(index, "base")
424
+ });
425
+ }
426
+ return;
427
+ }
428
+ for (const key of LSP_INHERITANCE_KEYS) {
429
+ const value = this.pairValue(index, key);
430
+ if (value?.type !== "array") continue;
431
+ const label = key === "mixins" || key === "implements" ? "implements" : "extends";
432
+ const items = (value.namedChildren ?? []).filter((c) => c.type !== "[");
433
+ for (const item of items) {
434
+ const reference = this.referenceLiteralName(this.objectKeyIndex(item));
435
+ if (reference === void 0) continue;
436
+ ctx.inheritanceCandidates.push({
437
+ source: path,
438
+ target: reference,
439
+ key,
440
+ form: "lsp_reference_literal",
441
+ label,
442
+ line: item.startPosition.row + 1
443
+ });
444
+ }
445
+ }
446
+ }
447
+ /**
448
+ * Resolve the candidates against the declarations collected in the same walk and emit.
449
+ *
450
+ * A candidate whose target this document never declares produces NO edge. Measured: 16 of
451
+ * the supertype statements name a type declared in a sibling file, and an edge to a symbol
452
+ * this file does not contain is exactly the fabrication the ontology workstream calls the
453
+ * most expensive mistake available.
454
+ *
455
+ * The endpoints are QUALIFIED PATHS, which is the lane's established convention for a
456
+ * relation between two declarations rather than between two keys — the same choice
457
+ * `references` makes, and for the same reason: the thing that inherits is an object in the
458
+ * document, not a key. It has a measured cost, declared in the doc: these endpoints bind by
459
+ * qualified name and not by bare name, so the store cannot resolve them by name.
460
+ */
461
+ extractDeclaredInheritance(ctx) {
462
+ for (const c of ctx.inheritanceCandidates) {
463
+ const targetPath = ctx.typeDeclarations.get(c.target);
464
+ if (targetPath === void 0) continue;
465
+ if (targetPath === c.source) continue;
466
+ ctx.relationships.push({
467
+ source: c.source,
468
+ target: targetPath,
469
+ type: c.label === "implements" ? ASTEngine.RelationshipType.Implements : ASTEngine.RelationshipType.Extends,
470
+ confidence: c.label === "implements" ? 0.9 : 1,
471
+ line_number: c.line,
472
+ metadata: {
473
+ jsonSpecific: true,
474
+ inheritanceForm: c.form,
475
+ declarationKey: c.key,
476
+ declaredType: c.target,
477
+ reasonCode: c.label === "implements" ? "declared_interface_mixin" : "declared_supertype",
478
+ evidenceClass: "structural",
479
+ endpoints: "qualified_path"
480
+ }
481
+ });
482
+ }
483
+ }
484
+ /** An LSP reference literal: an object with `kind` == "reference" and a `name` string. */
485
+ referenceLiteralName(index) {
486
+ if (this.stringPairValue(index, "kind") !== "reference") return void 0;
487
+ const name = this.stringPairValue(index, "name");
488
+ return name !== void 0 && name.length > 0 ? name : void 0;
489
+ }
490
+ /**
491
+ * The name an LSP-style object declares ITSELF under, or undefined when the object is not a
492
+ * type declaration.
493
+ *
494
+ * Two keys name the type (`name` for a structure or an enumeration, `typeName` for a
495
+ * request or a notification) and one of five makes the object a TYPE rather than one of its
496
+ * members: `properties`, `values`, `extends`, `mixins`, `implements`. A property descriptor
497
+ * (`{"name":"textDocument","type":{"kind":"reference",…}}`) has a `name` and none of those,
498
+ * so it is declined — and it sits inside a declaration in the same file, which is what makes
499
+ * the test necessary rather than tidy.
500
+ */
501
+ lspDeclaredName(index) {
502
+ if (!index.has("properties") && !index.has("values") && !index.has("extends") && !index.has("mixins") && !index.has("implements")) {
503
+ return void 0;
504
+ }
505
+ return this.stringPairValue(index, "name") ?? this.stringPairValue(index, "typeName");
506
+ }
507
+ /** A key -> pair index for one object, built once. */
508
+ objectKeyIndex(node) {
509
+ const index = /* @__PURE__ */ new Map();
510
+ const children = node?.namedChildren ?? [];
511
+ for (let i = 0; i < children.length; i++) {
512
+ const child = children[i];
513
+ if (child?.type !== "pair") continue;
514
+ const key = this.keyText(child);
515
+ if (key.length === 0) continue;
516
+ if (!index.has(key)) index.set(key, child);
517
+ }
518
+ return index;
519
+ }
520
+ pairValue(index, key) {
521
+ const pair = index.get(key);
522
+ return pair ? pair.childForFieldName("value") : void 0;
523
+ }
524
+ stringPairValue(index, key) {
525
+ const value = this.pairValue(index, key);
526
+ if (value?.type !== "string") return void 0;
527
+ const text = this.stripQuotes(String(value.text ?? ""));
528
+ return text.length > 0 ? text : void 0;
529
+ }
530
+ pairLine(index, key) {
531
+ const pair = index.get(key);
532
+ return pair ? pair.startPosition.row + 1 : 0;
533
+ }
534
+ addSymbol(ctx, symbol) {
535
+ ctx.symbols.push(symbol);
536
+ ctx.symbolMap.set(symbol.name, symbol);
537
+ const fullPath = symbol.metadata?.fullPath;
538
+ if (fullPath) ctx.symbolMap.set(fullPath, symbol);
539
+ if (symbol.kind === ASTEngine.SymbolKind.Class) ctx.objectCount++;
540
+ if (symbol.kind === ASTEngine.SymbolKind.Property) ctx.propertyCount++;
541
+ }
542
+ extractRelationships(ctx) {
543
+ for (const symbol of ctx.symbols) {
544
+ if (symbol.ownerName) {
545
+ ctx.relationships.push({
546
+ source: this.ownerPathOf(symbol),
547
+ target: String(symbol.qualifiedName || symbol.name),
548
+ type: ASTEngine.RelationshipType.Contains,
549
+ confidence: 1,
550
+ line_number: this.declLine(symbol),
551
+ metadata: {
552
+ fullPath: symbol.metadata?.fullPath || "",
553
+ ownerName: symbol.ownerName,
554
+ endpoints: "qualified_path"
555
+ }
556
+ });
557
+ }
558
+ }
559
+ for (const ref of ctx.referenceSymbols) {
560
+ const refPath = ref.metadata?.refPath;
561
+ if (!refPath) continue;
562
+ const declaredIdentity = this.documentDeclaredIdentity(ctx, refPath);
563
+ const target = declaredIdentity ?? this.referenceTarget(ctx, refPath);
564
+ const source = String(ref.metadata?.fullPath || ref.name);
565
+ const fileCandidate = this.fileRefCandidate(ctx, refPath);
566
+ const claimedFile = fileCandidate !== null && this.projectCarriesFile(ctx, fileCandidate) ? fileCandidate : null;
567
+ if (this.isFileNamingRef(refPath)) {
568
+ ctx.relationships.push({
569
+ source,
570
+ target: refPath,
571
+ type: ASTEngine.RelationshipType.Imports,
572
+ confidence: 1,
573
+ line_number: this.declLine(ref),
574
+ metadata: {
575
+ jsonSpecific: true,
576
+ importForm: "json_schema_ref",
577
+ resolvedPath: false,
578
+ // the dependency this edge asserts, in the spelling the project's own file list uses
579
+ // — or, where the project does not carry it, the external claim that says so. The
580
+ // fallback is not optional: an `imports` edge with no claim at all is UNRESOLVED and
581
+ // counts against the lane, and "the project does not carry this file" is a statement,
582
+ // not a gap. A path-shaped target carrying `isExternal` is UNVERIFIABLE by the
583
+ // arbiter's owner lookup by design (`verifyExternalClaim` refuses to read a stem as an
584
+ // owner — a rule fixed in `5a611de4b`), which is exactly right for it.
585
+ ...claimedFile ? { importProjectFile: claimedFile } : { isExternal: true },
586
+ evidenceClass: "structural"
587
+ }
588
+ });
589
+ }
590
+ ctx.relationships.push({
591
+ source,
592
+ target,
593
+ type: ASTEngine.RelationshipType.References,
594
+ confidence: target === refPath ? 0.8 : 1,
595
+ line_number: this.declLine(ref),
596
+ metadata: {
597
+ relationshipType: "json_reference",
598
+ refPath,
599
+ // THE EXTERNAL CLAIM, and it is a claim about the CORPUS, so it may only be made where
600
+ // the shape says the target is outside this document. A LOCAL POINTER never is: `#`
601
+ // (the root) and `#/a/b` (a location in this file) are answered by this file. Before
602
+ // this test, `#` was marked external by `!refPath.startsWith("#/")` — nine edges in one
603
+ // vendored meta-schema — and a URI/path-shaped target can never be verified by the
604
+ // gate's owner lookup, so those nine made the lane's verdict unreadable: the arbiter
605
+ // returns a branch that prints no amb/unres/edges totals (measured, 2026-10-09).
606
+ // A pointer that names a location this document does not publish is a BROKEN pointer,
607
+ // not an external one; `reasonCode` says exactly that and the confidence carries it.
608
+ // A `$ref` that names a FILE carries the file claim instead, on both labels, so the two
609
+ // rows for one dependency are adjudicated the same way — and where the project does not
610
+ // carry the file, BOTH labels carry the external claim, so one dependency is never
611
+ // adjudicated two ways.
612
+ // AN IDENTITY THE DOCUMENT DECLARES IS NOT EXTERNAL EITHER: the third same-document
613
+ // form. `$id`/`$anchor` declarations are what make the target a declaration HERE, so the
614
+ // corpus claim that made the edge `ext?` would be a claim this file refutes.
615
+ ...claimedFile ? { importProjectFile: claimedFile } : { isExternal: !this.isLocalPointer(refPath) && declaredIdentity === null },
616
+ // Honesty contract §4 point 1: anything below 1.0 carries a machine-readable code.
617
+ // The 0.8 case is exactly one thing — the pointer names a location this document
618
+ // does not declare — so the code says so, and the 1.0 case says the opposite.
619
+ reasonCode: target === refPath ? "ref_target_not_declared_in_document" : "json_schema_ref",
620
+ evidenceClass: "structural"
621
+ }
622
+ });
623
+ }
624
+ for (const endpoint of ctx.apiEndpointSymbols) {
625
+ ctx.relationships.push({
626
+ // QUALIFIED PATHS on both ends. An endpoint is a KEY of the `paths` object, so it has
627
+ // a position in the document and a path exists for it — `endpoint.qualifiedName` IS
628
+ // that path, and `endpoint.scope` is the `paths` object's own path. This is what
629
+ // separates this site from a manifest dependency's `uses`, whose target is an npm
630
+ // package name and genuinely has no position: see `classifyUnresolved` in
631
+ // validation/m14/fixtures/json/namespace-orphan-probe.ts, which reads the two apart
632
+ // from the discriminator each site publishes.
633
+ source: endpoint.scope || endpoint.ownerName || ctx.rootName,
634
+ target: endpoint.qualifiedName || endpoint.name,
635
+ type: ASTEngine.RelationshipType.Uses,
636
+ confidence: 0.8,
637
+ line_number: this.declLine(endpoint),
638
+ metadata: {
639
+ relationshipType: "json_api_endpoint",
640
+ // Honesty contract §4 point 1. The endpoints are a fact about the tree; the claim
641
+ // that declaring an HTTP method in a `paths` entry is a USE rather than mere
642
+ // containment is a reading of the OpenAPI document shape, so it is 0.8 with a code.
643
+ reasonCode: "openapi_path_entry_declares_http_method",
644
+ evidenceClass: "usage_contextual",
645
+ endpoints: "qualified_path"
646
+ }
647
+ });
648
+ }
649
+ }
650
+ /**
651
+ * The OWNER's qualified path, recovered from a child's own path — or the bare `ownerName`
652
+ * when the path does not reduce to one.
653
+ *
654
+ * Two shapes occur and both are handled by string surgery on the child's path, because the
655
+ * path IS the child's `qualifiedName` in this analyzer:
656
+ *
657
+ * an object property — path `ar.dashboard.analytics.unknown`, name `unknown`; the owner is
658
+ * everything before the final `.unknown`, i.e. `ar.dashboard.analytics`.
659
+ *
660
+ * an array item — path `ar.items[0]`, name `ar.items[0]` (see `arrayItemName`: an item is
661
+ * named after the array that holds it plus its index). There is no `.` immediately before
662
+ * the name, so the `.`-suffix test misses it and the `[i]` suffix test catches it, leaving
663
+ * `ar.items`.
664
+ *
665
+ * The fallback returns `ownerName` unchanged, which is what the previous version always
666
+ * emitted. That direction was chosen on purpose: a path can only ever resolve MORE than a
667
+ * bare name (strategy 1 exact-matches it), so an unreducible path degrades to today's
668
+ * behaviour and never to a worse one.
669
+ */
670
+ ownerPathOf(symbol) {
671
+ const owner = String(symbol.ownerName ?? "");
672
+ if (owner === "") return owner;
673
+ const p = String(symbol.qualifiedName ?? "");
674
+ const n = String(symbol.name ?? "");
675
+ if (n === "" || p === "") return owner;
676
+ if (symbol.metadata?.isArrayItem === true) {
677
+ const bracket = p.lastIndexOf("[");
678
+ if (bracket > 0) return p.slice(0, bracket);
679
+ return owner;
680
+ }
681
+ if (p.length > n.length && p.endsWith(`.${n}`)) return p.slice(0, p.length - n.length - 1);
682
+ if (p.length > n.length && p.endsWith(n)) return p.slice(0, p.length - n.length);
683
+ return owner;
684
+ }
685
+ /**
686
+ * What a manifest PUBLISHES, as `exports` edges.
687
+ *
688
+ * The lane's first ruling excluded `exports` on a grammar inventory — "no
689
+ * `export_statement` node" — and that is the SAME unsound reasoning that produced
690
+ * the wrong `imports` ruling: a grammar cannot see a CONVENTION, and a Node
691
+ * manifest's `exports` field is a convention expressed as ordinary object keys. The
692
+ * audit found 1 of 36 manifests carrying `exports` and 1 carrying `main`, so the
693
+ * mass is small and the principle is not, and the type is emitted.
694
+ *
695
+ * A published entry point IS an export, and each of its names is exported: a
696
+ * subpath key for `exports`, the field name for `main`, the command name for `bin`.
697
+ *
698
+ * Two silent failures lived in this function before it worked, and both are worth
699
+ * recording because neither would ever have been noticed by reading it. It first
700
+ * reached for a `__node` property nothing set, and then iterated the `document`
701
+ * WRAPPER's children when the top-level `pair`s are one level down inside the root
702
+ * `object`. Both type-check, both compile, both do nothing. The only reason this
703
+ * version works is that it was measured — 24 edges on svelte's manifest, and 0 when
704
+ * either guard was wrong.
705
+ */
706
+ extractManifestExports(ctx) {
707
+ if (!this.fileEndsWith(ctx.filePath, "package.json")) return;
708
+ const document = ctx.rootNode;
709
+ if (document.type !== "document") return;
710
+ const rootObject = document.namedChildren?.find((c) => c.type === "object");
711
+ if (!rootObject) return;
712
+ const emit = (name, line, parent) => {
713
+ if (name.length === 0) return;
714
+ const target = [ctx.rootName, ...parent ? [parent] : [], name].join(".");
715
+ ctx.relationships.push({
716
+ source: ctx.rootName,
717
+ target,
718
+ type: ASTEngine.RelationshipType.Exports,
719
+ confidence: 1,
720
+ line_number: line,
721
+ metadata: { jsonSpecific: true, exportForm: "manifest_entry_point", evidenceClass: "structural" }
722
+ });
723
+ };
724
+ for (const pair of rootObject.namedChildren ?? []) {
725
+ if (pair?.type !== "pair") continue;
726
+ const key = this.keyText(pair);
727
+ const value = pair.childForFieldName("value");
728
+ if (!key || !value) continue;
729
+ const line = pair.startPosition.row + 1;
730
+ if (key === "main") {
731
+ if (value.type === "string") emit("main", line);
732
+ } else if (key === "bin") {
733
+ if (value.type === "string") emit("bin", line);
734
+ else {
735
+ for (const binPair of value.namedChildren ?? []) {
736
+ if (binPair?.type !== "pair") continue;
737
+ const binName = this.keyText(binPair);
738
+ if (binName) emit(binName, binPair.startPosition.row + 1, "bin");
739
+ }
740
+ }
741
+ } else if (key === "exports") {
742
+ if (value.type === "string") emit("exports", line);
743
+ else {
744
+ for (const sub of value.namedChildren ?? []) {
745
+ if (sub?.type !== "pair") continue;
746
+ const subPath = this.keyText(sub);
747
+ if (subPath) emit(subPath, sub.startPosition.row + 1, "exports");
748
+ }
749
+ }
750
+ }
751
+ }
752
+ }
753
+ /**
754
+ * JSON Schema COMPOSITION, as `extends`.
755
+ *
756
+ * `allOf` means "must satisfy ALL of these" — a superset-of relationship, and the
757
+ * only schema keyword that means one. `anyOf` and `oneOf` are ALTERNATIVES and have
758
+ * no ontology type, so they are correctly emitted as nothing beyond the `references`
759
+ * their `$ref`s already produce.
760
+ *
761
+ * This was ruled out twice on the same wrong reasoning, and the pattern is now
762
+ * recognisable: a grammar inventory says the PARSER cannot express a construct, which
763
+ * is not the same as the LANGUAGE lacking one. `allOf` is a keyword, and a grammar
764
+ * has no node type for a keyword any more than it has one for a manifest's `exports`
765
+ * field. Measured over the corpus: **1,226 `allOf` `$ref`s** against 88 `anyOf` and
766
+ * 89 `oneOf`, which is the shape the reading predicts — composition is the common
767
+ * case and alternatives are rare.
768
+ */
769
+ extractSchemaComposition(ctx) {
770
+ for (const ref of ctx.referenceSymbols) {
771
+ const fullPath = ref.metadata?.fullPath;
772
+ if (!fullPath) continue;
773
+ let parent = fullPath.slice(0, fullPath.lastIndexOf("."));
774
+ const bracket = parent.lastIndexOf("[");
775
+ if (bracket > 0 && parent.endsWith("]")) parent = parent.slice(0, bracket);
776
+ if (!parent.endsWith(".allOf")) continue;
777
+ const composer = parent.length > ".allOf".length ? parent.slice(0, parent.length - ".allOf".length) : ctx.rootName;
778
+ const refPath = ref.metadata?.refPath;
779
+ if (!refPath) continue;
780
+ const isLocal = refPath.startsWith("#/");
781
+ const isSiblingFile = !isLocal && this.isFileNamingRef(refPath);
782
+ if (!isLocal && !isSiblingFile) continue;
783
+ const target = isLocal ? this.referenceTarget(ctx, refPath) : refPath;
784
+ const siblingFile = isSiblingFile ? this.fileRefCandidate(ctx, refPath) : null;
785
+ const claimedSibling = siblingFile !== null && this.projectCarriesFile(ctx, siblingFile) ? siblingFile : null;
786
+ ctx.relationships.push({
787
+ source: composer,
788
+ target,
789
+ type: ASTEngine.RelationshipType.Extends,
790
+ confidence: 1,
791
+ line_number: this.declLine(ref),
792
+ metadata: {
793
+ jsonSpecific: true,
794
+ composition: "allOf",
795
+ compositionSite: parent,
796
+ refForm: isLocal ? "intra_document" : "sibling_file",
797
+ resolvedPath: isLocal,
798
+ ...claimedSibling ? { importProjectFile: claimedSibling } : isSiblingFile ? { isExternal: true } : {},
799
+ note: "allOf is a superset-of relationship; anyOf/oneOf are alternatives and emit only `references`",
800
+ reasonCode: "json_schema_all_of",
801
+ evidenceClass: "structural",
802
+ endpoints: "qualified_path"
803
+ }
804
+ });
805
+ }
806
+ }
807
+ extractPackageDependencies(ctx) {
808
+ if (!this.fileEndsWith(ctx.filePath, "package.json") && !this.fileEndsWith(ctx.filePath, "composer.json")) return;
809
+ for (const dependency of ctx.dependencySymbols) {
810
+ ctx.imports.push({
811
+ module: dependency.name,
812
+ symbols: [],
813
+ range: dependency.range,
814
+ type: "dependency",
815
+ resolvedPath: "package-manager"
816
+ });
817
+ ctx.relationships.push({
818
+ source: ctx.rootName,
819
+ target: String(dependency.qualifiedName ?? dependency.name),
820
+ type: ASTEngine.RelationshipType.Uses,
821
+ confidence: 1,
822
+ line_number: dependency.range?.startLine ?? 0,
823
+ metadata: {
824
+ relationshipType: "package_dependency",
825
+ package: dependency.name,
826
+ version: this.dependencyVersion(ctx, dependency)
827
+ }
828
+ });
829
+ }
830
+ }
831
+ /**
832
+ * An npm script that runs a SIBLING script, as `calls`.
833
+ *
834
+ * `calls` was ruled out for this lane twice, on a grammar inventory: the JSON
835
+ * grammar has no node type for a function, so it has none for an invocation either.
836
+ * That is the same unsound question that produced the wrong `imports`, `exports` and
837
+ * `extends` rulings, and the corpus settles it. A `package.json` `scripts` object
838
+ * declares its entries as ordinary keys, so both endpoints of the relation are
839
+ * symbols IN THE SAME FILE, and a command string naming one of them is an
840
+ * invocation of it:
841
+ *
842
+ * "scripts": { "pretest": "npm run compile", "compile": "tsc -p ." }
843
+ *
844
+ * `pretest` calls `compile`, and both names resolve. This is not a name-match guess
845
+ * across files — the callee must be a key of the SAME `scripts` object, which is the
846
+ * only condition under which the edge can be checked at all.
847
+ *
848
+ * Three things are declined rather than left silent, each because emitting them would
849
+ * be a fabricated edge:
850
+ *
851
+ * - `npm run <name>` where `<name>` is NOT a key of this `scripts` object. The
852
+ * callee is then a binary, a workspace script in another package, or nothing;
853
+ * `package.json` does not say, so the edge would name a symbol that is not there.
854
+ * Measured separately by `validation/m14/fixtures/json/probes.ts` so the
855
+ * undercount is visible instead of assumed.
856
+ * - a script invoking ITSELF. npm rejects it, and a self-edge carries no order.
857
+ * - npm's IMPLICIT lifecycle hooks (`pre<script>` / `post<script>` around any
858
+ * script). npm runs them without the command naming them, which is real
859
+ * behaviour and is NOT an explicit invocation, so emitting it would attribute an
860
+ * edge to a name the file never wrote in that position.
861
+ *
862
+ * Confidence is 0.8, not 1, and the reason is carried in `reasonCode`: the two
863
+ * endpoints are structural facts but the LINK is read out of a string value by a
864
+ * known-CLI convention, which is exactly the class the honesty contract §4 requires
865
+ * to be below the deterministic threshold and countable.
866
+ */
867
+ /**
868
+ * A key declared twice in one object, as `overrides`.
869
+ *
870
+ * `overrides` was ruled out for this lane on a grammar inventory — the JSON grammar
871
+ * has no node type for a redefinition — which is the unsound question this lane has
872
+ * now had to retract three times (`imports`, `exports`, `extends`, and `calls` on the
873
+ * same corpus evidence). The construct is measurable and it occurs: 21 NAMED
874
+ * duplicate keys across 11 of 11,713 admitted files, e.g.
875
+ * dart-sdk/pkg/front_end/test/language_versioning/data/
876
+ * package_default_version_is_wrong_2/.dart_tool/package_config.json:8 declaring
877
+ * `languageVersion` twice, and duckdb/data/json/11152.json.
878
+ *
879
+ * The edge shape is the BASH lane's, deliberately, because it is the only shape that
880
+ * works when both declarations share one name: source is the OWNER symbol, target is
881
+ * the repeated key, and `overridesLine` carries the PRIOR declaration's line so the
882
+ * replacement is readable without re-deriving it.
883
+ *
884
+ * Both endpoints are QUALIFIED PATHS, like every other type on this lane. It used to be
885
+ * the last type emitting bare names, and the bare form was ambiguous on EVERY corpus
886
+ * edge: measured over the whole admitted population, 22 of 22 targets matched more than
887
+ * one published symbol by name, against 14 of 22 by path. The 8 that become unique are
888
+ * real gains — a bare `rec2` names both elements of an array and a bare `1` names four
889
+ * keys, so the bare endpoint could bind to a declaration in a DIFFERENT object. The 14
890
+ * that stay ambiguous are the two declarations of ONE key sharing a name AND a path,
891
+ * which is declined below and carried in `overridesLine`. The names are retained in
892
+ * metadata as `ownerName`/`key`, where they read as names.
893
+ *
894
+ * `contains` already emits a row for each declaration, so the pair exists independently
895
+ * of this edge.
896
+ *
897
+ * Three things are declined, each because the edge would assert something the file
898
+ * does not say:
899
+ *
900
+ * - The same key in two DIFFERENT objects of one file. A key is unique only within
901
+ * its object, so this is not a redefinition, and it is the corpus's dominant shape
902
+ * (120,174 occurrences across 7,679 files against 21 real duplicates). Emitting
903
+ * it would have been ~5,700 false positives.
904
+ * - An EMPTY key. 10 occurrences across 5 files, every one a document the grammar
905
+ * already recovered with ERROR nodes; an empty key is not a named member.
906
+ * - Any attempt to name the two declarations apart. They share a name AND a
907
+ * qualified path, so distinguishing them would mean minting a symbol the document
908
+ * does not declare. The line metadata carries the distinction instead.
909
+ *
910
+ * Confidence is 1: two `pair` nodes with one key inside one `object` is a fact about
911
+ * the parse tree, read at the same place the symbol is created.
912
+ */
913
+ extractDuplicateKeyOverrides(ctx) {
914
+ const seen = /* @__PURE__ */ new Set();
915
+ for (const c of ctx.overrideCandidates) {
916
+ const identity = `${c.ownerPath}\0${c.key}`;
917
+ if (seen.has(identity)) continue;
918
+ seen.add(identity);
919
+ ctx.relationships.push({
920
+ source: c.ownerPath,
921
+ target: c.targetPath,
922
+ type: ASTEngine.RelationshipType.Overrides,
923
+ confidence: 1,
924
+ line_number: c.line,
925
+ metadata: {
926
+ jsonSpecific: true,
927
+ redefinition: true,
928
+ overridesLine: c.overridesLine,
929
+ ownerName: c.ownerName,
930
+ key: c.key,
931
+ endpoints: "qualified_path",
932
+ evidenceClass: "structural"
933
+ }
934
+ });
935
+ }
936
+ }
937
+ extractNpmScriptCalls(ctx) {
938
+ if (!this.fileEndsWith(ctx.filePath, "package.json")) return;
939
+ const document = ctx.rootNode;
940
+ if (document.type !== "document") return;
941
+ const rootObject = document.namedChildren?.find((c) => c.type === "object");
942
+ if (!rootObject) return;
943
+ const scriptsObject = this.topLevelObjectValue(rootObject, "scripts");
944
+ if (!scriptsObject) return;
945
+ const declared = /* @__PURE__ */ new Map();
946
+ for (const pair of scriptsObject.namedChildren ?? []) {
947
+ if (pair?.type !== "pair") continue;
948
+ const key = this.keyText(pair);
949
+ if (key) declared.set(key, pair);
950
+ }
951
+ if (declared.size === 0) return;
952
+ const scriptsPath = this.childPath(ctx.rootName, "scripts");
953
+ for (const [name, pair] of declared) {
954
+ const value = pair.childForFieldName("value");
955
+ if (value?.type !== "string") continue;
956
+ for (const callee of this.npmRunTargets(this.stripQuotes(value.text))) {
957
+ if (callee === name) continue;
958
+ if (!declared.has(callee)) continue;
959
+ ctx.relationships.push({
960
+ source: this.childPath(scriptsPath, name),
961
+ target: this.childPath(scriptsPath, callee),
962
+ type: ASTEngine.RelationshipType.Calls,
963
+ confidence: 0.8,
964
+ line_number: pair.startPosition.row + 1,
965
+ metadata: {
966
+ jsonSpecific: true,
967
+ callForm: "npm_run_sibling_script",
968
+ evidenceClass: "usage_contextual",
969
+ reasonCode: "npm_run_sibling_script",
970
+ endpoints: "qualified_path",
971
+ scriptName: name,
972
+ calleeName: callee
973
+ }
974
+ });
975
+ }
976
+ }
977
+ }
978
+ /**
979
+ * The script names a command string invokes through a package runner.
980
+ *
981
+ * Tokenised with a character scan and no regex, because the lane's contract is zero
982
+ * regex literals in this file (`script/m12/json-contract-probe.ts` enforces it) and
983
+ * because a runner name is only meaningful as a whole token: `npm` inside `npx` or
984
+ * a path like `./scripts/npm` is not a runner. A QUOTED target is declined, because
985
+ * the quoting means the shell rewrote the argument and the file no longer states
986
+ * the name plainly.
987
+ */
988
+ npmRunTargets(command) {
989
+ const RUNNERS = ["npm", "pnpm", "yarn", "bun"];
990
+ const VERBS = ["run", "run-script", "run-s"];
991
+ const SEPARATORS = [" ", " ", "\n", "\r", ";", "&", "|", "(", ")", "<", ">"];
992
+ const tokens = [];
993
+ let current = "";
994
+ let quoted = false;
995
+ let inQuotes = false;
996
+ for (const ch of command) {
997
+ if (ch === '"') {
998
+ inQuotes = !inQuotes;
999
+ quoted = true;
1000
+ continue;
1001
+ }
1002
+ if (!inQuotes && SEPARATORS.includes(ch)) {
1003
+ if (current.length > 0) tokens.push({ text: current, quoted });
1004
+ current = "";
1005
+ quoted = false;
1006
+ continue;
1007
+ }
1008
+ current += ch;
1009
+ }
1010
+ if (current.length > 0) tokens.push({ text: current, quoted });
1011
+ const out = [];
1012
+ for (let i = 0; i < tokens.length; i++) {
1013
+ if (!RUNNERS.includes(tokens[i].text)) continue;
1014
+ const verb = tokens[i + 1];
1015
+ if (!verb || verb.quoted || !VERBS.includes(verb.text)) continue;
1016
+ const target = tokens[i + 2];
1017
+ if (!target || target.quoted) continue;
1018
+ out.push(target.text);
1019
+ i += 2;
1020
+ }
1021
+ return out;
1022
+ }
1023
+ /** The object value of a named top-level key, or undefined. */
1024
+ topLevelObjectValue(rootObject, name) {
1025
+ for (const pair of rootObject.namedChildren ?? []) {
1026
+ if (pair?.type !== "pair") continue;
1027
+ if (this.keyText(pair) !== name) continue;
1028
+ const value = pair.childForFieldName("value");
1029
+ if (value?.type === "object") return value;
1030
+ }
1031
+ return void 0;
1032
+ }
1033
+ dependencyVersion(ctx, dependency) {
1034
+ const fullPath = String(dependency.metadata?.fullPath || "");
1035
+ const recorded = ctx.dependencyVersions.get(fullPath);
1036
+ if (recorded !== void 0) return recorded;
1037
+ return "unspecified";
1038
+ }
1039
+ /**
1040
+ * Does this `$ref` value name ANOTHER FILE rather than something inside this one?
1041
+ *
1042
+ * Three shapes, and they are genuinely different things: a local pointer (`#/a/b`)
1043
+ * names a location in THIS document; a URL names a vocabulary (a JSON-Schema draft
1044
+ * identifier); a relative path with a document extension names a FILE, and that is a
1045
+ * dependency. Decided structurally — an extension and a path separator or a leading
1046
+ * `./` — with no regex, so the lane's zero-regex contract holds.
1047
+ */
1048
+ /**
1049
+ * DOES THE PROJECT CARRY THE FILE THIS EDGE DEPENDS ON? The precondition of the file claim.
1050
+ *
1051
+ * `candidate` is already project-relative and cannot escape the root (`fileRefCandidate` pops
1052
+ * `..` and returns null when one would leave the project), so the join cannot address anything
1053
+ * outside the project.
1054
+ *
1055
+ * ENOENT / ENOTDIR mean the project does not have it, and the claim is then NOT MADE — the
1056
+ * arbiter would refute it, correctly, because the claim says the project has the file. ANY
1057
+ * OTHER error means this check did not run rather than that the file is absent, and a claim is
1058
+ * not dropped on a check that did not happen: those cases keep the claim, which is exactly the
1059
+ * behaviour every measurement before this one recorded.
1060
+ *
1061
+ * This is a STAT, not a read. The lane's contract is that every decision comes from the TREE
1062
+ * and that the file body is never scanned as text (`BD-regex`, enforced by
1063
+ * `script/m12/json-contract-probe.ts`); asking the filesystem whether a path exists asserts
1064
+ * nothing about that file's contents and adds no regular expression.
1065
+ */
1066
+ projectCarriesFile(ctx, candidate) {
1067
+ const root = ctx.projectRoot;
1068
+ if (typeof root !== "string" || root.length === 0) return false;
1069
+ try {
1070
+ return statSync(join(root, candidate)).isFile();
1071
+ } catch (e) {
1072
+ return !(e?.code === "ENOENT" || e?.code === "ENOTDIR");
1073
+ }
1074
+ }
1075
+ isFileNamingRef(refPath) {
1076
+ if (refPath.length === 0) return false;
1077
+ if (refPath.startsWith("#/")) return false;
1078
+ if (refPath.startsWith("http://") || refPath.startsWith("https://")) return false;
1079
+ const DOCUMENT_EXTENSIONS = ["json", "jsonc", "json5", "yaml", "yml", "avsc", "avdl", "proto"];
1080
+ const withoutFragment = refPath.split("#")[0];
1081
+ const dot = withoutFragment.lastIndexOf(".");
1082
+ if (dot < 0) return false;
1083
+ const ext = withoutFragment.slice(dot + 1);
1084
+ if (!DOCUMENT_EXTENSIONS.includes(ext)) return false;
1085
+ return true;
1086
+ }
1087
+ /**
1088
+ * A `$ref`'s TARGET, and a local pointer is a pointer INTO THIS DOCUMENT.
1089
+ *
1090
+ * The gate found the defect this fixes (`incoming-gate.ts json json 60`): nine `references`
1091
+ * edges whose target was the literal string `#` and whose metadata said `isExternal: true`,
1092
+ * all nine from one file — kubernetes' vendored draft-04 meta-schema
1093
+ * (`.../gnostic-models/jsonschema/schema.json`), whose `definitions.schemaArray.items` is
1094
+ * `{ "$ref": "#" }`. `#` is the JSON-Schema ROOT POINTER ("the schema itself"), so it is the
1095
+ * most local reference a document can write, and BOTH rules below got it wrong for the same
1096
+ * reason: they test `startsWith("#/")`, and a bare `#` is not that prefix.
1097
+ *
1098
+ * the TARGET fell through to the raw `#` — a string no symbol can be named, so the edge
1099
+ * named nothing. It now resolves to `ctx.rootName`, which IS the published
1100
+ * identity of the document root (the module symbol `analyze` pushes first), so
1101
+ * the pointer binds to the thing it points at.
1102
+ * the MARKER (see the emission site) called it EXTERNAL, which is a claim about the corpus
1103
+ * that a pointer into the file's own document cannot support — the same
1104
+ * file-local-class-as-corpus-claim error the fsharp lane retired this week.
1105
+ *
1106
+ * Measured after: the gate reads 29 of 29 `references` resolved, `ext?` 0, `extFAIL` 0 — and
1107
+ * the verdict, which had been returning a branch that prints NO totals, prints them again.
1108
+ */
1109
+ referenceTarget(ctx, refPath) {
1110
+ if (refPath === "#") return ctx.rootName;
1111
+ if (!refPath.startsWith("#/")) return refPath;
1112
+ const local = [ctx.rootName, ...refPath.slice(2).split("/").map((seg) => this.decodePointerSegment(seg))].join(".");
1113
+ return ctx.symbolMap.has(local) ? local : refPath;
1114
+ }
1115
+ /**
1116
+ * A `$ref` THAT NAMES AN IDENTITY THIS DOCUMENT DECLARES — or `null`.
1117
+ *
1118
+ * This is the third form a `$ref` can take, and the one whose target was being published
1119
+ * verbatim with a corpus claim it cannot support:
1120
+ *
1121
+ * 1 `#/a/b` a JSON POINTER — a location in this document; `referenceTarget` answers it.
1122
+ * 2 `x.json` a SIBLING FILE — `imports`, with the project's file claim on both labels.
1123
+ * 3 `<uri>` an IDENTITY — `$id` declares a base URI for a schema resource and `$anchor`
1124
+ * declares a fragment name inside one. JSON Schema resolves the reference
1125
+ * against the declared base, and the declaration IS the object carrying the
1126
+ * `$id`; this analyzer already publishes that object's qualified path.
1127
+ *
1128
+ * MEASURED (`validation/m14/fixtures/json/document-identity-census.ts`): 6 corpus `$ref`s in
1129
+ * 2 repositories name an identity their own document declares; 4 of them were this lane's
1130
+ * whole `ext?` class on `--repo=gql-graphql-mesh`.
1131
+ *
1132
+ * The returned path is the DECLARING OBJECT'S `qualifiedName`, i.e. production resolver
1133
+ * strategy 1's exact key — never a basename stem — so this cannot mass-move edges into
1134
+ * `collRes` the way a stem rebase would, and it mints no symbol.
1135
+ *
1136
+ * A fragment is accepted only where the base's resource really is where it lives: for an
1137
+ * `$anchor`, the declaring object must BE the base object or sit UNDER it. A bare anchor name
1138
+ * scopes nothing on its own, so a name declared in another resource of the same document is
1139
+ * refused rather than guessed.
1140
+ */
1141
+ documentDeclaredIdentity(ctx, refPath) {
1142
+ const hash = refPath.indexOf("#");
1143
+ const base = this.withoutEmptyFragment(hash < 0 ? refPath : refPath.slice(0, hash));
1144
+ if (base.length === 0) return null;
1145
+ const declared = ctx.documentIds.get(base);
1146
+ if (declared === void 0) return null;
1147
+ const fragment = hash < 0 ? "" : refPath.slice(hash + 1);
1148
+ if (fragment.length === 0) return declared;
1149
+ if (fragment.startsWith("/")) return null;
1150
+ const anchorPaths = ctx.documentAnchors.get(fragment);
1151
+ if (!anchorPaths) return null;
1152
+ const underBase = anchorPaths.find((p) => p === declared || p.startsWith(`${declared}.`));
1153
+ return underBase ?? null;
1154
+ }
1155
+ /** One trailing empty fragment is not part of an identity: `…/schema` and `…/schema#` name the
1156
+ * same base URI (RFC 3986 §6.2.2), and `$id`/`$ref` use both spellings. */
1157
+ withoutEmptyFragment(uri) {
1158
+ return uri.endsWith("#") ? uri.slice(0, -1) : uri;
1159
+ }
1160
+ /** RFC 6901: within a pointer segment, `~1` is `/` and `~0` is `~`, decoded in that order. */
1161
+ decodePointerSegment(segment) {
1162
+ if (!segment.includes("~")) return segment;
1163
+ let out = "";
1164
+ for (let i = 0; i < segment.length; i++) {
1165
+ if (segment[i] === "~" && i + 1 < segment.length) {
1166
+ const next = segment[i + 1];
1167
+ if (next === "0") {
1168
+ out += "~";
1169
+ i++;
1170
+ continue;
1171
+ }
1172
+ if (next === "1") {
1173
+ out += "/";
1174
+ i++;
1175
+ continue;
1176
+ }
1177
+ }
1178
+ out += segment[i];
1179
+ }
1180
+ return out;
1181
+ }
1182
+ /**
1183
+ * IS THIS `$ref` A LOCAL POINTER? A pointer into the file's own document — `#` (the root) or
1184
+ * `#/a/b` (a location inside it) — is answered by the document and by nothing else, so it is
1185
+ * neither an import nor an external target. Everything else (`./other.json`,
1186
+ * `schemas/x.json#/defs/Y`, `https://…`) names something the document does not contain.
1187
+ */
1188
+ isLocalPointer(refPath) {
1189
+ return refPath === "#" || refPath.startsWith("#/");
1190
+ }
1191
+ /**
1192
+ * THE FILE A `$ref` NAMES, in the spelling the PROJECT's own file list uses — or `null`.
1193
+ *
1194
+ * `null` is the honest answer whenever the path cannot be derived soundly: no project root was
1195
+ * supplied, the referring file does not sit under the root, the refPath is not a relative
1196
+ * document path (a URL, an absolute path, a bare fragment), or folding `..` escapes the project
1197
+ * (a sibling checkout is not an admitted file, and claiming one would be REFUTED by the gate).
1198
+ * A wrong candidate is worse than none: the arbiter refutes a file claim it cannot find, and a
1199
+ * refutation is counted against the lane exactly as an unresolved edge is.
1200
+ */
1201
+ fileRefCandidate(ctx, refPath) {
1202
+ if (!this.isFileNamingRef(refPath)) return null;
1203
+ const self = projectRelativePath(ctx.filePath, ctx.projectRoot);
1204
+ if (self === null) return null;
1205
+ const cut = self.lastIndexOf("/");
1206
+ const dir = cut < 0 ? "" : self.slice(0, cut + 1);
1207
+ const noFragment = refPath.split("#")[0];
1208
+ if (noFragment.length === 0) return null;
1209
+ const out = [];
1210
+ for (const seg of (dir + noFragment).split("/")) {
1211
+ if (seg.length === 0 || seg === ".") continue;
1212
+ if (seg === "..") {
1213
+ if (out.length === 0) return null;
1214
+ out.pop();
1215
+ continue;
1216
+ }
1217
+ out.push(seg);
1218
+ }
1219
+ return out.length > 0 ? out.join("/") : null;
1220
+ }
1221
+ isReference(key, value) {
1222
+ if (key === "$ref") return true;
1223
+ return value.type === "string" && isPathReference(this.stripQuotes(value.text));
1224
+ }
1225
+ referencePath(key, value) {
1226
+ if (!this.isReference(key, value) || value.type !== "string") return void 0;
1227
+ return this.stripQuotes(value.text);
1228
+ }
1229
+ isRequired(ctx, parentPath, key) {
1230
+ return ctx.requiredByObject.get(parentPath)?.has(key) ?? false;
1231
+ }
1232
+ hasValidation(ctx, key, value) {
1233
+ if (VALIDATION_KEYS.has(key)) return true;
1234
+ if (value.type !== "object") return false;
1235
+ for (const pair of value.namedChildren ?? []) {
1236
+ if (pair?.type === "pair" && VALIDATION_KEYS.has(this.keyText(pair))) return true;
1237
+ }
1238
+ return false;
1239
+ }
1240
+ isApiEndpoint(key, value) {
1241
+ if (key.length === 0 || key[0] !== "/") return false;
1242
+ if (value.type !== "object") return false;
1243
+ for (const pair of value.namedChildren ?? []) {
1244
+ if (pair?.type === "pair" && API_METHOD_KEYS.has(this.keyText(pair))) return true;
1245
+ }
1246
+ return false;
1247
+ }
1248
+ detectFrameworks(ctx) {
1249
+ const frameworks = [];
1250
+ const file = ctx.filePath.toLowerCase();
1251
+ const has = (name) => ctx.symbolMap.has(name);
1252
+ const dependency = (name) => {
1253
+ for (const symbol of ctx.dependencySymbols) if (symbol.name === name) return true;
1254
+ return false;
1255
+ };
1256
+ if (this.fileEndsWith(file, "package.json")) frameworks.push("npm/Node.js");
1257
+ if (this.fileEndsWith(file, "composer.json")) frameworks.push("Composer/PHP");
1258
+ if (this.fileEndsWith(file, "tsconfig.json") || has("compilerOptions")) frameworks.push("TypeScript");
1259
+ if (has("openapi") || has("swagger")) frameworks.push("OpenAPI/Swagger");
1260
+ if (has("$schema") || ctx.schemaObjects.size > 0) frameworks.push("JSON Schema");
1261
+ if (has("Version") && has("Resources")) frameworks.push("AWS CloudFormation");
1262
+ if (dependency("react") || dependency("@types/react")) frameworks.push("React");
1263
+ if (dependency("vue") || dependency("@vue/core")) frameworks.push("Vue.js");
1264
+ if (dependency("next")) frameworks.push("Next.js");
1265
+ return frameworks;
1266
+ }
1267
+ detectPatterns(ctx) {
1268
+ const patterns = [];
1269
+ if (ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema")) patterns.push("JSON Schema Validation");
1270
+ if (ctx.referenceSymbols.length > 0) patterns.push("Reference-based Relationships");
1271
+ if (ctx.apiEndpointSymbols.length > 0) patterns.push("RESTful API Design");
1272
+ if (ctx.hasArrays) patterns.push("Collection Data Types");
1273
+ if (ctx.maxDepth > 3) patterns.push("Deep Nested Structures");
1274
+ if (ctx.symbolMap.has("dependencies") || ctx.symbolMap.has("devDependencies")) patterns.push("Dependency Management");
1275
+ return patterns;
1276
+ }
1277
+ structuralScores(ctx) {
1278
+ let dataModelingScore = 70;
1279
+ if (ctx.objectCount > 0 && ctx.propertyCount / ctx.objectCount >= 2) dataModelingScore += 10;
1280
+ if (ctx.maxDepth <= 4) dataModelingScore += 10;
1281
+ let schemaValidationScore = 50;
1282
+ if (ctx.schemaObjects.size > 0) schemaValidationScore += 25;
1283
+ if (ctx.validationSymbols.length > 0) schemaValidationScore += 15;
1284
+ if (ctx.referenceSymbols.length > 0) schemaValidationScore += 10;
1285
+ return { dataModelingScore: Math.min(100, dataModelingScore), schemaValidationScore: Math.min(100, schemaValidationScore) };
1286
+ }
1287
+ insights(ctx) {
1288
+ const out = [];
1289
+ if (ctx.referenceSymbols.length > 0) out.push(`JSON references detected (${ctx.referenceSymbols.length})`);
1290
+ if (ctx.validationSymbols.length > 0) out.push(`Schema validation keywords detected (${ctx.validationSymbols.length})`);
1291
+ if (ctx.hasArrays) out.push(`Arrays detected (${ctx.arrayCount})`);
1292
+ return out;
1293
+ }
1294
+ valueType(node) {
1295
+ switch (node.type) {
1296
+ case "object":
1297
+ return "object";
1298
+ case "array":
1299
+ return "array";
1300
+ case "string":
1301
+ return "string";
1302
+ case "number":
1303
+ return "number";
1304
+ case "true":
1305
+ case "false":
1306
+ return "boolean";
1307
+ case "null":
1308
+ return "null";
1309
+ default:
1310
+ return "unknown";
1311
+ }
1312
+ }
1313
+ keyText(pair) {
1314
+ const key = pair.type === "pair" ? pair.childForFieldName("key") : pair;
1315
+ return key ? this.stripQuotes(key.text) : "";
1316
+ }
1317
+ stripQuotes(text) {
1318
+ const value = text.trim();
1319
+ if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') return value.slice(1, -1);
1320
+ return value;
1321
+ }
1322
+ /**
1323
+ * An array item's SYMBOL name and the SYMBOL name of the array that holds it.
1324
+ *
1325
+ * Derived from the array's own QUALIFIED path (minus the module prefix) plus the index,
1326
+ * never from the item's path and never from the array's bare NAME — and the second half of
1327
+ * that is the fix. The array's own NAME is not enough once an array can be an ELEMENT of
1328
+ * another array, because then two different arrays share it:
1329
+ *
1330
+ * { "rows": [ [ {"cell": 1}, {"cell": 2} ] ] }
1331
+ *
1332
+ * The inner array's name arrives as `rows`, the same name the OUTER array has, so its two
1333
+ * elements came out as `rows[0]` and `rows[1]` — names that collide with the outer array's
1334
+ * own element namespace — while their qualified paths said `rows[0][0]` and `rows[0][1]`.
1335
+ * The consequence was not cosmetic: `contains` is emitted from the OWNER symbol, and the
1336
+ * owner was derived from the truncated name, so the document stated
1337
+ *
1338
+ * rows[0] contains rows[0][1]
1339
+ *
1340
+ * between two objects that are not nested at all. Measured over the corpus: 30 of the 37
1341
+ * `contains` edges an independent oracle could not corroborate, and 183 at the whole-population
1342
+ * count before the endpoint change. The two walkers agreed on the defect, which is the case
1343
+ * the honesty contract calls out: agreement is evidence that two implementations agree, not
1344
+ * that either is right.
1345
+ *
1346
+ * Deriving from the array's own NAME is still right for the ordinary case, because a key's
1347
+ * qualified path minus the module prefix IS its name — `a.rows` → `rows` — and it is what
1348
+ * makes a dotted module name survive: `index.svelte.warnings.json` gives the module
1349
+ * `index.svelte.warnings` and the item owner `items`, not `warnings`.
1350
+ */
1351
+ arrayItemName(arrayPath, rootName, index) {
1352
+ if (arrayPath === rootName) return { item: `${rootName}[${index}]`, holder: rootName };
1353
+ const lastSegment = arrayPath.slice(arrayPath.lastIndexOf(".") + 1);
1354
+ const self = lastSegment.endsWith("]") ? this.afterModuleImpl(arrayPath, rootName) : lastSegment;
1355
+ return { item: `${self}[${index}]`, holder: self };
1356
+ }
1357
+ /**
1358
+ * The name a qualified path carries once the module prefix is removed — the ONE place that
1359
+ * strips it, because getting it wrong is invisible in isolation and expensive in aggregate.
1360
+ *
1361
+ * The character at `rootName.length` says what kind of path this is, and the two cases need
1362
+ * OPPOSITE handling:
1363
+ *
1364
+ * `.` the path continues under a KEY, and the key is the name — drop the separator, so
1365
+ * `mod.rows[0]` gives `rows[0]`.
1366
+ * `[` the path hangs off the module by an INDEX and there is no key at all, so the
1367
+ * bracket IS the name's first character — keep it, so `mod[4]` gives `[4]`.
1368
+ *
1369
+ * A fixed `slice(rootName.length + 1)` assumes the first case and therefore eats the `[` in
1370
+ * the second, publishing `4]`, `4][2]`, `4][2][6]`. Measured on a sample of the corpus's
1371
+ * top-level-array documents: 19,708 names over 214 files, 14.9% of that population — a
1372
+ * family (`4][2][6][1]`) no keyed or element name can produce, which is what distinguishes
1373
+ * it from the correct `Statement[0]` spelling that also ends in `]`.
1374
+ *
1375
+ * A path that is neither (no separator at all) returns the path unchanged rather than
1376
+ * chopping a leading character off a name that has no prefix to remove.
1377
+ */
1378
+ afterModuleImpl(path, rootName) {
1379
+ if (path === rootName) return path;
1380
+ const boundary = path.charAt(rootName.length);
1381
+ if (boundary === "[") return path.slice(rootName.length);
1382
+ if (boundary === ".") return path.slice(rootName.length + 1);
1383
+ return path;
1384
+ }
1385
+ addArrayObjectSymbol(ctx, node, path, itemName) {
1386
+ const bracket = itemName.lastIndexOf("[");
1387
+ const name = itemName;
1388
+ const ownerName = bracket > 0 ? itemName.slice(0, bracket) : ctx.rootName;
1389
+ this.addSymbol(ctx, {
1390
+ name,
1391
+ kind: ASTEngine.SymbolKind.Class,
1392
+ qualifiedName: path,
1393
+ range: this.nodeToRange(node),
1394
+ ownerName,
1395
+ ownerKind: ASTEngine.SymbolKind.Class,
1396
+ scope: ownerName,
1397
+ visibility: "public",
1398
+ returnType: "object",
1399
+ metadata: {
1400
+ jsonSpecific: true,
1401
+ nodeType: "object",
1402
+ valueType: "object",
1403
+ fullPath: path,
1404
+ isObject: true,
1405
+ isArrayItem: true,
1406
+ depth: this.pathDepth(path, ctx.rootName)
1407
+ }
1408
+ });
1409
+ }
1410
+ /**
1411
+ * The symbol for an array element that is itself an ARRAY.
1412
+ *
1413
+ * Same shape as `addArrayObjectSymbol` and for the same reason: `contains` is emitted from
1414
+ * the OWNER symbol, so an element that owns elements needs a symbol of its own. Its name is
1415
+ * the qualified path minus the module prefix (`rows[0]`), which is the same string the
1416
+ * object-element rule produces, so the two kinds cannot collide on one position.
1417
+ */
1418
+ addArrayElementSymbol(ctx, node, path, itemName) {
1419
+ const lastSegment = path.slice(path.lastIndexOf(".") + 1);
1420
+ const name = lastSegment.endsWith("]") ? this.afterModuleImpl(path, ctx.rootName) : lastSegment;
1421
+ const bracket = name.lastIndexOf("[");
1422
+ const ownerName = bracket > 0 ? name.slice(0, bracket) : ctx.rootName;
1423
+ this.addSymbol(ctx, {
1424
+ name,
1425
+ kind: ASTEngine.SymbolKind.Class,
1426
+ qualifiedName: path,
1427
+ range: this.nodeToRange(node),
1428
+ ownerName,
1429
+ ownerKind: ASTEngine.SymbolKind.Class,
1430
+ scope: ownerName,
1431
+ visibility: "public",
1432
+ returnType: "array",
1433
+ metadata: {
1434
+ jsonSpecific: true,
1435
+ nodeType: "array",
1436
+ valueType: "array",
1437
+ fullPath: path,
1438
+ isArray: true,
1439
+ isArrayItem: true,
1440
+ depth: this.pathDepth(path, ctx.rootName)
1441
+ }
1442
+ });
1443
+ }
1444
+ childPath(parentPath, key) {
1445
+ return parentPath ? `${parentPath}.${key}` : key;
1446
+ }
1447
+ pushArrayChildren(stack, node, parentPath, parentName, rootName) {
1448
+ const children = node.namedChildren ?? [];
1449
+ for (let i = children.length - 1; i >= 0; i--) {
1450
+ const child = children[i];
1451
+ if (!child) continue;
1452
+ const itemPath = `${parentPath}[${i}]`;
1453
+ const { item } = this.arrayItemName(parentPath, rootName, i);
1454
+ const isElement = child.type === "object" || child.type === "array";
1455
+ stack.push({
1456
+ node: child,
1457
+ parentPath: itemPath,
1458
+ parentName: isElement ? item : parentName,
1459
+ emitObject: isElement
1460
+ });
1461
+ }
1462
+ }
1463
+ /**
1464
+ * Depth of a qualified path below the module, counted in dot-separated segments.
1465
+ *
1466
+ * This reads the same prefix `afterModuleImpl` removes, so it calls that rather than
1467
+ * repeating the arithmetic. The two derivations had drifted independently and the second
1468
+ * one was wrong for the same reason: for a top-level array the character at
1469
+ * `rootName.length` is `[`, so the fixed `+ 1` ate it. It happened to cost nothing here —
1470
+ * both `4][2][6]` and `[4][2][6]` split to one segment — which is exactly why it survived
1471
+ * a fix to the naming rule beside it. Two copies of one derivation is a second chance to be
1472
+ * wrong for free.
1473
+ */
1474
+ pathDepth(path, rootName) {
1475
+ if (path === rootName) return 0;
1476
+ return this.afterModuleImpl(path, rootName).split(".").length;
1477
+ }
1478
+ updateDepth(ctx, path) {
1479
+ const depth = this.pathDepth(path, ctx.rootName);
1480
+ if (depth > ctx.maxDepth) ctx.maxDepth = depth;
1481
+ }
1482
+ pushNamedChildren(stack, node, parentPath, parentName = parentPath, duplicateKeys) {
1483
+ const children = node.namedChildren ?? [];
1484
+ for (let i = children.length - 1; i >= 0; i--) {
1485
+ const child = children[i];
1486
+ if (child) stack.push({ node: child, parentPath, parentName, duplicateKeys });
1487
+ }
1488
+ }
1489
+ /**
1490
+ * The node types that are JSON VALUES. A `comment` is a named child of `document` in
1491
+ * this grammar and is not a value: it introduces no key and cannot publish a name. An
1492
+ * `ERROR` node is the grammar's recovery shape and says nothing about how many documents
1493
+ * the file really holds.
1494
+ */
1495
+ isValueNode(node) {
1496
+ const t = node.type;
1497
+ return t === "object" || t === "array" || t === "string" || t === "number" || t === "true" || t === "false" || t === "null";
1498
+ }
1499
+ /**
1500
+ * Push a document's top-level values, giving each one its OWN position when the file
1501
+ * holds more than one.
1502
+ *
1503
+ * ## The defect
1504
+ *
1505
+ * Concatenated JSON is real input — cargo/rustfix's recorded compiler diagnostics are
1506
+ * sequences of 2 to 5 top-level objects in one file — and this grammar parses that as ONE
1507
+ * `document` node with N named children. `pushNamedChildren` gave every one of them the
1508
+ * SAME `parentPath`, so all N published `<module>.<key>` and the qualified name stopped
1509
+ * identifying anything:
1510
+ *
1511
+ * name=code kind=class qn=str-lit-type-mismatch.code line 3
1512
+ * name=code kind=class qn=str-lit-type-mismatch.code line 73
1513
+ * name=code kind=class qn=str-lit-type-mismatch.code line 143
1514
+ * name=code kind=property qn=str-lit-type-mismatch.code line 213
1515
+ *
1516
+ * Four symbols under one name, the fourth of a different kind — so the shared arbiter
1517
+ * counts it AMBIGUOUS and the reverse walk picks a winner that is simply the wrong line.
1518
+ * This is the whole of bucket A in the 601-file ambiguity split.
1519
+ *
1520
+ * ## Why the position is conditional
1521
+ *
1522
+ * A document with ONE value keeps `<module>` as its parent path, unchanged. That is 13,835
1523
+ * of 13,951 admitted files and it is the contract every fixture encodes. 358 more hold
1524
+ * one value beside comments or ERROR nodes, and `comment+object` is the commonest
1525
+ * multi-child shape in the corpus at 24 files — gating on `namedChildCount > 1` would
1526
+ * rename all of them for no reason. `top-level-value-positions.ts` measures the split:
1527
+ * 45 files have >=2 values, 358 have 1 value plus extras, 13,477 have exactly one child.
1528
+ *
1529
+ * ## Why the spelling is `<module>[i]` and not a new separator
1530
+ *
1531
+ * It is the spelling `arrayItemName` already gives a top-level array's elements, and a
1532
+ * concatenated document is the same shape written without brackets: N values in sequence
1533
+ * rather than N elements in an array. Reusing it means one convention, not two, and
1534
+ * `afterModuleImpl` already handles the `[` boundary.
1535
+ */
1536
+ pushTopLevelValues(stack, node, parentPath, parentName) {
1537
+ let valueCount = 0;
1538
+ const children = node.namedChildren ?? [];
1539
+ for (const child of children) {
1540
+ if (child && this.isValueNode(child)) valueCount++;
1541
+ }
1542
+ if (valueCount <= 1) {
1543
+ this.pushNamedChildren(stack, node, parentPath, parentName);
1544
+ return;
1545
+ }
1546
+ let position = 0;
1547
+ for (let i = children.length - 1; i >= 0; i--) {
1548
+ const child = children[i];
1549
+ if (!child) continue;
1550
+ if (!this.isValueNode(child)) continue;
1551
+ const name = `${parentPath}[${position}]`;
1552
+ stack.push({ node: child, parentPath: name, parentName: name, emitObject: child.type === "object" });
1553
+ position++;
1554
+ }
1555
+ }
1556
+ /**
1557
+ * A document's IDENTITY, and it is scoped to the file.
1558
+ *
1559
+ * ## The defect this fixes
1560
+ *
1561
+ * The identity was the filesystem BASENAME, and every `package.json` in a project publishes a
1562
+ * module symbol named `package`. Measured over a 601-file stride of the json corpus
1563
+ * (`validation/m14/measurements/json-ambiguity-split-601-after.json`): 90 colliding target names
1564
+ * across 1,218 collision symbols, and the `package.*` family — `package.dependencies` alone
1565
+ * carries 3 declarations — is the bucket that file scoping resolves on its own. So the `contains`
1566
+ * edge asserting that `package` owns `dependencies` could not be walked in either direction:
1567
+ * the shared resolver binds a bare name only under `HAVING COUNT(*) = 1`, and an exact-key match
1568
+ * has to be unique too.
1569
+ *
1570
+ * ## The rule
1571
+ *
1572
+ * Scoping by the project-relative directory chain, dot-joined, basename last. Dot-joined because
1573
+ * every downstream consumer already reads a dotted string as a path; basename last because the
1574
+ * leaf of every member path is computed by slicing a fixed number of characters off the module
1575
+ * prefix, so moving the basename would break that arithmetic. The full rule and its three
1576
+ * properties are in `src/ast/project-scoped-identity.ts`, which is shared on purpose.
1577
+ *
1578
+ * ## What it does NOT do
1579
+ *
1580
+ * It does not make the BASENAME key unique — `package` is still published by every package.json,
1581
+ * because the basename is the readable name a human uses. It makes the QUALIFIED PATH unique,
1582
+ * and that path is what `contains` emits and what exact-match resolution binds.
1583
+ */
1584
+ documentName(filePath, projectRoot) {
1585
+ const slash = filePath.lastIndexOf("/");
1586
+ const base = slash >= 0 ? filePath.slice(slash + 1) : filePath;
1587
+ const lower = base.toLowerCase();
1588
+ for (const suffix of [".json", ".jsonc", ".json5"]) {
1589
+ if (lower.endsWith(suffix)) return this.scoped(base.slice(0, base.length - suffix.length) || "document", filePath, projectRoot);
1590
+ }
1591
+ return this.scoped(base || "document", filePath, projectRoot);
1592
+ }
1593
+ /**
1594
+ * Fold the project-relative directory chain onto the document's stem.
1595
+ *
1596
+ * One place, so the scope cannot be applied on one return path and missed on another — the
1597
+ * early-return shape this replaced is exactly how a document keeps its basename on the branch
1598
+ * that strips an extension and gets scoped on the branch that does not.
1599
+ */
1600
+ scoped(stem, filePath, projectRoot) {
1601
+ return applyDocumentScope(stem, documentDirScope(filePath, projectRoot));
1602
+ }
1603
+ fileEndsWith(filePath, suffix) {
1604
+ return filePath.toLowerCase().endsWith(suffix.toLowerCase());
1605
+ }
1606
+ documentType(ctx) {
1607
+ if (this.fileEndsWith(ctx.filePath, "package.json")) return "npm_package";
1608
+ if (this.fileEndsWith(ctx.filePath, "composer.json")) return "composer_package";
1609
+ if (this.isApiSpec(ctx)) return "openapi_spec";
1610
+ if (ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema")) return "json_schema";
1611
+ return "generic_json";
1612
+ }
1613
+ isApiSpec(ctx) {
1614
+ return (ctx.symbolMap.has("openapi") || ctx.symbolMap.has("swagger")) && ctx.symbolMap.has("paths");
1615
+ }
1616
+ /**
1617
+ * The 1-based line a symbol was DECLARED on, for the `line_number` every
1618
+ * relationship must carry. M60 requires a site line on every edge, and this
1619
+ * analyzer emitted NONE: the string `line_number` did not occur in the file, so
1620
+ * every JSON edge in the index was stored with a NULL line and no consumer —
1621
+ * the graph panel included — could point at where an edge came from. Symbols
1622
+ * carried `range` all along, so the position was known and simply not published.
1623
+ */
1624
+ declLine(symbol) {
1625
+ return symbol?.range?.startLine ?? 0;
1626
+ }
1627
+ nodeToRange(node) {
1628
+ return {
1629
+ startLine: node.startPosition.row + 1,
1630
+ startColumn: node.startPosition.column + 1,
1631
+ endLine: node.endPosition.row + 1,
1632
+ endColumn: node.endPosition.column + 1,
1633
+ startIndex: node.startIndex,
1634
+ endIndex: node.endIndex
1635
+ };
1636
+ }
1637
+ };
1638
+ export {
1639
+ RealJsonAnalyzer
1640
+ };