aontu 0.52.0 → 0.53.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 (273) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. package/src/walk.ts +99 -0
package/src/subsume.ts ADDED
@@ -0,0 +1,690 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // Subsumption as a first-class query (G3 phases 1-2,
4
+ // docs/capability-review/g3-subsumption-evolution.md): does the GENERAL
5
+ // value admit every instance the SPECIFIC value admits?
6
+ //
7
+ // The recursion is a dedicated structural walk over EVALUATED values —
8
+ // design option B. It never mutates its inputs (it runs after
9
+ // evaluation, on finished trees), carries no fixpoint, and returns a
10
+ // THREE-VALUED verdict: `subsumes`, `does_not_subsume` (with the
11
+ // failing path and both sides' canons as the witness), or `undecided`
12
+ // (with a reason code, never silently). Where a rule cannot decide the
13
+ // answer folds toward "not subsumed" or "undecided" — the safe
14
+ // directions: a compatibility gate that wrongly reports "breaking"
15
+ // costs a second look, one that wrongly reports "compatible" ships the
16
+ // break (docs/reference-language.md, "Subsumption").
17
+ //
18
+ // Findings reuse G2's report vocabulary (the vet finding object), with
19
+ // the G3 codes appended to the shared registry: `compat_narrowed`,
20
+ // `compat_required_added`, `compat_default_changed`,
21
+ // `compat_marks_changed`, and the `sub_*` undecided reasons — all
22
+ // class `compat`.
23
+
24
+
25
+ import type { TrustOptions } from './type'
26
+ import { Aontu } from './aontu'
27
+ import { anchorAt } from './vet'
28
+ import { hcanon } from './hcanon'
29
+ import type { VetFinding, VetSite } from './vet'
30
+ import {
31
+ constraintSubsumesConstraint,
32
+ constraintAdmitsScalar,
33
+ } from './val/ConstraintVal'
34
+ import { kindSubsumes } from './val/ScalarKindVal'
35
+ import { prefInnerPeg } from './val/PrefVal'
36
+
37
+
38
+ export type SubsumeVerdict =
39
+ | 'subsumes' | 'does_not_subsume' | 'undecided' | 'error'
40
+
41
+ export type SubsumeProfile = 'values' | 'defaults' | 'gen'
42
+
43
+ export type SubsumeOptions = {
44
+ profile?: SubsumeProfile
45
+ at?: string
46
+ generalUrl?: string // provenance label for general sites
47
+ specificUrl?: string // provenance label for specific sites
48
+ // Where each document CAME FROM, so a relative `@"file"` load inside
49
+ // it resolves from its own directory — vet's schemaPath/dataPath
50
+ // precedent, one per document because they need not live together.
51
+ generalPath?: string
52
+ specificPath?: string
53
+ // The include capability both documents evaluate under (G5,
54
+ // docs/trust.md). vet's precedent: the verb passes the profile the
55
+ // caller asked for, and an absent option means today's default.
56
+ trust?: TrustOptions
57
+ }
58
+
59
+ export type SubsumeReport = {
60
+ verdict: SubsumeVerdict
61
+ findings: VetFinding[]
62
+ }
63
+
64
+
65
+ const DEFAULT_GENERAL_URL = 'general'
66
+ const DEFAULT_SPECIFIC_URL = 'specific'
67
+
68
+
69
+ // One comparison's running state: the finding list, the two provenance
70
+ // labels, and the profile.
71
+ type SubState = {
72
+ profile: SubsumeProfile
73
+ // Set inside a distribution trial: see trialSubsume.
74
+ distributing?: boolean
75
+ findings: VetFinding[]
76
+ generalUrl: string
77
+ specificUrl: string
78
+ }
79
+
80
+ // The per-node answer. Aggregation: any `no` makes the verdict
81
+ // does_not_subsume; otherwise any `undecided` makes it undecided.
82
+ type Tri = 'yes' | 'no' | 'undecided'
83
+
84
+
85
+ function pathText(path: string[]): string {
86
+ return '$' + (0 < path.length ? '.' + path.join('.') : '')
87
+ }
88
+
89
+
90
+ // Every recorded operand is a real evaluated Val or the sited topLike
91
+ // stand-in, so no null guards: a Val's site is always an object, and a
92
+ // value parsed without a usable frame (a disjunct, say) already carries
93
+ // the -1 coordinates in it.
94
+ function siteOf(v: any, role: string, url: string): VetSite {
95
+ return {
96
+ file: url,
97
+ row: v.site.row,
98
+ col: v.site.col,
99
+ len: v.site.len,
100
+ role: role as any,
101
+ src: v.site.src,
102
+ value: v.canon,
103
+ }
104
+ }
105
+
106
+
107
+ // One finding: general and specific sites (general first — it is the
108
+ // contract being weakened or broken), canons as expected/actual.
109
+ function record(
110
+ state: SubState, code: string, path: string[],
111
+ g: any, s: any, message: string,
112
+ ): void {
113
+ state.findings.push({
114
+ code,
115
+ class: 'compat',
116
+ severity: 'error',
117
+ path: pathText(path),
118
+ message,
119
+ sites: [
120
+ siteOf(g, 'general', state.generalUrl),
121
+ siteOf(s, 'specific', state.specificUrl),
122
+ ],
123
+ expected: g.canon,
124
+ actual: s.canon,
125
+ })
126
+ }
127
+
128
+
129
+ // The admission view of a value under a profile: a BARE preference
130
+ // admits what its superior admits (the engine's own PrefVal.superpeg
131
+ // semantics, and the docs' "a bare preference is gated by kind"); the
132
+ // default itself is compared separately, by the `defaults` and `gen`
133
+ // profiles.
134
+ function admission(v: any): any {
135
+ return true === v?.isPref ? v.superpeg : v
136
+ }
137
+
138
+
139
+ // A PREFERRED BRANCH CONTRIBUTES EXACTLY ITS OWN VALUE (ADR-004). The
140
+ // admission gate made that the engine's rule -- `*'auto'|'literal'|'data'`
141
+ // admits those three strings and nothing else -- and the subsumption
142
+ // walk kept comparing a pref MEMBER by its kind superior, the
143
+ // pre-ADR-004 reading. So a disjunction with a default did not subsume
144
+ // ITSELF: every member of the specific side widened to `string`, which
145
+ // no general member admits, and the walk answered the distribution
146
+ // case. `aontu_policy: hide({compat: *backward|forward|full|none})` --
147
+ // the verbatim idiom from reference-api.md -- failed self-subsumption
148
+ // under --profile gen (use-cases/BUGS.md §29).
149
+ //
150
+ // Only for a member of a disjunction: a bare `*x` standing alone is
151
+ // still gated by kind, which is the rule above and the documented one.
152
+ function memberAdmission(v: any): any {
153
+ return true === v?.isPref ? prefInnerPeg(v) : v
154
+ }
155
+
156
+
157
+ // The effective default of a value, or undefined when it has none, or
158
+ // 'indeterminate' when equal-rank preferences disagree (which the
159
+ // engine itself refuses only at generation).
160
+ // Exported (with subsumeNode) for the default-validity lint in
161
+ // ts/src/vet.ts (G3 phase 5): the lint asks exactly this walk's two
162
+ // questions — what is the effective default, and does a member admit
163
+ // it.
164
+ export function effectiveDefault(v: any): any {
165
+ // EVERY pref layer is unwrapped (prefInnerPeg), not just one: a
166
+ // ranked default's effective value is the innermost peg — `**member`
167
+ // generates "member" exactly as `*member` does (the rank-uniform
168
+ // meet, ADR-004). The one-layer unwrap left a rank-2 default wearing
169
+ // a `*`-wrapper no plain alternative subsumes, which is the
170
+ // pref_not_instance lint's ranked false positive of
171
+ // use-cases/BUGS.md §4 (`**member|member|admin|owner` warned while
172
+ // generating a value its own branch admits).
173
+ if (true === v?.isPref) {
174
+ return prefInnerPeg(v)
175
+ }
176
+ if (true === v?.isDisjunct && Array.isArray(v.peg)) {
177
+ const prefs = v.peg.filter((m: any) => true === m?.isPref)
178
+ if (0 === prefs.length) {
179
+ return undefined
180
+ }
181
+ // Generation picks the LOWEST rank (`a:**1|*2` generates 2 —
182
+ // test/spec/edge.tsv), so the effective default does too.
183
+ const minRank = Math.min(...prefs.map((p: any) => p.rank))
184
+ const top = prefs.filter((p: any) => p.rank === minRank)
185
+ const first: any = prefInnerPeg(top[0])
186
+ for (const p of top.slice(1)) {
187
+ if (!first.same?.(prefInnerPeg(p))) {
188
+ return 'indeterminate'
189
+ }
190
+ }
191
+ return first
192
+ }
193
+ return undefined
194
+ }
195
+
196
+
197
+ // Is this evaluated value concrete enough to serve as a witness — a
198
+ // value that certainly IS an instance of the specific side?
199
+ function isConcrete(v: any): boolean {
200
+ if (true === v?.isScalar) {
201
+ return true
202
+ }
203
+ if ((true === v?.isMap || true === v?.isList) && null != v.peg) {
204
+ const children = Array.isArray(v.peg) ? v.peg : Object.values(v.peg)
205
+ return children.every((c: any) => isConcrete(c))
206
+ }
207
+ return false
208
+ }
209
+
210
+
211
+ // Unresolved residue makes the relation undecidable at this node:
212
+ // references, variables, unreduced conjuncts and functions have no
213
+ // admitted set to compare.
214
+ function unresolved(v: any): boolean {
215
+ return true === v?.isRef || true === v?.isVar ||
216
+ true === v?.isConjunct || true === v?.isExpect ||
217
+ (true === v?.isFunc && true !== v?.isConstraint)
218
+ }
219
+
220
+
221
+ // Exported for the defensive-arm unit test (ADR-002,
222
+ // ts/test/coverage3.test.ts): the no-rule fold at the walk's tail is
223
+ // unreachable through subsume() today — the ladder is total for every
224
+ // evaluated former — and the walk is otherwise private.
225
+ export function subsumeNode(
226
+ state: SubState, path: string[], g0: any, s0: any): Tri {
227
+
228
+ const g = admission(g0)
229
+ const s = admission(s0)
230
+
231
+ // Marks change the OUTPUT shape, not the admitted set: only the `gen`
232
+ // profile reports them, and only when they differ on corresponding
233
+ // nodes.
234
+ if ('gen' === state.profile && true !== state.distributing &&
235
+ (!!g?.mark?.type !== !!s?.mark?.type ||
236
+ !!g?.mark?.hide !== !!s?.mark?.hide)) {
237
+ record(state, 'compat_marks_changed', path, g, s,
238
+ 'marks differ: general ' + JSON.stringify(g?.mark) +
239
+ ', specific ' + JSON.stringify(s?.mark))
240
+ return 'no'
241
+ }
242
+
243
+ // TOP admits everything. There is no nil rule: an error-free
244
+ // evaluated document carries no nil (failing disjunct members are
245
+ // discarded, every other nil collects an error, and subsume() answers
246
+ // `error` for a source that does not stand alone), so a nil handed to
247
+ // the walk by a future caller falls to the no-rule fold below —
248
+ // `undecided`, the safe direction.
249
+ if (true === g?.isTop) {
250
+ return 'yes'
251
+ }
252
+
253
+ if (unresolved(g) || unresolved(s)) {
254
+ // REFLEXIVITY IS A LAW, not a rule the ladder gets to skip. Every
255
+ // value admits itself, residue included: the set admitted by
256
+ // `integer & min(0)` is exactly the set admitted by
257
+ // `integer & min(0)`. Without this, a constraint inside a spread
258
+ // template made a contract non-SELF-subsumable -- expected and
259
+ // actual byte-identical, verdict `undecided` -- so `breaking` on
260
+ // the documented close-per-entry idiom hard-failed reflexivity and
261
+ // had to run --allow-undecided, which then masks the genuine
262
+ // undecideds it exists to surface (use-cases/BUGS.md §28).
263
+ //
264
+ // Identity is the HASH FORM, not the canon: canon drops closedness
265
+ // and the marks, so `close({a:1})` and `{a:1}` share a canon while
266
+ // admitting different sets. Computed only on this branch, where
267
+ // the answer would otherwise be undecided, so the hot path is
268
+ // untouched.
269
+ if (hcanon(g) === hcanon(s)) {
270
+ return 'yes'
271
+ }
272
+ record(state, 'sub_unresolved', path, g, s,
273
+ 'unresolved residue: the admitted set is not comparable')
274
+ return 'undecided'
275
+ }
276
+
277
+ // Disjunctions. General side first: a specific member must be
278
+ // subsumed by SOME general member; member-wise failure is not proof
279
+ // (the distribution case), so a concrete failing member becomes the
280
+ // witness and anything else is honestly undecided.
281
+ if (true === s?.isDisjunct) {
282
+ let out: Tri = 'yes'
283
+ for (const raw of s.peg as any[]) {
284
+ const member = memberAdmission(raw)
285
+ const trial = trialSubsume(state, path, g, member)
286
+ if ('yes' !== trial) {
287
+ if (isConcrete(admission(member))) {
288
+ record(state, 'compat_narrowed', path, g, member,
289
+ 'a specific alternative is not admitted by the general value')
290
+ return 'no'
291
+ }
292
+ record(state, 'sub_disjunct_distribution', path, g, member,
293
+ 'a specific alternative is not admitted member-wise, and no' +
294
+ ' concrete counterexample settles the distribution case')
295
+ out = 'undecided'
296
+ }
297
+ }
298
+ return out
299
+ }
300
+ if (true === g?.isDisjunct) {
301
+ for (const raw of g.peg as any[]) {
302
+ if ('yes' === trialSubsume(state, path, memberAdmission(raw), s)) {
303
+ return 'yes'
304
+ }
305
+ }
306
+ if (isConcrete(s)) {
307
+ record(state, 'compat_narrowed', path, g, s,
308
+ 'no general alternative admits the specific value')
309
+ return 'no'
310
+ }
311
+ record(state, 'sub_disjunct_distribution', path, g, s,
312
+ 'no general alternative admits the specific value member-wise,' +
313
+ ' and no concrete counterexample settles the distribution case')
314
+ return 'undecided'
315
+ }
316
+
317
+ // Scalar kinds: a kind subsumes its scalars, narrower kinds, and the
318
+ // constraint residuals of its domain.
319
+ if (true === g?.isScalarKind) {
320
+ if (true === s?.isScalarKind) {
321
+ if (g.peg === s.peg || kindSubsumes(g.peg, s.peg)) {
322
+ return 'yes'
323
+ }
324
+ record(state, 'compat_narrowed', path, g, s,
325
+ 'the general kind does not admit the specific kind')
326
+ return 'no'
327
+ }
328
+ if (true === s?.isScalar) {
329
+ const leaf = s.superior?.()
330
+ if (true === leaf?.isScalarKind &&
331
+ (g.peg === leaf.peg || kindSubsumes(g.peg, leaf.peg))) {
332
+ return 'yes'
333
+ }
334
+ record(state, 'compat_narrowed', path, g, s,
335
+ 'the general kind does not admit the specific scalar')
336
+ return 'no'
337
+ }
338
+ if (true === s?.isConstraint) {
339
+ // A kind covers a residual of its own domain: `number` admits any
340
+ // numeric residual (the supertype admits every leaf a bound can
341
+ // pin), a numeric LEAF kind admits only a residual pinned to that
342
+ // leaf, and `string` admits any pattern residual. A residual with
343
+ // no domain (sizing-only) admits containers no kind covers.
344
+ const dom = (s as any).domain
345
+ const skind = (s as any).kind
346
+ if ('number' === dom &&
347
+ (Number === g.peg ||
348
+ (null != skind && kindSubsumes(g.peg, skind)))) {
349
+ return 'yes'
350
+ }
351
+ if ('string' === dom && String === g.peg) {
352
+ return 'yes'
353
+ }
354
+ record(state, 'compat_narrowed', path, g, s,
355
+ 'the general kind does not cover the specific residual')
356
+ return 'no'
357
+ }
358
+ record(state, 'compat_narrowed', path, g, s,
359
+ 'the general kind admits no such value')
360
+ return 'no'
361
+ }
362
+
363
+ // Constraint residuals.
364
+ if (true === g?.isConstraint) {
365
+ if (true === s?.isConstraint) {
366
+ const r = constraintSubsumesConstraint(g, s)
367
+ if (true === r) {
368
+ return 'yes'
369
+ }
370
+ if ('undecided' === r) {
371
+ record(state, 'sub_evaluate_only', path, g, s,
372
+ 'an evaluate-only check (must) makes the admitted set opaque')
373
+ return 'undecided'
374
+ }
375
+ record(state, 'compat_narrowed', path, g, s,
376
+ 'the general residual does not contain the specific residual')
377
+ return 'no'
378
+ }
379
+ if (true === s?.isScalar) {
380
+ const r = constraintAdmitsScalar(g, s)
381
+ if (true === r) {
382
+ return 'yes'
383
+ }
384
+ if ('undecided' === r) {
385
+ record(state, 'sub_evaluate_only', path, g, s,
386
+ 'an evaluate-only check (must) makes the admitted set opaque')
387
+ return 'undecided'
388
+ }
389
+ record(state, 'compat_narrowed', path, g, s,
390
+ 'the general residual does not admit the specific scalar')
391
+ return 'no'
392
+ }
393
+ record(state, 'compat_narrowed', path, g, s,
394
+ 'the general residual constrains a domain the specific value is not in')
395
+ return 'no'
396
+ }
397
+
398
+ // Concrete scalars subsume only themselves (identity compares kind
399
+ // as well as value).
400
+ if (true === g?.isScalar) {
401
+ if (true === s?.isScalar && true === g.same?.(s)) {
402
+ return 'yes'
403
+ }
404
+ record(state, 'compat_narrowed', path, g, s,
405
+ 'a concrete value subsumes only itself')
406
+ return 'no'
407
+ }
408
+
409
+ // Maps.
410
+ if (true === g?.isMap) {
411
+ if (true !== s?.isMap) {
412
+ record(state, 'compat_narrowed', path, g, s,
413
+ 'the general value is a map and the specific value is not')
414
+ return 'no'
415
+ }
416
+ return subsumeBag(state, path, g, s, Object.keys(g.peg),
417
+ Object.keys(s.peg), (v: any, k: string) => v.peg[k])
418
+ }
419
+
420
+ // Lists: element-wise by position; the same required/optional shape
421
+ // as maps, with positions as keys.
422
+ if (true === g?.isList) {
423
+ if (true !== s?.isList) {
424
+ record(state, 'compat_narrowed', path, g, s,
425
+ 'the general value is a list and the specific value is not')
426
+ return 'no'
427
+ }
428
+ const gk = (g.peg as any[]).map((_: any, i: number) => '' + i)
429
+ const sk = (s.peg as any[]).map((_: any, i: number) => '' + i)
430
+ return subsumeBag(state, path, g, s, gk, sk,
431
+ (v: any, k: string) => v.peg[Number(k)])
432
+ }
433
+
434
+ // The ladder above is total in practice: every evaluated former is a
435
+ // scalar, kind, constraint, map, list, disjunct, or top, or is caught
436
+ // by admission (pref) or unresolved (ref, var, conjunct, expect,
437
+ // func). The arm is kept because "in practice" is evaluation's
438
+ // property, not this walk's, and a future value class (or a nil, see
439
+ // the top rule) must land on an honest `undecided`, not fall out of
440
+ // the walk with no answer.
441
+ record(state, 'sub_unresolved', path, g, s,
442
+ 'no subsumption rule covers this pair of value formers')
443
+ return 'undecided'
444
+ }
445
+
446
+
447
+ // Maps and lists share one shape: required keys of the general side
448
+ // must be required in the specific side and subsume; optional keys
449
+ // compare when present; closedness bounds the specific key set; spread
450
+ // templates govern the specific side's surplus.
451
+ function subsumeBag(
452
+ state: SubState, path: string[], g: any, s: any,
453
+ gKeys: string[], sKeys: string[],
454
+ child: (v: any, k: string) => any,
455
+ ): Tri {
456
+ let out: Tri = 'yes'
457
+ const worse = (r: Tri) => {
458
+ if ('no' === r || ('undecided' === r && 'no' !== out)) {
459
+ out = 'no' === r ? 'no' : 'undecided'
460
+ }
461
+ }
462
+
463
+ // Always arrays on an evaluated bag (BagVal initialises them).
464
+ const gOptional: string[] = g.optionalKeys
465
+ const sOptional: string[] = s.optionalKeys
466
+
467
+ for (const k of gKeys) {
468
+ const gChild = child(g, k)
469
+ const has = sKeys.includes(k)
470
+ const optional = gOptional.includes(k)
471
+ if (!has) {
472
+ if (optional) {
473
+ continue
474
+ }
475
+ record(state, 'compat_required_added', [...path, k], gChild, s,
476
+ 'the general value requires this key; the specific value admits' +
477
+ ' instances without it')
478
+ worse('no')
479
+ continue
480
+ }
481
+ if (!optional && sOptional.includes(k)) {
482
+ record(state, 'compat_required_added', [...path, k], gChild,
483
+ child(s, k),
484
+ 'the general value requires this key; the specific value makes' +
485
+ ' it optional, so instances without it are admitted')
486
+ worse('no')
487
+ continue
488
+ }
489
+ worse(subsumeNode(state, [...path, k], gChild, child(s, k)))
490
+ }
491
+
492
+ // Closedness: a closed general bag admits only key sets it declares,
493
+ // so the specific side must be closed and inside it.
494
+ if (true === g.closed) {
495
+ if (true !== s.closed) {
496
+ record(state, 'compat_narrowed', path, g, s,
497
+ 'the general value is closed; the open specific value admits' +
498
+ ' surplus keys')
499
+ worse('no')
500
+ }
501
+ else {
502
+ for (const k of sKeys) {
503
+ if (!gKeys.includes(k)) {
504
+ record(state, 'compat_narrowed', [...path, k], g, child(s, k),
505
+ 'the closed general value does not declare this key')
506
+ worse('no')
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ // Spread templates: a path-dependent template's meaning depends on
513
+ // where it lands, which no structural comparison can decide.
514
+ const gcj = g.spread?.cj
515
+ const scj = s.spread?.cj
516
+ if (null != gcj || null != scj) {
517
+ if (true === gcj?.isPathDependent || true === scj?.isPathDependent) {
518
+ record(state, 'sub_path_dependent_spread', path, gcj ?? g, scj ?? s,
519
+ 'a path-dependent spread template cannot be compared structurally')
520
+ worse('undecided')
521
+ }
522
+ else if (null != gcj) {
523
+ // The general template governs the specific side's surplus keys
524
+ // and its template.
525
+ for (const k of sKeys) {
526
+ if (!gKeys.includes(k)) {
527
+ worse(subsumeNode(state, [...path, k], gcj, child(s, k)))
528
+ }
529
+ }
530
+ worse(subsumeNode(state, [...path, '&'], gcj, scj ?? topLike()))
531
+ }
532
+ // A specific-only template narrows the specific side; nothing for
533
+ // the (unconstrained) general side to refuse.
534
+ }
535
+
536
+ return out
537
+ }
538
+
539
+
540
+ // A stand-in TOP for "the specific side leaves this unconstrained".
541
+ let topVal: any
542
+ function topLike(): any {
543
+ if (null == topVal) {
544
+ // A full site, not a partial one: `len` and `src` are as much part
545
+ // of the shape as row and col, and leaving them undefined dropped
546
+ // both keys from the emitted report — so this stand-in was the one
547
+ // site in either port with no `len` at all, and the Go twin (which
548
+ // has no undefined) disagreed. It occupies no source, so the values
549
+ // are the "unknown" ones.
550
+ topVal = {
551
+ isTop: true, canon: 'top',
552
+ site: { row: -1, col: -1, len: -1, src: '' },
553
+ }
554
+ }
555
+ return topVal
556
+ }
557
+
558
+
559
+ // A trial comparison whose findings are DISCARDED: disjunct
560
+ // member-matching asks many "would this member do?" questions, and only
561
+ // the aggregated outcome is a finding.
562
+ // A DISTRIBUTION TRIAL IS NOT A NODE CORRESPONDENCE. It asks whether
563
+ // one ALTERNATIVE of one side admits one alternative of the other,
564
+ // which is a question about admitted sets; the two values it compares
565
+ // are not the same node of the two documents. The `gen` profile's mark
566
+ // rule is a correspondence question -- did a field that used to be
567
+ // generated become hidden -- and firing it here compared a whole
568
+ // disjunction (carrying its enclosing bag's mark) against a member
569
+ // extracted out of one (which does not), so `hide({c: *a|b})` stopped
570
+ // subsuming ITSELF under --profile gen (use-cases/BUGS.md §29). The
571
+ // enclosing node's marks are compared where they correspond: at that
572
+ // node, by the ordinary walk.
573
+ function trialSubsume(
574
+ state: SubState, path: string[], g: any, s: any): Tri {
575
+ const trial: SubState = { ...state, findings: [], distributing: true }
576
+ return subsumeNode(trial, path, g, s)
577
+ }
578
+
579
+
580
+ // The defaults comparison (the `defaults` and `gen` profiles): the
581
+ // specific side's effective default must survive into the general side
582
+ // unchanged. Adding a default where none existed is compatible;
583
+ // changing or removing one is not (removal turns previously generable
584
+ // documents incomplete).
585
+ function subsumeDefaults(
586
+ state: SubState, path: string[], g: any, s: any): Tri {
587
+ const sd = effectiveDefault(s)
588
+ if (undefined === sd) {
589
+ return 'yes'
590
+ }
591
+ const gd = effectiveDefault(g)
592
+ if ('indeterminate' === sd || 'indeterminate' === gd) {
593
+ record(state, 'sub_default_indeterminate', path, g, s,
594
+ 'equal-rank preferences disagree, so the effective default is' +
595
+ ' not a single value')
596
+ return 'undecided'
597
+ }
598
+ if (undefined === gd || true !== gd.same?.(sd)) {
599
+ record(state, 'compat_default_changed', path, g, s,
600
+ 'the effective default changed: previously generable documents' +
601
+ ' materialise differently or become incomplete')
602
+ return 'no'
603
+ }
604
+ return 'yes'
605
+ }
606
+
607
+
608
+ // Walk both trees for default agreement wherever the specific side has
609
+ // one: at the node itself and inside every corresponding bag child.
610
+ function subsumeDefaultsWalk(
611
+ state: SubState, path: string[], g: any, s: any): Tri {
612
+ let out: Tri = subsumeDefaults(state, path, g, s)
613
+
614
+ const gm = true === g?.isMap ? g.peg : undefined
615
+ const sm = true === s?.isMap ? s.peg : undefined
616
+ if (null != gm && null != sm) {
617
+ for (const k of Object.keys(sm)) {
618
+ if (null != gm[k]) {
619
+ const r = subsumeDefaultsWalk(state, [...path, k], gm[k], sm[k])
620
+ if ('no' === r || ('undecided' === r && 'no' !== out)) {
621
+ out = 'no' === r ? 'no' : 'undecided'
622
+ }
623
+ }
624
+ }
625
+ }
626
+ return out
627
+ }
628
+
629
+
630
+ /**
631
+ * Does `generalSrc` subsume `specificSrc` — is every instance the
632
+ * specific admits admitted by the general too?
633
+ *
634
+ * Both sources are evaluated fresh (single-use trees make this
635
+ * mandatory), and the recursion runs on the finished values. The
636
+ * verdict is three-valued plus `error` (a source that does not stand up
637
+ * on its own, mirroring vet's schema-error verdict); findings reuse
638
+ * G2's object with class `compat`.
639
+ */
640
+ export function subsume(
641
+ generalSrc: string, specificSrc: string, opts?: SubsumeOptions,
642
+ ): SubsumeReport {
643
+ const options = opts ?? {}
644
+ const profile: SubsumeProfile = options.profile ?? 'defaults'
645
+ const state: SubState = {
646
+ profile,
647
+ findings: [],
648
+ generalUrl: options.generalUrl ?? DEFAULT_GENERAL_URL,
649
+ specificUrl: options.specificUrl ?? DEFAULT_SPECIFIC_URL,
650
+ }
651
+
652
+ const load = (src: string, path?: string): any => {
653
+ const aontu = new Aontu(
654
+ null == options.trust ? undefined : { trust: options.trust })
655
+ const ctx = aontu.ctx({ collect: true })
656
+ const v: any = aontu.unify(
657
+ src, null == path ? undefined : { path }, ctx)
658
+ if (0 < ctx.err.length || true === v?.isNil) {
659
+ return undefined
660
+ }
661
+ return v
662
+ }
663
+
664
+ let g = load(generalSrc, options.generalPath)
665
+ let s = load(specificSrc, options.specificPath)
666
+ if (null == g || null == s) {
667
+ return { verdict: 'error', findings: [] }
668
+ }
669
+
670
+ if (null != options.at) {
671
+ g = anchorAt(g, options.at)
672
+ s = anchorAt(s, options.at)
673
+ if (null == g || null == s) {
674
+ return { verdict: 'error', findings: [] }
675
+ }
676
+ }
677
+
678
+ let out: Tri = subsumeNode(state, [], g, s)
679
+ if ('values' !== profile) {
680
+ const d = subsumeDefaultsWalk(state, [], g, s)
681
+ if ('no' === d || ('undecided' === d && 'no' !== out)) {
682
+ out = 'no' === d ? 'no' : 'undecided'
683
+ }
684
+ }
685
+
686
+ const verdict: SubsumeVerdict =
687
+ 'no' === out ? 'does_not_subsume' :
688
+ 'undecided' === out ? 'undecided' : 'subsumes'
689
+ return { verdict, findings: state.findings }
690
+ }