aontu 0.53.0 → 0.55.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/dist/aontu.d.ts +3 -2
- package/dist/aontu.js +36 -7
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +500 -8
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +5 -0
- package/dist/ctx.js +1 -0
- package/dist/ctx.js.map +1 -1
- package/dist/diff.js.map +1 -1
- package/dist/err.js +7 -1
- package/dist/err.js.map +1 -1
- package/dist/graph.d.ts +2 -5
- package/dist/graph.js +83 -46
- package/dist/graph.js.map +1 -1
- package/dist/hcanon.js +9 -10
- package/dist/hcanon.js.map +1 -1
- package/dist/hints.js +94 -26
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.js +34 -0
- package/dist/jsonschema.js.map +1 -1
- package/dist/lang.js +593 -191
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.js +86 -6
- package/dist/lsp.js.map +1 -1
- package/dist/mcp.js +153 -6
- package/dist/mcp.js.map +1 -1
- package/dist/mod-tool.js +42 -8
- package/dist/mod-tool.js.map +1 -1
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +97 -2
- package/dist/mod.js.map +1 -1
- package/dist/patch.d.ts +5 -0
- package/dist/patch.js +25 -25
- package/dist/patch.js.map +1 -1
- package/dist/provenance.d.ts +1 -0
- package/dist/provenance.js +2 -1
- package/dist/provenance.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/reach.d.ts +1 -0
- package/dist/reach.js +49 -21
- package/dist/reach.js.map +1 -1
- package/dist/relation.d.ts +4 -0
- package/dist/relation.js +125 -200
- package/dist/relation.js.map +1 -1
- package/dist/sig.d.ts +25 -0
- package/dist/sig.js +277 -0
- package/dist/sig.js.map +1 -0
- package/dist/sigdecl.d.ts +2 -0
- package/dist/sigdecl.js +11 -0
- package/dist/sigdecl.js.map +1 -0
- package/dist/siggate.d.ts +4 -0
- package/dist/siggate.js +90 -0
- package/dist/siggate.js.map +1 -0
- package/dist/std.js +75 -16
- package/dist/std.js.map +1 -1
- package/dist/subsume.js +57 -10
- package/dist/subsume.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/unify.d.ts +2 -2
- package/dist/unify.js +93 -114
- package/dist/unify.js.map +1 -1
- package/dist/utility.d.ts +1 -2
- package/dist/utility.js +7 -61
- package/dist/utility.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +12 -1
- package/dist/val/AggFuncVal.js +165 -3
- package/dist/val/AggFuncVal.js.map +1 -1
- package/dist/val/BagVal.d.ts +2 -0
- package/dist/val/BagVal.js +56 -8
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/ConstraintVal.js +33 -5
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/ContainerKindVal.d.ts +35 -0
- package/dist/val/ContainerKindVal.js +99 -0
- package/dist/val/ContainerKindVal.js.map +1 -0
- package/dist/val/CopyFuncVal.js +0 -7
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/DisjunctVal.d.ts +1 -2
- package/dist/val/DisjunctVal.js +146 -39
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/ExpectVal.js +37 -2
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FuncBaseVal.d.ts +1 -0
- package/dist/val/FuncBaseVal.js +19 -8
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/GraphAtomVal.d.ts +39 -0
- package/dist/val/GraphAtomVal.js +184 -0
- package/dist/val/GraphAtomVal.js.map +1 -0
- package/dist/val/JunctionVal.js +22 -5
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/ListVal.js +27 -34
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/MapVal.d.ts +1 -0
- package/dist/val/MapVal.js +82 -35
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/PathFuncVal.d.ts +2 -2
- package/dist/val/PathFuncVal.js +75 -16
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PathVal.d.ts +25 -0
- package/dist/val/PathVal.js +150 -0
- package/dist/val/PathVal.js.map +1 -0
- package/dist/val/PlusOpVal.d.ts +2 -1
- package/dist/val/PlusOpVal.js +50 -35
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefVal.d.ts +2 -0
- package/dist/val/PrefVal.js +159 -32
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RecurseVal.d.ts +19 -0
- package/dist/val/RecurseVal.js +217 -0
- package/dist/val/RecurseVal.js.map +1 -0
- package/dist/val/RefVal.d.ts +2 -1
- package/dist/val/RefVal.js +105 -72
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.d.ts +30 -7
- package/dist/val/ReferFuncVal.js +395 -94
- package/dist/val/ReferFuncVal.js.map +1 -1
- package/dist/val/ScalarKindVal.d.ts +4 -2
- package/dist/val/ScalarKindVal.js +12 -1
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/SuperFuncVal.d.ts +4 -2
- package/dist/val/SuperFuncVal.js +118 -14
- package/dist/val/SuperFuncVal.js.map +1 -1
- package/dist/val/TopVal.d.ts +1 -1
- package/dist/val/Val.d.ts +2 -3
- package/dist/val/Val.js +39 -20
- package/dist/val/Val.js.map +1 -1
- package/dist/val/arith.js +4 -1
- package/dist/val/arith.js.map +1 -1
- package/dist/vet.d.ts +1 -0
- package/dist/vet.js +32 -1
- package/dist/vet.js.map +1 -1
- package/dist/view.d.ts +90 -0
- package/dist/view.js +2168 -0
- package/dist/view.js.map +1 -0
- package/grammar/aontu.gbnf +18 -10
- package/grammar/aontu.lark +15 -10
- package/grammar/aontu.tmLanguage.json +184 -0
- package/package.json +10 -3
- package/skill/grammar-card.md +1 -2
- package/src/aontu.ts +37 -7
- package/src/cli.ts +553 -8
- package/src/ctx.ts +20 -0
- package/src/diff.ts +4 -2
- package/src/err.ts +8 -1
- package/src/graph.ts +125 -75
- package/src/hcanon.ts +9 -11
- package/src/hints.ts +112 -29
- package/src/jsonschema.ts +41 -0
- package/src/lang.ts +642 -203
- package/src/lsp.ts +75 -6
- package/src/mcp.ts +164 -6
- package/src/mod-tool.ts +48 -9
- package/src/mod.ts +110 -1
- package/src/patch.ts +31 -27
- package/src/provenance.ts +8 -1
- package/src/query.ts +4 -2
- package/src/reach.ts +52 -23
- package/src/relation.ts +139 -236
- package/src/sig.ts +345 -0
- package/src/sigdecl.ts +11 -0
- package/src/siggate.ts +144 -0
- package/src/std.ts +77 -16
- package/src/subsume.ts +59 -10
- package/src/unify.ts +102 -125
- package/src/utility.ts +7 -67
- package/src/val/AggFuncVal.ts +231 -4
- package/src/val/BagVal.ts +58 -9
- package/src/val/ConstraintVal.ts +34 -5
- package/src/val/ContainerKindVal.ts +158 -0
- package/src/val/CopyFuncVal.ts +0 -7
- package/src/val/DisjunctVal.ts +152 -40
- package/src/val/ExpectVal.ts +39 -4
- package/src/val/FuncBaseVal.ts +21 -8
- package/src/val/GraphAtomVal.ts +264 -0
- package/src/val/JunctionVal.ts +23 -6
- package/src/val/ListVal.ts +30 -37
- package/src/val/MapVal.ts +87 -39
- package/src/val/PathFuncVal.ts +107 -19
- package/src/val/PathVal.ts +221 -0
- package/src/val/PlusOpVal.ts +56 -37
- package/src/val/PrefVal.ts +186 -38
- package/src/val/RecurseVal.ts +285 -0
- package/src/val/RefVal.ts +105 -83
- package/src/val/ReferFuncVal.ts +445 -100
- package/src/val/ScalarKindVal.ts +12 -0
- package/src/val/SuperFuncVal.ts +137 -13
- package/src/val/TopVal.ts +1 -1
- package/src/val/Val.ts +44 -35
- package/src/val/arith.ts +4 -1
- package/src/vet.ts +41 -4
- package/src/view.ts +2882 -0
- package/dist/val/IdFuncVal.d.ts +0 -13
- package/dist/val/IdFuncVal.js +0 -54
- package/dist/val/IdFuncVal.js.map +0 -1
- package/src/val/IdFuncVal.ts +0 -91
package/src/provenance.ts
CHANGED
|
@@ -55,6 +55,12 @@ export type WhyConjunct = {
|
|
|
55
55
|
// actually written. Empty when the contribution occupies no source —
|
|
56
56
|
// a value unification minted rather than a document wrote.
|
|
57
57
|
src: string
|
|
58
|
+
// The PREFERENCE RANK, 0-based, when the contribution is one: `*x`
|
|
59
|
+
// is 0, `**x` is 1. Absent for anything else. The engine's own
|
|
60
|
+
// number (PrefVal.rank), so a reader arbitrating between ranked
|
|
61
|
+
// contributions -- the meet ladder -- need not count stars in a
|
|
62
|
+
// canon string.
|
|
63
|
+
rank?: number
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export type WhyRecord = {
|
|
@@ -76,7 +82,7 @@ export const FROM_SPREAD = '_fromSpread'
|
|
|
76
82
|
// which is true of the parsed tree and of nothing derived from it. So
|
|
77
83
|
// every value that reached a path through a CLONE was dark: a default
|
|
78
84
|
// flowing into a `pack()`-generated child, a shape carried by a `$ref`,
|
|
79
|
-
//
|
|
85
|
+
// a value a spread template stamped. `why` answered "(no contributions:
|
|
80
86
|
// nothing met at this path)" over a value it had just printed, with
|
|
81
87
|
// exit 0 -- a false statement, and the one an audit surface may not
|
|
82
88
|
// make (use-cases/BUGS.md §23, §24).
|
|
@@ -338,6 +344,7 @@ export class Provenance {
|
|
|
338
344
|
row: v.site.row,
|
|
339
345
|
},
|
|
340
346
|
src: v.site.src,
|
|
347
|
+
...(true === v.isPref ? { rank: v.rank } : {}),
|
|
341
348
|
})
|
|
342
349
|
}
|
|
343
350
|
|
package/src/query.ts
CHANGED
|
@@ -51,8 +51,10 @@ export type QueryOptions = {
|
|
|
51
51
|
// The trust profile this run evaluates under (G5, docs/trust.md).
|
|
52
52
|
// The source arrives from a caller, so the caller must be able to
|
|
53
53
|
// say what it may reach: without this the include chain is the
|
|
54
|
-
// default one, and `@"
|
|
55
|
-
//
|
|
54
|
+
// default one, and `@"../../etc/passwd.aon"` reads whatever the
|
|
55
|
+
// process can. (`@"x.js"` no longer executes -- ADR-012 refuses the
|
|
56
|
+
// extension -- but reading is enough.) A server passes
|
|
57
|
+
// `{include:'none'}`.
|
|
56
58
|
trust?: TrustOptions
|
|
57
59
|
}
|
|
58
60
|
|
package/src/reach.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
2
|
|
|
3
|
-
// REACHABILITY OVER THE
|
|
3
|
+
// REACHABILITY OVER THE LINK GRAPH (the review's finding J,
|
|
4
4
|
// use-cases/REVIEW.md): "ship a transitive `reaches(a, b)` check verb".
|
|
5
5
|
//
|
|
6
6
|
// `relations` answers questions about the edge set as a whole --- is it
|
|
@@ -41,7 +41,7 @@ export type ReachVerdict = 'reaches' | 'unreachable' | 'error'
|
|
|
41
41
|
export type ReachReport = {
|
|
42
42
|
verdict: ReachVerdict
|
|
43
43
|
|
|
44
|
-
// The path found, as
|
|
44
|
+
// The path found, as `$.dotted` node paths from the source to the
|
|
45
45
|
// destination, both included. Present ONLY on `reaches`: a path is
|
|
46
46
|
// the evidence for the answer, and there is no evidence for a
|
|
47
47
|
// negative one.
|
|
@@ -49,7 +49,7 @@ export type ReachReport = {
|
|
|
49
49
|
|
|
50
50
|
// WHY the graph could not be looked at, in vet's finding shape (the
|
|
51
51
|
// review's finding F): a document that does not stand up, or an
|
|
52
|
-
// endpoint that names no
|
|
52
|
+
// endpoint that names no node. Present ONLY on `error`.
|
|
53
53
|
errors?: VetFinding[]
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -66,14 +66,37 @@ export type ReachOptions = {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
// The
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
// The segments a `$.dotted` endpoint spells, or undefined when it is
|
|
70
|
+
// not one. Reachability is between TREE POSITIONS (ADR-014), so an
|
|
71
|
+
// endpoint is a path and nothing else --- the same spelling the report
|
|
72
|
+
// prints back.
|
|
73
|
+
export function parseNodePath(s: string): string[] | undefined {
|
|
74
|
+
if ('$' === s) {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
if (!s.startsWith('$.')) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
const parts = s.slice(2).split('.')
|
|
81
|
+
return parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p)) ? parts : undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// Whether a path names a node of the evaluated tree. An endpoint that
|
|
86
|
+
// exists but has no edges is a perfectly good question with the answer
|
|
87
|
+
// `unreachable`; only one that names NOTHING is an error.
|
|
88
|
+
function nodeAt(root: any, path: string[]): boolean {
|
|
89
|
+
let node: any = root
|
|
90
|
+
for (const seg of path) {
|
|
91
|
+
if (true !== node?.isMap && true !== node?.isList) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
node = node.peg[seg]
|
|
95
|
+
if (null == node) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null != node
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
|
|
@@ -83,14 +106,14 @@ function endpointFinding(name: string, known: string[]): VetFinding {
|
|
|
83
106
|
class: 'reference',
|
|
84
107
|
severity: 'error',
|
|
85
108
|
path: '$',
|
|
86
|
-
// NOT "unreachable". An endpoint that names no
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
message: `${name} names no
|
|
109
|
+
// NOT "unreachable". An endpoint that names no node is a question
|
|
110
|
+
// the document cannot answer, and answering it `no` would report a
|
|
111
|
+
// typo as a fact about the model --- the fail-open shape this
|
|
112
|
+
// review exists to retire.
|
|
113
|
+
message: `${name} names no node in this document.`,
|
|
91
114
|
sites: [],
|
|
92
115
|
...(0 === known.length ? {} : {
|
|
93
|
-
note: '
|
|
116
|
+
note: 'nodes with links: ' + known.join(', '),
|
|
94
117
|
}),
|
|
95
118
|
}
|
|
96
119
|
}
|
|
@@ -117,12 +140,19 @@ export function reachCheck(
|
|
|
117
140
|
}
|
|
118
141
|
|
|
119
142
|
const graph = graphOf(root)
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
// The nodes the graph actually touches, for the error note: a
|
|
144
|
+
// document has every path in it, and listing them all would drown the
|
|
145
|
+
// one fact a mistyped endpoint needs.
|
|
146
|
+
const linked = [...new Set(graph.edges
|
|
147
|
+
.flatMap((e) => [e.from, e.to]))].sort(cmpCodePoint)
|
|
148
|
+
const missing = [from, to].filter((n) => {
|
|
149
|
+
const parts = parseNodePath(n)
|
|
150
|
+
return undefined === parts || !nodeAt(root, parts)
|
|
151
|
+
})
|
|
122
152
|
if (0 < missing.length) {
|
|
123
153
|
return {
|
|
124
154
|
verdict: 'error',
|
|
125
|
-
errors: missing.map((n) => endpointFinding(n,
|
|
155
|
+
errors: missing.map((n) => endpointFinding(n, linked)),
|
|
126
156
|
}
|
|
127
157
|
}
|
|
128
158
|
|
|
@@ -131,12 +161,11 @@ export function reachCheck(
|
|
|
131
161
|
// both ports.
|
|
132
162
|
const succ = new Map<string, string[]>()
|
|
133
163
|
for (const e of graph.edges) {
|
|
134
|
-
if (
|
|
135
|
-
(null != options.relation && options.relation !== e.key)) {
|
|
164
|
+
if (null != options.relation && options.relation !== e.key) {
|
|
136
165
|
continue
|
|
137
166
|
}
|
|
138
167
|
const list = succ.get(e.from)
|
|
139
|
-
const dest =
|
|
168
|
+
const dest = e.to
|
|
140
169
|
if (undefined === list) {
|
|
141
170
|
succ.set(e.from, [dest])
|
|
142
171
|
}
|
package/src/relation.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
2
|
|
|
3
|
-
// RELATION GRAPH
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// RELATION GRAPH VERDICTS (RELATIONS.0.md §3.3, replacing the G4
|
|
4
|
+
// phase 5 magic-key pass): acyclicity and inverse consistency over
|
|
5
|
+
// the edge set, DECLARED by the graph atoms -- `acyclic()` and
|
|
6
|
+
// `inverse(name)` conjoined at the field whose key is the predicate
|
|
7
|
+
// -- and decided AFTER unification, never by it.
|
|
7
8
|
//
|
|
8
9
|
// Why not in the lattice. Both properties are GLOBAL and NON-MONOTONE:
|
|
9
10
|
// an acyclic graph becomes cyclic when one more edge unifies in, and an
|
|
@@ -11,27 +12,27 @@
|
|
|
11
12
|
// The lattice guarantee is that more information never falsifies what
|
|
12
13
|
// has been observed, so a constraint that could be true and then false
|
|
13
14
|
// is not a constraint the lattice may hold. These are facts about the
|
|
14
|
-
// finished model,
|
|
15
|
-
//
|
|
15
|
+
// finished model, so the atoms only REGISTER during unification
|
|
16
|
+
// (GraphAtomVal.register, onto ctx._reldecls), and the verdict lands
|
|
17
|
+
// at GENERATION -- the sizing atoms' model -- where no more
|
|
18
|
+
// information can arrive. The `relations` verb reports the same
|
|
19
|
+
// verdict from the same declarations: one decision, two surfaces.
|
|
16
20
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// }
|
|
23
|
-
//
|
|
24
|
-
// Nothing in the engine knows the name `relations`; this pass does,
|
|
25
|
-
// and says so.
|
|
21
|
+
// The old declaration channel -- a `relations:` key at the document
|
|
22
|
+
// root, read by name -- is GONE, discharging ADR-010's grandfather
|
|
23
|
+
// clause: the engine no longer knows any spellable key. The target
|
|
24
|
+
// half of the old declaration is `rel(t)`'s flow at the site, which
|
|
25
|
+
// checks by unifying rather than by a report-layer probe.
|
|
26
26
|
|
|
27
27
|
import { Aontu } from './aontu'
|
|
28
28
|
import { failureFinding } from './vet'
|
|
29
29
|
import type { VetFinding } from './vet'
|
|
30
|
-
import type { TrustOptions
|
|
30
|
+
import type { TrustOptions } from './type'
|
|
31
31
|
import { graphOf } from './graph'
|
|
32
|
-
import type { Edge,
|
|
32
|
+
import type { Edge, Graph } from './graph'
|
|
33
33
|
import { cmpCodePoint } from './keyorder'
|
|
34
|
-
import {
|
|
34
|
+
import { makeNilErr } from './err'
|
|
35
|
+
import type { RelDecl } from './val/GraphAtomVal'
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
export type RelationVerdict = 'pass' | 'fail' | 'error'
|
|
@@ -42,7 +43,7 @@ export type RelationFinding = {
|
|
|
42
43
|
relation: string
|
|
43
44
|
// Where the offending edge is written, as a `$.dotted.path`.
|
|
44
45
|
at: string
|
|
45
|
-
// For a cycle, the
|
|
46
|
+
// For a cycle, the node paths it runs through, in the order the walk
|
|
46
47
|
// found them, closing back on the first. For a missing inverse, the
|
|
47
48
|
// two ends and the relation that should have mirrored it.
|
|
48
49
|
detail: string[]
|
|
@@ -72,143 +73,7 @@ export type RelationOptions = {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
-
type Declared = {
|
|
77
|
-
name: string
|
|
78
|
-
inverse?: string
|
|
79
|
-
acyclic: boolean
|
|
80
|
-
// What the FAR END must satisfy, if the relation says. Absent when
|
|
81
|
-
// the relation declares none, and when it declares `top` -- which
|
|
82
|
-
// constrains nothing and would report nothing, so reading it as a
|
|
83
|
-
// declaration would only cost a meet per edge.
|
|
84
|
-
target?: Val
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// The entity an address names — everything before the first dot. An
|
|
89
|
-
// edge into `svc/auth.ports.http` is an edge to `svc/auth`: a relation
|
|
90
|
-
// holds between ENTITIES, and the path inside one says which part of it
|
|
91
|
-
// the link reaches.
|
|
92
|
-
function entityOf(addr: string): string {
|
|
93
|
-
const dot = addr.indexOf('.')
|
|
94
|
-
return dot < 0 ? addr : addr.slice(0, dot)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// The node an address names, or undefined. The entity's own position
|
|
99
|
-
// comes from the graph (a merged entity sits at every position that
|
|
100
|
-
// declared it, and they hold the same value, so the FIRST in the
|
|
101
|
-
// graph's sorted list is as good as any and is the same one in both
|
|
102
|
-
// ports); the rest of the address walks into it, exactly as the
|
|
103
|
-
// address's own grammar says.
|
|
104
|
-
// NO GUARD ON THE LOOKUP OR THE WALK. An edge exists only because
|
|
105
|
-
// `refer()` RESOLVED its full address, so `find` cannot miss and no
|
|
106
|
-
// segment can fall off: an address that does not walk is
|
|
107
|
-
// `refer_unresolved` at unification and the document never reaches the
|
|
108
|
-
// graph (probed for a missing key, a scalar mid-path and an
|
|
109
|
-
// out-of-range index, in both ports). An unreachable `if` is a branch
|
|
110
|
-
// arm the ADR-002 gate counts and no marker suppresses, so the Go twin
|
|
111
|
-
// keeps its guards — where a nil would PANIC rather than propagate,
|
|
112
|
-
// and where the marker mechanism can carry them — and this one relies
|
|
113
|
-
// on optional chaining instead.
|
|
114
|
-
function addressed(
|
|
115
|
-
root: any, graph: Graph, addr: string
|
|
116
|
-
): any {
|
|
117
|
-
const dot = addr.indexOf('.')
|
|
118
|
-
const name = dot < 0 ? addr : addr.slice(0, dot)
|
|
119
|
-
const entry = graph.entities.find((e) => e.id === name) as EntityEntry
|
|
120
|
-
const segs = entry.paths[0].slice(2).split('.')
|
|
121
|
-
.concat(dot < 0 ? [] : addr.slice(dot + 1).split('.'))
|
|
122
|
-
let node: any = root
|
|
123
|
-
for (const seg of segs) {
|
|
124
|
-
node = node?.peg?.[seg]
|
|
125
|
-
}
|
|
126
|
-
return node
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Does the far end satisfy the declared target? A TEST, never a flow:
|
|
131
|
-
// the check reports on a finished model and writing into it would be
|
|
132
|
-
// generation, which `relations` does not do (the same rule that keeps
|
|
133
|
-
// it from writing an author's inverse for them). So both sides are
|
|
134
|
-
// CLONED into a throwaway context and the meet is taken there; what
|
|
135
|
-
// the document holds is untouched either way.
|
|
136
|
-
//
|
|
137
|
-
// `refer(t)` is the other half of this and does flow, at the site. The
|
|
138
|
-
// two agree on what "satisfies" means -- a meet that is not a nil --
|
|
139
|
-
// which is what lets a relation declare once what every site would
|
|
140
|
-
// otherwise repeat.
|
|
141
|
-
// `root` is the DOCUMENT, not the node: a target lifted out of the
|
|
142
|
-
// model (`target: $.std.Service`) can still hold a reference that
|
|
143
|
-
// resolves against the document, and a probe rooted at the far end
|
|
144
|
-
// answers `no_path` for it -- which the worked example in
|
|
145
|
-
// test/spec/relation.tsv caught the moment this check existed.
|
|
146
|
-
function meets(
|
|
147
|
-
aontu: any, root: any, node: any, target: any
|
|
148
|
-
): string | undefined {
|
|
149
|
-
const ctx = aontu.ctx({ collect: true })
|
|
150
|
-
ctx.root = root
|
|
151
|
-
const out: any = unite(ctx, node.clone(ctx), target.clone(ctx), 'relation-target')
|
|
152
|
-
if (true === out?.isNil) {
|
|
153
|
-
return out.why as string
|
|
154
|
-
}
|
|
155
|
-
if (0 < ctx.err.length) {
|
|
156
|
-
return ctx.err[0].why as string
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// A MEET THAT LEAVES A HOLE IS NOT SATISFACTION. `target:
|
|
160
|
-
// {kind: service, port: integer}` against a far end with no `port`
|
|
161
|
-
// does not CONFLICT -- the meet simply carries `integer` into a key
|
|
162
|
-
// that had none -- and a check that stopped at "no conflict" would
|
|
163
|
-
// pass a far end that is missing half of what the relation demands.
|
|
164
|
-
//
|
|
165
|
-
// What `refer(t)` does at the site is the yardstick: it flows `t` in,
|
|
166
|
-
// and the document then fails to generate, because `integer` is not a
|
|
167
|
-
// value. So the same question is asked here -- can the far end still
|
|
168
|
-
// generate once the target is met? -- and the answer is compared with
|
|
169
|
-
// the far end ALONE, so a node that was already incomplete for its
|
|
170
|
-
// own reasons is not blamed on the relation that points at it.
|
|
171
|
-
// The REASON reported is the engine's own -- the code `refer(t)` at
|
|
172
|
-
// the site would have raised -- rather than a name invented here.
|
|
173
|
-
const probe = (v: any): string | undefined => {
|
|
174
|
-
const gctx = aontu.ctx({ collect: true })
|
|
175
|
-
gctx.root = root
|
|
176
|
-
v.gen(gctx)
|
|
177
|
-
return 0 === gctx.err.length ? undefined : (gctx.err[0].why as string)
|
|
178
|
-
}
|
|
179
|
-
const alone = probe(node.clone(aontu.ctx({ collect: true })))
|
|
180
|
-
const met = probe(out)
|
|
181
|
-
return undefined === alone && undefined !== met ? met : undefined
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
function declaredRelations(root: any): Declared[] {
|
|
186
|
-
const rels = root?.peg?.relations
|
|
187
|
-
if (true !== rels?.isMap) {
|
|
188
|
-
return []
|
|
189
|
-
}
|
|
190
|
-
const out: Declared[] = []
|
|
191
|
-
for (const name of Object.keys(rels.peg).sort(cmpCodePoint)) {
|
|
192
|
-
const r: any = rels.peg[name]
|
|
193
|
-
if (true !== r?.isMap) {
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
const inv: any = r.peg.inverse
|
|
197
|
-
const acy: any = r.peg.acyclic
|
|
198
|
-
const tgt: any = r.peg.target
|
|
199
|
-
out.push({
|
|
200
|
-
name,
|
|
201
|
-
inverse: true === inv?.isScalar && 'string' === typeof inv.peg
|
|
202
|
-
? inv.peg : undefined,
|
|
203
|
-
acyclic: true === acy?.isScalar && true === acy.peg,
|
|
204
|
-
target: true === tgt?.isVal && true !== tgt.isTop ? tgt : undefined,
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
return out
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// The first cycle reachable from `start`, as the entities it runs
|
|
76
|
+
// The first cycle reachable from `start`, as the node paths it runs
|
|
212
77
|
// through, or undefined. Depth-first with the path as the stack, and
|
|
213
78
|
// the successors visited in sorted order, so the cycle a report names
|
|
214
79
|
// is the same one in both ports.
|
|
@@ -245,44 +110,19 @@ function findCycle(
|
|
|
245
110
|
}
|
|
246
111
|
|
|
247
112
|
|
|
248
|
-
// The
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
256
|
-
const root: any = aontu.unify(src, parseOpts, ctx)
|
|
257
|
-
|
|
258
|
-
// A document that does not stand up is not a document with a bad
|
|
259
|
-
// graph: the errors it already has are the answer, and blaming its
|
|
260
|
-
// relations on top would be noise.
|
|
261
|
-
if (0 < ctx.err.length || true === root?.isNil) {
|
|
262
|
-
return {
|
|
263
|
-
verdict: 'error',
|
|
264
|
-
findings: [],
|
|
265
|
-
errors: [failureFinding(ctx, options.path, root)],
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const declared = declaredRelations(root)
|
|
270
|
-
if (0 === declared.length) {
|
|
271
|
-
return { verdict: 'pass', findings: [] }
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const graph = graphOf(root)
|
|
275
|
-
const edges = graph.edges
|
|
113
|
+
// The verdict itself, pure over what the evaluation produced: the
|
|
114
|
+
// registered declarations and the edge set. Shared by the generation
|
|
115
|
+
// hook (relationErrors) and the `relations` verb, so the two surfaces
|
|
116
|
+
// cannot disagree.
|
|
117
|
+
export function relationFindings(
|
|
118
|
+
decls: Map<string, RelDecl>, graph: Graph
|
|
119
|
+
): RelationFinding[] {
|
|
276
120
|
const findings: RelationFinding[] = []
|
|
277
121
|
|
|
278
122
|
// The edge set, indexed the two ways the checks read it.
|
|
279
123
|
const byRelation = new Map<string, Edge[]>()
|
|
280
124
|
const pairs = new Set<string>()
|
|
281
|
-
for (const e of edges) {
|
|
282
|
-
if ('' === e.from) {
|
|
283
|
-
// An edge outside every entity has no source to be a relation OF.
|
|
284
|
-
continue
|
|
285
|
-
}
|
|
125
|
+
for (const e of graph.edges) {
|
|
286
126
|
const list = byRelation.get(e.key)
|
|
287
127
|
if (undefined === list) {
|
|
288
128
|
byRelation.set(e.key, [e])
|
|
@@ -290,17 +130,22 @@ export function relationCheck(
|
|
|
290
130
|
else {
|
|
291
131
|
list.push(e)
|
|
292
132
|
}
|
|
293
|
-
pairs.add(e.key + ' ' + e.from + ' ' +
|
|
133
|
+
pairs.add(e.key + ' ' + e.from + ' ' + e.to)
|
|
294
134
|
}
|
|
295
135
|
|
|
296
|
-
|
|
297
|
-
|
|
136
|
+
// Predicates in sorted order, so the findings arrive the same way
|
|
137
|
+
// in both ports (the registry is insertion-ordered here, random in
|
|
138
|
+
// Go).
|
|
139
|
+
const names = [...decls.keys()].sort(cmpCodePoint)
|
|
140
|
+
for (const name of names) {
|
|
141
|
+
const decl = decls.get(name) as RelDecl
|
|
142
|
+
const mine = byRelation.get(name) ?? []
|
|
298
143
|
|
|
299
|
-
if (
|
|
144
|
+
if (true === decl.acyclic) {
|
|
300
145
|
const succ = new Map<string, string[]>()
|
|
301
146
|
for (const e of mine) {
|
|
302
147
|
const list = succ.get(e.from)
|
|
303
|
-
const to =
|
|
148
|
+
const to = e.to
|
|
304
149
|
if (undefined === list) {
|
|
305
150
|
succ.set(e.from, [to])
|
|
306
151
|
}
|
|
@@ -325,7 +170,7 @@ export function relationCheck(
|
|
|
325
170
|
const at = mine.find((e) => e.from === cycle[0]) as Edge
|
|
326
171
|
findings.push({
|
|
327
172
|
code: 'relation_cycle',
|
|
328
|
-
relation:
|
|
173
|
+
relation: name,
|
|
329
174
|
at: at.at,
|
|
330
175
|
detail: cycle,
|
|
331
176
|
})
|
|
@@ -334,45 +179,16 @@ export function relationCheck(
|
|
|
334
179
|
}
|
|
335
180
|
}
|
|
336
181
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// by nothing, on the reasoning that `refer(t)` already flows the
|
|
340
|
-
// type in at the site. It does, and that is exactly why the
|
|
341
|
-
// declaration was worth nothing: the site has to REPEAT it, and the
|
|
342
|
-
// idiom that avoids repeating it (`refer($.std.Service)`) tripped
|
|
343
|
-
// the fixpoint until §19 was fixed, so every real model wrote bare
|
|
344
|
-
// `refer()` and a typed-endpoint declaration checked nothing.
|
|
345
|
-
//
|
|
346
|
-
// Reported per EDGE, not per entity: an entity reached by two
|
|
347
|
-
// relations must satisfy both, and the report points at the link
|
|
348
|
-
// that made the demand.
|
|
349
|
-
if (undefined !== rel.target) {
|
|
182
|
+
const inverses = [...decl.inverses].sort(cmpCodePoint)
|
|
183
|
+
for (const inv of inverses) {
|
|
350
184
|
for (const e of mine) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// tree, so this document would not have reached the graph.
|
|
354
|
-
const why = meets(aontu, root, addressed(root, graph, e.to),
|
|
355
|
-
rel.target)
|
|
356
|
-
if (undefined !== why) {
|
|
357
|
-
findings.push({
|
|
358
|
-
code: 'relation_target_unmet',
|
|
359
|
-
relation: rel.name,
|
|
360
|
-
at: e.at,
|
|
361
|
-
detail: [e.from, e.to, why],
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (undefined !== rel.inverse) {
|
|
368
|
-
for (const e of mine) {
|
|
369
|
-
const to = entityOf(e.to)
|
|
370
|
-
if (!pairs.has(rel.inverse + ' ' + to + ' ' + e.from)) {
|
|
185
|
+
const to = e.to
|
|
186
|
+
if (!pairs.has(inv + ' ' + to + ' ' + e.from)) {
|
|
371
187
|
findings.push({
|
|
372
188
|
code: 'relation_inverse_missing',
|
|
373
|
-
relation:
|
|
189
|
+
relation: name,
|
|
374
190
|
at: e.at,
|
|
375
|
-
detail: [e.from, to,
|
|
191
|
+
detail: [e.from, to, inv],
|
|
376
192
|
})
|
|
377
193
|
}
|
|
378
194
|
}
|
|
@@ -380,14 +196,101 @@ export function relationCheck(
|
|
|
380
196
|
}
|
|
381
197
|
|
|
382
198
|
// SORTED, because a report is read by a machine that diffs it: by the
|
|
383
|
-
// position the offending edge is written at, then by code
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
// order, so what order remains is fixed anyway.
|
|
199
|
+
// position the offending edge is written at, then by code, then by
|
|
200
|
+
// the detail (two inverse declarations on one predicate can flag one
|
|
201
|
+
// edge twice). The sort is STABLE and the predicates were iterated
|
|
202
|
+
// in sorted order, so what order remains is fixed anyway.
|
|
388
203
|
findings.sort((a, b) =>
|
|
389
|
-
cmpCodePoint(a.at, b.at) || cmpCodePoint(a.code, b.code)
|
|
204
|
+
cmpCodePoint(a.at, b.at) || cmpCodePoint(a.code, b.code)
|
|
205
|
+
|| cmpCodePoint(a.detail.join(' '), b.detail.join(' ')))
|
|
206
|
+
|
|
207
|
+
return findings
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
// The generation hook (Aontu.generate, between unification success
|
|
212
|
+
// and value generation): each finding becomes a LOCATED evaluation
|
|
213
|
+
// error at the offending edge, exactly as an unmet sizing atom
|
|
214
|
+
// refuses at generation. Findings name node paths and positions the
|
|
215
|
+
// document spelled, so the walk to the site cannot miss.
|
|
216
|
+
export function relationErrors(ctx: any, root: any): void {
|
|
217
|
+
const decls: Map<string, RelDecl> = ctx._reldecls
|
|
218
|
+
if (0 === decls.size) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const findings = relationFindings(decls, graphOf(root))
|
|
222
|
+
for (const f of findings) {
|
|
223
|
+
let node: any = root
|
|
224
|
+
for (const seg of f.at.slice(2).split('.')) {
|
|
225
|
+
// Graph atoms hold the field's value -- possibly nested, one
|
|
226
|
+
// atom carrying another -- and the path steps through them
|
|
227
|
+
// exactly as the graph walk does.
|
|
228
|
+
while (true === node?.isGraphAtom) {
|
|
229
|
+
node = node.held
|
|
230
|
+
}
|
|
231
|
+
node = node?.peg?.[seg]
|
|
232
|
+
}
|
|
233
|
+
// No unwrap AFTER the walk: a finding's `at` names an edge
|
|
234
|
+
// element, and an edge's element is a string -- an atom-wrapped
|
|
235
|
+
// element mints no edge in the first place (the graph visit
|
|
236
|
+
// descends atoms only at field values), so the walk cannot end on
|
|
237
|
+
// an atom.
|
|
238
|
+
ctx.adderr(makeNilErr(ctx, f.code, node, undefined, 'relate', {
|
|
239
|
+
relation: f.relation,
|
|
240
|
+
detail: f.detail.join(' -> '),
|
|
241
|
+
}))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// The relation checks for one document: evaluate, then report the
|
|
247
|
+
// same verdict generation enforces.
|
|
248
|
+
export function relationCheck(
|
|
249
|
+
src: string, opts?: RelationOptions): RelationReport {
|
|
250
|
+
const options = opts ?? {}
|
|
251
|
+
const aontu = new Aontu(
|
|
252
|
+
null == options.trust ? undefined : { trust: options.trust })
|
|
253
|
+
const ctx = aontu.ctx({ collect: true })
|
|
254
|
+
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
255
|
+
const root: any = aontu.unify(src, parseOpts, ctx)
|
|
256
|
+
|
|
257
|
+
// A document that does not stand up is not a document with a bad
|
|
258
|
+
// graph: the errors it already has are the answer, and blaming its
|
|
259
|
+
// relations on top would be noise.
|
|
260
|
+
if (0 < ctx.err.length || true === root?.isNil) {
|
|
261
|
+
return {
|
|
262
|
+
verdict: 'error',
|
|
263
|
+
findings: [],
|
|
264
|
+
errors: [failureFinding(ctx, options.path, root)],
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const decls: Map<string, RelDecl> = (ctx as any)._reldecls
|
|
269
|
+
if (0 === decls.size) {
|
|
270
|
+
return { verdict: 'pass', findings: [] }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// NOR IS A DOCUMENT THAT CANNOT BE GENERATED. Unification can
|
|
274
|
+
// succeed over a tree that still holds an unsettled disjunction --
|
|
275
|
+
// more than one alternative admitted, which `disjunct_no_gen`
|
|
276
|
+
// refuses at generation -- and the graph walk cannot read inside
|
|
277
|
+
// one: the links in every alternative are invisible, so a mirror
|
|
278
|
+
// the document plainly writes is missing from the edge set and
|
|
279
|
+
// `inverse(n)` calls it missing. Generation is therefore asked
|
|
280
|
+
// FIRST, on a collecting context of its own, and what it says is
|
|
281
|
+
// the answer. Generation is what the atoms' verdict lands at, so
|
|
282
|
+
// this is the same order the engine itself now runs in.
|
|
283
|
+
const gctx = ctx.clone({ err: [], collect: true })
|
|
284
|
+
root.gen(gctx)
|
|
285
|
+
if (0 < gctx.err.length) {
|
|
286
|
+
return {
|
|
287
|
+
verdict: 'error',
|
|
288
|
+
findings: [],
|
|
289
|
+
errors: [failureFinding(gctx, options.path, root)],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
390
292
|
|
|
293
|
+
const findings = relationFindings(decls, graphOf(root))
|
|
391
294
|
return {
|
|
392
295
|
verdict: 0 === findings.length ? 'pass' : 'fail',
|
|
393
296
|
findings,
|