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.
- package/README.md +88 -0
- package/bin/aontu-mcp.js +4 -0
- package/dist/agentsmd.d.ts +16 -0
- package/dist/agentsmd.js +107 -0
- package/dist/agentsmd.js.map +1 -0
- package/dist/aontu.d.ts +14 -3
- package/dist/aontu.js +145 -4
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +44 -1
- package/dist/cli.js +2401 -44
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +16 -0
- package/dist/ctx.js +44 -0
- package/dist/ctx.js.map +1 -1
- package/dist/diff.d.ts +22 -0
- package/dist/diff.js +141 -0
- package/dist/diff.js.map +1 -0
- package/dist/err.d.ts +3 -1
- package/dist/err.js +48 -8
- package/dist/err.js.map +1 -1
- package/dist/graph.d.ts +16 -0
- package/dist/graph.js +73 -0
- package/dist/graph.js.map +1 -0
- package/dist/hcanon.d.ts +3 -0
- package/dist/hcanon.js +146 -0
- package/dist/hcanon.js.map +1 -0
- package/dist/hints.js +223 -5
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.d.ts +20 -0
- package/dist/jsonschema.js +391 -0
- package/dist/jsonschema.js.map +1 -0
- package/dist/lang.js +698 -35
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +9 -2
- package/dist/lsp.js +262 -46
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.d.ts +20 -0
- package/dist/mcp-server.js +147 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.js +814 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mod-tool.d.ts +58 -0
- package/dist/mod-tool.js +498 -0
- package/dist/mod-tool.js.map +1 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +250 -0
- package/dist/mod.js.map +1 -0
- package/dist/patch.d.ts +44 -0
- package/dist/patch.js +506 -0
- package/dist/patch.js.map +1 -0
- package/dist/provenance.d.ts +40 -0
- package/dist/provenance.js +335 -0
- package/dist/provenance.js.map +1 -0
- package/dist/query.d.ts +27 -0
- package/dist/query.js +294 -0
- package/dist/query.js.map +1 -0
- package/dist/reach.d.ts +14 -0
- package/dist/reach.js +140 -0
- package/dist/reach.js.map +1 -0
- package/dist/relation.d.ts +19 -0
- package/dist/relation.js +305 -0
- package/dist/relation.js.map +1 -0
- package/dist/report-sarif.d.ts +14 -0
- package/dist/report-sarif.js +102 -0
- package/dist/report-sarif.js.map +1 -0
- package/dist/site.d.ts +4 -0
- package/dist/site.js +31 -0
- package/dist/site.js.map +1 -1
- package/dist/std.d.ts +1 -0
- package/dist/std.js +73 -0
- package/dist/std.js.map +1 -0
- package/dist/subsume.d.ts +39 -0
- package/dist/subsume.js +526 -0
- package/dist/subsume.js.map +1 -0
- package/dist/trim.d.ts +19 -0
- package/dist/trim.js +155 -0
- package/dist/trim.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +17 -1
- package/dist/type.js.map +1 -1
- package/dist/unify.d.ts +3 -1
- package/dist/unify.js +287 -18
- package/dist/unify.js.map +1 -1
- package/dist/utility.d.ts +9 -1
- package/dist/utility.js +122 -1
- package/dist/utility.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +33 -0
- package/dist/val/AggFuncVal.js +202 -0
- package/dist/val/AggFuncVal.js.map +1 -0
- package/dist/val/ArithFuncVal.d.ts +31 -0
- package/dist/val/ArithFuncVal.js +62 -0
- package/dist/val/ArithFuncVal.js.map +1 -0
- package/dist/val/BagVal.d.ts +5 -0
- package/dist/val/BagVal.js +96 -5
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/CloseFuncVal.js +9 -1
- package/dist/val/CloseFuncVal.js.map +1 -1
- package/dist/val/ConjunctVal.d.ts +1 -1
- package/dist/val/ConjunctVal.js +19 -0
- package/dist/val/ConjunctVal.js.map +1 -1
- package/dist/val/ConstraintVal.d.ts +48 -1
- package/dist/val/ConstraintVal.js +1501 -110
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/CopyFuncVal.d.ts +1 -2
- package/dist/val/CopyFuncVal.js +7 -0
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/Decimal.d.ts +1 -0
- package/dist/val/Decimal.js +13 -0
- package/dist/val/Decimal.js.map +1 -1
- package/dist/val/DeprecateFuncVal.d.ts +11 -0
- package/dist/val/DeprecateFuncVal.js +47 -0
- package/dist/val/DeprecateFuncVal.js.map +1 -0
- package/dist/val/DisjunctVal.js +130 -21
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/EachFuncVal.d.ts +15 -0
- package/dist/val/EachFuncVal.js +75 -0
- package/dist/val/EachFuncVal.js.map +1 -0
- package/dist/val/ExpectVal.d.ts +1 -0
- package/dist/val/ExpectVal.js +41 -4
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FeatureVal.js +1 -1
- package/dist/val/FeatureVal.js.map +1 -1
- package/dist/val/FilterFuncVal.d.ts +15 -0
- package/dist/val/FilterFuncVal.js +91 -0
- package/dist/val/FilterFuncVal.js.map +1 -0
- package/dist/val/FuncBaseVal.d.ts +6 -1
- package/dist/val/FuncBaseVal.js +178 -2
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/HideFuncVal.js.map +1 -1
- package/dist/val/IdFuncVal.d.ts +13 -0
- package/dist/val/IdFuncVal.js +54 -0
- package/dist/val/IdFuncVal.js.map +1 -0
- package/dist/val/JunctionVal.js +7 -1
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/KeyFuncVal.d.ts +1 -1
- package/dist/val/KeyFuncVal.js +38 -30
- package/dist/val/KeyFuncVal.js.map +1 -1
- package/dist/val/ListVal.js +117 -17
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +102 -8
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.d.ts +15 -0
- package/dist/val/MatchFuncVal.js +107 -0
- package/dist/val/MatchFuncVal.js.map +1 -0
- package/dist/val/MoveFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +24 -0
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -1
- package/dist/val/OpBaseVal.js +24 -2
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/OpenFuncVal.js +4 -1
- package/dist/val/OpenFuncVal.js.map +1 -1
- package/dist/val/PackFuncVal.d.ts +15 -0
- package/dist/val/PackFuncVal.js +108 -0
- package/dist/val/PackFuncVal.js.map +1 -0
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +13 -0
- package/dist/val/PlaceVal.js +131 -0
- package/dist/val/PlaceVal.js.map +1 -0
- package/dist/val/PlusOpVal.js +11 -2
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefFuncVal.js.map +1 -1
- package/dist/val/PrefVal.d.ts +2 -2
- package/dist/val/PrefVal.js +78 -23
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -1
- package/dist/val/RefVal.js +158 -30
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.d.ts +36 -0
- package/dist/val/ReferFuncVal.js +303 -0
- package/dist/val/ReferFuncVal.js.map +1 -0
- package/dist/val/ScalarKindVal.d.ts +1 -2
- package/dist/val/ScalarKindVal.js +0 -11
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/TopVal.js.map +1 -1
- package/dist/val/TypeFuncVal.js.map +1 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/Val.d.ts +9 -2
- package/dist/val/Val.js +150 -4
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js.map +1 -1
- package/dist/val/arith.d.ts +6 -0
- package/dist/val/arith.js +170 -0
- package/dist/val/arith.js.map +1 -0
- package/dist/vet.d.ts +45 -0
- package/dist/vet.js +776 -0
- package/dist/vet.js.map +1 -0
- package/dist/walk.d.ts +2 -0
- package/dist/walk.js +91 -0
- package/dist/walk.js.map +1 -0
- package/grammar/aontu.gbnf +130 -0
- package/grammar/aontu.lark +113 -0
- package/package.json +30 -15
- package/skill/SKILL.md +37 -0
- package/skill/error-codes.md +62 -0
- package/skill/examples.md +99 -0
- package/skill/grammar-card.md +57 -0
- package/src/agentsmd.ts +135 -0
- package/src/aontu.ts +192 -4
- package/src/cli.ts +2858 -71
- package/src/ctx.ts +81 -0
- package/src/diff.ts +196 -0
- package/src/err.ts +52 -8
- package/src/graph.ts +135 -0
- package/src/hcanon.ts +169 -0
- package/src/hints.ts +271 -5
- package/src/jsonschema.ts +511 -0
- package/src/lang.ts +779 -37
- package/src/lsp.ts +281 -47
- package/src/mcp-server.ts +187 -0
- package/src/mcp.ts +993 -0
- package/src/mod-tool.ts +679 -0
- package/src/mod.ts +344 -0
- package/src/patch.ts +624 -0
- package/src/provenance.ts +430 -0
- package/src/query.ts +379 -0
- package/src/reach.ts +184 -0
- package/src/relation.ts +395 -0
- package/src/report-sarif.ts +137 -0
- package/src/site.ts +36 -1
- package/src/std.ts +73 -0
- package/src/subsume.ts +690 -0
- package/src/trim.ts +195 -0
- package/src/tsconfig.json +10 -4
- package/src/type.ts +51 -2
- package/src/unify.ts +311 -16
- package/src/utility.ts +139 -1
- package/src/val/AggFuncVal.ts +319 -0
- package/src/val/ArithFuncVal.ts +108 -0
- package/src/val/BagVal.ts +101 -4
- package/src/val/CloseFuncVal.ts +9 -1
- package/src/val/ConjunctVal.ts +20 -0
- package/src/val/ConstraintVal.ts +1699 -116
- package/src/val/CopyFuncVal.ts +7 -1
- package/src/val/Decimal.ts +15 -0
- package/src/val/DeprecateFuncVal.ts +84 -0
- package/src/val/DisjunctVal.ts +139 -28
- package/src/val/EachFuncVal.ts +133 -0
- package/src/val/ExpectVal.ts +43 -6
- package/src/val/FeatureVal.ts +1 -1
- package/src/val/FilterFuncVal.ts +154 -0
- package/src/val/FuncBaseVal.ts +200 -3
- package/src/val/HideFuncVal.ts +0 -2
- package/src/val/IdFuncVal.ts +91 -0
- package/src/val/JunctionVal.ts +7 -1
- package/src/val/KeyFuncVal.ts +39 -35
- package/src/val/ListVal.ts +125 -18
- package/src/val/LowerFuncVal.ts +0 -1
- package/src/val/MapVal.ts +110 -8
- package/src/val/MatchFuncVal.ts +176 -0
- package/src/val/MoveFuncVal.ts +0 -2
- package/src/val/NilVal.ts +25 -0
- package/src/val/OpBaseVal.ts +26 -3
- package/src/val/OpenFuncVal.ts +4 -2
- package/src/val/PackFuncVal.ts +175 -0
- package/src/val/PathFuncVal.ts +0 -1
- package/src/val/PlaceVal.ts +193 -0
- package/src/val/PlusOpVal.ts +11 -2
- package/src/val/PrefFuncVal.ts +0 -1
- package/src/val/PrefVal.ts +79 -36
- package/src/val/RefVal.ts +163 -30
- package/src/val/ReferFuncVal.ts +387 -0
- package/src/val/ScalarKindVal.ts +0 -13
- package/src/val/TopVal.ts +0 -1
- package/src/val/TypeFuncVal.ts +0 -2
- package/src/val/UpperFuncVal.ts +0 -1
- package/src/val/Val.ts +213 -3
- package/src/val/VarVal.ts +0 -1
- package/src/val/arith.ts +316 -0
- package/src/vet.ts +992 -0
- 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
|
|
65
|
-
let v2src = resolveSrc(v2, errctx
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
+
}
|