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/query.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// THE QUERY SURFACE (G7 phase 1,
|
|
4
|
+
// docs/capability-review/g7-machine-access.md): select one node of an
|
|
5
|
+
// evaluated document by path and render it — the slice an agent asks
|
|
6
|
+
// for, instead of the whole file as one JSON blob.
|
|
7
|
+
//
|
|
8
|
+
// Evaluation is still GLOBAL. Unification has no partial mode to sell:
|
|
9
|
+
// the whole document is evaluated and then one node is selected. What
|
|
10
|
+
// `get` buys is the SIZE OF THE ANSWER, not the cost of producing it.
|
|
11
|
+
//
|
|
12
|
+
// The three projections are lattice ABSTRACTIONS, and each is defined
|
|
13
|
+
// so that the view it prints is a valid Aontu document that SUBSUMES
|
|
14
|
+
// the truth — generalisation, never distortion:
|
|
15
|
+
//
|
|
16
|
+
// - `types` replaces every concrete leaf with its own kind, using
|
|
17
|
+
// the lattice's own superior() rather than a table of this file's
|
|
18
|
+
// opinions: {"replicas":3} becomes {"replicas":integer}.
|
|
19
|
+
// - `depth n` keeps structure to depth n and renders every elided
|
|
20
|
+
// subtree as `top` — "no further information at this tier".
|
|
21
|
+
// - `keys` is `depth 1` degenerated to a listing.
|
|
22
|
+
//
|
|
23
|
+
// That property is not a promise here: every projection row of
|
|
24
|
+
// test/spec/query.tsv asserts subsume(view, truth) == 'subsumes' in
|
|
25
|
+
// both runners, which G3 made mechanically checkable.
|
|
26
|
+
//
|
|
27
|
+
// Projections are NOT canonical form and are never fed to G6's hash,
|
|
28
|
+
// which is why they are named views rather than spellings of --canon.
|
|
29
|
+
|
|
30
|
+
import { Aontu } from './aontu'
|
|
31
|
+
import type { TrustOptions } from './type'
|
|
32
|
+
import { exactJSON } from './exactjson'
|
|
33
|
+
import { anchorAt } from './vet'
|
|
34
|
+
import type { VetFinding } from './vet'
|
|
35
|
+
import { cmpCodePoint } from './keyorder'
|
|
36
|
+
import { Provenance } from './provenance'
|
|
37
|
+
import type { WhyRecord } from './provenance'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export type QueryView = 'json' | 'canon' | 'types' | 'keys'
|
|
41
|
+
|
|
42
|
+
export type QueryOptions = {
|
|
43
|
+
view?: QueryView
|
|
44
|
+
// Levels of structure kept below the selected node; everything
|
|
45
|
+
// deeper renders as `top`. Undefined means the whole subtree.
|
|
46
|
+
depth?: number
|
|
47
|
+
// Where the document CAME FROM, so a relative `@"file"` load inside
|
|
48
|
+
// it resolves from its own directory (vet's schemaPath precedent).
|
|
49
|
+
path?: string
|
|
50
|
+
|
|
51
|
+
// The trust profile this run evaluates under (G5, docs/trust.md).
|
|
52
|
+
// The source arrives from a caller, so the caller must be able to
|
|
53
|
+
// say what it may reach: without this the include chain is the
|
|
54
|
+
// default one, and `@"x.js"` is arbitrary code execution in the
|
|
55
|
+
// evaluating process. A server passes `{include:'none'}`.
|
|
56
|
+
trust?: TrustOptions
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type QueryReport = {
|
|
60
|
+
ok: boolean
|
|
61
|
+
out: string
|
|
62
|
+
// Empty when ok. G2's finding shape, deliberately: `get` invents no
|
|
63
|
+
// error format (G7's own rule).
|
|
64
|
+
findings: VetFinding[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const TOP = 'top'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// The nearest key at the parent of a path that named nothing, by a
|
|
72
|
+
// plain edit distance over the sibling names — the "did you mean"
|
|
73
|
+
// half of the no_path contract. Undefined when nothing is close
|
|
74
|
+
// enough to be worth suggesting.
|
|
75
|
+
export function nearestKey(
|
|
76
|
+
want: string, have: string[]): string | undefined {
|
|
77
|
+
let best: string | undefined
|
|
78
|
+
let bestd = Infinity
|
|
79
|
+
for (const k of have) {
|
|
80
|
+
const d = editDistance(want, k)
|
|
81
|
+
if (d < bestd) {
|
|
82
|
+
bestd = d
|
|
83
|
+
best = k
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Half the name may differ, no more: past that the suggestion is
|
|
87
|
+
// noise, and a wrong suggestion costs more than none.
|
|
88
|
+
return bestd <= Math.max(1, Math.floor(want.length / 2)) ? best : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
function editDistance(a: string, b: string): number {
|
|
93
|
+
const prev: number[] = []
|
|
94
|
+
for (let j = 0; j <= b.length; j++) {
|
|
95
|
+
prev[j] = j
|
|
96
|
+
}
|
|
97
|
+
for (let i = 1; i <= a.length; i++) {
|
|
98
|
+
let diag = prev[0]
|
|
99
|
+
prev[0] = i
|
|
100
|
+
for (let j = 1; j <= b.length; j++) {
|
|
101
|
+
const tmp = prev[j]
|
|
102
|
+
prev[j] = Math.min(
|
|
103
|
+
prev[j] + 1,
|
|
104
|
+
prev[j - 1] + 1,
|
|
105
|
+
diag + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
106
|
+
diag = tmp
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return prev[b.length]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
// The path split anchorAt walks: `$` and empty segments dropped, so
|
|
114
|
+
// `$`, `$.` and `` all name the root.
|
|
115
|
+
export function pathParts(path: string): string[] {
|
|
116
|
+
const trimmed = path.startsWith('$') ? path.slice(1) : path
|
|
117
|
+
return trimmed.split('.').filter((p) => '' !== p)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// The queried path, normalised the way anchorAt reads it — so a
|
|
122
|
+
// finding names `$.a.b` whether the caller wrote that, `a.b` or
|
|
123
|
+
// `$.a.b.` — and `$` for the root.
|
|
124
|
+
function pathText(path: string): string {
|
|
125
|
+
const parts = pathParts(path)
|
|
126
|
+
return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
// The projection walk, exported for the direct unit tests (ADR-002,
|
|
131
|
+
// ts/test/coverage3.test.ts): a junction member that is itself a
|
|
132
|
+
// junction of more than one term keeps its parens, and no SOURCE
|
|
133
|
+
// reaches that arm because norm flattens junctions at unification.
|
|
134
|
+
export function projectFor(
|
|
135
|
+
v: any, view: QueryView, depth: number): string {
|
|
136
|
+
return project(v, view, depth)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
// The canon-shaped views. One walk, two knobs: `types` generalises
|
|
141
|
+
// each leaf through the lattice, `depth` elides below its level. Bags
|
|
142
|
+
// recurse (their canon getters would render children through plain
|
|
143
|
+
// canon, which neither knob can reach); everything else is a leaf.
|
|
144
|
+
function project(v: any, view: QueryView, depth: number): string {
|
|
145
|
+
if (depth <= 0) {
|
|
146
|
+
return TOP
|
|
147
|
+
}
|
|
148
|
+
if (true === v?.isMap) {
|
|
149
|
+
const keys = Object.keys(v.peg).sort(cmpCodePoint)
|
|
150
|
+
return '{' +
|
|
151
|
+
(v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
|
|
152
|
+
(0 < keys.length ? ',' : '') : '') +
|
|
153
|
+
keys.map((k) =>
|
|
154
|
+
JSON.stringify(k) +
|
|
155
|
+
(v.optionalKeys.includes(k) ? '?' : '') +
|
|
156
|
+
':' +
|
|
157
|
+
project(v.peg[k], view, depth - 1)).join(',') +
|
|
158
|
+
'}'
|
|
159
|
+
}
|
|
160
|
+
if (true === v?.isList) {
|
|
161
|
+
const keys = Object.keys(v.peg)
|
|
162
|
+
return '[' +
|
|
163
|
+
(v.spread.cj ? '&:' + project(v.spread.cj, view, depth - 1) +
|
|
164
|
+
(0 < keys.length ? ',' : '') : '') +
|
|
165
|
+
keys.map((k) => project(v.peg[k], view, depth - 1)).join(',') +
|
|
166
|
+
']'
|
|
167
|
+
}
|
|
168
|
+
// Junctions and prefs are TRANSPARENT: not a structural tier (so
|
|
169
|
+
// they do not spend a level of depth) but not a leaf either (so
|
|
170
|
+
// `*8080|integer` generalises to `*integer|integer` rather than
|
|
171
|
+
// collapsing to `top` and throwing the alternatives away).
|
|
172
|
+
if (true === v?.isPref) {
|
|
173
|
+
return '*' + project(v.peg, view, depth)
|
|
174
|
+
}
|
|
175
|
+
if (true === v?.isConjunct || true === v?.isDisjunct) {
|
|
176
|
+
return v.peg.map((m: any) =>
|
|
177
|
+
true === m?.isJunction && 1 < m.peg.length
|
|
178
|
+
? '(' + project(m, view, depth) + ')'
|
|
179
|
+
: project(m, view, depth))
|
|
180
|
+
.join(true === v.isConjunct ? '&' : '|')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A LEAF. Under `types` a CONCRETE scalar lifts to its own kind —
|
|
184
|
+
// superior() is the lattice's answer, so the view subsumes the truth
|
|
185
|
+
// by construction and not by this file's good intentions. Everything
|
|
186
|
+
// else is already an abstraction (a kind marker, a constraint, an
|
|
187
|
+
// unresolved reference) and is left alone: lifting `integer` to
|
|
188
|
+
// `number` would generalise a shape view that was already a shape.
|
|
189
|
+
return 'types' === view && true === v?.isScalar ? v.superior().canon : v.canon
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
// The `keys` listing: the node's own key names (or list indices), one
|
|
194
|
+
// per line, code-point ordered as canon orders them. A leaf has none,
|
|
195
|
+
// which is an empty answer rather than an error — "nothing below
|
|
196
|
+
// here" is a true statement about a scalar.
|
|
197
|
+
function keyList(v: any): string {
|
|
198
|
+
if (true === v?.isMap) {
|
|
199
|
+
return Object.keys(v.peg).sort(cmpCodePoint).join('\n')
|
|
200
|
+
}
|
|
201
|
+
if (true === v?.isList) {
|
|
202
|
+
return Object.keys(v.peg).join('\n')
|
|
203
|
+
}
|
|
204
|
+
return ''
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
function finding(
|
|
209
|
+
code: string, path: string, message: string, note?: string): VetFinding {
|
|
210
|
+
return {
|
|
211
|
+
code,
|
|
212
|
+
class: 'reference',
|
|
213
|
+
severity: 'error',
|
|
214
|
+
path,
|
|
215
|
+
message,
|
|
216
|
+
sites: [],
|
|
217
|
+
...(null == note ? {} : { note }),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
// A document that does not stand up has no node to select. The
|
|
223
|
+
// engine's own first error IS the report: the query surface adds
|
|
224
|
+
// nothing to a diagnosis the evaluator already made. The path is the
|
|
225
|
+
// DOCUMENT — what failed is the whole thing standing up, not the node
|
|
226
|
+
// the caller asked about, which may never have existed.
|
|
227
|
+
export function evalFailure(ctx: any): VetFinding {
|
|
228
|
+
// ctx.err is never empty at a call site: every failure that reaches
|
|
229
|
+
// one collected an error first — a parse that did not stand up, a
|
|
230
|
+
// root that came back nil. Not coalesced, on the vet siteOf
|
|
231
|
+
// precedent: an impossible state should fail loudly rather than be
|
|
232
|
+
// quietly papered over with a made-up code.
|
|
233
|
+
const err: any = ctx.err[0]
|
|
234
|
+
return finding(err.why, '$', err.msg)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// The refusal for a path that names nothing, shared by `get` and
|
|
239
|
+
// `why`: WHICH segment failed, and what was there instead — the "did
|
|
240
|
+
// you mean" the no_path contract promises. Walking again is cheap (the
|
|
241
|
+
// tree is in hand) and is the only way to name the parent.
|
|
242
|
+
export function noPathFinding(root: any, path: string): VetFinding {
|
|
243
|
+
const parts = pathParts(path)
|
|
244
|
+
let at: any = root
|
|
245
|
+
let want = ''
|
|
246
|
+
for (const part of parts) {
|
|
247
|
+
const next: any = anchorAt(at, part)
|
|
248
|
+
if (null == next) {
|
|
249
|
+
want = part
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
at = next
|
|
253
|
+
}
|
|
254
|
+
const have = true === at?.isMap
|
|
255
|
+
? Object.keys(at.peg).sort(cmpCodePoint)
|
|
256
|
+
: (true === at?.isList ? Object.keys(at.peg) : [])
|
|
257
|
+
const near = nearestKey(want, have)
|
|
258
|
+
return finding(
|
|
259
|
+
'no_path',
|
|
260
|
+
pathText(path),
|
|
261
|
+
`The path ${path} names nothing in this document.`,
|
|
262
|
+
null == near ? undefined : `did you mean ${near}?`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
// Evaluate the document, select the node at `path`, and render it.
|
|
267
|
+
export function get(
|
|
268
|
+
src: string, path: string, opts?: QueryOptions): QueryReport {
|
|
269
|
+
const options = opts ?? {}
|
|
270
|
+
const view: QueryView = options.view ?? 'json'
|
|
271
|
+
|
|
272
|
+
const aontu = new Aontu(
|
|
273
|
+
null == options.trust ? undefined : { trust: options.trust })
|
|
274
|
+
const ctx = aontu.ctx({ collect: true })
|
|
275
|
+
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
276
|
+
const root: any = aontu.unify(src, parseOpts, ctx)
|
|
277
|
+
|
|
278
|
+
if (0 < ctx.err.length || null == root || true === root.isNil) {
|
|
279
|
+
return { ok: false, out: '', findings: [evalFailure(ctx)] }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const node: any = anchorAt(root, path)
|
|
283
|
+
if (null == node) {
|
|
284
|
+
return { ok: false, out: '', findings: [noPathFinding(root, path)] }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if ('json' === view) {
|
|
288
|
+
// GENERATION CAN FAIL WHERE UNIFICATION DID NOT: `k: integer` is a
|
|
289
|
+
// perfectly good unified document and not a concrete value, so the
|
|
290
|
+
// json view of it is an error, exactly as `aontu file.aon` on the
|
|
291
|
+
// same document is. Under `collect` the failure lands on the
|
|
292
|
+
// context rather than throwing, so it has to be read back — the Go
|
|
293
|
+
// port's Gen returns it as an error and the two must agree.
|
|
294
|
+
const before = ctx.err.length
|
|
295
|
+
const gen = node.gen(ctx)
|
|
296
|
+
if (before < ctx.err.length) {
|
|
297
|
+
const err: any = ctx.err[before]
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
out: '',
|
|
301
|
+
findings: [finding(
|
|
302
|
+
err?.why ?? 'no_gen',
|
|
303
|
+
pathText(path),
|
|
304
|
+
err?.msg ?? 'The value at this path is not concrete.')],
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { ok: true, out: exactJSON(gen, 2), findings: [] }
|
|
308
|
+
}
|
|
309
|
+
if ('keys' === view) {
|
|
310
|
+
return { ok: true, out: keyList(node), findings: [] }
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
out: project(node, view, options.depth ?? Infinity),
|
|
315
|
+
findings: [],
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
export type WhyReport = {
|
|
321
|
+
ok: boolean
|
|
322
|
+
record?: WhyRecord
|
|
323
|
+
findings: VetFinding[]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
// WHY does the value at this path hold? Evaluate with the provenance
|
|
328
|
+
// recorder on, select the node, and answer the ordered contributions
|
|
329
|
+
// that met there — the positive twin of G2's error report.
|
|
330
|
+
//
|
|
331
|
+
// Two evaluations are NOT needed: the recorder rides the one run this
|
|
332
|
+
// call makes. What it costs is site materialisation and one map entry
|
|
333
|
+
// per path met, which an instrumented run pays knowingly.
|
|
334
|
+
export function why(
|
|
335
|
+
src: string, path: string, opts?: QueryOptions): WhyReport {
|
|
336
|
+
const options = opts ?? {}
|
|
337
|
+
const aontu = new Aontu(
|
|
338
|
+
null == options.trust ? undefined : { trust: options.trust })
|
|
339
|
+
const prov = new Provenance()
|
|
340
|
+
const ctx = aontu.ctx({ collect: true, prov })
|
|
341
|
+
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
342
|
+
|
|
343
|
+
// Parse and unify SEPARATELY, so the parsed tree can be stamped
|
|
344
|
+
// before the fixpoint runs: a contribution is a value the author
|
|
345
|
+
// wrote, and after unification there is no longer any way to tell
|
|
346
|
+
// one from a value the engine minted on the way.
|
|
347
|
+
const parsed: any = aontu.parse(src, parseOpts, ctx)
|
|
348
|
+
if (0 < ctx.err.length || null == parsed) {
|
|
349
|
+
return { ok: false, findings: [evalFailure(ctx)] }
|
|
350
|
+
}
|
|
351
|
+
prov.writtenFrom(parsed)
|
|
352
|
+
|
|
353
|
+
const root: any = aontu.unify(parsed, parseOpts, ctx)
|
|
354
|
+
if (0 < ctx.err.length || null == root || true === root.isNil) {
|
|
355
|
+
return { ok: false, findings: [evalFailure(ctx)] }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const node: any = anchorAt(root, path)
|
|
359
|
+
if (null == node) {
|
|
360
|
+
return { ok: false, findings: [noPathFinding(root, path)] }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The value that stands here is a contribution when nothing met
|
|
364
|
+
// (see Provenance.stands): a generator places a value without a
|
|
365
|
+
// meet, and "nothing met at this path" is not an answer to "where
|
|
366
|
+
// did this come from".
|
|
367
|
+
const parts = pathParts(path)
|
|
368
|
+
prov.stands(parts, node)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
record: {
|
|
373
|
+
conjuncts: prov.at(parts),
|
|
374
|
+
path: pathText(path),
|
|
375
|
+
value: node.canon,
|
|
376
|
+
},
|
|
377
|
+
findings: [],
|
|
378
|
+
}
|
|
379
|
+
}
|
package/src/reach.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// REACHABILITY OVER THE ENTITY GRAPH (the review's finding J,
|
|
4
|
+
// use-cases/REVIEW.md): "ship a transitive `reaches(a, b)` check verb".
|
|
5
|
+
//
|
|
6
|
+
// `relations` answers questions about the edge set as a whole --- is it
|
|
7
|
+
// acyclic, does every edge have its inverse, is every far end what the
|
|
8
|
+
// relation says it is. This answers the question that needs the CLOSURE
|
|
9
|
+
// rather than the edges: does anything `a` depends on, at any remove,
|
|
10
|
+
// end up at `b`? That is the shape of every blast-radius question an
|
|
11
|
+
// operator asks ("if the billing database goes, what falls over?") and
|
|
12
|
+
// every containment question a policy asks ("nothing in the public tier
|
|
13
|
+
// may reach the ledger"), and neither can be expressed by looking one
|
|
14
|
+
// edge at a time.
|
|
15
|
+
//
|
|
16
|
+
// It is a VERB and not a constraint, for the same reason acyclicity is
|
|
17
|
+
// (docs/reference-language.md, "Declared relations"): reachability is
|
|
18
|
+
// global and non-monotone. One more edge can make an unreachable pair
|
|
19
|
+
// reachable, so a lattice citizen asserting non-reachability could be
|
|
20
|
+
// true and then false, and the lattice guarantee is that more
|
|
21
|
+
// information never falsifies what has already been observed.
|
|
22
|
+
//
|
|
23
|
+
// TRANSITIVE, NOT REFLEXIVE-TRANSITIVE: `reaches(a, a)` is true only
|
|
24
|
+
// when a path of one or more edges returns to `a`, which is the useful
|
|
25
|
+
// answer (it says the graph has a cycle through `a`) rather than the
|
|
26
|
+
// vacuous one.
|
|
27
|
+
//
|
|
28
|
+
// The Go twin is go/reach.go; what the two ports must agree on --- the
|
|
29
|
+
// verdict and the path --- is test/spec/reach.tsv.
|
|
30
|
+
|
|
31
|
+
import { Aontu } from './aontu'
|
|
32
|
+
import { failureFinding } from './vet'
|
|
33
|
+
import type { VetFinding } from './vet'
|
|
34
|
+
import type { TrustOptions } from './type'
|
|
35
|
+
import { graphOf } from './graph'
|
|
36
|
+
import { cmpCodePoint } from './keyorder'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export type ReachVerdict = 'reaches' | 'unreachable' | 'error'
|
|
40
|
+
|
|
41
|
+
export type ReachReport = {
|
|
42
|
+
verdict: ReachVerdict
|
|
43
|
+
|
|
44
|
+
// The path found, as entity names from the source to the
|
|
45
|
+
// destination, both included. Present ONLY on `reaches`: a path is
|
|
46
|
+
// the evidence for the answer, and there is no evidence for a
|
|
47
|
+
// negative one.
|
|
48
|
+
path?: string[]
|
|
49
|
+
|
|
50
|
+
// WHY the graph could not be looked at, in vet's finding shape (the
|
|
51
|
+
// review's finding F): a document that does not stand up, or an
|
|
52
|
+
// endpoint that names no entity. Present ONLY on `error`.
|
|
53
|
+
errors?: VetFinding[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ReachOptions = {
|
|
57
|
+
// Where the document CAME FROM, so a relative `@"file"` load inside
|
|
58
|
+
// it resolves from its own directory (relationCheck's precedent).
|
|
59
|
+
path?: string
|
|
60
|
+
// The include capability this document evaluates under (G5,
|
|
61
|
+
// docs/trust.md).
|
|
62
|
+
trust?: TrustOptions
|
|
63
|
+
// Follow only edges under this relation. Absent means follow every
|
|
64
|
+
// edge, which is the whole graph and the commoner question.
|
|
65
|
+
relation?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// The entity an address names --- everything before the first dot. A
|
|
70
|
+
// link into `svc/auth.ports.http` reaches `svc/auth`: reachability is
|
|
71
|
+
// between ENTITIES, and the path inside one says which part of it the
|
|
72
|
+
// link arrives at. Same rule as relation.ts's entityOf, and it has to
|
|
73
|
+
// be, or the two verbs would disagree about what an edge connects.
|
|
74
|
+
function entityOf(addr: string): string {
|
|
75
|
+
const dot = addr.indexOf('.')
|
|
76
|
+
return dot < 0 ? addr : addr.slice(0, dot)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
function endpointFinding(name: string, known: string[]): VetFinding {
|
|
81
|
+
return {
|
|
82
|
+
code: 'refer_unresolved',
|
|
83
|
+
class: 'reference',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
path: '$',
|
|
86
|
+
// NOT "unreachable". An endpoint that names no entity is a
|
|
87
|
+
// question the document cannot answer, and answering it `no` would
|
|
88
|
+
// report a typo as a fact about the model --- the fail-open shape
|
|
89
|
+
// this review exists to retire.
|
|
90
|
+
message: `${name} names no entity in this document.`,
|
|
91
|
+
sites: [],
|
|
92
|
+
...(0 === known.length ? {} : {
|
|
93
|
+
note: 'known entities: ' + known.join(', '),
|
|
94
|
+
}),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// The reachability check for one document.
|
|
100
|
+
export function reachCheck(
|
|
101
|
+
src: string, from: string, to: string, opts?: ReachOptions
|
|
102
|
+
): ReachReport {
|
|
103
|
+
const options = opts ?? {}
|
|
104
|
+
const aontu = new Aontu(
|
|
105
|
+
null == options.trust ? undefined : { trust: options.trust })
|
|
106
|
+
const ctx = aontu.ctx({ collect: true })
|
|
107
|
+
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
108
|
+
const root: any = aontu.unify(src, parseOpts, ctx)
|
|
109
|
+
|
|
110
|
+
// A document that does not stand up is not a document with an
|
|
111
|
+
// unreachable pair: the errors it already has are the answer.
|
|
112
|
+
if (0 < ctx.err.length || true === root?.isNil) {
|
|
113
|
+
return {
|
|
114
|
+
verdict: 'error',
|
|
115
|
+
errors: [failureFinding(ctx, options.path, root)],
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const graph = graphOf(root)
|
|
120
|
+
const known = graph.entities.map((e) => e.id).sort(cmpCodePoint)
|
|
121
|
+
const missing = [from, to].filter((n) => !known.includes(n))
|
|
122
|
+
if (0 < missing.length) {
|
|
123
|
+
return {
|
|
124
|
+
verdict: 'error',
|
|
125
|
+
errors: missing.map((n) => endpointFinding(n, known)),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The successor map, restricted to one relation when the caller asked
|
|
130
|
+
// for one. Sorted, so the path the search finds is the same one in
|
|
131
|
+
// both ports.
|
|
132
|
+
const succ = new Map<string, string[]>()
|
|
133
|
+
for (const e of graph.edges) {
|
|
134
|
+
if ('' === e.from ||
|
|
135
|
+
(null != options.relation && options.relation !== e.key)) {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const list = succ.get(e.from)
|
|
139
|
+
const dest = entityOf(e.to)
|
|
140
|
+
if (undefined === list) {
|
|
141
|
+
succ.set(e.from, [dest])
|
|
142
|
+
}
|
|
143
|
+
else if (!list.includes(dest)) {
|
|
144
|
+
list.push(dest)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const list of succ.values()) {
|
|
148
|
+
list.sort(cmpCodePoint)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// BREADTH-FIRST, so the path reported is a SHORTEST one --- the
|
|
152
|
+
// evidence an operator wants is the tightest chain, not whichever the
|
|
153
|
+
// walk happened to find first --- and, with the successors sorted, a
|
|
154
|
+
// determined one: among shortest paths, the first in code-point order
|
|
155
|
+
// at the first step that distinguishes them.
|
|
156
|
+
const prev = new Map<string, string>()
|
|
157
|
+
const seen = new Set<string>()
|
|
158
|
+
let front: string[] = [from]
|
|
159
|
+
while (0 < front.length) {
|
|
160
|
+
const next: string[] = []
|
|
161
|
+
for (const node of front) {
|
|
162
|
+
for (const dest of succ.get(node) ?? []) {
|
|
163
|
+
if (dest === to) {
|
|
164
|
+
const path = [dest]
|
|
165
|
+
let step = node
|
|
166
|
+
while (step !== from) {
|
|
167
|
+
path.unshift(step)
|
|
168
|
+
step = prev.get(step) as string
|
|
169
|
+
}
|
|
170
|
+
path.unshift(from)
|
|
171
|
+
return { verdict: 'reaches', path }
|
|
172
|
+
}
|
|
173
|
+
if (!seen.has(dest)) {
|
|
174
|
+
seen.add(dest)
|
|
175
|
+
prev.set(dest, node)
|
|
176
|
+
next.push(dest)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
front = next
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { verdict: 'unreachable' }
|
|
184
|
+
}
|