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/ctx.ts CHANGED
@@ -21,6 +21,11 @@ type AontuContextConfig = {
21
21
  cc?: number
22
22
  err?: any[] // Omit<NilVal[], "push">
23
23
  explain?: any[] | boolean | null
24
+
25
+ // The provenance recorder (G7 phase 3), or absent for an
26
+ // uninstrumented run. Shared by reference down every descend, as the
27
+ // error list is, so one run has one record.
28
+ prov?: any
24
29
  fs?: any
25
30
  path?: string[]
26
31
  root?: Val
@@ -41,6 +46,26 @@ class AontuContext {
41
46
  path: string[] // Path to current Val.
42
47
  vc: number // Val counter to create unique val ids.
43
48
  cc: number = -1
49
+
50
+ // THE STAGING RULE (G8 phase 0,
51
+ // docs/capability-review/g8-generation.md). A value whose answer
52
+ // depends on WHERE IT IS -- `key()` today, the generation
53
+ // combinators next -- must not answer while anything is still
54
+ // moving it: resolved early it reports the position it was WRITTEN
55
+ // at rather than the one it ends up at. Such a value RESIDUATES
56
+ // while this is false, and fires exactly once on the pass where it
57
+ // is true.
58
+ //
59
+ // The pass loop (ts/src/unify.ts) sets it on the first pass whose
60
+ // input tree is IDENTICAL to the previous pass's: everything that
61
+ // was going to move has moved, and what is left is the staged
62
+ // values themselves, which is precisely the moment they may answer.
63
+ // It replaces a `ctx.cc < 3` pass count in KeyFuncVal -- a magic
64
+ // number, right for the documents it was tuned on and silently
65
+ // wrong for anything that took a fourth pass to place a value. The
66
+ // comment it replaces said as much: "this delay makes keys in
67
+ // spreads and refs work, but it is a hack - find a better way".
68
+ settle: boolean = false
44
69
  vars: Record<string, Val> = {}
45
70
  src?: string
46
71
  fs?: FST
@@ -50,6 +75,25 @@ class AontuContext {
50
75
 
51
76
  collect: boolean
52
77
 
78
+ // THE COMPLETENESS PROBE (the review's finding C). vet detects
79
+ // residue by GENERATING the anchored meet and keeping the
80
+ // incomplete-class failures. Generation honours the OUTPUT marks --
81
+ // `type()` and `hide()` say "do not emit this" -- so a `--at` anchor
82
+ // sitting under a mark generated nothing at all, reported nothing,
83
+ // and vetted VALID for data missing a required field, while the same
84
+ // anchor without the mark answered incomplete (use-cases/BUGS.md
85
+ // §14). A mark is a decision about OUTPUT; it is not a statement
86
+ // about what the data must satisfy, and `--at` names the truth to
87
+ // validate against explicitly. Under this flag the generation walk
88
+ // descends through marked values; nothing else changes, and no
89
+ // output is produced from a probe run -- only its findings are read.
90
+ probe: boolean = false
91
+
92
+ // The provenance recorder (G7 phase 3), or undefined for an
93
+ // uninstrumented run. Inherited by every descended and cloned
94
+ // context through the prototype chain, so one run has one record.
95
+ prov?: any
96
+
53
97
  // errlist: Omit<NilVal[], "push"> // Nil error log of current unify.
54
98
  err: any[]
55
99
  explain: any[] | null
@@ -71,6 +115,29 @@ class AontuContext {
71
115
  _pathTrie: Map<number, Map<string, { idx: number, path: string[] }>>
72
116
  _pathidxNext: { n: number }
73
117
 
118
+ // Current `unite` recursion depth, checked against the depth budget
119
+ // (ts/src/unify.ts). Held in a shared mutable box, like _pathidxNext,
120
+ // because clone() uses Object.create: the box is inherited by
121
+ // reference, so a nested clone's increments are visible to the frame
122
+ // that will decrement them. The Go port keeps the same counter
123
+ // directly on its Ctx pointer (go/unify.go, maxUniteDepth).
124
+ _depth: { n: number }
125
+
126
+ // The evaluation budgets (G5 trust profile, docs/trust.md): integer
127
+ // counts of engine events, never wall-clock. Always present, defaults
128
+ // from the shared spec-visible constants (test/spec/budget.tsv), so
129
+ // the hot-path reads in unify.ts are plain property loads. Inherited
130
+ // by clone() through the prototype chain. `revisits` is NOT profile
131
+ // surface (the Go port has no revisit counter to configure — see
132
+ // TrustBudget in type.ts); it is carried here so unify.ts reads one
133
+ // budget object, at its fixed spec constant.
134
+ budget: { passes: number, revisits: number, depth: number }
135
+
136
+ // The include manifest sink (G5, docs/trust.md): every include the
137
+ // resolver reads is recorded here as { path, capability }, and
138
+ // Aontu.parse() sorts and dedups it onto the result's `deps`.
139
+ manifest: { path: string, capability: string }[]
140
+
74
141
  // Trial mode: set by DisjunctVal.unify while each member is tried
75
142
  // against the peer. When true, makeNilErr returns the shared
76
143
  // TRIAL_NIL sentinel instead of allocating a fresh NilVal, and
@@ -94,6 +161,7 @@ class AontuContext {
94
161
  this.src = cfg.src
95
162
 
96
163
  this.collect = cfg.collect ?? null != cfg.err
164
+ this.prov = cfg.prov
97
165
 
98
166
  this.err = cfg.err ?? []
99
167
  this.explain = Array.isArray(cfg.explain) ? cfg.explain : null
@@ -116,10 +184,23 @@ class AontuContext {
116
184
  this._pathmap = new Map()
117
185
  this._pathTrie = new Map()
118
186
  this._pathidxNext = { n: 1 } // 0 reserved for the root path
187
+ this._depth = { n: 0 }
119
188
  this._pathidx = 0
120
189
 
190
+ this.manifest = []
191
+
121
192
  this.opts = DEFAULT_OPTS()
122
193
  this.addopts(cfg.opts)
194
+
195
+ // Budget defaults are the shared spec-visible constants
196
+ // (test/spec/budget.tsv pins the boundaries in both ports); the
197
+ // trust profile may lower or raise them, deterministically.
198
+ const budget = (this.opts as any).trust?.budget ?? {}
199
+ this.budget = {
200
+ passes: budget.passes ?? 9,
201
+ revisits: 999,
202
+ depth: budget.depth ?? 1000,
203
+ }
123
204
  }
124
205
 
125
206
 
package/src/diff.ts ADDED
@@ -0,0 +1,196 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // PATH-ADDRESSED DIFF (G7 phase 6,
4
+ // docs/capability-review/g7-machine-access.md): what changed, at which
5
+ // paths, between two documents — the dyff-style answer, which
6
+ // deterministic canon makes possible without phantom noise. Two
7
+ // documents that mean the same thing canon the same way, so a diff of
8
+ // canons reports semantic change and not reformatting.
9
+ //
10
+ // The text compared is the HASH FORM (G6's `hcanon`), not the plain
11
+ // canon, for the reason G6 gives: canon drops closedness and the
12
+ // type/hide marks, so a canon diff calls `close({a:1})` and `{a:1}`
13
+ // identical. A false "changed" costs a needless read; a false
14
+ // "unchanged" is a change nobody reviewed, which is the one direction
15
+ // that must not happen.
16
+ //
17
+ // WHETHER a change is BREAKING is a different question, and it belongs
18
+ // to G3: `subsume` and `breaking` answer it with the lattice's own
19
+ // rules. This verb answers "what moved", which is what a reviewer
20
+ // reads first and what an agent needs before it can ask the other
21
+ // question at all.
22
+
23
+ import { Aontu } from './aontu'
24
+ import type { TrustOptions } from './type'
25
+ import { anchorAt } from './vet'
26
+ import type { VetFinding } from './vet'
27
+ import { cmpCodePoint } from './keyorder'
28
+ import { hcanon } from './hcanon'
29
+ import { evalFailure } from './query'
30
+
31
+
32
+ export type DiffKind = 'added' | 'removed' | 'changed'
33
+
34
+ export type DiffChange = {
35
+ kind: DiffKind
36
+ // The canon on each side, absent where there is no value.
37
+ left?: string
38
+ path: string
39
+ right?: string
40
+ }
41
+
42
+ export type DiffReport = {
43
+ changes: DiffChange[]
44
+ findings: VetFinding[]
45
+ ok: boolean
46
+ // True when nothing moved: the two documents mean the same thing.
47
+ same: boolean
48
+ }
49
+
50
+ export type DiffOptions = {
51
+ leftPath?: string
52
+ rightPath?: string
53
+ // Compare at this path of both documents, rather than at the root.
54
+ at?: string
55
+
56
+ // The trust profile this run evaluates under (G5, docs/trust.md).
57
+ // The source arrives from a caller, so the caller must be able to
58
+ // say what it may reach: without this the include chain is the
59
+ // default one, and `@"x.js"` is arbitrary code execution in the
60
+ // evaluating process. A server passes `{include:'none'}`.
61
+ trust?: TrustOptions
62
+ }
63
+
64
+
65
+ function pathText(parts: string[]): string {
66
+ return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
67
+ }
68
+
69
+
70
+ // Both sides of one node — never both absent: keys come from the
71
+ // union of the two bags, and list indices run to the longer side, so
72
+ // every walk has at least one value. Bags of the SAME kind recurse,
73
+ // which is what makes the report path-addressed rather than one line
74
+ // saying the whole document changed; everything else compares text.
75
+ function walk(
76
+ left: any, right: any, parts: string[], out: DiffChange[]): void {
77
+ if (null == left) {
78
+ out.push({ kind: 'added', path: pathText(parts), right: hcanon(right) })
79
+ return
80
+ }
81
+ if (null == right) {
82
+ out.push({ kind: 'removed', left: hcanon(left), path: pathText(parts) })
83
+ return
84
+ }
85
+
86
+ const bothMaps = true === left.isMap && true === right.isMap
87
+ const bothLists = true === left.isList && true === right.isList
88
+ if (bothMaps || bothLists) {
89
+ // The bag's OWN attributes, at pseudo-keys under it: a recursing
90
+ // bag never compares its own text, so what the children do not
91
+ // carry has to be compared here. The spread is part of what a bag
92
+ // MEANS; so are closedness and the marks, which is exactly why the
93
+ // hash form spells them (G6).
94
+ const lc = null == left.spread.cj ? undefined : hcanon(left.spread.cj)
95
+ const rc = null == right.spread.cj ? undefined : hcanon(right.spread.cj)
96
+ if (lc !== rc) {
97
+ out.push({
98
+ kind: null == lc ? 'added' : null == rc ? 'removed' : 'changed',
99
+ ...(null == lc ? {} : { left: lc }),
100
+ path: pathText(parts.concat('&')),
101
+ ...(null == rc ? {} : { right: rc }),
102
+ })
103
+ }
104
+ flag(left.closed, right.closed, parts, 'closed', out)
105
+ flag(left.mark?.type, right.mark?.type, parts, 'type', out)
106
+ flag(left.mark?.hide, right.mark?.hide, parts, 'hide', out)
107
+
108
+ const keys = bothMaps
109
+ ? [...new Set([
110
+ ...Object.keys(left.peg), ...Object.keys(right.peg),
111
+ ])].sort(cmpCodePoint)
112
+ : [...new Set([
113
+ ...Object.keys(left.peg), ...Object.keys(right.peg),
114
+ ])].sort((a, b) => Number(a) - Number(b))
115
+ for (const k of keys) {
116
+ walk(left.peg[k], right.peg[k], parts.concat(k), out)
117
+ }
118
+ return
119
+ }
120
+
121
+ const lh = hcanon(left)
122
+ const rh = hcanon(right)
123
+ if (lh !== rh) {
124
+ out.push({ kind: 'changed', left: lh, path: pathText(parts), right: rh })
125
+ }
126
+ }
127
+
128
+
129
+ // One boolean attribute of a bag, as a pseudo-key: `$.a.&closed` says
130
+ // the map at `$.a` was closed (or opened) without saying anything
131
+ // about its keys.
132
+ function flag(
133
+ left: any, right: any, parts: string[], name: string,
134
+ out: DiffChange[]): void {
135
+ const l = true === left
136
+ const r = true === right
137
+ if (l !== r) {
138
+ out.push({
139
+ kind: 'changed',
140
+ left: String(l),
141
+ path: pathText(parts.concat('&' + name)),
142
+ right: String(r),
143
+ })
144
+ }
145
+ }
146
+
147
+
148
+ function evalSide(
149
+ aontu: Aontu, src: string, path: string | undefined, at: string | undefined,
150
+ ): { node?: any, finding?: VetFinding } {
151
+ const ctx = aontu.ctx({ collect: true })
152
+ const parseOpts = null == path ? undefined : { path }
153
+ const root: any = aontu.unify(src, parseOpts, ctx)
154
+ if (0 < ctx.err.length) {
155
+ // The query surface's own fold: a document that does not stand up
156
+ // has no meaning to compare, and the engine's diagnosis is the
157
+ // report.
158
+ return { finding: evalFailure(ctx) }
159
+ }
160
+ const node: any = null == at ? root : anchorAt(root, at)
161
+ if (null == node) {
162
+ return {
163
+ finding: {
164
+ code: 'no_path',
165
+ class: 'reference',
166
+ severity: 'error',
167
+ path: at as string,
168
+ message: `The path ${at} names nothing in this document.`,
169
+ sites: [],
170
+ },
171
+ }
172
+ }
173
+ return { node }
174
+ }
175
+
176
+
177
+ // Diff two documents. Each is evaluated on its own — a document that
178
+ // does not stand up has no meaning to compare, and the report says so
179
+ // rather than diffing a wreck.
180
+ export function diff(
181
+ leftSrc: string, rightSrc: string, opts?: DiffOptions): DiffReport {
182
+ const options = opts ?? {}
183
+ const aontu = new Aontu(
184
+ null == options.trust ? undefined : { trust: options.trust })
185
+
186
+ const l = evalSide(aontu, leftSrc, options.leftPath, options.at)
187
+ const r = evalSide(aontu, rightSrc, options.rightPath, options.at)
188
+ const findings = [l.finding, r.finding].filter(Boolean) as VetFinding[]
189
+ if (0 < findings.length) {
190
+ return { changes: [], findings, ok: false, same: false }
191
+ }
192
+
193
+ const changes: DiffChange[] = []
194
+ walk(l.node, r.node, [], changes)
195
+ return { changes, findings: [], ok: true, same: 0 === changes.length }
196
+ }
package/src/err.ts CHANGED
@@ -15,6 +15,39 @@ import { hints } from './hints'
15
15
  const { errmsg, strinject } = util
16
16
 
17
17
 
18
+ // COLOUR IS A DECISION ABOUT THE DESTINATION, not about the message.
19
+ // Every error frame hardcoded the ANSI escapes, so a piped report and
20
+ // a `--jsonl` answer carried terminal control codes into whatever read
21
+ // them -- a log file, a CI annotation, an agent's parser (the review's
22
+ // finding F).
23
+ //
24
+ // NO_COLOR (no-color.org: set, to anything, means no colour) turns them
25
+ // off everywhere, library callers included. The CLI additionally turns
26
+ // them off when its stderr is not a terminal, through setColor: a
27
+ // library cannot see the destination, and a caller who has one is the
28
+ // only one who can say.
29
+ let COLOR: boolean | undefined
30
+
31
+ function setColor(on: boolean | undefined): void {
32
+ COLOR = on
33
+ }
34
+
35
+ function colorActive(): boolean {
36
+ if (null != COLOR) {
37
+ return COLOR
38
+ }
39
+ // `globalThis` always exists; `process` need not (this library runs
40
+ // in a browser too), so the optional chain starts at the part that
41
+ // can actually be missing -- and stays a CHAIN rather than becoming
42
+ // an `if`, because the browser arm is unreachable from any test this
43
+ // suite can run and ADR-002 does not accept an arm nothing takes.
44
+ // Set-but-EMPTY is the documented exception and does not disable
45
+ // colour (no-color.org).
46
+ const no = (globalThis as any).process?.env?.NO_COLOR
47
+ return null == no || '' === no
48
+ }
49
+
50
+
18
51
  function getHint(why: any, details?: Record<string, any>): string | undefined {
19
52
  if (hints[why]) {
20
53
  let txt = hints[why]
@@ -61,10 +94,19 @@ function descErr<NILS extends NilVal | NilVal[]>(
61
94
  let v1: any = err.primary
62
95
  let v2: any = err.secondary
63
96
 
64
- let v1src = resolveSrc(v1, errctx, 'primary')
65
- let v2src = resolveSrc(v2, errctx, 'secondary')
97
+ let v1src = resolveSrc(v1, errctx)
98
+ let v2src = resolveSrc(v2, errctx)
66
99
 
67
- let path = ['$', ...err.path].filter((p: any) => null != p && '' != p)
100
+ // STRICT `!==` against the empty string. The loose `!=` here dropped
101
+ // the list index 0, because `'' != 0` is FALSE in JavaScript ('' and
102
+ // 0 are both coerced to 0): `a:[1]&[2]` reported its conflict at
103
+ // `$.a` while `a:[1,5]&[1,6]` reported `$.a.1`, so the one index a
104
+ // reader is most likely to meet was the one silently erased, and a
105
+ // nested `a:[[1]]&[[2]]` lost both segments (issue #37). Numeric
106
+ // segments arrive here as numbers, so only `===`/`!==` compares them
107
+ // for what they are. `null != p` stays loose on purpose -- it is the
108
+ // idiomatic null-and-undefined test.
109
+ let path = ['$', ...err.path].filter((p: any) => null != p && '' !== p)
68
110
 
69
111
  // '$' is neither null nor '', so the filter always leaves it.
70
112
  let valpath = path.join('.')
@@ -74,7 +116,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
74
116
 
75
117
  err.msg = [
76
118
  errmsg({
77
- color: { active: true },
119
+ color: { active: colorActive() },
78
120
  name: 'aontu',
79
121
  code: err.why,
80
122
  txts: {
@@ -90,7 +132,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
90
132
 
91
133
  (null != v1 && errmsg({
92
134
  // TODO: color should come from jsonic config
93
- color: { active: true, line: '\x1b[34m' },
135
+ color: { active: colorActive(), line: '\x1b[34m' },
94
136
  txts: {
95
137
  msg: 'Cannot ' + attempt + ' value: ' + v1.canon +
96
138
  (null == v2 ? '' : ' with value: ' + v2.canon), // + ' #' + err.id,
@@ -106,7 +148,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
106
148
 
107
149
  (null != v2 && errmsg({
108
150
  // TODO: color should come from jsonic config
109
- color: { active: true, line: '\x1b[34m' },
151
+ color: { active: colorActive(), line: '\x1b[34m' },
110
152
  txts: {
111
153
  msg: 'Cannot ' + attempt + ' value: ' + v2.canon +
112
154
  ' with value: ' + v1.canon, // + ' #' + err.id,
@@ -144,7 +186,7 @@ function resolveFile(url: string | undefined) {
144
186
  }
145
187
 
146
188
 
147
- function resolveSrc(v: Val, errctx: ErrContext | undefined, position: string) {
189
+ function resolveSrc(v: Val, errctx: ErrContext | undefined) {
148
190
  let src: string | undefined = undefined
149
191
  const url = v?.site.url
150
192
 
@@ -201,7 +243,7 @@ class AontuError extends Error {
201
243
  }
202
244
 
203
245
  errs: () => NilVal[]
204
- } /* node:coverage ignore next 9 */
246
+ } /* node:coverage ignore next 11 */
205
247
 
206
248
 
207
249
  export {
@@ -209,4 +251,6 @@ export {
209
251
  makeNilErr,
210
252
  descErr,
211
253
  AontuError,
254
+ setColor,
255
+ colorActive,
212
256
  }
package/src/graph.ts ADDED
@@ -0,0 +1,135 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // THE DERIVED STRUCTURES (G4 phase 3,
4
+ // docs/capability-review/g4-identity-relations.md): an evaluated
5
+ // document has, besides its value, a GRAPH — an entity index (id → the
6
+ // tree paths that hold it) and an edge set (the checked links, each
7
+ // from one entity to one address).
8
+ //
9
+ // G4's deliverable is that these exist and are DETERMINISTIC. What is
10
+ // built on them — impact analysis ("what reaches svc/auth?"),
11
+ // reachability, context-window-sized entity slices — is a traversal,
12
+ // and its exposure as verbs and projections belongs to G7. Relation
13
+ // properties (acyclicity, inverse consistency) are G4 phase 5's, and
14
+ // consume exactly this edge set.
15
+
16
+ import type { Val } from './type'
17
+
18
+ import { cmpCodePoint } from './keyorder'
19
+
20
+
21
+ export type EntityEntry = {
22
+ // The id, as `id(name)` spelled it.
23
+ id: string
24
+ // Every tree path that holds this entity, in code-point order. More
25
+ // than one is the normal case: the merge puts the entity's value at
26
+ // every position that declared it.
27
+ paths: string[]
28
+ }
29
+
30
+ export type Edge = {
31
+ // The entity the link is INSIDE — the nearest identified ancestor,
32
+ // or '' for a link outside every entity. This is the
33
+ // entity/component distinction: a node without an id is a component
34
+ // of its nearest identified ancestor.
35
+ from: string
36
+ // The RELATION: the nearest map key on the way down from the entity,
37
+ // so a link inside a list (`dependsOn: [&: refer(), svc/auth]`) is an
38
+ // edge under `dependsOn` rather than under `0`.
39
+ key: string
40
+ // The address, as the link spells it.
41
+ to: string
42
+ // Where the link is, as a `$.dotted.path`, so a report can point at
43
+ // it.
44
+ at: string
45
+ }
46
+
47
+ export type Graph = {
48
+ entities: EntityEntry[]
49
+ edges: Edge[]
50
+ }
51
+
52
+
53
+ const formatPath = (path: string[]): string =>
54
+ 0 === path.length ? '$' : '$.' + path.join('.')
55
+
56
+
57
+ // The nearest map key on the path below an entity: list indices are
58
+ // positions within a relation, not relations of their own. Digits-only
59
+ // segments are the indices, which is exactly how the rest of the engine
60
+ // spells them.
61
+ const relationKey = (tail: string[]): string => {
62
+ for (let i = tail.length - 1; 0 <= i; i--) {
63
+ if (!/^[0-9]+$/.test(tail[i])) {
64
+ return tail[i]
65
+ }
66
+ }
67
+ return ''
68
+ }
69
+
70
+
71
+ // The graph of an evaluated tree. Walks POSITIONS, not values: two
72
+ // positions of one entity share a value object after the merge, so a
73
+ // walk guarded by object identity would find the entity once and miss
74
+ // every other place it is declared. The guard is therefore the
75
+ // ancestor chain — which is what a cycle actually is.
76
+ export function graphOf(root: Val): Graph {
77
+ const byId = new Map<string, string[]>()
78
+ const edges: Edge[] = []
79
+
80
+ const visit = (
81
+ node: any, path: string[], entity: string, tail: string[],
82
+ ancestors: Set<any>
83
+ ): void => {
84
+ if (null == node || true !== node.isVal || ancestors.has(node)) {
85
+ return
86
+ }
87
+
88
+ let inside = entity
89
+ let below = tail
90
+ const name = node.entity
91
+ if (null != name) {
92
+ let paths = byId.get(name)
93
+ if (undefined === paths) {
94
+ paths = []
95
+ byId.set(name, paths)
96
+ }
97
+ paths.push(formatPath(path))
98
+ // A nested entity is not a component of the one above it: the
99
+ // key path restarts at the identified node.
100
+ inside = name
101
+ below = []
102
+ }
103
+
104
+ const link = node.link
105
+ if (null != link) {
106
+ edges.push({
107
+ from: inside,
108
+ key: relationKey(below),
109
+ to: link,
110
+ at: formatPath(path),
111
+ })
112
+ }
113
+
114
+ if ((true === node.isMap || true === node.isList) && null != node.peg) {
115
+ ancestors.add(node)
116
+ for (const k of Object.keys(node.peg)) {
117
+ visit(node.peg[k], [...path, k], inside, [...below, k], ancestors)
118
+ }
119
+ ancestors.delete(node)
120
+ }
121
+ }
122
+
123
+ visit(root, [], '', [], new Set())
124
+
125
+ // DETERMINISTIC by construction, not by luck: ids in code-point
126
+ // order, each id's paths in code-point order, edges by the position
127
+ // they are written at (which is unique — one link, one place).
128
+ const entities: EntityEntry[] = [...byId.keys()]
129
+ .sort(cmpCodePoint)
130
+ .map((id) => ({ id, paths: (byId.get(id) as string[]).sort(cmpCodePoint) }))
131
+
132
+ edges.sort((a, b) => cmpCodePoint(a.at, b.at))
133
+
134
+ return { entities, edges }
135
+ }