aontu 0.52.1 → 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 (272) 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 +96 -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 +13 -0
  13. package/dist/ctx.js +43 -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 +38 -7
  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 +167 -4
  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 +512 -69
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -47
  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 +2 -1
  83. package/dist/unify.js +253 -36
  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 +7 -1
  103. package/dist/val/ConstraintVal.js +490 -40
  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.js +27 -4
  120. package/dist/val/ExpectVal.js.map +1 -1
  121. package/dist/val/FeatureVal.js +1 -1
  122. package/dist/val/FeatureVal.js.map +1 -1
  123. package/dist/val/FilterFuncVal.d.ts +15 -0
  124. package/dist/val/FilterFuncVal.js +91 -0
  125. package/dist/val/FilterFuncVal.js.map +1 -0
  126. package/dist/val/FuncBaseVal.d.ts +6 -1
  127. package/dist/val/FuncBaseVal.js +171 -1
  128. package/dist/val/FuncBaseVal.js.map +1 -1
  129. package/dist/val/HideFuncVal.js.map +1 -1
  130. package/dist/val/IdFuncVal.d.ts +13 -0
  131. package/dist/val/IdFuncVal.js +54 -0
  132. package/dist/val/IdFuncVal.js.map +1 -0
  133. package/dist/val/JunctionVal.js +7 -1
  134. package/dist/val/JunctionVal.js.map +1 -1
  135. package/dist/val/KeyFuncVal.d.ts +1 -1
  136. package/dist/val/KeyFuncVal.js +38 -30
  137. package/dist/val/KeyFuncVal.js.map +1 -1
  138. package/dist/val/ListVal.js +101 -14
  139. package/dist/val/ListVal.js.map +1 -1
  140. package/dist/val/LowerFuncVal.js.map +1 -1
  141. package/dist/val/MapVal.js +93 -8
  142. package/dist/val/MapVal.js.map +1 -1
  143. package/dist/val/MatchFuncVal.d.ts +15 -0
  144. package/dist/val/MatchFuncVal.js +107 -0
  145. package/dist/val/MatchFuncVal.js.map +1 -0
  146. package/dist/val/MoveFuncVal.js.map +1 -1
  147. package/dist/val/NilVal.js +24 -0
  148. package/dist/val/NilVal.js.map +1 -1
  149. package/dist/val/OpBaseVal.d.ts +1 -1
  150. package/dist/val/OpBaseVal.js +19 -1
  151. package/dist/val/OpBaseVal.js.map +1 -1
  152. package/dist/val/OpenFuncVal.js +4 -1
  153. package/dist/val/OpenFuncVal.js.map +1 -1
  154. package/dist/val/PackFuncVal.d.ts +15 -0
  155. package/dist/val/PackFuncVal.js +108 -0
  156. package/dist/val/PackFuncVal.js.map +1 -0
  157. package/dist/val/PathFuncVal.js.map +1 -1
  158. package/dist/val/PlaceVal.d.ts +13 -0
  159. package/dist/val/PlaceVal.js +131 -0
  160. package/dist/val/PlaceVal.js.map +1 -0
  161. package/dist/val/PlusOpVal.js +11 -2
  162. package/dist/val/PlusOpVal.js.map +1 -1
  163. package/dist/val/PrefFuncVal.js.map +1 -1
  164. package/dist/val/PrefVal.d.ts +2 -2
  165. package/dist/val/PrefVal.js +78 -23
  166. package/dist/val/PrefVal.js.map +1 -1
  167. package/dist/val/RefVal.d.ts +1 -1
  168. package/dist/val/RefVal.js +89 -7
  169. package/dist/val/RefVal.js.map +1 -1
  170. package/dist/val/ReferFuncVal.d.ts +36 -0
  171. package/dist/val/ReferFuncVal.js +303 -0
  172. package/dist/val/ReferFuncVal.js.map +1 -0
  173. package/dist/val/ScalarKindVal.d.ts +1 -2
  174. package/dist/val/ScalarKindVal.js +0 -11
  175. package/dist/val/ScalarKindVal.js.map +1 -1
  176. package/dist/val/TopVal.js.map +1 -1
  177. package/dist/val/TypeFuncVal.js.map +1 -1
  178. package/dist/val/UpperFuncVal.js.map +1 -1
  179. package/dist/val/Val.d.ts +8 -1
  180. package/dist/val/Val.js +150 -1
  181. package/dist/val/Val.js.map +1 -1
  182. package/dist/val/VarVal.js.map +1 -1
  183. package/dist/val/arith.d.ts +6 -0
  184. package/dist/val/arith.js +170 -0
  185. package/dist/val/arith.js.map +1 -0
  186. package/dist/vet.d.ts +45 -0
  187. package/dist/vet.js +776 -0
  188. package/dist/vet.js.map +1 -0
  189. package/dist/walk.d.ts +2 -0
  190. package/dist/walk.js +91 -0
  191. package/dist/walk.js.map +1 -0
  192. package/grammar/aontu.gbnf +130 -0
  193. package/grammar/aontu.lark +113 -0
  194. package/package.json +21 -6
  195. package/skill/SKILL.md +37 -0
  196. package/skill/error-codes.md +62 -0
  197. package/skill/examples.md +99 -0
  198. package/skill/grammar-card.md +57 -0
  199. package/src/agentsmd.ts +135 -0
  200. package/src/aontu.ts +135 -4
  201. package/src/cli.ts +2858 -71
  202. package/src/ctx.ts +73 -1
  203. package/src/diff.ts +196 -0
  204. package/src/err.ts +42 -7
  205. package/src/graph.ts +135 -0
  206. package/src/hcanon.ts +169 -0
  207. package/src/hints.ts +208 -4
  208. package/src/jsonschema.ts +511 -0
  209. package/src/lang.ts +570 -70
  210. package/src/lsp.ts +281 -48
  211. package/src/mcp-server.ts +187 -0
  212. package/src/mcp.ts +993 -0
  213. package/src/mod-tool.ts +679 -0
  214. package/src/mod.ts +344 -0
  215. package/src/patch.ts +624 -0
  216. package/src/provenance.ts +430 -0
  217. package/src/query.ts +379 -0
  218. package/src/reach.ts +184 -0
  219. package/src/relation.ts +395 -0
  220. package/src/report-sarif.ts +137 -0
  221. package/src/site.ts +36 -1
  222. package/src/std.ts +73 -0
  223. package/src/subsume.ts +690 -0
  224. package/src/trim.ts +195 -0
  225. package/src/tsconfig.json +10 -4
  226. package/src/type.ts +51 -2
  227. package/src/unify.ts +274 -36
  228. package/src/utility.ts +139 -1
  229. package/src/val/AggFuncVal.ts +319 -0
  230. package/src/val/ArithFuncVal.ts +108 -0
  231. package/src/val/BagVal.ts +101 -4
  232. package/src/val/CloseFuncVal.ts +9 -1
  233. package/src/val/ConjunctVal.ts +20 -0
  234. package/src/val/ConstraintVal.ts +542 -43
  235. package/src/val/CopyFuncVal.ts +7 -1
  236. package/src/val/Decimal.ts +15 -0
  237. package/src/val/DeprecateFuncVal.ts +84 -0
  238. package/src/val/DisjunctVal.ts +139 -28
  239. package/src/val/EachFuncVal.ts +133 -0
  240. package/src/val/ExpectVal.ts +28 -6
  241. package/src/val/FeatureVal.ts +1 -1
  242. package/src/val/FilterFuncVal.ts +154 -0
  243. package/src/val/FuncBaseVal.ts +192 -1
  244. package/src/val/HideFuncVal.ts +0 -2
  245. package/src/val/IdFuncVal.ts +91 -0
  246. package/src/val/JunctionVal.ts +7 -1
  247. package/src/val/KeyFuncVal.ts +39 -35
  248. package/src/val/ListVal.ts +108 -15
  249. package/src/val/LowerFuncVal.ts +0 -1
  250. package/src/val/MapVal.ts +99 -8
  251. package/src/val/MatchFuncVal.ts +176 -0
  252. package/src/val/MoveFuncVal.ts +0 -2
  253. package/src/val/NilVal.ts +25 -0
  254. package/src/val/OpBaseVal.ts +20 -1
  255. package/src/val/OpenFuncVal.ts +4 -2
  256. package/src/val/PackFuncVal.ts +175 -0
  257. package/src/val/PathFuncVal.ts +0 -1
  258. package/src/val/PlaceVal.ts +193 -0
  259. package/src/val/PlusOpVal.ts +11 -2
  260. package/src/val/PrefFuncVal.ts +0 -1
  261. package/src/val/PrefVal.ts +79 -36
  262. package/src/val/RefVal.ts +91 -8
  263. package/src/val/ReferFuncVal.ts +387 -0
  264. package/src/val/ScalarKindVal.ts +0 -13
  265. package/src/val/TopVal.ts +0 -1
  266. package/src/val/TypeFuncVal.ts +0 -2
  267. package/src/val/UpperFuncVal.ts +0 -1
  268. package/src/val/Val.ts +205 -2
  269. package/src/val/VarVal.ts +0 -1
  270. package/src/val/arith.ts +316 -0
  271. package/src/vet.ts +992 -0
  272. 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,7 +115,7 @@ class AontuContext {
71
115
  _pathTrie: Map<number, Map<string, { idx: number, path: string[] }>>
72
116
  _pathidxNext: { n: number }
73
117
 
74
- // Current `unite` recursion depth, checked against MAXDEPTH
118
+ // Current `unite` recursion depth, checked against the depth budget
75
119
  // (ts/src/unify.ts). Held in a shared mutable box, like _pathidxNext,
76
120
  // because clone() uses Object.create: the box is inherited by
77
121
  // reference, so a nested clone's increments are visible to the frame
@@ -79,6 +123,21 @@ class AontuContext {
79
123
  // directly on its Ctx pointer (go/unify.go, maxUniteDepth).
80
124
  _depth: { n: number }
81
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
+
82
141
  // Trial mode: set by DisjunctVal.unify while each member is tried
83
142
  // against the peer. When true, makeNilErr returns the shared
84
143
  // TRIAL_NIL sentinel instead of allocating a fresh NilVal, and
@@ -102,6 +161,7 @@ class AontuContext {
102
161
  this.src = cfg.src
103
162
 
104
163
  this.collect = cfg.collect ?? null != cfg.err
164
+ this.prov = cfg.prov
105
165
 
106
166
  this.err = cfg.err ?? []
107
167
  this.explain = Array.isArray(cfg.explain) ? cfg.explain : null
@@ -127,8 +187,20 @@ class AontuContext {
127
187
  this._depth = { n: 0 }
128
188
  this._pathidx = 0
129
189
 
190
+ this.manifest = []
191
+
130
192
  this.opts = DEFAULT_OPTS()
131
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
+ }
132
204
  }
133
205
 
134
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,8 +94,8 @@ 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
100
  // STRICT `!==` against the empty string. The loose `!=` here dropped
68
101
  // the list index 0, because `'' != 0` is FALSE in JavaScript ('' and
@@ -83,7 +116,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
83
116
 
84
117
  err.msg = [
85
118
  errmsg({
86
- color: { active: true },
119
+ color: { active: colorActive() },
87
120
  name: 'aontu',
88
121
  code: err.why,
89
122
  txts: {
@@ -99,7 +132,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
99
132
 
100
133
  (null != v1 && errmsg({
101
134
  // TODO: color should come from jsonic config
102
- color: { active: true, line: '\x1b[34m' },
135
+ color: { active: colorActive(), line: '\x1b[34m' },
103
136
  txts: {
104
137
  msg: 'Cannot ' + attempt + ' value: ' + v1.canon +
105
138
  (null == v2 ? '' : ' with value: ' + v2.canon), // + ' #' + err.id,
@@ -115,7 +148,7 @@ function descErr<NILS extends NilVal | NilVal[]>(
115
148
 
116
149
  (null != v2 && errmsg({
117
150
  // TODO: color should come from jsonic config
118
- color: { active: true, line: '\x1b[34m' },
151
+ color: { active: colorActive(), line: '\x1b[34m' },
119
152
  txts: {
120
153
  msg: 'Cannot ' + attempt + ' value: ' + v2.canon +
121
154
  ' with value: ' + v1.canon, // + ' #' + err.id,
@@ -153,7 +186,7 @@ function resolveFile(url: string | undefined) {
153
186
  }
154
187
 
155
188
 
156
- function resolveSrc(v: Val, errctx: ErrContext | undefined, position: string) {
189
+ function resolveSrc(v: Val, errctx: ErrContext | undefined) {
157
190
  let src: string | undefined = undefined
158
191
  const url = v?.site.url
159
192
 
@@ -210,7 +243,7 @@ class AontuError extends Error {
210
243
  }
211
244
 
212
245
  errs: () => NilVal[]
213
- } /* node:coverage ignore next 9 */
246
+ } /* node:coverage ignore next 11 */
214
247
 
215
248
 
216
249
  export {
@@ -218,4 +251,6 @@ export {
218
251
  makeNilErr,
219
252
  descErr,
220
253
  AontuError,
254
+ setColor,
255
+ colorActive,
221
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
+ }