aontu 0.52.1 → 0.54.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 (310) 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 +15 -3
  7. package/dist/aontu.js +126 -5
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +45 -1
  10. package/dist/cli.js +2845 -69
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +18 -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 +45 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +13 -0
  22. package/dist/graph.js +110 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +145 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +235 -8
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +425 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +955 -110
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +343 -48
  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 +941 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +532 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +35 -0
  47. package/dist/mod.js +345 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +49 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +41 -0
  53. package/dist/provenance.js +336 -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 +15 -0
  59. package/dist/reach.js +168 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +23 -0
  62. package/dist/relation.js +230 -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/sig.d.ts +25 -0
  68. package/dist/sig.js +277 -0
  69. package/dist/sig.js.map +1 -0
  70. package/dist/sigdecl.d.ts +2 -0
  71. package/dist/sigdecl.js +11 -0
  72. package/dist/sigdecl.js.map +1 -0
  73. package/dist/siggate.d.ts +4 -0
  74. package/dist/siggate.js +90 -0
  75. package/dist/siggate.js.map +1 -0
  76. package/dist/site.d.ts +4 -0
  77. package/dist/site.js +31 -0
  78. package/dist/site.js.map +1 -1
  79. package/dist/std.d.ts +1 -0
  80. package/dist/std.js +129 -0
  81. package/dist/std.js.map +1 -0
  82. package/dist/subsume.d.ts +39 -0
  83. package/dist/subsume.js +573 -0
  84. package/dist/subsume.js.map +1 -0
  85. package/dist/trim.d.ts +19 -0
  86. package/dist/trim.js +155 -0
  87. package/dist/trim.js.map +1 -0
  88. package/dist/tsconfig.tsbuildinfo +1 -1
  89. package/dist/type.d.ts +17 -1
  90. package/dist/type.js.map +1 -1
  91. package/dist/unify.d.ts +2 -1
  92. package/dist/unify.js +234 -38
  93. package/dist/unify.js.map +1 -1
  94. package/dist/utility.d.ts +8 -1
  95. package/dist/utility.js +68 -1
  96. package/dist/utility.js.map +1 -1
  97. package/dist/val/AggFuncVal.d.ts +44 -0
  98. package/dist/val/AggFuncVal.js +364 -0
  99. package/dist/val/AggFuncVal.js.map +1 -0
  100. package/dist/val/ArithFuncVal.d.ts +31 -0
  101. package/dist/val/ArithFuncVal.js +62 -0
  102. package/dist/val/ArithFuncVal.js.map +1 -0
  103. package/dist/val/BagVal.d.ts +7 -0
  104. package/dist/val/BagVal.js +150 -11
  105. package/dist/val/BagVal.js.map +1 -1
  106. package/dist/val/CloseFuncVal.js +9 -1
  107. package/dist/val/CloseFuncVal.js.map +1 -1
  108. package/dist/val/ConjunctVal.d.ts +1 -1
  109. package/dist/val/ConjunctVal.js +19 -0
  110. package/dist/val/ConjunctVal.js.map +1 -1
  111. package/dist/val/ConstraintVal.d.ts +7 -1
  112. package/dist/val/ConstraintVal.js +523 -45
  113. package/dist/val/ConstraintVal.js.map +1 -1
  114. package/dist/val/ContainerKindVal.d.ts +35 -0
  115. package/dist/val/ContainerKindVal.js +99 -0
  116. package/dist/val/ContainerKindVal.js.map +1 -0
  117. package/dist/val/CopyFuncVal.d.ts +1 -2
  118. package/dist/val/CopyFuncVal.js.map +1 -1
  119. package/dist/val/Decimal.d.ts +1 -0
  120. package/dist/val/Decimal.js +13 -0
  121. package/dist/val/Decimal.js.map +1 -1
  122. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  123. package/dist/val/DeprecateFuncVal.js +47 -0
  124. package/dist/val/DeprecateFuncVal.js.map +1 -0
  125. package/dist/val/DisjunctVal.d.ts +1 -2
  126. package/dist/val/DisjunctVal.js +269 -53
  127. package/dist/val/DisjunctVal.js.map +1 -1
  128. package/dist/val/EachFuncVal.d.ts +15 -0
  129. package/dist/val/EachFuncVal.js +75 -0
  130. package/dist/val/EachFuncVal.js.map +1 -0
  131. package/dist/val/ExpectVal.js +62 -4
  132. package/dist/val/ExpectVal.js.map +1 -1
  133. package/dist/val/FeatureVal.js +1 -1
  134. package/dist/val/FeatureVal.js.map +1 -1
  135. package/dist/val/FilterFuncVal.d.ts +15 -0
  136. package/dist/val/FilterFuncVal.js +91 -0
  137. package/dist/val/FilterFuncVal.js.map +1 -0
  138. package/dist/val/FuncBaseVal.d.ts +7 -1
  139. package/dist/val/FuncBaseVal.js +183 -2
  140. package/dist/val/FuncBaseVal.js.map +1 -1
  141. package/dist/val/GraphAtomVal.d.ts +39 -0
  142. package/dist/val/GraphAtomVal.js +184 -0
  143. package/dist/val/GraphAtomVal.js.map +1 -0
  144. package/dist/val/HideFuncVal.js.map +1 -1
  145. package/dist/val/JunctionVal.js +24 -1
  146. package/dist/val/JunctionVal.js.map +1 -1
  147. package/dist/val/KeyFuncVal.d.ts +1 -1
  148. package/dist/val/KeyFuncVal.js +38 -30
  149. package/dist/val/KeyFuncVal.js.map +1 -1
  150. package/dist/val/ListVal.js +94 -14
  151. package/dist/val/ListVal.js.map +1 -1
  152. package/dist/val/LowerFuncVal.js.map +1 -1
  153. package/dist/val/MapVal.d.ts +1 -0
  154. package/dist/val/MapVal.js +146 -14
  155. package/dist/val/MapVal.js.map +1 -1
  156. package/dist/val/MatchFuncVal.d.ts +15 -0
  157. package/dist/val/MatchFuncVal.js +107 -0
  158. package/dist/val/MatchFuncVal.js.map +1 -0
  159. package/dist/val/MoveFuncVal.js.map +1 -1
  160. package/dist/val/NilVal.js +24 -0
  161. package/dist/val/NilVal.js.map +1 -1
  162. package/dist/val/OpBaseVal.d.ts +1 -1
  163. package/dist/val/OpBaseVal.js +19 -1
  164. package/dist/val/OpBaseVal.js.map +1 -1
  165. package/dist/val/OpenFuncVal.js +4 -1
  166. package/dist/val/OpenFuncVal.js.map +1 -1
  167. package/dist/val/PackFuncVal.d.ts +15 -0
  168. package/dist/val/PackFuncVal.js +108 -0
  169. package/dist/val/PackFuncVal.js.map +1 -0
  170. package/dist/val/PathFuncVal.d.ts +2 -2
  171. package/dist/val/PathFuncVal.js +75 -16
  172. package/dist/val/PathFuncVal.js.map +1 -1
  173. package/dist/val/PathVal.d.ts +25 -0
  174. package/dist/val/PathVal.js +150 -0
  175. package/dist/val/PathVal.js.map +1 -0
  176. package/dist/val/PlaceVal.d.ts +13 -0
  177. package/dist/val/PlaceVal.js +131 -0
  178. package/dist/val/PlaceVal.js.map +1 -0
  179. package/dist/val/PlusOpVal.d.ts +2 -1
  180. package/dist/val/PlusOpVal.js +61 -37
  181. package/dist/val/PlusOpVal.js.map +1 -1
  182. package/dist/val/PrefFuncVal.js.map +1 -1
  183. package/dist/val/PrefVal.d.ts +4 -2
  184. package/dist/val/PrefVal.js +223 -41
  185. package/dist/val/PrefVal.js.map +1 -1
  186. package/dist/val/RecurseVal.d.ts +19 -0
  187. package/dist/val/RecurseVal.js +217 -0
  188. package/dist/val/RecurseVal.js.map +1 -0
  189. package/dist/val/RefVal.d.ts +3 -2
  190. package/dist/val/RefVal.js +181 -66
  191. package/dist/val/RefVal.js.map +1 -1
  192. package/dist/val/ReferFuncVal.d.ts +59 -0
  193. package/dist/val/ReferFuncVal.js +604 -0
  194. package/dist/val/ReferFuncVal.js.map +1 -0
  195. package/dist/val/ScalarKindVal.d.ts +4 -3
  196. package/dist/val/ScalarKindVal.js +12 -12
  197. package/dist/val/ScalarKindVal.js.map +1 -1
  198. package/dist/val/SuperFuncVal.d.ts +4 -2
  199. package/dist/val/SuperFuncVal.js +118 -14
  200. package/dist/val/SuperFuncVal.js.map +1 -1
  201. package/dist/val/TopVal.d.ts +1 -1
  202. package/dist/val/TopVal.js.map +1 -1
  203. package/dist/val/TypeFuncVal.js.map +1 -1
  204. package/dist/val/UpperFuncVal.js.map +1 -1
  205. package/dist/val/Val.d.ts +7 -1
  206. package/dist/val/Val.js +170 -2
  207. package/dist/val/Val.js.map +1 -1
  208. package/dist/val/VarVal.js.map +1 -1
  209. package/dist/val/arith.d.ts +6 -0
  210. package/dist/val/arith.js +173 -0
  211. package/dist/val/arith.js.map +1 -0
  212. package/dist/vet.d.ts +45 -0
  213. package/dist/vet.js +802 -0
  214. package/dist/vet.js.map +1 -0
  215. package/dist/view.d.ts +85 -0
  216. package/dist/view.js +1948 -0
  217. package/dist/view.js.map +1 -0
  218. package/dist/walk.d.ts +2 -0
  219. package/dist/walk.js +91 -0
  220. package/dist/walk.js.map +1 -0
  221. package/grammar/aontu.gbnf +138 -0
  222. package/grammar/aontu.lark +118 -0
  223. package/grammar/aontu.tmLanguage.json +184 -0
  224. package/package.json +30 -8
  225. package/skill/SKILL.md +37 -0
  226. package/skill/error-codes.md +62 -0
  227. package/skill/examples.md +99 -0
  228. package/skill/grammar-card.md +56 -0
  229. package/src/agentsmd.ts +135 -0
  230. package/src/aontu.ts +166 -5
  231. package/src/cli.ts +3324 -72
  232. package/src/ctx.ts +93 -1
  233. package/src/diff.ts +198 -0
  234. package/src/err.ts +50 -8
  235. package/src/graph.ts +185 -0
  236. package/src/hcanon.ts +167 -0
  237. package/src/hints.ts +291 -8
  238. package/src/jsonschema.ts +552 -0
  239. package/src/lang.ts +1049 -110
  240. package/src/lsp.ts +351 -49
  241. package/src/mcp-server.ts +187 -0
  242. package/src/mcp.ts +1130 -0
  243. package/src/mod-tool.ts +718 -0
  244. package/src/mod.ts +453 -0
  245. package/src/patch.ts +628 -0
  246. package/src/provenance.ts +437 -0
  247. package/src/query.ts +381 -0
  248. package/src/reach.ts +213 -0
  249. package/src/relation.ts +298 -0
  250. package/src/report-sarif.ts +137 -0
  251. package/src/sig.ts +345 -0
  252. package/src/sigdecl.ts +11 -0
  253. package/src/siggate.ts +144 -0
  254. package/src/site.ts +36 -1
  255. package/src/std.ts +131 -0
  256. package/src/subsume.ts +739 -0
  257. package/src/trim.ts +195 -0
  258. package/src/tsconfig.json +10 -4
  259. package/src/type.ts +51 -2
  260. package/src/unify.ts +253 -38
  261. package/src/utility.ts +79 -1
  262. package/src/val/AggFuncVal.ts +546 -0
  263. package/src/val/ArithFuncVal.ts +108 -0
  264. package/src/val/BagVal.ts +158 -12
  265. package/src/val/CloseFuncVal.ts +9 -1
  266. package/src/val/ConjunctVal.ts +20 -0
  267. package/src/val/ConstraintVal.ts +576 -48
  268. package/src/val/ContainerKindVal.ts +158 -0
  269. package/src/val/CopyFuncVal.ts +0 -1
  270. package/src/val/Decimal.ts +15 -0
  271. package/src/val/DeprecateFuncVal.ts +84 -0
  272. package/src/val/DisjunctVal.ts +282 -59
  273. package/src/val/EachFuncVal.ts +133 -0
  274. package/src/val/ExpectVal.ts +65 -8
  275. package/src/val/FeatureVal.ts +1 -1
  276. package/src/val/FilterFuncVal.ts +154 -0
  277. package/src/val/FuncBaseVal.ts +206 -2
  278. package/src/val/GraphAtomVal.ts +264 -0
  279. package/src/val/HideFuncVal.ts +0 -2
  280. package/src/val/JunctionVal.ts +24 -1
  281. package/src/val/KeyFuncVal.ts +39 -35
  282. package/src/val/ListVal.ts +101 -15
  283. package/src/val/LowerFuncVal.ts +0 -1
  284. package/src/val/MapVal.ts +153 -14
  285. package/src/val/MatchFuncVal.ts +176 -0
  286. package/src/val/MoveFuncVal.ts +0 -2
  287. package/src/val/NilVal.ts +25 -0
  288. package/src/val/OpBaseVal.ts +20 -1
  289. package/src/val/OpenFuncVal.ts +4 -2
  290. package/src/val/PackFuncVal.ts +175 -0
  291. package/src/val/PathFuncVal.ts +107 -20
  292. package/src/val/PathVal.ts +221 -0
  293. package/src/val/PlaceVal.ts +193 -0
  294. package/src/val/PlusOpVal.ts +67 -39
  295. package/src/val/PrefFuncVal.ts +0 -1
  296. package/src/val/PrefVal.ts +251 -60
  297. package/src/val/RecurseVal.ts +285 -0
  298. package/src/val/RefVal.ts +183 -78
  299. package/src/val/ReferFuncVal.ts +732 -0
  300. package/src/val/ScalarKindVal.ts +12 -13
  301. package/src/val/SuperFuncVal.ts +137 -13
  302. package/src/val/TopVal.ts +1 -2
  303. package/src/val/TypeFuncVal.ts +0 -2
  304. package/src/val/UpperFuncVal.ts +0 -1
  305. package/src/val/Val.ts +216 -4
  306. package/src/val/VarVal.ts +0 -1
  307. package/src/val/arith.ts +319 -0
  308. package/src/vet.ts +1025 -0
  309. package/src/view.ts +2563 -0
  310. package/src/walk.ts +99 -0
@@ -57,6 +57,7 @@ import { empty } from './Val'
57
57
  import { cmpCodePoint } from '../keyorder'
58
58
 
59
59
  import { ConjunctVal } from './ConjunctVal'
60
+ import { sizingResidue } from './BagVal'
60
61
 
61
62
  import { top } from './top'
62
63
 
@@ -115,6 +116,8 @@ type ConstraintState = {
115
116
  // over the integer domain -- the count atom reuses
116
117
  // this same algebra recursively
117
118
  uniq: boolean // members must be pairwise distinct (unique())
119
+ uniqBy: string[] // ... and distinct ON EACH OF THESE KEYS
120
+ // (unique(k)), sorted and deduplicated
118
121
  musts: MustAtom[] // Band B checks, kept in written order, never simplified
119
122
  clash?: boolean // a kind disagreement inside a length() argument, recorded
120
123
  // rather than raised: the argument's own meet has no
@@ -180,6 +183,18 @@ type ConstraintState = {
180
183
  // and `test/spec/files/regex-corpus.txt` pins that the two produce
181
184
  // byte-identical output for every pattern in a generated corpus.
182
185
 
186
+ // The largest repeat count Aontu's pattern language admits. RE2 caps a
187
+ // repeat at 1000 and refuses to compile past it, where JavaScript's
188
+ // backtracking engine accepts any count -- so `re("^a{1001}$")` was
189
+ // VALID in TypeScript and an `error` verdict in Go, a cross-port
190
+ // verdict flip on a schema an agent could be handed
191
+ // (status-2026-08-21.md section 4). Under ADR-003 the host's own
192
+ // compile failure must never be what a user sees, so the bound is
193
+ // Aontu's: at most RE_REPEAT_MAX, refused identically in both ports
194
+ // before either engine compiles. Nested products beyond the cap were
195
+ // already refused, by the quantified-group rule below.
196
+ const RE_REPEAT_MAX = 1000
197
+
183
198
  // The normative expansions. These are Aontu's definitions, not either
184
199
  // host's; both hosts are rewritten to them.
185
200
  const RE_CLASS_DIGIT = '0-9'
@@ -196,6 +211,67 @@ const RE_ESCAPE_PUNCT = '\\.+*?()[]{}|^$/'
196
211
  // word boundary, and `\xHH`. Each was probed in both engines.
197
212
  const RE_ESCAPE_PASS = 'tnrfv'
198
213
 
214
+ // A counted quantifier `{n}`, `{n,}` or `{n,m}` starting at `src[at]`.
215
+ // Two rules, both about what the hosts do NOT share:
216
+ //
217
+ // - A bound above RE_REPEAT_MAX: RE2 refuses to compile it, JavaScript
218
+ // accepts it.
219
+ // - A brace that does not open a well-formed counted quantifier at
220
+ // all (`x{y}`, `a{`, `{,5}`): JavaScript compiled with the `u` flag
221
+ // makes it a SYNTAX ERROR, while RE2 reads it as a literal brace, so
222
+ // `re("^x{y}$")` was `constraint_pattern` in TypeScript and a plain
223
+ // unresolved value in Go -- the same class of verdict flip as the
224
+ // repeat cap, found by sweeping around it. A literal brace is
225
+ // written escaped (`a\\{1001\\}`), which both engines share.
226
+ function repeatWhy(src: string, at: number): [string, number] {
227
+ const bad = (what: string): [string, number] => [
228
+ 'a ' + what + ', which the two engines do not read the same way', -1]
229
+
230
+ let i = at + 1
231
+ let digits = ''
232
+ const bounds: number[] = []
233
+ let commas = 0
234
+ for (; i < src.length; i++) {
235
+ const c = src[i]
236
+ if ('0' <= c && c <= '9') {
237
+ digits += c
238
+ continue
239
+ }
240
+ if (',' === c) {
241
+ if ('' === digits || 0 < commas) {
242
+ return bad('{ that does not open a counted quantifier')
243
+ }
244
+ bounds.push(parseInt(digits, 10))
245
+ digits = ''
246
+ commas++
247
+ continue
248
+ }
249
+ if ('}' === c) {
250
+ if ('' !== digits) {
251
+ bounds.push(parseInt(digits, 10))
252
+ }
253
+ else if (0 === commas) {
254
+ return bad('{ that does not open a counted quantifier')
255
+ }
256
+ break
257
+ }
258
+ return bad('{ that does not open a counted quantifier')
259
+ }
260
+ if (i >= src.length) {
261
+ return bad('{ that does not open a counted quantifier')
262
+ }
263
+ for (const b of bounds) {
264
+ if (RE_REPEAT_MAX < b) {
265
+ return ['a repeat count above ' + RE_REPEAT_MAX +
266
+ ', which RE2 refuses to compile', -1]
267
+ }
268
+ }
269
+ // A descending range (`{5,2}`) is refused by both engines' own
270
+ // compilers, so it needs no rule here.
271
+ return ['', i]
272
+ }
273
+
274
+
199
275
  function isHexDigit(c: string | undefined): boolean {
200
276
  return null != c && (
201
277
  ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F'))
@@ -302,8 +378,16 @@ function normaliseRe(src: string): [string, string] {
302
378
  }
303
379
  }
304
380
 
381
+ // Where the counted quantifier validated below closes, so its own
382
+ // `}` is told apart from a stray one; and whether the atom just
383
+ // emitted was `^` or `$`, which cannot be quantified.
384
+ let repeatEnd = -1
385
+ let anchorPrev = false
386
+
305
387
  for (let i = 0; i < src.length; i++) {
306
388
  const c = src[i]
389
+ const afterAnchor = anchorPrev
390
+ anchorPrev = false
307
391
 
308
392
  if ('\\' === c) {
309
393
  const [emit, why, extra] = normaliseEscape(src[i + 1], src, i, inClass)
@@ -311,6 +395,17 @@ function normaliseRe(src: string): [string, string] {
311
395
  return ['', why]
312
396
  }
313
397
  out.push(emit)
398
+ // `\b` AND `\B` ARE ASSERTIONS TOO, and quantifying one is the
399
+ // same disagreement `^{1}` is: JavaScript under `u` calls it a
400
+ // syntax error, RE2 quantifies the assertion happily. The rule
401
+ // said "`\b` and `\B` quantify identically in both and are left
402
+ // alone" and that was measured wrong -- `re("\\b{1}x")` is
403
+ // `constraint_pattern` in TypeScript and an accepted schema in
404
+ // Go. Only OUTSIDE a class: inside one, `\b` is a backspace in
405
+ // both engines and repeats like any other character.
406
+ if (!inClass && ('b' === src[i + 1] || 'B' === src[i + 1])) {
407
+ anchorPrev = true
408
+ }
314
409
  i += 1 + extra
315
410
  continue
316
411
  }
@@ -387,12 +482,43 @@ function normaliseRe(src: string): [string, string] {
387
482
  }
388
483
 
389
484
  if ('*' === c || '+' === c || '?' === c || '{' === c) {
485
+ // Nothing to repeat. JavaScript under the `u` flag makes this a
486
+ // SYNTAX ERROR, where RE2 quantifies the assertion happily and
487
+ // matches, so `re("^{1}")` was `constraint_pattern` in
488
+ // TypeScript and an accepted schema in Go. The four assertions
489
+ // behave alike here -- `^`, `$`, `\b` and `\B` -- so the rule
490
+ // names all four.
491
+ if (afterAnchor) {
492
+ return ['', 'a quantifier applied to `^`, `$`, `\\b` or ' +
493
+ '`\\B`, which has nothing to repeat']
494
+ }
495
+ if ('{' === c) {
496
+ const [why, end] = repeatWhy(src, i)
497
+ if ('' !== why) {
498
+ return ['', why]
499
+ }
500
+ repeatEnd = end
501
+ }
390
502
  mark('q')
391
503
  out.push(c)
392
504
  continue
393
505
  }
394
506
 
507
+ // A `}` that closes no counted quantifier: JavaScript under `u`
508
+ // refuses it as a lone quantifier bracket, RE2 reads it as a
509
+ // literal. Written escaped inside a class (`[}]`) it is a literal
510
+ // in both, which is the spelling that survives.
511
+ if ('}' === c) {
512
+ if (i !== repeatEnd) {
513
+ return ['', 'a `}` that closes no counted quantifier, which ' +
514
+ 'the two engines do not read the same way']
515
+ }
516
+ out.push(c)
517
+ continue
518
+ }
519
+
395
520
  out.push(c)
521
+ anchorPrev = ('^' === c || '$' === c)
396
522
  }
397
523
 
398
524
  if (inClass) {
@@ -418,6 +544,16 @@ function stringLeaf(v: any): boolean {
418
544
  return true === v?.isScalar && 'string' === typeof v.peg && v.isString
419
545
  }
420
546
 
547
+ // The VALUE-admission widening of stringLeaf: a path value is a string
548
+ // with more structure (path <: string, ADR-016), so the string-domain
549
+ // constraints admit one -- `refer() & re("auth$")` must still check a
550
+ // link whose value is now a path. The ARGUMENT sites keep the strict
551
+ // test: a pattern, a bound or an exclusion is spelled with a plain
552
+ // string, never a path.
553
+ function stringishLeaf(v: any): boolean {
554
+ return stringLeaf(v) || (true === v?.isScalar && true === v.isPath)
555
+ }
556
+
421
557
 
422
558
  // Scalar identity: leaf AND value, the lattice's own rule (1 and 1.0
423
559
  // are different scalars). Exact numeric comparison decides the value
@@ -426,6 +562,12 @@ function sameScalar(a: any, b: any): boolean {
426
562
  if (numericLeaf(a) && numericLeaf(b)) {
427
563
  return towerRank(a) === towerRank(b) && 0 === cmpNumeric(a, b)
428
564
  }
565
+ if (true === a?.isPath || true === b?.isPath) {
566
+ // Path identity is kind AND spelling (ADR-016): `neq(path($.x))`
567
+ // excludes exactly that address, and never a plain string that
568
+ // happens to spell it.
569
+ return true === a?.isPath && true === b?.isPath && a.peg === b.peg
570
+ }
429
571
  if (stringLeaf(a) && stringLeaf(b)) {
430
572
  return a.peg === b.peg
431
573
  }
@@ -480,6 +622,7 @@ class ConstraintVal extends FeatureVal {
480
622
  res: ReAtom[] = []
481
623
  count?: ConstraintState
482
624
  uniq = false
625
+ uniqBy: string[] = []
483
626
  musts: MustAtom[] = []
484
627
  // An atom whose arguments have not settled yet (G1 phase 4). Held
485
628
  // until unify has a ctx to resolve them through; never present on a
@@ -506,6 +649,7 @@ class ConstraintVal extends FeatureVal {
506
649
  this.res = spec.state.res ?? []
507
650
  this.count = spec.state.count
508
651
  this.uniq = spec.state.uniq ?? false
652
+ this.uniqBy = spec.state.uniqBy ?? []
509
653
  this.musts = spec.state.musts ?? []
510
654
  this.invalid = spec.state.invalid
511
655
  }
@@ -517,7 +661,14 @@ class ConstraintVal extends FeatureVal {
517
661
  // in unify, where there is a ctx to resolve through, and the
518
662
  // residual is built from the settled arguments. Only a settled
519
663
  // argument of the wrong shape is an `invalid-arg`.
520
- if (args.some((a: any) => true !== a?.done)) {
664
+ // The effectful-argument refusal happens HERE, on the written
665
+ // form, and not in fromAtom: settling is what runs the effect, so
666
+ // by the time fromAtom sees a settled `move($.b)` the move has
667
+ // already happened and the argument is just its result.
668
+ if ('must' === spec.atom && args.some((a: any) => holdsMove(a))) {
669
+ this.invalid = 'invalid-arg'
670
+ }
671
+ else if (args.some((a: any) => true !== a?.done)) {
521
672
  this.pending = { atom: spec.atom, args }
522
673
  }
523
674
  else {
@@ -525,7 +676,8 @@ class ConstraintVal extends FeatureVal {
525
676
  }
526
677
  }
527
678
 
528
- if (null != this.count || this.uniq || 0 < this.musts.length ||
679
+ if (null != this.count || this.uniq || 0 < this.uniqBy.length ||
680
+ 0 < this.musts.length ||
529
681
  (null != this.pending && lateAtom(this.pending.atom))) {
530
682
  this.cjo = LATE_CJO
531
683
  }
@@ -546,19 +698,32 @@ class ConstraintVal extends FeatureVal {
546
698
  // SETTLED — the constructor routes an unsettled one to `pending` —
547
699
  // so a shape this cannot use is a genuine `invalid-arg`, not a
548
700
  // not-yet.
549
- private fromAtom(atom: string, args: any[]) {
550
- // Mark the residual invalid and report so (a plain boolean, so no
551
- // void value is consumed by the callers' `return` statements).
552
- const bad = (why: string): boolean => {
701
+ private fromAtom(atom: string, args: any[]): void {
702
+ // Mark the residual invalid. It answers NOTHING: every caller
703
+ // spells its exit `return bad(...)` and none reads a value, so a
704
+ // return type would be a value nobody consumes -- and a routine
705
+ // that answers on some paths and not others is the shape a reader
706
+ // (and a static analyser) has to stop and check.
707
+ const bad = (why: string): void => {
553
708
  this.invalid = why
554
- return true
555
709
  }
556
710
 
557
- // `unique()` takes no argument: it is a property of the container,
558
- // not a comparison against a value. Arity is checked at parse, so a
559
- // written argument never reaches here.
711
+ // `unique()` is a property of the container -- not a comparison
712
+ // against a value -- so with no argument it says the members are
713
+ // pairwise DISTINCT. THE ONE ARGUMENT IS A PROJECTOR (the review's
714
+ // finding I: "unique()-by-field is reserved but absent", and the
715
+ // arity was reserved for exactly this): `unique(port)` says no two
716
+ // members share a `port`, which is how "no two services share a
717
+ // port" and "event ids are unique" are said.
560
718
  if ('unique' === atom) {
561
- this.uniq = true
719
+ if (0 === args.length) {
720
+ this.uniq = true
721
+ return
722
+ }
723
+ if (1 !== args.length || !stringLeaf(args[0])) {
724
+ return bad('invalid-arg')
725
+ }
726
+ this.uniqBy = [args[0].peg]
562
727
  return
563
728
  }
564
729
 
@@ -583,6 +748,9 @@ class ConstraintVal extends FeatureVal {
583
748
  if (holdsNil(args[0])) {
584
749
  return bad('invalid-arg')
585
750
  }
751
+ // (An effectful argument is refused at construction, in the
752
+ // constructor: by the time this arm sees a settled `move($.b)`
753
+ // the move has already run.)
586
754
  this.musts = [{ v: args[0], msg: args[1] }]
587
755
  return
588
756
  }
@@ -593,7 +761,7 @@ class ConstraintVal extends FeatureVal {
593
761
  }
594
762
  let domain: 'number' | 'string' | undefined = undefined
595
763
  for (const a of args) {
596
- const d = numericLeaf(a) ? 'number' : stringLeaf(a) ? 'string' : undefined
764
+ const d = numericLeaf(a) ? 'number' : stringishLeaf(a) ? 'string' : undefined
597
765
  if (null == d || (null != domain && d !== domain)) {
598
766
  return bad('invalid-arg')
599
767
  }
@@ -676,7 +844,7 @@ class ConstraintVal extends FeatureVal {
676
844
  return
677
845
  }
678
846
 
679
- const domain = numericLeaf(a) ? 'number' : stringLeaf(a) ? 'string' : undefined
847
+ const domain = numericLeaf(a) ? 'number' : stringishLeaf(a) ? 'string' : undefined
680
848
  if (null == domain) {
681
849
  return bad('invalid-arg')
682
850
  }
@@ -700,7 +868,20 @@ class ConstraintVal extends FeatureVal {
700
868
  // residual is stable and the ladder is total.
701
869
  let out: Val
702
870
 
703
- if (null != this.pending) {
871
+ // A rel residual or a graph atom HOLDS constraints for the value
872
+ // they stand for (RELATIONS P1/P2): hand the drive over before
873
+ // any settling, exactly as a container does, so `rel(t) & re(x)`
874
+ // reads the same in either order. Reached through the INCLUDE
875
+ // flow (a loaded schema's conjunct re-drives with the constraint
876
+ // on the left) -- inline documents route rel/atom peers through
877
+ // unite's b-drives first, which is why use-cases/12-relations
878
+ // refused without this arm while every inline row stayed green;
879
+ // pinned by constraint-hands-drive-to-rel-and-atom.
880
+ if (true === (peer as any)?.isRel
881
+ || true === (peer as any)?.isGraphAtom) {
882
+ out = (peer as any).unify(this, ctx)
883
+ }
884
+ else if (null != this.pending) {
704
885
  out = this.settle(peer, ctx)
705
886
  }
706
887
  else if (null != this.invalid) {
@@ -801,9 +982,9 @@ class ConstraintVal extends FeatureVal {
801
982
  if (null == peer || (peer as any).isTop) {
802
983
  return again
803
984
  }
804
- if ((peer as any).isNil) {
805
- return peer
806
- }
985
+ // No nil-peer arm: `unite` returns a nil operand before dispatching
986
+ // to any Val's unify (ts/src/unify.ts), so a nil never reaches here
987
+ // — and were one to, the conjunct below folds to it unchanged.
807
988
  return new ConjunctVal({ peg: [again, peer] }, ctx)
808
989
  }
809
990
 
@@ -811,8 +992,9 @@ class ConstraintVal extends FeatureVal {
811
992
  // Membership: the peer scalar passes every part of the residual, or
812
993
  // the whole meet is a located conflict.
813
994
  private admit(peer: any, ctx: AontuContext): Val {
814
- // No scalar has members, so a `unique()` residual admits none.
815
- if (this.uniq) {
995
+ // No scalar has members, so a `unique()` residual admits none --
996
+ // and neither does a `unique(k)` one, for the same reason.
997
+ if (this.uniq || 0 < this.uniqBy.length) {
816
998
  return this.fail(ctx, peer)
817
999
  }
818
1000
  if (!stateAdmits(this, peer)) {
@@ -822,14 +1004,17 @@ class ConstraintVal extends FeatureVal {
822
1004
  // Only a string among the scalars has a length, and it is counted
823
1005
  // in CODE POINTS -- not UTF-16 units (this host's native count)
824
1006
  // and not bytes (Go's). Iterating a string yields code points.
825
- if (!stringLeaf(peer)) {
1007
+ if (!stringishLeaf(peer)) {
826
1008
  return this.fail(ctx, peer)
827
1009
  }
828
1010
  if (!stateAdmits(this.count, countVal([...peer.peg].length))) {
829
1011
  return this.fail(ctx, peer)
830
1012
  }
831
1013
  }
832
- const bad = this.checkMusts(peer, ctx)
1014
+ // A SCALAR HAS NO MEMBERS to accumulate, so its musts are decided
1015
+ // here and never residuate: the final reading is the only reading
1016
+ // a scalar has.
1017
+ const bad = this.checkMusts(peer, ctx, true)
833
1018
  if (null != bad) {
834
1019
  return bad
835
1020
  }
@@ -845,10 +1030,30 @@ class ConstraintVal extends FeatureVal {
845
1030
  // The trial runs in an isolated collect context so a failing check
846
1031
  // leaves nothing on the caller's error list — only the located nil
847
1032
  // this returns, carrying the author's own message.
848
- private checkMusts(peer: any, ctx: AontuContext): Val | undefined {
1033
+ // `final` is the generation-time reading (see settleContainer): a
1034
+ // must whose own check is a SIZING atom inherits that atom's
1035
+ // provisionality, because `must(length(2), …)` against a map that may
1036
+ // still gain members has not failed yet -- it has not been answered.
1037
+ // Without this the must would decide against whatever the container
1038
+ // held when it first settled, which is the very defect the sizing
1039
+ // atoms were fixed for (use-cases/BUGS.md §16, §17).
1040
+ private checkMusts(
1041
+ peer: any, ctx: AontuContext, final?: boolean): Val | undefined {
849
1042
  for (const m of this.musts) {
850
1043
  const trial = ctx.clone({ err: [], collect: true })
851
- const got = unite(trial, m.v.clone(trial), peer.clone(trial), 'must')
1044
+ let got: any = unite(trial, m.v.clone(trial), peer.clone(trial), 'must')
1045
+ // A must whose check is a SIZING atom answers a residue rather
1046
+ // than a verdict, because the atom is waiting for members that
1047
+ // may still arrive. Before generation that residue IS the answer
1048
+ // -- the must has not failed, it has not been asked yet -- and at
1049
+ // generation it is settled, because nothing more can arrive.
1050
+ const residue = sizingResidue(got)
1051
+ if (undefined !== residue) {
1052
+ if (true !== final) {
1053
+ continue
1054
+ }
1055
+ got = residue.con.settleContainer(residue.bag, trial)
1056
+ }
852
1057
  if (true === (got as any)?.isNil || 0 < trial.err.length) {
853
1058
  return makeNilErr(ctx, 'must', this, peer, undefined, {
854
1059
  message: m.msg.peg,
@@ -861,6 +1066,16 @@ class ConstraintVal extends FeatureVal {
861
1066
  }
862
1067
 
863
1068
 
1069
+ // THE FINAL READING of a sizing atom over a container: the same
1070
+ // checks admitContainer makes, with the provisional ones taken as
1071
+ // decided. Called from ConjunctVal.gen, which is where no more
1072
+ // members can arrive. Returns the container (to generate) or the
1073
+ // constraint's own nil (to report).
1074
+ settleContainer(peer: any, ctx: AontuContext): Val {
1075
+ return this.admitContainer(peer, ctx, true)
1076
+ }
1077
+
1078
+
864
1079
  // Membership for a container peer. Only the SIZING atoms have anything
865
1080
  // to say about a map or a list; every other atom is scalar-domain and
866
1081
  // refuses one.
@@ -872,7 +1087,8 @@ class ConstraintVal extends FeatureVal {
872
1087
  // isolated collect context so nothing leaks into the caller's errors.
873
1088
  // A mirror would be a second copy of a filter that has already grown
874
1089
  // subtle, free to drift from it; asking is correct by construction.
875
- private admitContainer(peer: any, ctx: AontuContext): Val {
1090
+ private admitContainer(
1091
+ peer: any, ctx: AontuContext, final?: boolean): Val {
876
1092
  // A scalar-domain residual has no reading over a container.
877
1093
  if (null != this.domain) {
878
1094
  return this.fail(ctx, peer)
@@ -885,24 +1101,81 @@ class ConstraintVal extends FeatureVal {
885
1101
  return new ConjunctVal({ peg: [this, peer] }, ctx)
886
1102
  }
887
1103
 
888
- const bad = this.checkMusts(peer, ctx)
1104
+ const bad = this.checkMusts(peer, ctx, final)
889
1105
  if (null != bad) {
890
1106
  return bad
891
1107
  }
892
1108
 
893
- if (!this.uniq && null == this.count) {
894
- return peer
1109
+ // A MUST OVER A CONTAINER RESIDUATES with the atoms, for the same
1110
+ // reason (use-cases/BUGS.md §17): `must({t: max(60)}, …)` beside a
1111
+ // `{t: integer}` schema was answered against the schema layer
1112
+ // ALONE, and discharged before the data it was written to judge
1113
+ // ever arrived. It is decided at generation, where the value is
1114
+ // whole -- unless there is nothing else on the constraint AND the
1115
+ // reading is already final.
1116
+ if (!this.uniq && 0 === this.uniqBy.length && null == this.count) {
1117
+ if (true === final || 0 === this.musts.length) {
1118
+ return peer
1119
+ }
1120
+ return this.hold(peer, ctx)
895
1121
  }
896
1122
 
897
1123
  const members = emittedMembers(peer, ctx)
898
1124
  if (null == members) {
899
- // The container cannot generate at all; its own error is the one
900
- // worth reporting, so pass it through untouched.
901
- return peer
902
- }
903
-
904
- if (null != this.count && !stateAdmits(this.count, countVal(members.length))) {
905
- return this.fail(ctx, peer)
1125
+ // NOT COUNTABLE YET IS NOT A PASS. A container that cannot
1126
+ // generate cannot be counted, and a SCHEMA is exactly that: the
1127
+ // members of `{a: integer}` are types, so nothing is emitted and
1128
+ // nothing can be counted. Discharging the atom here was the §16
1129
+ // defect wearing its other face -- `length(min(2)) & {a: integer}`
1130
+ // dropped its bound while still alone, so the data half of a
1131
+ // `vet` meet was never measured at all and short data vetted
1132
+ // clean against a bound the evaluator enforces.
1133
+ //
1134
+ // At generation the reading IS final: a container that still
1135
+ // cannot generate has its own error, and that error is the one
1136
+ // worth reporting, so it passes through untouched.
1137
+ if (true === final) {
1138
+ return peer
1139
+ }
1140
+ return this.hold(peer, ctx)
1141
+ }
1142
+
1143
+ // MONOTONE READINGS ONLY (the review's finding C, use-cases/BUGS.md
1144
+ // §16). Members ACCUMULATE under unification -- a meet adds keys and
1145
+ // never removes them -- so a settled container is settled ON ITS
1146
+ // OWN and not against every value that may still contribute. The
1147
+ // schema half of a `vet` meet, a document that is about to receive
1148
+ // an `@` include, a bag a later `pack` will fill: each settles, and
1149
+ // an atom that decided there decided too early.
1150
+ //
1151
+ // So a verdict is taken only when MORE MEMBERS CANNOT CHANGE IT:
1152
+ // - an upper bound VIOLATED is permanent -> refuse now;
1153
+ // satisfied is provisional -> keep the atom, re-check later.
1154
+ // - a lower bound SATISFIED is permanent -> that part may go;
1155
+ // violated is provisional -> keep the atom (which is why
1156
+ // `length(min(1))` beside a template no longer kills the schema
1157
+ // it was written for).
1158
+ // - a DUPLICATE is permanent -> refuse now; distinctness is
1159
+ // provisional -> keep the atom.
1160
+ // Anything provisional residuates, exactly as an unsettled
1161
+ // container does above, and the two spellings then agree.
1162
+ const count = null == this.count ? undefined : this.count
1163
+ const n = members.length
1164
+ if (null != count) {
1165
+ if (null != count.hi && !stateAdmits({ ...count, lo: undefined },
1166
+ countVal(n))) {
1167
+ return this.fail(ctx, peer)
1168
+ }
1169
+ if (0 < count.neqs.length && !stateAdmits(
1170
+ { ...count, lo: undefined, hi: undefined }, countVal(n))) {
1171
+ return this.fail(ctx, peer)
1172
+ }
1173
+ // The provisional half, decided only when nothing more can
1174
+ // arrive: a lower bound still short is a refusal at generation
1175
+ // and a residue before it.
1176
+ if (true === final && !stateAdmits(count, countVal(n))) {
1177
+ return this.fail(ctx, peer)
1178
+ }
906
1179
  }
907
1180
 
908
1181
  if (this.uniq) {
@@ -921,7 +1194,68 @@ class ConstraintVal extends FeatureVal {
921
1194
  }
922
1195
  }
923
1196
 
924
- return peer
1197
+ // ... and the same test per PROJECTED key. A member that has no such
1198
+ // key FAILS the constraint rather than being skipped: distinctness
1199
+ // it cannot be shown to have is distinctness it does not have, and
1200
+ // skipping would let one keyless record hide a duplicate.
1201
+ for (const field of this.uniqBy) {
1202
+ const seen = new Set<string>()
1203
+ for (const m of members) {
1204
+ const at: any = true === (m as any).isMap ?
1205
+ (m as any).peg[field] : undefined
1206
+ if (null == at) {
1207
+ return this.fail(ctx, peer)
1208
+ }
1209
+ const key = at.canon
1210
+ if (seen.has(key)) {
1211
+ return this.fail(ctx, peer)
1212
+ }
1213
+ seen.add(key)
1214
+ }
1215
+ }
1216
+
1217
+ // WHAT IS LEFT IS PROVISIONAL, so the atom stays on the value. A
1218
+ // lower bound already met is the one reading that cannot be undone,
1219
+ // and an atom holding nothing else is spent: that is when it goes.
1220
+ const spent = true === final || (0 === this.musts.length &&
1221
+ !this.uniq && 0 === this.uniqBy.length &&
1222
+ (null == count ||
1223
+ (null == count.hi && 0 === count.neqs.length &&
1224
+ stateAdmits(count, countVal(n)))))
1225
+ if (spent) {
1226
+ return peer
1227
+ }
1228
+ // DONE, unlike the unsettled case above. The container HAS settled;
1229
+ // the atom is kept only because a LATER value could still add
1230
+ // members, and a residue that reported itself unresolved would
1231
+ // leave every enclosing value unresolved with it -- a `type()`
1232
+ // waiting on its argument for ever, and use case 09's whole
1233
+ // registry with it.
1234
+ // The ATOM is done too, not just the conjunct. An earlier pass may
1235
+ // have set `dc = 0` on the unsettled branch above, and a conjunct
1236
+ // recomputes its own doneness from its terms on every later meet --
1237
+ // so a stale 0 here would drag the residue, and every value holding
1238
+ // it, back to unresolved for ever.
1239
+ return this.hold(peer, ctx)
1240
+ }
1241
+
1242
+
1243
+ // The atom kept on a value whose reading is still provisional.
1244
+ // DONE, unlike the unsettled container above: the value HAS settled;
1245
+ // the atom is kept only because a LATER value could still add
1246
+ // members, and a residue that reported itself unresolved would leave
1247
+ // every enclosing value unresolved with it -- a `type()` waiting on
1248
+ // its argument for ever, and use case 09's whole registry with it.
1249
+ // The ATOM is done too, not just the conjunct. An earlier pass may
1250
+ // have set `dc = 0` on the unsettled branch, and a conjunct
1251
+ // recomputes its own doneness from its terms on every later meet --
1252
+ // so a stale 0 here would drag the residue, and every value holding
1253
+ // it, back to unresolved for ever.
1254
+ private hold(peer: any, ctx: AontuContext): Val {
1255
+ this.dc = DONE
1256
+ const held: any = new ConjunctVal({ peg: [this, peer] }, ctx)
1257
+ held.dc = DONE
1258
+ return held
925
1259
  }
926
1260
 
927
1261
 
@@ -994,6 +1328,11 @@ class ConstraintVal extends FeatureVal {
994
1328
  null == peer.count ? this.count : meetCount(this.count, peer.count)
995
1329
  // `unique()` is idempotent: two of them are one.
996
1330
  merged.uniq = this.uniq || peer.uniq
1331
+ // `unique(a) & unique(b)` is BOTH, not the later one: each names a
1332
+ // key on which the members must differ, and dropping either would
1333
+ // silently weaken the constraint. Sorted and deduplicated, so the
1334
+ // meet is commutative and `unique(a) & unique(a)` is one atom.
1335
+ merged.uniqBy = [...new Set([...this.uniqBy, ...peer.uniqBy])].sort()
997
1336
  // Band B checks accumulate in written order and are never merged,
998
1337
  // deduplicated or reordered: each carries its own author message,
999
1338
  // and two checks with the same shape may still say different things.
@@ -1042,6 +1381,7 @@ class ConstraintVal extends FeatureVal {
1042
1381
  res: [...this.res],
1043
1382
  count: this.count,
1044
1383
  uniq: this.uniq,
1384
+ uniqBy: [...this.uniqBy],
1045
1385
  musts: [...this.musts],
1046
1386
  invalid: this.invalid,
1047
1387
  }
@@ -1061,6 +1401,7 @@ class ConstraintVal extends FeatureVal {
1061
1401
  out.res = [...this.res]
1062
1402
  out.count = this.count
1063
1403
  out.uniq = this.uniq
1404
+ out.uniqBy = [...this.uniqBy]
1064
1405
  out.musts = [...this.musts]
1065
1406
  out.pending = this.pending
1066
1407
  out.cjo = this.cjo
@@ -1137,10 +1478,11 @@ function dedupSorted(domain: 'number' | 'string', neqs: any[]): any[] {
1137
1478
  // Is this value, or anything inside it, a nil? A written argument that
1138
1479
  // holds one can never be satisfied, so the atom refuses it as an
1139
1480
  // argument rather than reporting a mystery failure against every peer.
1481
+ // Every caller passes a Val: `args[0]` comes from atomArgs, and the
1482
+ // walk below descends only into container pegs, which hold Vals. The
1483
+ // null/non-Val guard this function used to open with was therefore
1484
+ // unreachable, and the coverage gate said so.
1140
1485
  function holdsNil(v: any): boolean {
1141
- if (null == v || true !== v.isVal) {
1142
- return false
1143
- }
1144
1486
  if (true === v.isNil) {
1145
1487
  return true
1146
1488
  }
@@ -1159,6 +1501,31 @@ function holdsNil(v: any): boolean {
1159
1501
  }
1160
1502
 
1161
1503
 
1504
+ // Whether a value, or anything inside it, is an effectful call — one
1505
+ // whose evaluation changes a node OTHER than the one being computed.
1506
+ // `move()` is the only builtin that does: it hides its resolution
1507
+ // target in place. Band B refuses one as an argument, because settling
1508
+ // it runs the effect against the live root before `must`'s trial clone
1509
+ // is taken, and a check that mutates cannot be report-only.
1510
+ function holdsMove(v: any): boolean {
1511
+ if (true === v.isFunc && 'move' === v.funcname?.()) {
1512
+ return true
1513
+ }
1514
+ const peg = v.peg
1515
+ if (Array.isArray(peg)) {
1516
+ return peg.some((c: any) => holdsMove(c))
1517
+ }
1518
+ if (null != peg && 'object' === typeof peg) {
1519
+ for (const k in peg) {
1520
+ if (holdsMove(peg[k])) {
1521
+ return true
1522
+ }
1523
+ }
1524
+ }
1525
+ return false
1526
+ }
1527
+
1528
+
1162
1529
  // The written arguments of an atom, flattened.
1163
1530
  //
1164
1531
  // A multi-argument call arrives from the func-paren grammar as ONE
@@ -1217,6 +1584,12 @@ function canonState(s: ConstraintState): string {
1217
1584
  if (s.uniq) {
1218
1585
  parts.push('unique()')
1219
1586
  }
1587
+ // After the bare atom, and sorted: canon is a normal form, so two
1588
+ // documents writing the same keys in different orders must render
1589
+ // the same string.
1590
+ for (const key of s.uniqBy) {
1591
+ parts.push('unique(' + JSON.stringify(key) + ')')
1592
+ }
1220
1593
  for (const m of s.musts) {
1221
1594
  parts.push('must(' + m.v.canon + ',' + m.msg.canon + ')')
1222
1595
  }
@@ -1228,13 +1601,145 @@ function canonState(s: ConstraintState): string {
1228
1601
  }
1229
1602
 
1230
1603
 
1604
+ // SUBSUMPTION over two residuals (G3, the query built on the table in
1605
+ // docs/reference-language.md "Subsumption"): does every value `s`
1606
+ // admits pass `g` too? Implemented here because the compare machinery
1607
+ // (cmpVal, sameScalar, the recursive count state) is this module's.
1608
+ //
1609
+ // Three-valued: true / false / 'undecided'. `must` on the GENERAL side
1610
+ // is undecided by construction (a Band B predicate is opaque — G3 maps
1611
+ // the table's "never" to the query's honest sub_evaluate_only); every
1612
+ // other approximation folds toward false, the safe direction.
1613
+ function constraintStateSubsumes(
1614
+ g: {
1615
+ domain?: 'number' | 'string', kind?: any, lo?: Bound, hi?: Bound,
1616
+ neqs: any[], res: ReAtom[], count?: ConstraintState, uniq: boolean,
1617
+ uniqBy: string[],
1618
+ musts: MustAtom[],
1619
+ },
1620
+ s: {
1621
+ domain?: 'number' | 'string', kind?: any, lo?: Bound, hi?: Bound,
1622
+ neqs: any[], res: ReAtom[], count?: ConstraintState, uniq: boolean,
1623
+ uniqBy: string[],
1624
+ musts: MustAtom[],
1625
+ },
1626
+ ): boolean | 'undecided' {
1627
+ // A Band B predicate on the general side makes its admitted set
1628
+ // unknowable; an extra `must` on the SPECIFIC side only narrows it
1629
+ // and is ignored.
1630
+ if (0 < g.musts.length) {
1631
+ return 'undecided'
1632
+ }
1633
+
1634
+ // Domains must agree where both constrain one; a sizing-only residual
1635
+ // has no domain and passes this gate.
1636
+ if (null != g.domain && g.domain !== s.domain) {
1637
+ return false
1638
+ }
1639
+
1640
+ // A general leaf restriction requires the same leaf on the specific
1641
+ // side (leaves are disjoint; an unrestricted specific admits other
1642
+ // leaves the general refuses).
1643
+ if (null != g.kind && g.kind !== s.kind) {
1644
+ return false
1645
+ }
1646
+
1647
+ // Interval containment: the general's endpoints at or beyond the
1648
+ // specific's, and where they coincide the general's may not be the
1649
+ // open one.
1650
+ const d = g.domain ?? s.domain
1651
+ if (null != g.lo) {
1652
+ if (null == s.lo || null == d) {
1653
+ return false
1654
+ }
1655
+ const c = cmpVal(d, g.lo.v, s.lo.v)
1656
+ if (0 < c || (0 === c && g.lo.open && !s.lo.open)) {
1657
+ return false
1658
+ }
1659
+ }
1660
+ if (null != g.hi) {
1661
+ if (null == s.hi || null == d) {
1662
+ return false
1663
+ }
1664
+ const c = cmpVal(d, g.hi.v, s.hi.v)
1665
+ if (c < 0 || (0 === c && g.hi.open && !s.hi.open)) {
1666
+ return false
1667
+ }
1668
+ }
1669
+
1670
+ // Excluding FEWER values is more general: every general exclusion
1671
+ // must be excluded by the specific too.
1672
+ for (const n of g.neqs) {
1673
+ if (!s.neqs.some((m: any) => sameScalar(n, m))) {
1674
+ return false
1675
+ }
1676
+ }
1677
+
1678
+ // Patterns compare as TEXT sets (the sanctioned approximation:
1679
+ // deciding regex containment is what this algebra refuses to do).
1680
+ for (const r of g.res) {
1681
+ if (!s.res.some((q: ReAtom) => q.src === r.src)) {
1682
+ return false
1683
+ }
1684
+ }
1685
+
1686
+ // `unique()` subsumes only itself on that axis: a general uniqueness
1687
+ // demand admits no container the unconstrained specific also admits.
1688
+ // ... and a general `unique(k)` needs the same key on the specific
1689
+ // side: distinctness on `port` says nothing about distinctness on
1690
+ // `name`.
1691
+ if (g.uniqBy.some((k: string) => !s.uniqBy.includes(k))) {
1692
+ return false
1693
+ }
1694
+ if (g.uniq && !s.uniq) {
1695
+ return false
1696
+ }
1697
+
1698
+ // The count atom reuses this same table over the integer domain.
1699
+ if (null != g.count) {
1700
+ if (null == s.count) {
1701
+ return false
1702
+ }
1703
+ return constraintStateSubsumes(g.count, s.count)
1704
+ }
1705
+
1706
+ return true
1707
+ }
1708
+
1709
+
1710
+ // The query surface for ts/src/subsume.ts: residual-vs-residual, and
1711
+ // residual-vs-scalar membership (with `must` and `unique` making the
1712
+ // scalar case undecided/false exactly as the meet would).
1713
+ function constraintSubsumesConstraint(
1714
+ g: ConstraintVal, s: ConstraintVal): boolean | 'undecided' {
1715
+ return constraintStateSubsumes(g as any, s as any)
1716
+ }
1717
+
1718
+ function constraintAdmitsScalar(
1719
+ g: ConstraintVal, scalar: any): boolean | 'undecided' {
1720
+ if (0 < g.musts.length) {
1721
+ return 'undecided'
1722
+ }
1723
+ if (g.uniq || 0 < g.uniqBy.length) {
1724
+ return false
1725
+ }
1726
+ // A count residual admits by LENGTH, which is the meet's business;
1727
+ // the query answers false (the safe direction) rather than
1728
+ // reimplementing the sizing walk here.
1729
+ if (null != (g as any).count) {
1730
+ return false
1731
+ }
1732
+ return stateAdmits(g as any, scalar)
1733
+ }
1734
+
1735
+
1231
1736
  // Does this residual's ORDER and MEMBERSHIP part admit the scalar? The
1232
1737
  // sizing atoms are deliberately not consulted: `admit` applies them to
1233
1738
  // the peer's length, and the count check applies this same function to
1234
1739
  // the count.
1235
1740
  function stateAdmits(s: ConstraintState, peer: any): boolean {
1236
1741
  const domainOf = numericLeaf(peer) ? 'number' :
1237
- stringLeaf(peer) ? 'string' : undefined
1742
+ stringishLeaf(peer) ? 'string' : undefined
1238
1743
 
1239
1744
  if (null == s.domain) {
1240
1745
  // A sizing residual has no domain, and admits any scalar the sizing
@@ -1345,7 +1850,8 @@ function stateEmpty(s: ConstraintState): boolean {
1345
1850
  // members, so `integer & length(3)` and `min(2) & unique()` admit
1346
1851
  // nothing. Uniqueness over the string domain is empty for the same
1347
1852
  // reason -- a string's members are not values the algebra compares.
1348
- if ('number' === d && (null != s.count || s.uniq)) {
1853
+ if ('number' === d && (null != s.count || s.uniq ||
1854
+ 0 < s.uniqBy.length)) {
1349
1855
  return true
1350
1856
  }
1351
1857
  if ('string' === d && s.uniq) {
@@ -1374,7 +1880,9 @@ function countBase(): ConstraintState {
1374
1880
  neqs: [],
1375
1881
  res: [],
1376
1882
  musts: [],
1883
+ // A COUNT is a number, and a number has no members to be distinct.
1377
1884
  uniq: false,
1885
+ uniqBy: [],
1378
1886
  }
1379
1887
  }
1380
1888
 
@@ -1395,13 +1903,16 @@ function countVal(n: number): any {
1395
1903
  function meetCount(a: ConstraintState, b: ConstraintState): ConstraintState {
1396
1904
  return {
1397
1905
  domain: 'number',
1398
- kind: a.kind ?? b.kind,
1906
+ // `a` is always a countBase()-seeded residual, so its kind is
1907
+ // always Integer — there is no kindless side to fall back from.
1908
+ kind: a.kind,
1399
1909
  lo: tighter('number', a.lo, b.lo, true),
1400
1910
  hi: tighter('number', a.hi, b.hi, false),
1401
1911
  neqs: dedupSorted('number', [...a.neqs, ...b.neqs]),
1402
1912
  res: [],
1403
1913
  musts: [],
1404
1914
  uniq: false,
1915
+ uniqBy: [],
1405
1916
  clash: true === a.clash || true === b.clash ||
1406
1917
  (null != a.kind && null != b.kind && a.kind !== b.kind),
1407
1918
  }
@@ -1426,7 +1937,7 @@ function countArgState(arg: any): ConstraintState | undefined {
1426
1937
  domain: 'number',
1427
1938
  lo: { v: arg, open: false },
1428
1939
  hi: { v: arg, open: false },
1429
- neqs: [], res: [], musts: [], uniq: false,
1940
+ neqs: [], res: [], musts: [], uniq: false, uniqBy: [],
1430
1941
  }
1431
1942
  }
1432
1943
 
@@ -1434,7 +1945,8 @@ function countArgState(arg: any): ConstraintState | undefined {
1434
1945
  const c = arg as ConstraintVal
1435
1946
  // A pattern, a sizing atom or a string bound inside a count is not
1436
1947
  // a count constraint at all, and neither is a broken one.
1437
- if (null != c.invalid || 0 < c.res.length || c.uniq || null != c.count ||
1948
+ if (null != c.invalid || 0 < c.res.length || c.uniq ||
1949
+ 0 < c.uniqBy.length || null != c.count ||
1438
1950
  'number' !== c.domain) {
1439
1951
  return undefined
1440
1952
  }
@@ -1444,18 +1956,24 @@ function countArgState(arg: any): ConstraintState | undefined {
1444
1956
  lo: c.lo,
1445
1957
  hi: c.hi,
1446
1958
  neqs: [...c.neqs],
1447
- res: [], musts: [], uniq: false,
1959
+ res: [], musts: [], uniq: false, uniqBy: [],
1448
1960
  }
1449
1961
  }
1450
1962
 
1451
1963
  if (true === arg?.isScalarKind) {
1452
1964
  const marker = arg.peg
1453
1965
  if (Number === marker) {
1454
- return { domain: 'number', neqs: [], res: [], musts: [], uniq: false }
1966
+ return {
1967
+ domain: 'number', neqs: [], res: [], musts: [],
1968
+ uniq: false, uniqBy: [],
1969
+ }
1455
1970
  }
1456
1971
  if (Integer === marker || Float === marker ||
1457
1972
  BigInteger === marker || BigDecimal === marker) {
1458
- return { domain: 'number', kind: marker, neqs: [], res: [], musts: [], uniq: false }
1973
+ return {
1974
+ domain: 'number', kind: marker, neqs: [], res: [], musts: [],
1975
+ uniq: false, uniqBy: [],
1976
+ }
1459
1977
  }
1460
1978
  return undefined
1461
1979
  }
@@ -1487,7 +2005,12 @@ function genable(child: any): boolean {
1487
2005
  return true === child.isScalar || true === child.isMap ||
1488
2006
  true === child.isList || true === child.isPref ||
1489
2007
  true === child.isRef || true === child.isDisjunct ||
1490
- true === child.isNil
2008
+ true === child.isNil ||
2009
+ // A settled sizing residue generates through ConjunctVal.gen, so it
2010
+ // is a member like any other -- and a `length` that did not count
2011
+ // its sibling's `unique` would be the early-fold defect again, one
2012
+ // level up.
2013
+ undefined !== sizingResidue(child)
1491
2014
  }
1492
2015
 
1493
2016
 
@@ -1624,7 +2147,7 @@ class UniqueConstraintVal extends ConstraintVal {
1624
2147
  constructor(spec: ValSpec, ctx?: AontuContext) {
1625
2148
  super({ ...spec, atom: 'unique' }, ctx)
1626
2149
  }
1627
- } /* node:coverage ignore next 19 */
2150
+ } /* node:coverage ignore next 23 */
1628
2151
 
1629
2152
 
1630
2153
  export {
@@ -1632,6 +2155,11 @@ export {
1632
2155
  // normalisers must produce byte-identical output, and that can only be
1633
2156
  // checked by calling them.
1634
2157
  normaliseRe,
2158
+ // Exported for the subsumption query (G3, ts/src/subsume.ts): the
2159
+ // residual-vs-residual and residual-vs-scalar rules live here beside
2160
+ // the compare machinery they reuse.
2161
+ constraintSubsumesConstraint,
2162
+ constraintAdmitsScalar,
1635
2163
  ConstraintVal,
1636
2164
  MinConstraintVal,
1637
2165
  MaxConstraintVal,