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/val/RefVal.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /* Copyright (c) 2021-2025 Richard Rodger, MIT License */
2
2
 
3
3
 
4
- import Util from 'node:util'
5
4
 
6
5
  import {
7
6
  walk,
@@ -49,6 +48,30 @@ import { BigDecimalVal } from './BigDecimalVal'
49
48
  const UNSPELLABLE_SEGMENT = '\u0000unspellable'
50
49
 
51
50
 
51
+ // Is this value an unresolved type()/hide() call — or a conjunct still
52
+ // carrying one? A reference that lands on one must defer rather than
53
+ // clone it (see the call site in `find`): the marks such a call will
54
+ // stamp belong to the field it was WRITTEN at, and a clone resolving
55
+ // at the reference's site re-applies them after the reference's
56
+ // mark-clearing walk has already run. Only the two mark wrappers
57
+ // qualify — every other pending call resolves to an unmarked value,
58
+ // and the existing early-clone behaviour for those is pinned
59
+ // (move()/copy() ghost rows, hole-filling conjuncts).
60
+ function pendingMarkWrapper(v: any): boolean {
61
+ if (true === v.isTypeFunc || true === v.isHideFunc) {
62
+ return !v.done
63
+ }
64
+ if (true === v.isConjunct && Array.isArray(v.peg)) {
65
+ for (const t of v.peg) {
66
+ if (pendingMarkWrapper(t)) {
67
+ return true
68
+ }
69
+ }
70
+ }
71
+ return false
72
+ }
73
+
74
+
52
75
  class RefVal extends FeatureVal {
53
76
  isRef = true
54
77
  isGenable = true
@@ -175,7 +198,6 @@ class RefVal extends FeatureVal {
175
198
 
176
199
  const te = ctx.explain && explainOpen(ctx, ctx.explain, 'Ref', this, peer)
177
200
  let out: Val = this
178
- let why = 'id'
179
201
 
180
202
  if (this.id !== peer.id) {
181
203
 
@@ -191,29 +213,24 @@ class RefVal extends FeatureVal {
191
213
  if (resolved instanceof RefVal) {
192
214
  if (peer.isTop) {
193
215
  out = this
194
- why = 'pt'
195
216
  }
196
217
  else if (peer.isNil) {
197
218
  out = makeNilErr(ctx, 'ref[' + this.peg + ']', this, peer)
198
- why = 'pn'
199
219
  }
200
220
 
201
221
  // same path
202
222
  else if (this.canon === peer.canon) {
203
223
  out = this
204
- why = 'pp'
205
224
  }
206
225
 
207
226
  else {
208
227
  // Ensure RefVal done is incremented
209
228
  this.dc = DONE === this.dc ? DONE : this.dc + 1
210
229
  out = new ConjunctVal({ peg: [this, peer] }, ctx)
211
- why = 'cj'
212
230
  }
213
231
  }
214
232
  else {
215
233
  out = unite(te ? ctx.clone({ explain: ec(te, 'RES') }) : ctx, resolved, peer, 'ref')
216
- why = 'u'
217
234
  }
218
235
 
219
236
  out.dc = DONE === out.dc ? DONE : this.dc + 1
@@ -226,7 +243,15 @@ class RefVal extends FeatureVal {
226
243
  }
227
244
 
228
245
 
229
- find(ctx: AontuContext) {
246
+ // `snap` is set by snapshotRefSpread (MapVal): a SPREAD snapshot
247
+ // wants the target's pre-resolution STRUCTURE — key()/path() still
248
+ // unresolved, to be re-resolved per destination — so the
249
+ // pending-mark-wrapper defer below must not apply to it. Deferring
250
+ // there made the snapshot wait until the target's own key() had
251
+ // resolved at the target, and the literal leaked into every
252
+ // destination (the exact failure the snapshot exists to prevent —
253
+ // test/spec/spread-type.tsv, spread-type-key-ref).
254
+ find(ctx: AontuContext, snap?: boolean) {
230
255
  let out: Val | undefined = undefined
231
256
 
232
257
  // Check if self.path starts with peg (cycle detection).
@@ -265,6 +290,16 @@ class RefVal extends FeatureVal {
265
290
 
266
291
  for (let pI = 0; pI < this.peg.length; pI++) {
267
292
  let part = this.peg[pI]
293
+ // An unspellable segment MISSES BEFORE ANY LOOKUP. The marker
294
+ // is NUL-prefixed because no spelling produces one, but a
295
+ // document can still hold a key spelled with an escaped NUL
296
+ // (`a:{"unspellable":7}`), and matching it would turn the
297
+ // silent path-shortening this marker exists to prevent into a
298
+ // different silent wrong value. The marker is a marker, never a
299
+ // lookup key.
300
+ if (UNSPELLABLE_SEGMENT === part) {
301
+ return makeNilErr(ctx, 'no_path', this)
302
+ }
268
303
  if (part instanceof VarVal) {
269
304
  let strval = (part as VarVal).peg
270
305
  let name = strval ? '' + strval.peg : ''
@@ -413,14 +448,52 @@ class RefVal extends FeatureVal {
413
448
  else if (pI === refpath.length) {
414
449
  out = node
415
450
 
416
- // A reference landing on another reference may be a PROVEN
417
- // mutual cycle (a: $.b, b: $.a) -- follow the plain-ref chain
418
- // and, if it revisits a node, report path_cycle now instead of
419
- // deferring every pass and dying later as a generic ref error.
420
- // No proof (chain leaves plain refs, or ends) defers as before.
421
- if (null != out && (out as any).isRef && this.detectRefCycle(ctx)) {
451
+ // A reference landing on another reference -- or on a FUNCTION,
452
+ // whose arguments the chase now follows (issue #35) -- may be a
453
+ // PROVEN mutual cycle (a: $.b, b: $.a; a: $.b, b: upper($.a)).
454
+ // Follow the chain and, if it returns to a node still open above
455
+ // it, report path_cycle now instead of deferring every pass and
456
+ // dying later as a spent budget. No proof (the chain leaves plain
457
+ // refs and calls, or ends) defers as before.
458
+ if (null != out && ((out as any).isRef || (out as any).isFunc) &&
459
+ this.detectRefCycle(ctx)) {
422
460
  out = makeNilErr(ctx, 'path_cycle', this)
423
461
  }
462
+ // A PENDING MARK WRAPPER IS NOT YET A VALUE TO COPY (ADR-005).
463
+ // A type()/hide() call still waiting for its argument — an
464
+ // alias reference inside a type() body, a generator inside a
465
+ // hide() — would be cloned here as the CALL, and the clone
466
+ // then resolves at the REFERENCE's site, stamping marks that
467
+ // the mark-clearing walk below has already run too early to
468
+ // clear. That is how a type-marked alias silently suppressed
469
+ // the referring field's emission (use-cases/BUGS.md §12), how
470
+ // `hide(pack(...))` leaked its mark onto downstream packs
471
+ // (§11), and how hide() around a computed field swallowed the
472
+ // value into a silent [] (§35b). Defer instead: the reference
473
+ // residuates until the wrapper has resolved at its OWN field,
474
+ // and the ordinary marked-value path below then clears the
475
+ // marks on the clone as documented. The move() reference
476
+ // (`_hide_found`) is exempt: a move TRANSPLANTS the pending
477
+ // call, and the ghost rows (test/spec/func.tsv) pin that its
478
+ // innards resolve at the destination.
479
+ else if (null != out && !snap && !this.mark._hide_found &&
480
+ pendingMarkWrapper(out)) {
481
+ out = undefined
482
+ }
483
+ // A STAGED ARGUMENT SNAPSHOTS A SETTLED SOURCE (the argsnap
484
+ // flag, set by driveStagedArgs). A generator's data argument is
485
+ // a copy OUTSIDE the tree, so anything in the target that still
486
+ // resolves against its own tree location — a spread-injected
487
+ // relative reference, a pending template — must finish there
488
+ // BEFORE the copy is taken: cloned earlier, the copy's rebased
489
+ // relative refs dangle under the generator and the model dies
490
+ // as *_no_gen with the generator never firing. Deferring here
491
+ // is exactly the documented staging rule: the generator waits
492
+ // for the source, then snapshots it whole.
493
+ else if (null != out && !snap && true === (ctx as any).argsnap &&
494
+ !out.done) {
495
+ out = undefined
496
+ }
424
497
  // Types and hidden values are cloned and made concrete
425
498
  else if (null != out) { // && (out.mark.type || out.mark.hide)) {
426
499
 
@@ -449,6 +522,19 @@ class RefVal extends FeatureVal {
449
522
  walk(out, (_key: string | number | undefined, val: Val) => {
450
523
  val.mark.type = false
451
524
  val.mark.hide = false
525
+ // REFERENCES DO NOT CARRY IDENTITY (G4 phase 1, clearing
526
+ // rule 1). The clone is a copy of an entity, not the
527
+ // entity: without this, `w:b:$.q.a & {y:2,z:3}` (row
528
+ // `ref-and-merge`, test/spec/ref.tsv) would push `y:2`
529
+ // back into `q.a` through the identity merge — pinned
530
+ // behaviour, silently changed by a mark the author never
531
+ // wrote at the reference site.
532
+ //
533
+ // The LINK is NOT cleared (G4 phase 3): an identity says
534
+ // what a value IS, so a copy must not be that entity; a
535
+ // link says what a value POINTS AT, and a copy of a link
536
+ // points at the same thing.
537
+ val.entity = undefined
452
538
  return val
453
539
  })
454
540
  //}
@@ -468,33 +554,80 @@ class RefVal extends FeatureVal {
468
554
  // chain revisits a node -- a PROVEN reference cycle, distinct from a
469
555
  // merely unresolved reference. Detection is only on the resolution
470
556
  // chain revisiting a node, never on syntactic shape: a chain that
471
- // passes through a variable segment, a conjunct, a function, or any
472
- // non-ref value yields no proof and the ref defers as before.
557
+ // passes through a variable segment, a conjunct or any other non-ref
558
+ // value yields no proof and the ref defers as before.
559
+ //
560
+ // A FUNCTION is followed, through its arguments (issue #35). A
561
+ // function resolves only once every argument does, so a chain that
562
+ // reaches `b:upper($.a)` and out through `$.a` has proved the same
563
+ // dependency a bare `b:$.a` proves -- `a:$.b b:upper($.a)` is a cycle
564
+ // whichever link wears the call. Without this the shape exhausted the
565
+ // depth budget instead: a `unify_cycle`, which under the G5 taxonomy
566
+ // means "retry with more may help", where a proven structural cycle is
567
+ // FIX THE MODEL. A conjunct and a disjunct stay unfollowed for reasons
568
+ // that are not the same: a disjunct member may simply not be taken, so
569
+ // reaching one proves nothing; a conjunct would be sound to follow, and
570
+ // is left out only because nothing needs it yet.
571
+ //
572
+ // The Go port reaches the same verdict on this shape by a DIFFERENT
573
+ // arm, and that difference outlives this method: its clonePath re-paths
574
+ // a resolved clone to the referring site, so the inner `$.a` lands at
575
+ // path [a] and the plain isprefixpath test proves the cycle before any
576
+ // chase is needed. TypeScript's clone keeps the source paths. ADR-001
577
+ // asks for arm-for-arm correspondence, so the clone-path difference is
578
+ // worth closing on its own; it is wider than this issue and both ports
579
+ // now agree on the verdict and the code either way.
473
580
  detectRefCycle(ctx: AontuContext): boolean {
474
- const seen = new Set<number>()
475
- let cur: RefVal = this
476
- // No hop cap: every iteration either returns or adds a NEW ref to
477
- // `seen`, and the tree holds finitely many refs, so the chase
478
- // terminates at the first repetition or the first non-ref — a cap
479
- // would just make cycles longer than it invisible.
480
- for (; ;) {
481
- if (seen.has(cur.id)) {
482
- return true
483
- }
484
- seen.add(cur.id)
485
- const rp = cur.plainRefPath()
581
+ // Depth-first with an explicit ANCESTOR set, because a function may
582
+ // carry several reference arguments and the cycle can run through
583
+ // any one of them. The set holds the chain currently being walked,
584
+ // not every node ever walked: revisiting a node reached down a
585
+ // DIFFERENT branch is an ordinary shared reference (two keys reading
586
+ // one third key), and only revisiting one that is still open above
587
+ // us is a cycle.
588
+ //
589
+ // Identity is the RESOLVED PATH, not the RefVal instance: the same
590
+ // target can be reached through distinct ref instances, and it is
591
+ // returning to the same place that closes a loop.
592
+ const chase = (ref: RefVal, ancestors: Set<string>): boolean => {
593
+ const rp = ref.plainRefPath()
486
594
  if (null == rp) {
487
595
  return false
488
596
  }
597
+ const key = rp.join('')
598
+ if (ancestors.has(key)) {
599
+ return true
600
+ }
601
+
489
602
  let node: any = ctx.root
490
603
  for (let i = 0; i < rp.length && null != node; i++) {
491
604
  node = (node.isMap || node.isList) ? node.peg[rp[i]] : undefined
492
605
  }
493
- if (null == node || !node.isRef) {
606
+ if (null == node) {
494
607
  return false
495
608
  }
496
- cur = node
609
+
610
+ // Terminates: each level adds a path to `ancestors` and refuses a
611
+ // repeat, and the tree holds finitely many distinct paths.
612
+ ancestors.add(key)
613
+ let found = false
614
+ if (node.isRef) {
615
+ found = chase(node, ancestors)
616
+ }
617
+ else if (node.isFunc && Array.isArray(node.peg)) {
618
+ for (const arg of node.peg) {
619
+ if (null != arg && arg.isRef && chase(arg, ancestors)) {
620
+ found = true
621
+ break
622
+ }
623
+ }
624
+ }
625
+ ancestors.delete(key)
626
+
627
+ return found
497
628
  }
629
+
630
+ return chase(this, new Set<string>())
498
631
  }
499
632
 
500
633
 
@@ -0,0 +1,387 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // CHECKED, TYPED, LINK-SHAPED REFERENCES (G4 phase 2,
4
+ // docs/capability-review/g4-identity-relations.md): `refer(t)` is a
5
+ // constraint on a string-valued field. The string must be an ENTITY
6
+ // ADDRESS, the addressed node must exist in the evaluation, and — when
7
+ // `t` is given — `t` is unified INTO the target. The field's own value
8
+ // stays the address string: a LINK, not an embedding.
9
+ //
10
+ // This is the piece a plain reference cannot be. `$.a.b` resolves by
11
+ // CLONING its target into place, so `dependsOn: [$.services.auth]`
12
+ // generates a full copy of the auth node where the author meant a
13
+ // name. `refer` leaves the name and checks it.
14
+ //
15
+ // Constraint FLOW rather than a check: `refer(t)` does not merely test
16
+ // the target against `t`, it unifies `t` into it. Referring to
17
+ // something as a Service MAKES it one, and if it cannot be, the
18
+ // conflict is an ordinary located error. Check-only semantics would be
19
+ // non-monotone — true, then false as the target grows — and the
20
+ // lattice guarantee is that more information never falsifies what has
21
+ // been observed.
22
+
23
+ import type {
24
+ Val,
25
+ ValSpec,
26
+ } from '../type'
27
+
28
+ import {
29
+ DONE,
30
+ } from '../type'
31
+
32
+ import {
33
+ AontuContext,
34
+ } from '../ctx'
35
+
36
+ import { makeNilErr } from '../err'
37
+
38
+ import { FuncBaseVal } from './FuncBaseVal'
39
+ import { FeatureVal } from './FeatureVal'
40
+ import { StringVal } from './StringVal'
41
+ import { unite } from '../unify'
42
+ import { top } from './top'
43
+ import { propagateMarks, walk } from '../utility'
44
+
45
+
46
+ // A segment of the path INSIDE an entity. The entity name's own
47
+ // grammar (no dots) is what makes the split unambiguous: everything
48
+ // before the first dot names the entity, everything after walks its
49
+ // value.
50
+ const ADDR_SEGMENT = /^[A-Za-z0-9_-]+$/
51
+ const ADDR_NAME = /^[A-Za-z0-9_/-]+$/
52
+
53
+
54
+ export type Address = {
55
+ name: string
56
+ path: string[]
57
+ }
58
+
59
+
60
+ // The address a string spells, or undefined when it does not spell
61
+ // one. `svc/auth` is the entity; `svc/auth.ports.http` is a node
62
+ // inside it — the two addressing schemes reconciled: `$.a.b` answers
63
+ // WHERE, an address answers WHAT, and beneath entity granularity the
64
+ // tree is authoritative again.
65
+ export function parseAddress(s: string): Address | undefined {
66
+ const parts = s.split('.')
67
+ if (!ADDR_NAME.test(parts[0])) {
68
+ return undefined
69
+ }
70
+ for (const seg of parts.slice(1)) {
71
+ if (!ADDR_SEGMENT.test(seg)) {
72
+ return undefined
73
+ }
74
+ }
75
+ return { name: parts[0], path: parts.slice(1) }
76
+ }
77
+
78
+
79
+ // The value an address names, or undefined when the evaluation does
80
+ // not (yet) have one. Pending is not failure: an entity may be
81
+ // declared by a later conjunct, include or spread, so `refer`
82
+ // residuates exactly as a forward reference does.
83
+ export function findEntity(
84
+ reg: Map<string, Val> | undefined, addr: Address
85
+ ): { parent?: any, key?: string, val: Val } | undefined {
86
+ const rep: any = reg?.get(addr.name)
87
+ if (null == rep) {
88
+ return undefined
89
+ }
90
+ let parent: any = undefined
91
+ let key: string | undefined = undefined
92
+ let val: any = rep
93
+ for (const seg of addr.path) {
94
+ if (true !== val?.isMap && true !== val?.isList) {
95
+ return undefined
96
+ }
97
+ const next = val.peg[seg]
98
+ if (null == next) {
99
+ return undefined
100
+ }
101
+ parent = val
102
+ key = seg
103
+ val = next
104
+ }
105
+ return { parent, key, val }
106
+ }
107
+
108
+
109
+ // concreteFlow is `t` as it enters the target: a copy with the
110
+ // type/hide marks cleared at every depth. The clone matters as much as
111
+ // the clearing — `t` is shared by every position that refers to the
112
+ // same thing, and clearing in place would unmark the schema itself.
113
+ function concreteFlow(ctx: AontuContext, t: Val): Val {
114
+ let marked = false
115
+ walk(t, (_key: string | number | undefined, v: Val) => {
116
+ marked = marked || v.mark.type || v.mark.hide
117
+ return v
118
+ })
119
+ // An unmarked flow type is passed THROUGH: cloning one anyway would
120
+ // move the site an error names, and a conflict has to point at what
121
+ // the author wrote.
122
+ if (!marked) {
123
+ return t
124
+ }
125
+ const out = t.clone(ctx)
126
+ walk(out, (_key: string | number | undefined, v: Val) => {
127
+ v.mark.type = false
128
+ v.mark.hide = false
129
+ return v
130
+ })
131
+ return out
132
+ }
133
+
134
+
135
+ // ReferVal is what `refer(t)` RESOLVES to: the residual constraint,
136
+ // carrying the type to flow and — once it has met a string — the
137
+ // address to flow it into. A separate value from the function for the
138
+ // reason every residual is: the function is written once and the
139
+ // constraint is met many times, and only the constraint has state
140
+ // worth carrying.
141
+ class ReferVal extends FeatureVal {
142
+ isRefer = true
143
+ isGenable = true
144
+
145
+ // The type to flow into the target; TOP when `refer()` was written
146
+ // with no argument.
147
+ tval: Val
148
+ // The address, once a string has been met.
149
+ addr?: Address
150
+ // The address AS WRITTEN, for canon and for the error message.
151
+ addrsrc?: string
152
+ // Constraints met while the address was still pending — a kind, a
153
+ // regex, a preference. They meet the LINK once there is one.
154
+ held?: Val
155
+
156
+ constructor(spec: ValSpec, ctx?: AontuContext) {
157
+ super(spec, ctx)
158
+ this.tval = (spec as any).tval ?? top()
159
+ this.addr = (spec as any).addr
160
+ this.addrsrc = (spec as any).addrsrc
161
+ this.held = (spec as any).held
162
+ this.dc = 0
163
+ }
164
+
165
+ // The residual's own state — the type to flow, the address it has
166
+ // met, the constraints it holds — TRAVELS with the clone. A spread
167
+ // template holds the FUNCTION, so a template never needs this; a
168
+ // REFERENCE to a value that already contains a resolved link does
169
+ // (`z: id(a) & {u: refer() & "a"}` then `s: $.z`). Without it the
170
+ // clone came back as a bare `refer()` — the address silently
171
+ // dropped, and the copied link resolving to nothing.
172
+ //
173
+ // No path-dependence hook, though: a residual is minted at its
174
+ // destination, so `key()` inside a template resolves there already.
175
+ clone(ctx: AontuContext, spec?: ValSpec): Val {
176
+ const out: any = super.clone(ctx, spec)
177
+ out.tval = this.tval
178
+ out.addr = this.addr
179
+ out.addrsrc = this.addrsrc
180
+ out.held = this.held
181
+ return out
182
+ }
183
+
184
+ unify(peer: Val, ctx: AontuContext): Val {
185
+ const p: any = peer
186
+
187
+ // Another `refer` at the same position: one constraint, both
188
+ // types. `refer(A) & refer(B)` is a target that must be both.
189
+ if (true === p?.isRefer) {
190
+ return this.with(ctx, {
191
+ tval: unite(ctx, this.tval, p.tval, 'refer-t'),
192
+ addr: this.addr ?? p.addr,
193
+ addrsrc: this.addrsrc ?? p.addrsrc,
194
+ held: null == this.held ? p.held
195
+ : null == p.held ? this.held
196
+ : unite(ctx, this.held, p.held, 'refer-held'),
197
+ }, this)
198
+ }
199
+
200
+ if (null == peer || true === p.isTop) {
201
+ return this.settle(ctx, this)
202
+ }
203
+
204
+ if (true === p.isNil) {
205
+ return peer
206
+ }
207
+
208
+ // A STRING is the ADDRESS, when there is not one yet. It is the
209
+ // only thing that can be: a link's value is its address.
210
+ if (undefined === this.addr
211
+ && true === p.isScalar && 'string' === typeof p.peg) {
212
+ const addr = parseAddress(p.peg)
213
+ if (undefined === addr) {
214
+ return makeNilErr(ctx, 'refer_address', this, peer, 'refer',
215
+ { addr: p.peg })
216
+ }
217
+ return this.with(ctx, { addr, addrsrc: p.peg }, peer)
218
+ }
219
+
220
+ // A value that can never BE a string cannot constrain one either,
221
+ // and no later pass can repair it — so this arm refuses rather
222
+ // than defers. A KIND or a constraint is not in it: `string`,
223
+ // `re("^svc/")` and the like are perfectly good constraints on an
224
+ // address, and are held below until there is one to apply them to.
225
+ if ((true === p.isScalar && 'string' !== typeof p.peg)
226
+ || true === p.isMap || true === p.isList) {
227
+ return makeNilErr(ctx, 'refer_address', this, peer, 'refer')
228
+ }
229
+
230
+ // HELD: everything else waits for the address. Carried on the
231
+ // residual rather than parked in a conjunct, because a conjunct
232
+ // rebuilt every pass grows a level every pass; the held constraint
233
+ // meets the link the moment the address resolves, so
234
+ // `refer() & "x" & "y"` still conflicts and `refer() & string & "x"`
235
+ // still passes.
236
+ return this.with(ctx, {
237
+ held: null == this.held ? peer : unite(ctx, this.held, peer, 'refer-held'),
238
+ }, this)
239
+ }
240
+
241
+ // with is the residual reshaped: every arm above answers a NEW
242
+ // ReferVal rather than mutating this one, because a spread template's
243
+ // residual is shared by every child it is applied to.
244
+ with(ctx: AontuContext, spec: any, site: Val): Val {
245
+ const out = new ReferVal({}, ctx)
246
+ out.tval = spec.tval ?? this.tval
247
+ out.addr = spec.addr ?? this.addr
248
+ out.addrsrc = spec.addrsrc ?? this.addrsrc
249
+ out.held = spec.held ?? this.held
250
+ propagateMarks(this, out)
251
+ out.site = site.site
252
+ out.path = this.path
253
+ return out.settle(ctx, site)
254
+ }
255
+
256
+ // settle answers the address if the evaluation can, and stays
257
+ // pending if it cannot YET. `site` is the value whose position the
258
+ // resolved string should take.
259
+ settle(ctx: AontuContext, site: Val): Val {
260
+ if (undefined === this.addr) {
261
+ // NOT DONE, unlike `string` or `min(1)`. A refer without an
262
+ // address has not done its work — it exists to check one — and
263
+ // the pass loop must keep offering it the chance. The cost is
264
+ // that a SCHEMA mentioning a link never resolves either, so
265
+ // `type({from: refer($.std.Port)})` is not expressible today;
266
+ // G4 phase 4 records why, and what it would take.
267
+ this.dc = 0
268
+ return this
269
+ }
270
+ const reg: Map<string, Val> | undefined = (ctx as any)?.entities
271
+ const found = findEntity(reg, this.addr)
272
+ if (undefined === found) {
273
+ // PENDING, not failed — until the last pass. An entity may be
274
+ // declared by a later conjunct, include or spread, so `refer`
275
+ // residuates as a forward reference does; but within ONE
276
+ // evaluation the document-set is fixed, so existence IS
277
+ // decidable, and the final pass is where it is decided. A
278
+ // pending refer keeps the tree not-done, so the pass loop always
279
+ // reaches that pass when there is one to decide.
280
+ if (ctx.cc + 1 >= ctx.budget.passes) {
281
+ return makeNilErr(ctx, 'refer_unresolved', this, undefined, 'refer',
282
+ { addr: this.addrsrc as string })
283
+ }
284
+ this.dc = 0
285
+ return this
286
+ }
287
+
288
+ // THE FLOW. `t` is unified into the target and written back, so
289
+ // every position of the entity carries it after the pass's
290
+ // identity merge — the same channel the merge itself uses.
291
+ //
292
+ // RE-ENTRANT ONLY ONCE PER ENTITY (use-cases/BUGS.md §19). Uniting
293
+ // the target drives the target's OWN subtree, and if the target
294
+ // links back — `a` typed-refers `b`, `b` typed-refers `a`, the
295
+ // shape every inverse pair has — that drives this entity again,
296
+ // and the two flow into each other until the depth budget or the
297
+ // host stack ends it. `unify_cycle` on a model whose meet plainly
298
+ // converges: `{k:1}` meeting `{k:1}` is a fixpoint, and the
299
+ // evaluator never got far enough to notice.
300
+ //
301
+ // The guard is the set of entities a flow is currently inside, on
302
+ // the context. A flow that would re-enter one is SKIPPED, not
303
+ // failed: the outer flow it is nested in is already uniting that
304
+ // entity, so the same information arrives by the same channel one
305
+ // frame up. What each flow contributes is unchanged; only the
306
+ // order it arrives in is, and unification does not care.
307
+ const flowing: Set<string> = ((ctx as any)._referflow ??=
308
+ new Set<string>())
309
+ if (!this.tval.isTop && !flowing.has(this.addr.name)) {
310
+ flowing.add(this.addr.name)
311
+ try {
312
+ // The flowed type is CONCRETE at the target: a schema flowing
313
+ // into a value must not make the value a schema. Same reasoning
314
+ // as a reference's clone clearing marks — `refer($.std.Service)`
315
+ // says the target IS a Service, not that it is the definition
316
+ // of one — and without it the target silently stopped
317
+ // generating.
318
+ const merged = unite(ctx, found.val, concreteFlow(ctx, this.tval),
319
+ 'refer-flow')
320
+ if (true === (merged as any).isNil) {
321
+ return merged
322
+ }
323
+ if (undefined === found.parent) {
324
+ reg!.set(this.addr.name, merged)
325
+ }
326
+ else {
327
+ found.parent.peg[found.key as string] = merged
328
+ }
329
+ }
330
+ finally {
331
+ flowing.delete(this.addr.name)
332
+ }
333
+ }
334
+
335
+ // The value IS the address string: a link, not an embedding.
336
+ const out: any = new StringVal({ peg: this.addrsrc as string }, ctx)
337
+ out.dc = DONE
338
+ // STAMPED as a link (G4 phase 3): the value is the address string,
339
+ // so without this nothing downstream could tell a checked link from
340
+ // a literal that happens to look like one. The edge set is exactly
341
+ // the set of these stamps.
342
+ out.link = this.addrsrc
343
+ propagateMarks(this, out)
344
+ out.site = site.site
345
+ out.path = this.path
346
+ return null == this.held ? out : unite(ctx, out, this.held, 'refer-held')
347
+ }
348
+
349
+ get canon() {
350
+ const t = this.tval.isTop ? '' : this.tval.canon
351
+ const call = 'refer(' + t + ')' +
352
+ (null == this.held ? '' : '&' + this.held.canon)
353
+ return undefined === this.addrsrc
354
+ ? call : call + '&' + JSON.stringify(this.addrsrc)
355
+ }
356
+ }
357
+
358
+
359
+ class ReferFuncVal extends FuncBaseVal {
360
+ isReferFunc = true
361
+
362
+ constructor(spec: ValSpec, ctx?: AontuContext) {
363
+ super(spec, ctx)
364
+ }
365
+
366
+ make(_ctx: AontuContext, spec: ValSpec): Val {
367
+ return new ReferFuncVal(spec)
368
+ }
369
+
370
+ funcname() {
371
+ return 'refer'
372
+ }
373
+
374
+ resolve(ctx: AontuContext, args: Val[]) {
375
+ const out = new ReferVal({}, ctx)
376
+ out.tval = 0 < args.length ? args[0] : top()
377
+ out.site = this.site
378
+ out.path = this.path
379
+ return out
380
+ }
381
+ } /* node:coverage ignore next 6 */
382
+
383
+
384
+ export {
385
+ ReferFuncVal,
386
+ ReferVal,
387
+ }