aontu 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +4 -4
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +7 -28
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/alias.js.map +1 -1
  6. package/dist/allow.d.ts +23 -0
  7. package/dist/allow.js +138 -0
  8. package/dist/allow.js.map +1 -0
  9. package/dist/aontu.d.ts +4 -2
  10. package/dist/aontu.js +4 -80
  11. package/dist/aontu.js.map +1 -1
  12. package/dist/aontumodel.d.ts +4 -0
  13. package/dist/aontumodel.js +33 -0
  14. package/dist/aontumodel.js.map +1 -0
  15. package/dist/cli.d.ts +10 -1
  16. package/dist/cli.js +877 -479
  17. package/dist/cli.js.map +1 -1
  18. package/dist/ctx.js +0 -48
  19. package/dist/ctx.js.map +1 -1
  20. package/dist/diff.js +0 -32
  21. package/dist/diff.js.map +1 -1
  22. package/dist/err.js +0 -40
  23. package/dist/err.js.map +1 -1
  24. package/dist/escape.js +0 -45
  25. package/dist/escape.js.map +1 -1
  26. package/dist/exactjson.d.ts +0 -35
  27. package/dist/exactjson.js +0 -131
  28. package/dist/exactjson.js.map +1 -1
  29. package/dist/format.js +55 -189
  30. package/dist/format.js.map +1 -1
  31. package/dist/grammar.d.ts +9 -0
  32. package/dist/grammar.js +54 -0
  33. package/dist/grammar.js.map +1 -0
  34. package/dist/graph.js +0 -26
  35. package/dist/graph.js.map +1 -1
  36. package/dist/hcanon.js +0 -82
  37. package/dist/hcanon.js.map +1 -1
  38. package/dist/helpdoc.d.ts +16 -0
  39. package/dist/helpdoc.js +59 -0
  40. package/dist/helpdoc.js.map +1 -0
  41. package/dist/hints.d.ts +0 -6
  42. package/dist/hints.js +59 -47
  43. package/dist/hints.js.map +1 -1
  44. package/dist/jsonschema.js +0 -114
  45. package/dist/jsonschema.js.map +1 -1
  46. package/dist/keyorder.d.ts +0 -7
  47. package/dist/keyorder.js +0 -41
  48. package/dist/keyorder.js.map +1 -1
  49. package/dist/lang.js +32 -877
  50. package/dist/lang.js.map +1 -1
  51. package/dist/lower.d.ts +3 -0
  52. package/dist/lower.js +14 -61
  53. package/dist/lower.js.map +1 -1
  54. package/dist/lsp-server.js +0 -16
  55. package/dist/lsp-server.js.map +1 -1
  56. package/dist/lsp.d.ts +1 -1
  57. package/dist/lsp.js +12 -159
  58. package/dist/lsp.js.map +1 -1
  59. package/dist/mcp-server.js +0 -26
  60. package/dist/mcp-server.js.map +1 -1
  61. package/dist/mcp.js +0 -113
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/mod-tool.js +0 -130
  64. package/dist/mod-tool.js.map +1 -1
  65. package/dist/mod.js +0 -162
  66. package/dist/mod.js.map +1 -1
  67. package/dist/patch.js +0 -217
  68. package/dist/patch.js.map +1 -1
  69. package/dist/provenance.js +0 -140
  70. package/dist/provenance.js.map +1 -1
  71. package/dist/query.js +0 -75
  72. package/dist/query.js.map +1 -1
  73. package/dist/reach.js +0 -43
  74. package/dist/reach.js.map +1 -1
  75. package/dist/relation.d.ts +2 -0
  76. package/dist/relation.js +3 -58
  77. package/dist/relation.js.map +1 -1
  78. package/dist/render.js +33 -143
  79. package/dist/render.js.map +1 -1
  80. package/dist/report-sarif.d.ts +0 -11
  81. package/dist/report-sarif.js +0 -28
  82. package/dist/report-sarif.js.map +1 -1
  83. package/dist/sig.js +0 -35
  84. package/dist/sig.js.map +1 -1
  85. package/dist/sigdecl.js +1 -1
  86. package/dist/sigdecl.js.map +1 -1
  87. package/dist/siggate.js +0 -4
  88. package/dist/siggate.js.map +1 -1
  89. package/dist/site.js +3 -29
  90. package/dist/site.js.map +1 -1
  91. package/dist/subsume.d.ts +0 -10
  92. package/dist/subsume.js +0 -137
  93. package/dist/subsume.js.map +1 -1
  94. package/dist/template.d.ts +2 -1
  95. package/dist/template.js +58 -138
  96. package/dist/template.js.map +1 -1
  97. package/dist/trim.js +0 -41
  98. package/dist/trim.js.map +1 -1
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/dist/type.js.map +1 -1
  101. package/dist/unify.js +12 -242
  102. package/dist/unify.js.map +1 -1
  103. package/dist/utility.js +0 -22
  104. package/dist/utility.js.map +1 -1
  105. package/dist/val/AbnfFuncVal.d.ts +18 -0
  106. package/dist/val/AbnfFuncVal.js +132 -0
  107. package/dist/val/AbnfFuncVal.js.map +1 -0
  108. package/dist/val/AbsentVal.d.ts +11 -0
  109. package/dist/val/AbsentVal.js +30 -0
  110. package/dist/val/AbsentVal.js.map +1 -0
  111. package/dist/val/AggFuncVal.d.ts +10 -1
  112. package/dist/val/AggFuncVal.js +104 -116
  113. package/dist/val/AggFuncVal.js.map +1 -1
  114. package/dist/val/ArithFuncVal.js +0 -12
  115. package/dist/val/ArithFuncVal.js.map +1 -1
  116. package/dist/val/BagVal.js +1 -78
  117. package/dist/val/BagVal.js.map +1 -1
  118. package/dist/val/BigDecimalVal.js +0 -16
  119. package/dist/val/BigDecimalVal.js.map +1 -1
  120. package/dist/val/BigIntegerVal.js +0 -16
  121. package/dist/val/BigIntegerVal.js.map +1 -1
  122. package/dist/val/CloseFuncVal.js +0 -9
  123. package/dist/val/CloseFuncVal.js.map +1 -1
  124. package/dist/val/CmpFuncVal.d.ts +20 -0
  125. package/dist/val/CmpFuncVal.js +188 -0
  126. package/dist/val/CmpFuncVal.js.map +1 -0
  127. package/dist/val/ConjunctVal.js +0 -29
  128. package/dist/val/ConjunctVal.js.map +1 -1
  129. package/dist/val/ConstraintVal.js +0 -500
  130. package/dist/val/ConstraintVal.js.map +1 -1
  131. package/dist/val/ContainerKindVal.js +0 -2
  132. package/dist/val/ContainerKindVal.js.map +1 -1
  133. package/dist/val/CopyFuncVal.js +0 -3
  134. package/dist/val/CopyFuncVal.js.map +1 -1
  135. package/dist/val/Decimal.js +0 -179
  136. package/dist/val/Decimal.js.map +1 -1
  137. package/dist/val/DeprecateFuncVal.js.map +1 -1
  138. package/dist/val/DisjunctVal.js +0 -152
  139. package/dist/val/DisjunctVal.js.map +1 -1
  140. package/dist/val/EachFuncVal.d.ts +1 -2
  141. package/dist/val/EachFuncVal.js +12 -29
  142. package/dist/val/EachFuncVal.js.map +1 -1
  143. package/dist/val/EmitFuncVal.d.ts +1 -1
  144. package/dist/val/EmitFuncVal.js +6 -119
  145. package/dist/val/EmitFuncVal.js.map +1 -1
  146. package/dist/val/ExpectVal.js +0 -62
  147. package/dist/val/ExpectVal.js.map +1 -1
  148. package/dist/val/FilterFuncVal.js +0 -25
  149. package/dist/val/FilterFuncVal.js.map +1 -1
  150. package/dist/val/FuncBaseVal.d.ts +1 -0
  151. package/dist/val/FuncBaseVal.js +7 -127
  152. package/dist/val/FuncBaseVal.js.map +1 -1
  153. package/dist/val/GraphAtomVal.js +0 -15
  154. package/dist/val/GraphAtomVal.js.map +1 -1
  155. package/dist/val/HideFuncVal.js +0 -13
  156. package/dist/val/HideFuncVal.js.map +1 -1
  157. package/dist/val/IntegerVal.js +0 -61
  158. package/dist/val/IntegerVal.js.map +1 -1
  159. package/dist/val/JunctionVal.js +0 -20
  160. package/dist/val/JunctionVal.js.map +1 -1
  161. package/dist/val/KeyFuncVal.js +0 -46
  162. package/dist/val/KeyFuncVal.js.map +1 -1
  163. package/dist/val/ListVal.js +0 -57
  164. package/dist/val/ListVal.js.map +1 -1
  165. package/dist/val/LowerFuncVal.js +11 -6
  166. package/dist/val/LowerFuncVal.js.map +1 -1
  167. package/dist/val/MapVal.js +0 -151
  168. package/dist/val/MapVal.js.map +1 -1
  169. package/dist/val/MatchFuncVal.js +0 -27
  170. package/dist/val/MatchFuncVal.js.map +1 -1
  171. package/dist/val/{FormFuncVal.d.ts → MaybeFuncVal.d.ts} +5 -5
  172. package/dist/val/MaybeFuncVal.js +50 -0
  173. package/dist/val/MaybeFuncVal.js.map +1 -0
  174. package/dist/val/MoveFuncVal.js +0 -18
  175. package/dist/val/MoveFuncVal.js.map +1 -1
  176. package/dist/val/NilVal.js +2 -36
  177. package/dist/val/NilVal.js.map +1 -1
  178. package/dist/val/NomFuncVal.d.ts +12 -0
  179. package/dist/val/NomFuncVal.js +153 -0
  180. package/dist/val/NomFuncVal.js.map +1 -0
  181. package/dist/val/NumberVal.js +0 -15
  182. package/dist/val/NumberVal.js.map +1 -1
  183. package/dist/val/OpBaseVal.d.ts +1 -0
  184. package/dist/val/OpBaseVal.js +3 -15
  185. package/dist/val/OpBaseVal.js.map +1 -1
  186. package/dist/val/PackFuncVal.js +0 -34
  187. package/dist/val/PackFuncVal.js.map +1 -1
  188. package/dist/val/PathFuncVal.js +0 -6
  189. package/dist/val/PathFuncVal.js.map +1 -1
  190. package/dist/val/PathVal.js +0 -41
  191. package/dist/val/PathVal.js.map +1 -1
  192. package/dist/val/PlaceVal.js +0 -25
  193. package/dist/val/PlaceVal.js.map +1 -1
  194. package/dist/val/PlusOpVal.d.ts +1 -7
  195. package/dist/val/PlusOpVal.js +13 -74
  196. package/dist/val/PlusOpVal.js.map +1 -1
  197. package/dist/val/PrefFuncVal.js +0 -1
  198. package/dist/val/PrefFuncVal.js.map +1 -1
  199. package/dist/val/PrefVal.js +0 -167
  200. package/dist/val/PrefVal.js.map +1 -1
  201. package/dist/val/RecurseVal.js +0 -55
  202. package/dist/val/RecurseVal.js.map +1 -1
  203. package/dist/val/RefVal.js +0 -282
  204. package/dist/val/RefVal.js.map +1 -1
  205. package/dist/val/ReferFuncVal.js +0 -232
  206. package/dist/val/ReferFuncVal.js.map +1 -1
  207. package/dist/val/ScalarKindVal.js +0 -49
  208. package/dist/val/ScalarKindVal.js.map +1 -1
  209. package/dist/val/ScalarVal.js +0 -11
  210. package/dist/val/ScalarVal.js.map +1 -1
  211. package/dist/val/StrFuncVal.js +0 -18
  212. package/dist/val/StrFuncVal.js.map +1 -1
  213. package/dist/val/SuperFuncVal.js +0 -32
  214. package/dist/val/SuperFuncVal.js.map +1 -1
  215. package/dist/val/TopVal.js +0 -1
  216. package/dist/val/TopVal.js.map +1 -1
  217. package/dist/val/TranslateFuncVal.d.ts +12 -0
  218. package/dist/val/TranslateFuncVal.js +99 -0
  219. package/dist/val/TranslateFuncVal.js.map +1 -0
  220. package/dist/val/UpperFuncVal.js +11 -6
  221. package/dist/val/UpperFuncVal.js.map +1 -1
  222. package/dist/val/Val.d.ts +1 -0
  223. package/dist/val/Val.js +2 -133
  224. package/dist/val/Val.js.map +1 -1
  225. package/dist/val/VarVal.js +0 -12
  226. package/dist/val/VarVal.js.map +1 -1
  227. package/dist/val/arith.js +0 -37
  228. package/dist/val/arith.js.map +1 -1
  229. package/dist/val/caserange.d.ts +3 -0
  230. package/dist/val/caserange.js +49 -0
  231. package/dist/val/caserange.js.map +1 -0
  232. package/dist/val/members.js +0 -6
  233. package/dist/val/members.js.map +1 -1
  234. package/dist/val/numcmp.js +0 -11
  235. package/dist/val/numcmp.js.map +1 -1
  236. package/dist/val/numkind.js +0 -145
  237. package/dist/val/numkind.js.map +1 -1
  238. package/dist/val/valutil.js +0 -16
  239. package/dist/val/valutil.js.map +1 -1
  240. package/dist/vet.d.ts +12 -0
  241. package/dist/vet.js +159 -412
  242. package/dist/vet.js.map +1 -1
  243. package/dist/view.js +0 -414
  244. package/dist/view.js.map +1 -1
  245. package/dist/walk.js +0 -41
  246. package/dist/walk.js.map +1 -1
  247. package/grammar/aontu.abnf +9 -7
  248. package/grammar/aontu.gbnf +5 -5
  249. package/grammar/aontu.lark +5 -5
  250. package/grammar/aontu.tmLanguage.json +1 -1
  251. package/package.json +4 -2
  252. package/skill/SKILL.md +8 -0
  253. package/skill/init/check.sh +28 -0
  254. package/skill/init/data.aon +12 -0
  255. package/skill/init/model.aon +19 -0
  256. package/skill/tasks.md +151 -0
  257. package/src/agentsmd.ts +8 -32
  258. package/src/alias.ts +0 -39
  259. package/src/allow.ts +221 -0
  260. package/src/aontu.ts +10 -108
  261. package/src/aontumodel.ts +32 -0
  262. package/src/cli.ts +1009 -540
  263. package/src/ctx.ts +0 -103
  264. package/src/diff.ts +0 -40
  265. package/src/err.ts +0 -40
  266. package/src/escape.ts +0 -46
  267. package/src/exactjson.ts +0 -131
  268. package/src/format.ts +63 -234
  269. package/src/grammar.ts +72 -0
  270. package/src/graph.ts +0 -61
  271. package/src/hcanon.ts +0 -82
  272. package/src/helpdoc.ts +77 -0
  273. package/src/hints.ts +72 -49
  274. package/src/jsonschema.ts +0 -123
  275. package/src/keyorder.ts +0 -42
  276. package/src/lang.ts +39 -895
  277. package/src/lower.ts +15 -65
  278. package/src/lsp-server.ts +0 -16
  279. package/src/lsp.ts +12 -180
  280. package/src/mcp-server.ts +0 -31
  281. package/src/mcp.ts +0 -130
  282. package/src/mod-tool.ts +0 -158
  283. package/src/mod.ts +0 -178
  284. package/src/patch.ts +0 -232
  285. package/src/provenance.ts +0 -183
  286. package/src/query.ts +0 -84
  287. package/src/reach.ts +0 -53
  288. package/src/relation.ts +7 -71
  289. package/src/render.ts +33 -180
  290. package/src/report-sarif.ts +0 -48
  291. package/src/sig.ts +0 -35
  292. package/src/sigdecl.ts +1 -1
  293. package/src/siggate.ts +0 -30
  294. package/src/site.ts +3 -29
  295. package/src/subsume.ts +1 -161
  296. package/src/template.ts +69 -140
  297. package/src/trim.ts +0 -53
  298. package/src/type.ts +2 -45
  299. package/src/unify.ts +13 -251
  300. package/src/utility.ts +0 -31
  301. package/src/val/AbnfFuncVal.ts +181 -0
  302. package/src/val/AbsentVal.ts +54 -0
  303. package/src/val/AggFuncVal.ts +152 -188
  304. package/src/val/ArithFuncVal.ts +0 -20
  305. package/src/val/BagVal.ts +1 -78
  306. package/src/val/BigDecimalVal.ts +0 -16
  307. package/src/val/BigIntegerVal.ts +0 -16
  308. package/src/val/CloseFuncVal.ts +0 -9
  309. package/src/val/CmpFuncVal.ts +249 -0
  310. package/src/val/ConjunctVal.ts +0 -33
  311. package/src/val/ConstraintVal.ts +2 -537
  312. package/src/val/ContainerKindVal.ts +0 -18
  313. package/src/val/CopyFuncVal.ts +0 -5
  314. package/src/val/Decimal.ts +1 -185
  315. package/src/val/DeprecateFuncVal.ts +0 -10
  316. package/src/val/DisjunctVal.ts +0 -157
  317. package/src/val/EachFuncVal.ts +12 -53
  318. package/src/val/EmitFuncVal.ts +8 -208
  319. package/src/val/ExpectVal.ts +0 -62
  320. package/src/val/FilterFuncVal.ts +0 -55
  321. package/src/val/FuncBaseVal.ts +9 -130
  322. package/src/val/GraphAtomVal.ts +0 -42
  323. package/src/val/HideFuncVal.ts +0 -15
  324. package/src/val/IntegerVal.ts +0 -61
  325. package/src/val/JunctionVal.ts +0 -20
  326. package/src/val/KeyFuncVal.ts +0 -48
  327. package/src/val/ListVal.ts +0 -59
  328. package/src/val/LowerFuncVal.ts +12 -7
  329. package/src/val/MapVal.ts +0 -151
  330. package/src/val/MatchFuncVal.ts +0 -59
  331. package/src/val/MaybeFuncVal.ts +86 -0
  332. package/src/val/MoveFuncVal.ts +0 -20
  333. package/src/val/NilVal.ts +2 -36
  334. package/src/val/NomFuncVal.ts +200 -0
  335. package/src/val/NumberVal.ts +0 -16
  336. package/src/val/OpBaseVal.ts +4 -17
  337. package/src/val/PackFuncVal.ts +0 -63
  338. package/src/val/PathFuncVal.ts +0 -32
  339. package/src/val/PathVal.ts +0 -66
  340. package/src/val/PlaceVal.ts +0 -45
  341. package/src/val/PlusOpVal.ts +18 -75
  342. package/src/val/PrefFuncVal.ts +0 -1
  343. package/src/val/PrefVal.ts +0 -179
  344. package/src/val/RecurseVal.ts +0 -81
  345. package/src/val/RefVal.ts +1 -285
  346. package/src/val/ReferFuncVal.ts +0 -255
  347. package/src/val/ScalarKindVal.ts +0 -50
  348. package/src/val/ScalarVal.ts +0 -12
  349. package/src/val/StrFuncVal.ts +0 -44
  350. package/src/val/SuperFuncVal.ts +0 -42
  351. package/src/val/TopVal.ts +0 -1
  352. package/src/val/TranslateFuncVal.ts +132 -0
  353. package/src/val/UpperFuncVal.ts +12 -7
  354. package/src/val/Val.ts +3 -192
  355. package/src/val/VarVal.ts +0 -15
  356. package/src/val/arith.ts +0 -92
  357. package/src/val/caserange.ts +53 -0
  358. package/src/val/members.ts +0 -23
  359. package/src/val/numcmp.ts +1 -27
  360. package/src/val/numkind.ts +0 -149
  361. package/src/val/valutil.ts +0 -16
  362. package/src/vet.ts +209 -504
  363. package/src/view.ts +0 -507
  364. package/src/walk.ts +0 -41
  365. package/dist/std.d.ts +0 -3
  366. package/dist/std.js +0 -637
  367. package/dist/std.js.map +0 -1
  368. package/dist/val/FormFuncVal.js +0 -55
  369. package/dist/val/FormFuncVal.js.map +0 -1
  370. package/src/std.ts +0 -648
  371. package/src/val/FormFuncVal.ts +0 -119
package/src/vet.ts CHANGED
@@ -1,28 +1,5 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
2
 
3
- // The validation verb: check a data document against a schema document
4
- // and return a MACHINE-READABLE report (G2 phase 2,
5
- // docs/capability-review/g2-validation-verb.md).
6
- //
7
- // This module is the engine only. The CLI verb, the JSON/SARIF
8
- // renderers and `--watch` are phase 3 and 5; the Go port is phase 4.
9
- // Keeping the engine transport-free is the split lsp.ts already uses,
10
- // and for the same reason: it is the part that has to be unit-testable
11
- // and embeddable.
12
- //
13
- // Two properties of the engine shape everything here, and both were
14
- // probed rather than assumed:
15
- //
16
- // - A CONTRADICTION surfaces as a NilVal in the unified tree, so
17
- // conflict findings come from a tree walk (the `walkNils` pattern
18
- // lsp.ts already uses, generalised).
19
- // - INCOMPLETENESS does not. `{name:"auth"}` against
20
- // `{name:string, port:integer}` unifies cleanly and leaves
21
- // `port:integer` standing — no nil, no error. It surfaces only when
22
- // something tries to GENERATE, which is why vet runs a generate
23
- // check in an isolated collect context and reads the
24
- // `incomplete`-class errors out of it. The two verdicts the report
25
- // distinguishes therefore come from two different mechanisms.
26
3
 
27
4
  import type { TrustOptions, Val } from './type'
28
5
 
@@ -40,12 +17,8 @@ import { collectDeprecations, walkBagVals, deprecationMessage,
40
17
  includeOpts,
41
18
  } from './utility'
42
19
  import { subsumeNode, effectiveDefault } from './subsume'
43
- // The `--at` refusal is the SAME refusal `get` and `why` give for a
44
- // path that names nothing, down to the "did you mean" -- so it is that
45
- // one, not a second spelling of it. A cycle in the module graph
46
- // (query imports anchorAt from here), and a benign one: both sides
47
- // use the other only from inside a function body, never at load time.
48
20
  import { noPathFinding } from './query'
21
+ import { cmpCodePoint } from './keyorder'
49
22
 
50
23
 
51
24
  export type VetVerdict = 'valid' | 'invalid' | 'incomplete' | 'error'
@@ -56,34 +29,8 @@ export type VetSite = {
56
29
  file: string
57
30
  row: number
58
31
  col: number
59
- // The extent in UTF-16 code units, or -1 when unknown — the same
60
- // "unknown" row and col already use.
61
- //
62
- // THIS IS WHAT MAKES A FINDING REPAIRABLE. `value` is the CANON, not
63
- // the source text: `port: 0x1F` reports canon `31` at column 7, so a
64
- // consumer replacing `(col, value.length)` writes `port: 5x1F` and
65
- // corrupts the document. With `len` the span is `(col, 4)` and the
66
- // replacement is exact.
67
- //
68
- // NEVER GUESSED. Where the span is unknown this is -1 and a consumer
69
- // must not edit — unlike the LSP, which falls back to canon because a
70
- // wonky highlight is cosmetic while a wrong edit is a lost file.
71
- // See ts/src/site.ts for what the extent covers.
72
32
  len: number
73
33
  role: VetRole
74
- // The SOURCE TEXT the span covers, or '' when unknown.
75
- //
76
- // This is what makes the span SELF-VERIFYING, and it is not the same
77
- // as `value`. For a scalar the two differ by normalisation — `0x1F`
78
- // has src `0x1F` and value (canon) `31`. For a COMPOUND the span
79
- // names the opening token only, exactly as row and col always have:
80
- // a constraint `min(1)` reports src `min`, and a reference `$.b`
81
- // reports src `$`.
82
- //
83
- // So a consumer must read the document at (row, col, len), compare it
84
- // to `src`, and REFUSE when they differ — and, seeing `min` where it
85
- // expected `min(1)`, refuse rather than replace the name and orphan
86
- // the arguments. Without this field that mistake is undetectable.
87
34
  src?: string
88
35
  value?: string
89
36
  }
@@ -95,21 +42,6 @@ export type VetFinding = {
95
42
  path: string
96
43
  message: string
97
44
 
98
- // THE REPAIR, not just the diagnosis. `message` is one line by
99
- // design -- it is the headline, and the frames under it are for a
100
- // human at a terminal -- but for several codes the part that says
101
- // what to DO about the failure lived only in those frames, so a
102
- // machine reader (an agent, a CI annotation, an editor) got the
103
- // complaint and none of the cure. `0d` is the clearest case: the
104
- // engine refuses a lossy integer literal and the hint names the
105
- // exact-decimal escape that fixes it. Populated from the shared
106
- // hints table (ts/src/hints.ts, mirrored by go/hints.go) whenever
107
- // the code has one; absent when it does not (the report-layer
108
- // compat_* codes, notably, carry no hint text).
109
- //
110
- // Excluded from spec goldens for the same reason `message` is: it is
111
- // prose, it is long, and the two ports hold it to the byte through
112
- // the message tests instead of through every vet row.
113
45
  hint?: string
114
46
 
115
47
  sites: VetSite[]
@@ -118,10 +50,27 @@ export type VetFinding = {
118
50
  note?: string
119
51
  }
120
52
 
53
+ export type VetCoverage = {
54
+ checked: number
55
+ declared: number
56
+ // Data leaves in all, under `coverageAt` when it is given. `checked`
57
+ // over this is the ratio a reader wants.
58
+ leaves: number
59
+ unchecked: string[]
60
+ // The SHALLOWEST declarations no data path met.
61
+ unused: string[]
62
+ // NO data leaf was constrained, over a document that has leaves.
63
+ // The exact condition of the `"*"` failure, and what
64
+ // `--strict-coverage` exits 1 on.
65
+ vacuous: boolean
66
+ }
67
+
121
68
  export type VetReport = {
122
69
  verdict: VetVerdict
123
70
  truncated: boolean
124
71
  findings: VetFinding[]
72
+ // Absent unless the run asked for it.
73
+ coverage?: VetCoverage
125
74
  }
126
75
 
127
76
  export type VetOptions = {
@@ -129,29 +78,18 @@ export type VetOptions = {
129
78
  closed?: boolean // close() the anchor for this run
130
79
  partial?: boolean // residue is not a failure
131
80
  maxErrors?: number // cap the finding list (default 20)
132
- schemaUrl?: string // provenance label for schema sites
81
+
82
+ // Account for what the check examined (G11 phase 5). Off by default:
83
+ // the report gains a `coverage` object only when this is set, so no
84
+ // existing caller's report changes shape.
85
+ coverage?: boolean
86
+ coverageAt?: string
87
+ schemaUrl?: string
133
88
  dataUrl?: string // provenance label for data sites
134
89
 
135
- // Where each document CAME FROM, used to resolve its relative
136
- // `@"file"` loads -- the file path, as `Aontu({path})` takes it, not
137
- // the directory. Vet takes two documents from its caller rather than
138
- // from the filesystem, so it cannot know this: without it a modular
139
- // schema resolved its includes against the process working
140
- // directory, which fails outside that directory and, worse, silently
141
- // reads a same-named file that happens to sit there. The two
142
- // documents get their OWN bases, because they need not live in the
143
- // same place.
144
90
  schemaPath?: string
145
91
  dataPath?: string
146
92
 
147
- // The trust profile this run evaluates under (G5, docs/trust.md).
148
- // Vet's whole job is to evaluate a document its caller did not
149
- // write, so the caller must be able to say what that document is
150
- // allowed to reach: without this the include chain is the default
151
- // one, and `@"../../etc/passwd.aon"` in hostile data reads whatever
152
- // the validating process can. (`@"x.js"` no longer executes --
153
- // ADR-012 refuses the extension -- but reading is enough.) A server
154
- // passes `{include:'none'}`.
155
93
  trust?: TrustOptions
156
94
 
157
95
  // Extensions additionally read as text (the CLI's `--text-ext`).
@@ -170,37 +108,10 @@ const DEFAULT_SCHEMA_URL = 'schema'
170
108
  const DEFAULT_DATA_URL = 'data'
171
109
 
172
110
 
173
- // Every site in a freshly parsed tree carries the same url, and for a
174
- // bare parse that url is the empty string: `site.url` is only populated
175
- // by the multisource loader (ts/src/lang.ts). Vet takes two documents
176
- // from its CALLER, not from the filesystem, so it stamps provenance
177
- // itself — which is what lets the report assign site ROLES by
178
- // provenance rather than by NilVal's source-order heuristic, exactly as
179
- // the design requires.
180
- // EVERY SITE NAMES THE FILE WHOSE TEXT IT EXCERPTS (the review's
181
- // finding F, use-cases/BUGS.md §25). The parser already names the file
182
- // each value was read from -- a value loaded through `@"lib/types.aon"`
183
- // carries that path, with that file's row and column -- and this walk
184
- // used to OVERWRITE every url with the entry document's name. The
185
- // coordinates stayed the included file's, so a finding cited
186
- // `entry.aon:3:7` for text that lives three files away, at a line the
187
- // entry may not even have. A repair agent that follows the site edits
188
- // the wrong file.
189
- //
190
- // Only the values that carry no name of their own are stamped: those
191
- // are the ones the engine minted rather than read, and the entry is
192
- // the honest name for them. The urls actually seen are collected, so
193
- // the report can still tell WHICH DOCUMENT a site belongs to without
194
- // pretending they all came from one file -- see roleOf.
195
111
  function stampUrl(v: any, url: string, seen?: Set<string>): Set<string> {
196
112
  const urls = seen ?? new Set<string>()
197
113
  urls.add(url)
198
114
  walkVals(v, (n: any) => {
199
- // UNDEFINED counts as unstamped, not just empty: the parser leaves
200
- // the url undefined on one of its two paths (ts/src/lang.ts), and
201
- // treating that as a name would put `undefined` in the url set --
202
- // where the OTHER document's unstamped values would then match it,
203
- // and every site would read `data`.
204
115
  if (null == n.site.url || '' === n.site.url) {
205
116
  n.site.url = url
206
117
  }
@@ -211,10 +122,6 @@ function stampUrl(v: any, url: string, seen?: Set<string>): Set<string> {
211
122
  }
212
123
 
213
124
 
214
- // The provenance a report projects its sites through: which urls
215
- // belong to the DATA document, and how the caller reached each of the
216
- // two documents (which is what says how to NAME a third file either of
217
- // them included).
218
125
  type Prov = {
219
126
  // The urls the data walk reached. Roles are decided by membership,
220
127
  // on the RAW url -- never by a name comparison.
@@ -226,20 +133,6 @@ type Prov = {
226
133
  }
227
134
 
228
135
 
229
- // A FILE THE READER CAN OPEN. The parser resolves an include to an
230
- // absolute path, which is the right identity (two files loading the
231
- // same library by different relative spellings must be one file) and
232
- // the wrong NAME: a report whose entry reads `contract.aon` and whose
233
- // included site reads `/home/someone/checkout/types.aon` is a report
234
- // that cannot be uploaded as SARIF, diffed between machines, or read
235
- // beside the command that produced it.
236
- //
237
- // So an included file is named as the ENTRY'S OWN NAME reaches it:
238
- // relative to the entry's directory, then re-anchored on however the
239
- // caller spelled the entry. `vet contract.aon` names `types.aon`;
240
- // `vet a/b/contract.aon` names `a/b/types.aon`; an absolute entry
241
- // keeps absolute includes. A caller who passed no path at all has no
242
- // base to relativise against and gets the url unchanged.
243
136
  export function displayFile(
244
137
  url: string, label: string, path?: string): string {
245
138
  if (url === label || null == path || '' === url || !isAbsolute(url)) {
@@ -251,12 +144,6 @@ export function displayFile(
251
144
  }
252
145
 
253
146
 
254
- // The name for one site, taken from the document the site BELONGS to
255
- // -- which is the role, already decided by url-set membership. Doing
256
- // it here rather than from a map built at stamping time is not a
257
- // shortcut: a nil's operands are off the tree by the time the report
258
- // is built, so a value first seen during the MEET (the commonest
259
- // schema site there is) would be missing from any such map.
260
147
  function displayOf(file: string, role: VetRole, prov: Prov): string {
261
148
  return 'data' === role
262
149
  ? displayFile(file, prov.dataUrl ?? file, prov.dataPath)
@@ -264,50 +151,21 @@ function displayOf(file: string, role: VetRole, prov: Prov): string {
264
151
  }
265
152
 
266
153
 
267
- // The ROLE of a site: which of the two documents it belongs to. Not a
268
- // name comparison -- a data document may itself include another file,
269
- // and that file's values are still data. Membership of the url set the
270
- // stamping walk collected is the question, and the answer is `schema`
271
- // for anything the data walk never reached (an engine-minted value
272
- // stamped with the schema entry, say).
273
154
  function roleOf(file: string, prov: Prov): VetRole {
274
155
  return prov.data.has(file) ? 'data' : 'schema'
275
156
  }
276
157
 
277
158
 
278
- // `$.a.b`, and `$` for the root. Deliberately NOT delimiter-escaped: a
279
- // map key may contain any character, including every separator a
280
- // compact summary might pick, so the path is carried as a JSON string
281
- // and never parsed back out of a larger token.
282
159
  function pathText(path?: string[]): string {
283
160
  return '$' + (null != path && 0 < path.length ? '.' + path.join('.') : '')
284
161
  }
285
162
 
286
163
 
287
- // `secondary` is the only operand that can be absent — a `closed` or an
288
- // incomplete finding has one side, a two-site conflict has both — so
289
- // this is the one nullable input, and every Val that does arrive
290
- // carries a site and a canon.
291
164
  function siteOf(v: any, prov: Prov): VetSite | undefined {
292
165
  if (null == v) {
293
166
  return undefined
294
167
  }
295
- // The report's `file` is whatever the site carries, and by the time a
296
- // site reaches a report that is always a stamped name: vet walks both
297
- // documents before they meet, and the walk reaches the off-peg values
298
- // a finding can name (ts/src/walk.ts). A consumer therefore reads
299
- // `file` without a presence check, and the Go port -- whose field is
300
- // a plain string -- writes the same key.
301
- //
302
- // NOT coalesced. The parser leaves the url undefined on one of its
303
- // two paths (ts/src/lang.ts), and a `?? ''` here would be dead code
304
- // that hides it: if a value ever reaches a report unstamped, the two
305
- // ports should disagree loudly rather than quietly agree on an empty
306
- // name that neither of them meant.
307
168
  const file = v.site.url
308
- // NAMED for the reader, ROLED by the raw url: the two questions are
309
- // different, and only the first is about how the file is spelled
310
- // (see displayFile).
311
169
  const role = roleOf(file, prov)
312
170
  return {
313
171
  file: displayOf(file, role, prov),
@@ -325,13 +183,6 @@ function siteOf(v: any, prov: Prov): VetSite | undefined {
325
183
  // The underlying NilVal fields are untouched: this is a report-layer
326
184
  // projection, so the existing error.tsv assertions do not move.
327
185
  function sitesOf(nil: any, prov: Prov): VetSite[] {
328
- // `?? nil`: a failure raised about a CONSTRUCT rather than about a
329
- // failed meet -- a lossy integer literal, say -- carries no operands
330
- // at all, and reporting it about ITSELF is what ctx.adderr already
331
- // does for the same reason. Without the fallback the report built a
332
- // site out of `undefined` and threw while partitioning it, which is
333
- // the one thing vet promises not to do: a bad value in the data is
334
- // DATA, and the caller gets a report.
335
186
  const sites: VetSite[] = [siteOf(nil.primary ?? nil, prov) as VetSite]
336
187
 
337
188
  const secondary = siteOf(nil.secondary, prov)
@@ -339,9 +190,6 @@ function sitesOf(nil: any, prov: Prov): VetSite[] {
339
190
  sites.push(secondary)
340
191
  }
341
192
 
342
- // Partitioned rather than sorted: which of the two NilVal operands is
343
- // `primary` follows source order within one document, which says
344
- // nothing useful when one side is a schema and the other is data.
345
193
  return [
346
194
  ...sites.filter((s) => 'data' === s.role),
347
195
  ...sites.filter((s) => 'schema' === s.role),
@@ -349,12 +197,6 @@ function sitesOf(nil: any, prov: Prov): VetSite[] {
349
197
  }
350
198
 
351
199
 
352
- // The message text is MATERIALISED on demand, exactly as handleErrors
353
- // materialises one before a caller sees it: makeNilErr defers it
354
- // because most NilVals are transient and never rendered, and only the
355
- // throwing path asks for it. Without this a finding could carry an
356
- // empty `message` -- which is what the incomplete half of every report
357
- // did, and what any nil built during the PARSE of a document did.
358
200
  function materialise(nil: any, ctx: any): void {
359
201
  if (null == nil.msg || '' === nil.msg) {
360
202
  descErr(nil, ctx)
@@ -379,32 +221,15 @@ function findingOf(nil: any, prov: Prov): VetFinding {
379
221
  class: nil.class,
380
222
  severity: 'error',
381
223
  path: pathText(nil.path),
382
- // The HEADLINE only, WITHOUT ANSI: the frames below it are for a
383
- // human reading a terminal, and the first line is the part the two
384
- // ports hold to byte parity. Materialised before this runs, so it
385
- // is always there (see materialise above). The escapes matter for
386
- // one family only -- a parse failure's text comes from the parser,
387
- // which colours its marker -- and a machine-readable report is no
388
- // place for terminal control codes.
389
224
  message: stripAnsi(nil.msg.split('\n')[0]),
390
225
  sites: sitesOf(nil, prov),
391
226
  }
392
227
 
393
- // The hint, whole, with its detail placeholders filled in exactly as
394
- // the terminal frame fills them. Trailing whitespace is dropped
395
- // because it is spacing for the frame that used to follow it, not
396
- // part of the text; the deliberate blank lines INSIDE a hint are
397
- // `\n \n` and survive.
398
228
  const hint = getHint(nil.why, nil.details)
399
229
  if (null != hint && '' !== hint) {
400
230
  finding.hint = stripAnsi(hint).replace(/\s+$/, '')
401
231
  }
402
232
 
403
- // `expected`/`actual` are the admissible-alternatives contract, and
404
- // the constraint algebra already produces them: G1's atoms attach the
405
- // normalised residual and the offending value, and `must` attaches
406
- // the author's message. Read them where they are rather than
407
- // re-deriving them here.
408
233
  if ('string' === typeof details.expected) {
409
234
  finding.expected = details.expected
410
235
  }
@@ -419,19 +244,6 @@ function findingOf(nil: any, prov: Prov): VetFinding {
419
244
  }
420
245
 
421
246
 
422
- // Findings are sorted BY VET, not by the walk. The underlying walk
423
- // iterates raw object keys and the two hosts disagree about their order
424
- // — `10:… 9:…` yields ["9","10"] in JavaScript, which hoists
425
- // integer-like keys, against Go's insertion order (ts/src/keyorder.ts
426
- // exists for exactly this) — so an unsorted report could never be in
427
- // cross-port parity.
428
- //
429
- // The order is by data site (file, row, column), then code, then path.
430
- // It is carried in ONE key string rather than a cascade of comparisons:
431
- // the row and column are zero-padded so lexicographic order is numeric
432
- // order, and NUL joins the fields because no field can contain one.
433
- // A cascade would need a test per tie-breaker to stay honest; a key
434
- // needs none, and cannot disagree with itself.
435
247
  const ORDER_PAD = 9
436
248
 
437
249
 
@@ -440,12 +252,6 @@ function pad(n: number): string {
440
252
  }
441
253
 
442
254
 
443
- // The walk index is the LAST field, which makes every key unique and
444
- // the sort below total: two findings can otherwise share everything the
445
- // key carries — same data site, same code, same path — and a comparator
446
- // that has to answer "equal" is one more thing to get right in two
447
- // languages. With the index appended, ties simply keep walk order, in
448
- // both ports, by construction rather than by the sort's promises.
449
255
  function orderKey(f: VetFinding, index: number): string {
450
256
  const site = f.sites[0]
451
257
  return [
@@ -459,44 +265,11 @@ function orderKey(f: VetFinding, index: number): string {
459
265
  }
460
266
 
461
267
 
462
- // A DOCUMENT THAT DOES NOT STAND UP, in the finding shape (the
463
- // review's finding F). `trim` and `relations` answered an unusable
464
- // document with `verdict: error` and an EMPTY list: the caller learned
465
- // that something was wrong and nothing about what, which is the one
466
- // thing a repair loop cannot work with. Both verbs take ONE document,
467
- // so there is no role to decide -- the document is the thing being
468
- // checked and the thing to edit, which is what `data` means here.
469
- //
470
- // The engine's own first error IS the finding: these verbs add nothing
471
- // to a diagnosis the evaluator already made, and the FIRST is enough
472
- // because everything after it is a consequence.
473
- //
474
- // The Go twin is failureFinding in go/vet.go.
475
268
  export function failureFinding(
476
269
  ctx: any, url?: string, failed?: any): VetFinding {
477
- // ctx.err IS SOMETIMES EMPTY, and the comment that used to stand here
478
- // said otherwise (use-cases/BUGS.md §43). `&: id(root)` fails with a
479
- // NIL ROOT and NO COLLECTED ERROR -- the id-spread refusal is the
480
- // root itself -- and every verb that reports "this document does not
481
- // stand up" then read `ctx.err[0]` as undefined and died: a TypeError
482
- // out of `relations`, `reaches` and `jsonschema` in TypeScript, a
483
- // panic in Go. The one shape where finding F's own invariant, that a
484
- // document which does not stand up SAYS SO in the finding shape, was
485
- // answered with a stack trace.
486
- //
487
- // `failed` is the caller's own root -- every caller has it, and its
488
- // condition is `0 < ctx.err.length || root.isNil`, so when the first
489
- // half is false the second holds and the root IS the reason.
490
270
  const nil: any = ctx.err[0] ?? failed
491
271
  materialise(nil, ctx)
492
272
 
493
- // STAMPED, as vet stamps both documents before they meet: siteOf does
494
- // not coalesce a missing name (deliberately -- see there), so a site
495
- // that reached the report unstamped would carry `file: undefined`.
496
- // The three Vals a finding can name are the nil and its two operands,
497
- // and the url set collects whatever name each already had, so a value
498
- // read from an included file keeps that file's name and still counts
499
- // as part of the one document being checked.
500
273
  const at = url ?? ''
501
274
  const urls = new Set([at])
502
275
  for (const v of [nil, nil.primary, nil.secondary]) {
@@ -522,28 +295,8 @@ export function anchorAt(root: any, at: string): Val | undefined {
522
295
 
523
296
  let node: any = root
524
297
  for (const part of parts) {
525
- // A SIZING RESIDUE IS ITS CONTAINER, plus a note about what the
526
- // container must still satisfy (use-cases/BUGS.md §16). The path
527
- // steps through it: `$.a.ports.0.port` names the same node whether
528
- // or not `ports` still carries a `unique()`, and an anchor that
529
- // stopped here would report `no_path` for a key the document
530
- // plainly has.
531
298
  node = throughResidue(node)
532
299
 
533
- // TYPE-DIRECTED, not a property lookup on whatever `peg` happens to
534
- // be. An anchor is a STRUCTURAL path into the schema — the same
535
- // thing a reference means by `$.a.b` — so it walks map keys and
536
- // list indices, and stops at anything else.
537
- //
538
- // Indexing the peg generically walked much further than that: into
539
- // a junction's branches (`a:1|2` with `--at $.a.0` validated
540
- // against ONE branch), into a constraint's atom arguments (so
541
- // `min(2)` with `--at $.a.0` reported the bound's own argument as
542
- // the truth), into a pref's wrapped value through the literal key
543
- // `peg`, and into an array's `length` — that last one handing back
544
- // a JavaScript NUMBER as the anchor, after which every document
545
- // whatsoever came back valid. The Go port has always been
546
- // type-directed here; this is the canonical side moving to it.
547
300
  if (true === node?.isMap) {
548
301
  const peg = node.peg
549
302
  if (null == peg || !Object.prototype.hasOwnProperty.call(peg, part)) {
@@ -568,34 +321,183 @@ export function anchorAt(root: any, at: string): Val | undefined {
568
321
  }
569
322
  }
570
323
 
571
- // THE ANCHOR KEEPS ITS ATOM. Stepping THROUGH a residue is right --
572
- // `$.x.a` names a key of the container whatever the container still
573
- // has to satisfy -- but ARRIVING at one and handing back the bare
574
- // container drops a constraint the author wrote, so `--at $.x` vetted
575
- // clean against a `length` the evaluator enforces. The residue is the
576
- // honest schema for the node: the meet drives it, and generation
577
- // settles it, exactly as it does without an anchor.
578
324
  return node
579
325
  }
580
326
 
581
327
 
582
- // The container inside a settled sizing residue, or the value itself.
583
- // EXPORTED for the `doc` figure, which walks the same shape the anchor
584
- // does: a list still carrying a `unique()` is a list, and a drawing
585
- // that stopped at the residue would omit keys the document plainly
586
- // has.
587
328
  export function throughResidue(v: any): any {
588
329
  return sizingResidue(v)?.bag ?? v
589
330
  }
590
331
 
591
332
 
592
- // Validate `dataSrc` against `schemaSrc`.
593
- //
594
- // Never throws for findings: a contradiction in the data is DATA, and
595
- // the caller gets a report. It throws only when the caller's own inputs
596
- // are unusable — which is why an unusable schema is a verdict (`error`)
597
- // rather than an exception too: "the schema is broken" is a fact the
598
- // agent loop needs to branch on, not an exceptional condition.
333
+ const COVER_TEMPLATE = '&'
334
+
335
+
336
+ // Is this value a bag with children to walk?
337
+ function coverKids(v: any): { key: string, val: any }[] {
338
+ const out: { key: string, val: any }[] = []
339
+ if (true === v.isMap && null != v.peg) {
340
+ for (const k of Object.keys(v.peg).sort(cmpCodePoint)) {
341
+ out.push({ key: k, val: v.peg[k] })
342
+ }
343
+ }
344
+ else if (true === v.isList && Array.isArray(v.peg)) {
345
+ v.peg.forEach((m: any, i: number) => out.push({ key: String(i), val: m }))
346
+ }
347
+ return out
348
+ }
349
+
350
+
351
+ function coverTemplate(v: any): any {
352
+ const cj = v?.spread?.cj
353
+ return null != cj && true === cj.isVal && true !== cj.isTop ? cj : undefined
354
+ }
355
+
356
+
357
+ function coverDeclare(
358
+ v: any, path: string[], out: Map<string, any>): void {
359
+ const tpl = coverTemplate(v)
360
+ if (null != tpl) {
361
+ const at = [...path, COVER_TEMPLATE]
362
+ out.set(pathText(at), tpl)
363
+ coverDeclare(tpl, at, out)
364
+ }
365
+ for (const { key, val } of coverKids(v)) {
366
+ const at = [...path, key]
367
+ out.set(pathText(at), val)
368
+ coverDeclare(val, at, out)
369
+ }
370
+ }
371
+
372
+
373
+ // Every path in the data, and whether it is a LEAF -- a node with no
374
+ // children, which is where a value lives.
375
+ function coverDataPaths(
376
+ v: any, path: string[],
377
+ out: { path: string, leaf: boolean }[]): void {
378
+ const kids = coverKids(v)
379
+ if (0 < path.length) {
380
+ out.push({ path: pathText(path), leaf: 0 === kids.length })
381
+ }
382
+ for (const { key, val } of kids) {
383
+ coverDataPaths(val, [...path, key], out)
384
+ }
385
+ }
386
+
387
+
388
+ // Walk one data path down the schema, naming the declaration that
389
+ // constrains it -- the exact key where the schema has one, else the
390
+ // covering template. Undefined when the schema declares nothing there.
391
+ function coverMatch(
392
+ anchor: any, segs: string[]): string | undefined {
393
+ let at: any = anchor
394
+ let decl = ''
395
+ for (const seg of segs) {
396
+ const named = true === at.isMap && null != at.peg ? at.peg[seg]
397
+ : true === at.isList && Array.isArray(at.peg) ? at.peg[Number(seg)]
398
+ : undefined
399
+ if (null != named && true === named.isVal) {
400
+ decl = '' === decl ? seg : decl + '.' + seg
401
+ at = named
402
+ continue
403
+ }
404
+ const tpl = coverTemplate(at)
405
+ if (null == tpl) {
406
+ return undefined
407
+ }
408
+ decl = '' === decl ? COVER_TEMPLATE : decl + '.' + COVER_TEMPLATE
409
+ at = tpl
410
+ }
411
+ return '$.' + decl
412
+ }
413
+
414
+
415
+ // The SHALLOWEST members of a set of paths: one whose parent is also in
416
+ // the set is covered by naming the parent, and naming both is noise.
417
+ // `render --coverage`'s `dead` is built on the same rule.
418
+ function coverShallowest(paths: string[]): string[] {
419
+ const held = new Set(paths)
420
+ return paths.filter((p) => {
421
+ for (let at = p; -1 !== at.lastIndexOf('.');) {
422
+ at = at.slice(0, at.lastIndexOf('.'))
423
+ if (held.has(at)) {
424
+ return false
425
+ }
426
+ }
427
+ return true
428
+ }).sort(cmpCodePoint)
429
+ }
430
+
431
+
432
+ // The accounting itself: what the schema declared, what the data holds,
433
+ // and which of each the other met.
434
+ export function vetCoverage(
435
+ anchor: any, dataVal: any, coverageAt?: string): VetCoverage {
436
+ const declarations = new Map<string, any>()
437
+ coverDeclare(anchor, [], declarations)
438
+
439
+ const dataPaths: { path: string, leaf: boolean }[] = []
440
+ coverDataPaths(dataVal, [], dataPaths)
441
+
442
+ // `--coverage-at` narrows the DATA side, which is the side a caller
443
+ // gating one subtree is asking about. The schema side follows from
444
+ // it: a declaration is unused only among the data actually measured.
445
+ const under = null == coverageAt ? undefined
446
+ : coverageAt.replace(/^\$\.?/, '')
447
+ const inScope = (p: string): boolean => {
448
+ if (null == under || '' === under) {
449
+ return true
450
+ }
451
+ const want = '$.' + under
452
+ return p === want || p.startsWith(want + '.')
453
+ }
454
+
455
+ const used = new Set<string>()
456
+ const unchecked: string[] = []
457
+ let checked = 0
458
+ let leaves = 0
459
+ for (const { path, leaf } of dataPaths) {
460
+ if (!inScope(path)) {
461
+ continue
462
+ }
463
+ const segs = path.replace(/^\$\.?/, '').split('.').filter((x) => '' !== x)
464
+ const decl = coverMatch(anchor, segs)
465
+ if (leaf) {
466
+ leaves++
467
+ }
468
+ if (null == decl) {
469
+ unchecked.push(path)
470
+ continue
471
+ }
472
+ used.add(decl)
473
+ if (leaf) {
474
+ checked++
475
+ }
476
+ }
477
+
478
+ // A declaration is met when it constrained a data path, or when a
479
+ // declaration BENEATH it was: `$.a` is used by `$.a.b` meeting
480
+ // `$.a.b`, and reporting the parent as unused would be false.
481
+ const unused: string[] = []
482
+ for (const decl of declarations.keys()) {
483
+ const covered = used.has(decl) ||
484
+ [...used].some((u) => u.startsWith(decl + '.'))
485
+ if (!covered) {
486
+ unused.push(decl)
487
+ }
488
+ }
489
+
490
+ return {
491
+ checked,
492
+ declared: declarations.size,
493
+ leaves,
494
+ unchecked: coverShallowest(unchecked),
495
+ unused: coverShallowest(unused),
496
+ vacuous: 0 === checked && 0 < leaves,
497
+ }
498
+ }
499
+
500
+
599
501
  export function vet(
600
502
  schemaSrc: string, dataSrc: string, opts?: VetOptions): VetReport {
601
503
  const options = opts ?? {}
@@ -603,9 +505,6 @@ export function vet(
603
505
  const dataUrl = options.dataUrl ?? DEFAULT_DATA_URL
604
506
  const maxErrors = options.maxErrors ?? VET_MAX_ERRORS
605
507
 
606
- // ONE instance, two bases: the path rides on each CALL rather than on
607
- // the constructor, because the schema and the data may live in
608
- // different directories (Lang.parse takes `opts.path` per parse).
609
508
  const aontu = new Aontu(includeOpts(options))
610
509
  const schemaOpts = null == options.schemaPath ?
611
510
  undefined : { path: options.schemaPath }
@@ -617,34 +516,8 @@ export function vet(
617
516
  const schemaCtx = aontu.ctx({ collect: true })
618
517
  const schemaVal: any = aontu.unify(schemaSrc, schemaOpts, schemaCtx)
619
518
  if (0 < schemaCtx.err.length || true === schemaVal?.isNil) {
620
- // A broken schema REPORTS, exactly as broken data does. It used to
621
- // answer `findings: []` with exit 4 and nothing else, in both
622
- // ports: the engine had collected the fault and vet threw it away,
623
- // so an agent -- or a person -- was told the schema was broken and
624
- // not what or where. The verdict stays `error` (the fault is in
625
- // the truth, not in the data, and that distinction is the whole
626
- // point of the class), but the finding travels with it.
627
- //
628
- // The FIRST error only, and the data path's reasoning applies
629
- // unchanged: later errors in a document that does not stand up are
630
- // consequences of the first rather than separate things to fix.
631
- //
632
- // ONE OF THE TWO IS ALWAYS THERE, and both are nils: the branch
633
- // condition admits a collected error or a nil root, and every
634
- // value on `schemaCtx.err` is a NilVal. There is no third case, so
635
- // there is no guard here -- a guard that cannot fire is dead code,
636
- // and dead code is what ADR-002 exists to keep out. (One stood
637
- // here and the TypeScript line report called it covered; the Go
638
- // gate, which measures blocks, refused the twin.)
639
519
  const failure: any =
640
520
  0 < schemaCtx.err.length ? schemaCtx.err[0] : schemaVal
641
- // The normal path stamps both documents before they meet
642
- // (stampUrl(anchor...) below), and this early return never reaches
643
- // it, so it stamps what it is about to report: the unified root,
644
- // and the failure itself -- a COLLECTED error is minted during
645
- // unification and hangs off no tree, so nothing else would name
646
- // it. The walk reaches a failure's operands (ts/src/walk.ts),
647
- // which is what makes the sites say which file.
648
521
  stampUrl(schemaVal, schemaUrl)
649
522
  stampUrl(failure, schemaUrl)
650
523
  materialise(failure, schemaCtx)
@@ -662,11 +535,6 @@ export function vet(
662
535
  if (null != options.at) {
663
536
  anchor = anchorAt(schemaVal, options.at)
664
537
  if (null == anchor) {
665
- // AND IT SAYS WHICH SEGMENT. `--at` naming nothing is an error
666
- // verdict for the same reason a broken schema is -- the run
667
- // could not be set up from the truth's side -- and it reports
668
- // for the same reason too: a caller handed exit 4 and an empty
669
- // list has nothing to act on.
670
538
  return {
671
539
  verdict: 'error',
672
540
  truncated: false,
@@ -680,22 +548,6 @@ export function vet(
680
548
  const dataCtx = aontu.ctx({ collect: true })
681
549
  const dataVal: any = aontu.parse(dataSrc, dataOpts, dataCtx)
682
550
  if (0 < dataCtx.err.length || null == dataVal) {
683
- // A DATA DOCUMENT THAT WILL NOT PARSE IS THE DATA'S FAULT, and the
684
- // report says so: verdict `invalid`, with a finding carrying the
685
- // parser's own code and a site in the data. `error` is left to mean
686
- // what the exit table says it means -- the run could not be set up
687
- // from the SCHEMA side.
688
- //
689
- // The engine already answered it this way one character earlier: a
690
- // refused CONSTRUCT (`a: 9007199254740993`) reaches the tree as an
691
- // ordinary nil and is reported as an invalid data finding. A stray
692
- // `]` took the throwing path instead and came back as a broken
693
- // SCHEMA -- the same fault, classified two opposite ways by which
694
- // branch the parser happened to take.
695
- //
696
- // The FIRST error only: the parser stops at the first syntax error,
697
- // so a second entry would be a consequence of the first rather than
698
- // a separate thing to fix.
699
551
  const failure = dataCtx.err[0]
700
552
  if (null == failure) {
701
553
  return { verdict: 'error', truncated: false, findings: [] }
@@ -708,17 +560,6 @@ export function vet(
708
560
  findings: [findingOf(failure, { data: new Set([dataUrl]) })],
709
561
  }
710
562
  }
711
- // STAMP THE WHOLE SETTLED SCHEMA, not just the lifted anchor.
712
- // Without `--at` these are the same tree. With it, the anchor is a
713
- // subtree and the rest of the schema is still REACHABLE from inside
714
- // it -- a `%alias` declaration (`[&: %U]`, target `$.%U`) or a
715
- // recursive residual's `$.spec.Step`, both of which the meet
716
- // resolves through _fixroot (RefVal.find, RecurseVal.body). A node
717
- // reached that way but never stamped carries no url, so its site
718
- // named no file while excerpting the schema's text -- against the
719
- // invariant that every site names the file whose text it shows
720
- // (finding F, §25). stampUrl only fills a url that is EMPTY, so
721
- // stamping the superset never renames a value read from an include.
722
563
  stampUrl(schemaVal, schemaUrl)
723
564
  const dataUrls = stampUrl(dataVal, dataUrl)
724
565
  // The projection every site in this report goes through: roles by
@@ -729,31 +570,18 @@ export function vet(
729
570
  dataUrl, dataPath: options.dataPath,
730
571
  }
731
572
 
732
- // Default-validity lint (G3 phase 5, re-examined under ADR-004): for
733
- // every disjunction in the SCHEMA carrying a preference, warn when
734
- // the effective default is not an instance of any REMAINING
735
- // alternative (code `pref_not_instance`, class compat, severity
736
- // warning).
737
- //
738
- // What the finding MEANS changed with the admission gate (ADR-004).
739
- // Before the gate it flagged a soundness hole: the preference held
740
- // the disjunction open, so `a:*5|string` both generated a value the
741
- // alternatives refuse AND admitted any same-kind override. The gate
742
- // closed that hole — a preferred branch now contributes exactly its
743
- // own value to the admitted set, so a default can no longer be
744
- // "invalid against its own disjunct" and the enum-with-default idiom
745
- // (`*'auto'|'literal'|'data'`) is sound as written. The lint is KEPT,
746
- // as an advisory: a default admitted only because it is the default
747
- // is also the exact shape of a typo'd default
748
- // (`level:*wran|info|warn|debug` — the intended `*warn` would be
749
- // silent), and nothing at meet time can catch that. The
750
- // repeated-branch spelling (`*warn|warn|...`) states "the default is
751
- // a first-class member", silences the lint, and — unlike before the
752
- // gate — enforces exactly the same admitted set. The message names
753
- // the REMAINING alternatives because that is what was scanned: the
754
- // preferred branch itself always admits its own default, so the old
755
- // wording ("any alternative of *5|string") read as false on its face
756
- // (use-cases/BUGS.md §4).
573
+ let coverage: VetCoverage | undefined
574
+ if (true === options.coverage) {
575
+ const coverCtx = aontu.ctx({ collect: true })
576
+ const settledData: any = aontu.unify(dataSrc, dataOpts, coverCtx)
577
+ // A data document that does not stand alone is already reported by
578
+ // the meet below; here it falls back to what was parsed, which is
579
+ // the same paths minus whatever an include would have added.
580
+ const measured = 0 === coverCtx.err.length && true !== settledData?.isNil
581
+ ? settledData : dataVal
582
+ coverage = vetCoverage(anchor, measured, options.coverageAt)
583
+ }
584
+
757
585
  const lintFindings: VetFinding[] = []
758
586
  walkBagVals(anchor, (v: any, path: string[]): void => {
759
587
  if (true === v.isDisjunct && Array.isArray(v.peg)) {
@@ -790,45 +618,10 @@ export function vet(
790
618
  }
791
619
  })
792
620
 
793
- // `--closed` sets the flag `close()` itself sets, rather than wrapping
794
- // the anchor in a CloseFuncVal: the anchor is an already-evaluated
795
- // tree, and a func value would have to resolve again to have any
796
- // effect. A scalar anchor has no keys to close, so the flag is only
797
- // meaningful on a bag.
798
621
  if (true === options.closed && (true === anchor.isMap || true === anchor.isList)) {
799
622
  anchor.closed = true
800
623
  }
801
624
 
802
- // THE MEET IS FROM A FRESH PARSE, NOT THE SETTLED SCHEMA (the
803
- // review's finding C, use-cases/BUGS.md §15).
804
- //
805
- // Step 1 evaluated the schema ALONE, to decide whether it stands up
806
- // before any data is blamed for it. That answer is a diagnosis, and
807
- // it was also being used as the left side of the meet -- so every
808
- // reference in the schema had already RESOLVED against the schema's
809
- // own values and been replaced by them. `a:integer b:$.a` settled to
810
- // `a:integer b:integer`, and data `{a:3,b:4}` then vetted VALID,
811
- // while the same four lines as one document refuse with
812
- // scalar_value. A reference is a statement about the FINAL model, and
813
- // vet is asking about a model the data is part of.
814
- //
815
- // Parsing again is what makes `vet(S,D)` and `eval(S ∪ D)` the same
816
- // question: the meet runs the fixpoint once, over both documents, so
817
- // references, spreads and generators all see the data. Parsed trees
818
- // are single-use, hence a second parse rather than a reuse of step
819
- // 1's. The lint above still reads the SETTLED tree, where
820
- // disjunctions are ranked and normalised.
821
- //
822
- // ONLY WHEN THERE IS NO `--at`. An anchor is a SUBTREE lifted out of
823
- // the schema, and an absolute reference inside it (`$.OrderPlaced`,
824
- // the discriminated-union idiom) names a sibling of the document
825
- // root -- which the lifted subtree no longer has. The settled tree is
826
- // where those references have already been resolved and substituted,
827
- // so an anchored run keeps meeting that, exactly as it always has.
828
- // Making the rule explicit rather than leaving it to whether
829
- // anchorAt happens to find the path in an unresolved tree: the two
830
- // ports answered that differently, which is an ADR-001 divergence
831
- // waiting to happen.
832
625
  const ctx = aontu.ctx({ collect: true })
833
626
  let meetAnchor: any = anchor
834
627
  if (null == options.at) {
@@ -844,39 +637,13 @@ export function vet(
844
637
  }
845
638
  }
846
639
  else {
847
- // A RECURSIVE residual inside the lifted anchor still names its
848
- // definition by absolute path (`then?: $.spec.Step` -- the
849
- // fixpoint, RECURSION.0.md), and the meet's root is the anchored
850
- // subtree, which does not contain `$.spec`. Without a tree to
851
- // walk, the residual held its peer forever and everything under a
852
- // recursive field vetted VALID unchecked. The settled schema root
853
- // is kept on the meet context for exactly that walk
854
- // (AontuContext._fixroot; RecurseVal.body and RefVal.find fall
855
- // back to it).
856
640
  ; (ctx as any)._fixroot = schemaVal
857
- // And the meet DRIVES AT the anchor's own path, so every finding
858
- // minted live during it sits in the schema's namespace
859
- // ($.spec.Step.then.approver), exactly where the findings carried
860
- // on the settled anchor's stored paths already sit -- the Go port
861
- // gets this for free from its clone discipline, and the shared
862
- // anchored-vet rows pin the agreement.
863
641
  ; (ctx as any).path = options.at.replace(/^\$\.?/, '')
864
642
  .split('.').filter((s: string) => '' !== s)
865
643
  }
866
644
  const pair = new ConjunctVal({ peg: [meetAnchor, dataVal] }, ctx)
867
645
  const unified: any = aontu.unify(pair, undefined, ctx)
868
646
 
869
- // 4. Contradictions: every NilVal standing in the result, PLUS the
870
- // ones that never made it into the tree.
871
- //
872
- // The second half is not belt-and-braces. When a parent collapses to
873
- // a nil the whole subtree goes with it, so `service: close({...})`
874
- // meeting a typo AND a kind conflict leaves ONE nil in the tree and
875
- // reports the other only on the context — the vet verb's own
876
- // motivating example, reporting half of what it found. The language
877
- // server already walks both for this reason; vet dedups by identity
878
- // the same way, and skips the transient disjunct-trial sentinel,
879
- // which is bookkeeping rather than a finding.
880
647
  const seen = new Set<any>()
881
648
  const nils: any[] = collectNils(unified, seen)
882
649
  for (const err of ctx.err) {
@@ -891,45 +658,17 @@ export function vet(
891
658
  return findingOf(n, prov)
892
659
  })
893
660
 
894
- // 5. Incompleteness: what is left standing that cannot generate. The
895
- // generate check runs in its own collect context so nothing it
896
- // raises reaches the caller's error list, and so a schema that is
897
- // merely unsatisfied does not look like one that is contradicted.
898
- // No try/catch: in collect mode `gen` records its reasons on the
899
- // context instead of throwing, which is the whole point of the mode.
900
661
  const genCtx: any = aontu.ctx({ collect: true })
901
662
  genCtx.root = unified
902
- // Under `--at` the probe descends through the OUTPUT marks: the
903
- // caller named this node as the truth to validate against, so a
904
- // `type()` or `hide()` on it (or propagated into it) is not a reason
905
- // to check nothing. See AontuContext.probe.
906
663
  genCtx.probe = null != options.at
907
664
  unified.gen(genCtx)
908
665
  for (const err of genCtx.err) {
909
- // A CONFLICT RAISED AT GENERATION COUNTS TOO (the review's finding
910
- // C, use-cases/BUGS.md §16). The filter used to keep the
911
- // `incomplete` class alone, on the reading that step 4 had already
912
- // found every contradiction -- true while every conflict was
913
- // decided during the meet, and untrue since a sizing atom or a
914
- // container `must` may hold a PROVISIONAL reading until generation,
915
- // which is where no more members can arrive. Dropping those left
916
- // `vet` answering `valid` for data the evaluator refuses, which is
917
- // the one disagreement the vet-equals-eval harness exists to catch
918
- // -- and did.
919
- //
920
- // Deduped against step 4 by the same cause key the loop below uses,
921
- // so a contradiction seen twice is still reported once.
922
666
  if ('incomplete' === err.class || 'conflict' === err.class) {
923
667
  materialise(err, genCtx)
924
668
  findings.push(findingOf(err, prov))
925
669
  }
926
670
  }
927
671
 
928
- // 5b. Deprecation warnings (G3 phase 4): a value that carries the
929
- // deprecate() record after the meet was USED — the data met a
930
- // deprecated schema value, or the schema's own default will
931
- // generate one. Severity `warning` (the slot G2 reserved for
932
- // exactly this mark), and warnings never touch the verdict below.
933
672
  findings.push(...lintFindings)
934
673
  for (const { val, path } of collectDeprecations(unified)) {
935
674
  const v: any = val
@@ -959,27 +698,6 @@ export function vet(
959
698
  keyed.sort((a, b) => a.key < b.key ? -1 : 1)
960
699
  let ordered = keyed.map((k) => k.finding)
961
700
 
962
- // ONE CAUSE, ONE FINDING. A reference resolves by CLONING its target,
963
- // so a target that later fails can fail once per referrer — same
964
- // code, same two source sites, a different path each time. Multi-pass
965
- // collection (G2 phase 6) made this reachable: the pass loop now
966
- // continues past the erroring pass, so the clones' own folds run too.
967
- // The dedup key is the CODE plus the SITES (file, row, col, value,
968
- // role): two findings that name the same meet of the same two source
969
- // positions are one contradiction observed from two paths. The key is
970
- // NOT (code, path) — the design's sketch — because the paths are
971
- // exactly what differ. Sorted order makes the kept finding the first
972
- // by data site then path, deterministically in both ports.
973
- //
974
- // THE KEPT PATH IS THE DEEPEST one (use-cases/BUGS.md §41). A meet
975
- // that fails inside a REFERENCED map is recorded twice: once at the
976
- // key that actually conflicts, and once at the enclosing map, which
977
- // collapsed as a consequence and carries the child's two sites. Both
978
- // are the same cause; only the deeper one names the field an author
979
- // or an agent has to edit, and `$.q` for a conflict in `$.q.a` sent a
980
- // repair loop to rewrite the whole record -- twice over, identically,
981
- // when two of its fields conflicted. Depth first, then the sort order
982
- // above, so the choice stays deterministic in both ports.
983
701
  const causeKey = (f: VetFinding): string =>
984
702
  f.code + '\u0000' + f.sites.map((s) =>
985
703
  [s.file, s.row, s.col, s.role, s.value].join('\u0000')).join('\u0000')
@@ -1005,22 +723,6 @@ export function vet(
1005
723
  const truncated = maxErrors < ordered.length
1006
724
  const kept = truncated ? ordered.slice(0, maxErrors) : ordered
1007
725
 
1008
- // 6. The verdict derives from finding CLASSES, never from codes, so a
1009
- // new code can never change exit behaviour.
1010
- //
1011
- // BY CLASS, NOT BY STAGE. The split used to be positional -- whatever
1012
- // step 4 found counted as contradiction and whatever step 5 added
1013
- // counted as incompleteness -- which stopped being true when a sizing
1014
- // atom or a container `must` began holding a provisional reading
1015
- // until generation (the review's finding C, use-cases/BUGS.md §16). A
1016
- // CONTRADICTION found at generation is still a contradiction: reading
1017
- // it as mere incompleteness answered `incomplete` where the evaluator
1018
- // refuses, and `vet` and `eval` have to agree.
1019
- //
1020
- // So: an error-severity finding that is not INCOMPLETENESS makes the
1021
- // document invalid, wherever it was found -- a contradiction, a parse
1022
- // refusal, an unresolvable reference alike. Warnings (the `compat`
1023
- // class: lint and deprecation) never touch the verdict.
1024
726
  let verdict: VetVerdict = 'valid'
1025
727
  const errors = ordered.filter((f) => 'error' === f.severity)
1026
728
  const unmet = errors.filter((f) => 'incomplete' === f.class).length
@@ -1031,5 +733,8 @@ export function vet(
1031
733
  verdict = 'incomplete'
1032
734
  }
1033
735
 
1034
- return { verdict, truncated, findings: kept }
736
+ return {
737
+ verdict, truncated, findings: kept,
738
+ ...(null == coverage ? {} : { coverage }),
739
+ }
1035
740
  }