aontu 0.60.0 → 0.62.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 +3 -3
- package/dist/agentsmd.d.ts +1 -0
- package/dist/agentsmd.js +7 -1
- package/dist/agentsmd.js.map +1 -1
- package/dist/allow.d.ts +23 -0
- package/dist/allow.js +230 -0
- package/dist/allow.js.map +1 -0
- package/dist/aontu.d.ts +4 -2
- package/dist/aontu.js +4 -2
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +1000 -47
- package/dist/cli.js.map +1 -1
- package/dist/format.d.ts +1 -0
- package/dist/format.js +154 -12
- package/dist/format.js.map +1 -1
- package/dist/helpdoc.d.ts +16 -0
- package/dist/helpdoc.js +59 -0
- package/dist/helpdoc.js.map +1 -0
- package/dist/hints.js +15 -2
- package/dist/hints.js.map +1 -1
- package/dist/lang.js +93 -40
- package/dist/lang.js.map +1 -1
- package/dist/lower.d.ts +3 -0
- package/dist/lower.js +3 -0
- package/dist/lower.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.js +1 -1
- package/dist/lsp.js.map +1 -1
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +7 -1
- package/dist/relation.js.map +1 -1
- package/dist/render.js +26 -21
- package/dist/render.js.map +1 -1
- package/dist/sigdecl.js +1 -1
- package/dist/sigdecl.js.map +1 -1
- package/dist/std.js +112 -69
- package/dist/std.js.map +1 -1
- package/dist/template.d.ts +2 -1
- package/dist/template.js +51 -15
- package/dist/template.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/val/AggFuncVal.js +2 -2
- package/dist/val/AggFuncVal.js.map +1 -1
- package/dist/val/CmpFuncVal.d.ts +20 -0
- package/dist/val/CmpFuncVal.js +245 -0
- package/dist/val/CmpFuncVal.js.map +1 -0
- package/dist/val/EachFuncVal.d.ts +1 -2
- package/dist/val/EachFuncVal.js +15 -29
- package/dist/val/EachFuncVal.js.map +1 -1
- package/dist/val/FormFuncVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js +17 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/NamerFuncVal.d.ts +12 -0
- package/dist/val/NamerFuncVal.js +176 -0
- package/dist/val/NamerFuncVal.js.map +1 -0
- package/dist/val/NilVal.js +29 -3
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/NomFuncVal.d.ts +12 -0
- package/dist/val/NomFuncVal.js +187 -0
- package/dist/val/NomFuncVal.js.map +1 -0
- package/dist/val/PackFuncVal.js +3 -3
- package/dist/val/PackFuncVal.js.map +1 -1
- package/dist/val/RefVal.js +1 -1
- package/dist/val/TranslateFuncVal.d.ts +12 -0
- package/dist/val/TranslateFuncVal.js +101 -0
- package/dist/val/TranslateFuncVal.js.map +1 -0
- package/dist/val/UpperFuncVal.js +17 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/caserange.d.ts +3 -0
- package/dist/val/caserange.js +110 -0
- package/dist/val/caserange.js.map +1 -0
- package/dist/vet.d.ts +12 -0
- package/dist/vet.js +209 -1
- package/dist/vet.js.map +1 -1
- package/grammar/aontu.abnf +1 -1
- package/grammar/aontu.gbnf +1 -1
- package/grammar/aontu.lark +1 -1
- package/grammar/aontu.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/skill/init/check.sh +28 -0
- package/skill/init/data.aon +12 -0
- package/skill/init/model.aon +19 -0
- package/skill/tasks.md +151 -0
- package/src/agentsmd.ts +13 -2
- package/src/allow.ts +316 -0
- package/src/aontu.ts +10 -1
- package/src/cli.ts +1131 -53
- package/src/format.ts +191 -14
- package/src/helpdoc.ts +77 -0
- package/src/hints.ts +16 -2
- package/src/lang.ts +98 -42
- package/src/lower.ts +3 -3
- package/src/lsp.ts +1 -1
- package/src/relation.ts +21 -1
- package/src/render.ts +26 -21
- package/src/sigdecl.ts +1 -1
- package/src/std.ts +112 -69
- package/src/template.ts +55 -15
- package/src/val/AggFuncVal.ts +2 -2
- package/src/val/CmpFuncVal.ts +411 -0
- package/src/val/EachFuncVal.ts +49 -50
- package/src/val/LowerFuncVal.ts +18 -1
- package/src/val/NilVal.ts +29 -3
- package/src/val/NomFuncVal.ts +287 -0
- package/src/val/PackFuncVal.ts +3 -3
- package/src/val/RefVal.ts +1 -1
- package/src/val/TranslateFuncVal.ts +182 -0
- package/src/val/UpperFuncVal.ts +18 -1
- package/src/val/caserange.ts +115 -0
- package/src/vet.ts +287 -1
- package/src/val/FormFuncVal.ts +0 -119
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# check.sh --- the four questions to ask of a model. Run it after
|
|
3
|
+
# every edit to model.aon or data.aon.
|
|
4
|
+
#
|
|
5
|
+
# `aontu help tasks` maps a job to a verb; `aontu explain <code>` says
|
|
6
|
+
# what a refusal means; `aontu help language` is the whole grammar on
|
|
7
|
+
# one page. None of them needs a network.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
AONTU="${AONTU:-aontu}"
|
|
11
|
+
cd "$(dirname "$0")"
|
|
12
|
+
|
|
13
|
+
# 1. Does the data satisfy the truth -- and did the check examine
|
|
14
|
+
# anything? --strict-coverage exits 1 on a check that constrained no
|
|
15
|
+
# value, which is the failure a passing gate hides.
|
|
16
|
+
$AONTU vet --strict-coverage model.aon data.aon
|
|
17
|
+
|
|
18
|
+
# 2. What does it say at a path?
|
|
19
|
+
$AONTU get '$.entity.planet.table' data.aon
|
|
20
|
+
|
|
21
|
+
# 3. Why does it say that? Every contribution, with the line it is on.
|
|
22
|
+
$AONTU why '$.entity.planet.table' data.aon
|
|
23
|
+
|
|
24
|
+
# 4. A pin for the truth: it survives reformatting and moves on any
|
|
25
|
+
# change of meaning.
|
|
26
|
+
$AONTU hash model.aon
|
|
27
|
+
|
|
28
|
+
echo "ok --- model.aon and data.aon agree"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# data.aon --- an instance of the truth in model.aon. Edit it and run
|
|
2
|
+
# check.sh: every change is checked against the model, and a change the
|
|
3
|
+
# model refuses is named with the line it is on.
|
|
4
|
+
entity: {
|
|
5
|
+
planet: {
|
|
6
|
+
table: "planets"
|
|
7
|
+
fields: {
|
|
8
|
+
name: { type: "string", required: true }
|
|
9
|
+
diameter: { type: "integer" }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# model.aon --- the truth. `aontu vet model.aon data.aon` asks whether
|
|
2
|
+
# a document satisfies it.
|
|
3
|
+
#
|
|
4
|
+
# `&:` is the construct to learn first: it meets EVERY key of the map
|
|
5
|
+
# it sits in, so one statement constrains every entity. A quoted "*" is
|
|
6
|
+
# not a wildcard -- it is a key named `*`, it meets nothing, and a
|
|
7
|
+
# schema written that way reports `valid` over data that violates it.
|
|
8
|
+
# `aontu vet --strict-coverage` refuses to pass such a check.
|
|
9
|
+
entity: {
|
|
10
|
+
&: {
|
|
11
|
+
table: string & re("^[a-z][a-z0-9_]*$")
|
|
12
|
+
fields: {
|
|
13
|
+
&: {
|
|
14
|
+
type: "string" | "integer" | "boolean"
|
|
15
|
+
required: *false | boolean
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/skill/tasks.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# What do you want to do?
|
|
2
|
+
|
|
3
|
+
The verb for a job, found by the word you arrived with. aontu's own
|
|
4
|
+
vocabulary is on the right; yours is probably on the left.
|
|
5
|
+
|
|
6
|
+
## Start from a document that works
|
|
7
|
+
|
|
8
|
+
Nothing written yet? Do not invent the first document, edit one:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
aontu init # into the working directory
|
|
12
|
+
aontu init model/ # or into a named one
|
|
13
|
+
sh check.sh # the four questions, on what was just written
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`init` writes `model.aon` (an entity map constrained with `&:`),
|
|
17
|
+
`data.aon` (an instance of it that holds) and `check.sh` (the four
|
|
18
|
+
checks to run after every edit). It never overwrites: if any
|
|
19
|
+
of the three already stands there, it refuses and writes none of them.
|
|
20
|
+
|
|
21
|
+
## Describe a domain
|
|
22
|
+
|
|
23
|
+
An **ontology**, a **schema**, a **data model**, a **contract** — in
|
|
24
|
+
aontu these are all one thing: a document. Write the entities as a
|
|
25
|
+
**map keyed by name**, and say what every entry must satisfy with the
|
|
26
|
+
`&:` template:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
entity: {
|
|
30
|
+
&: {
|
|
31
|
+
table: string
|
|
32
|
+
fields: { &: { type: string, required: *false | boolean } }
|
|
33
|
+
}
|
|
34
|
+
planet: { table: "planets", fields: { name: { type: "string" } } }
|
|
35
|
+
moon: { table: "moons", fields: { name: { type: "string" } } }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`&:` is the construct to reach for first, and the one most easily
|
|
40
|
+
missed. It meets **every** key of the map it sits in. A quoted `"*"`
|
|
41
|
+
is not a wildcard — it is a key named `*`, and a schema written that
|
|
42
|
+
way constrains nothing while still reporting `valid`.
|
|
43
|
+
|
|
44
|
+
Prefer a named map to a list unless the order is a fact (a migration
|
|
45
|
+
sequence, a rule table tried in order). A key is an address, a name
|
|
46
|
+
other parts can refer to, and a diff that shows one insertion instead
|
|
47
|
+
of every following element renumbered.
|
|
48
|
+
|
|
49
|
+
## Check data against a model
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
aontu vet model.aon data.aon
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Exit `0` valid, `1` a contradiction, `3` incomplete (nothing
|
|
56
|
+
contradicts, but the model is not yet satisfied), `4` the model does
|
|
57
|
+
not stand up on its own. `--format json` for the machine-readable
|
|
58
|
+
report, `--closed` to refuse keys the model does not declare,
|
|
59
|
+
`--max-errors <n>` to cap the list.
|
|
60
|
+
|
|
61
|
+
**Make the check prove it checked something.** A check that examined
|
|
62
|
+
nothing answers exactly like one that passed:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
aontu vet --coverage model.aon data.aon # what did it examine?
|
|
66
|
+
aontu vet --strict-coverage model.aon data.aon # exit 1 if nothing
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--coverage` reports how many data leaves a declaration constrained,
|
|
70
|
+
the data paths none did, and the declarations no data met.
|
|
71
|
+
`--strict-coverage` exits 1 when the answer is nothing. The usual
|
|
72
|
+
cause is the `"*"` mistake above: reach for `&:`.
|
|
73
|
+
|
|
74
|
+
## Check the model is coherent with itself
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
aontu relations model.aon # declared entity edges: targets resolve, no cycles
|
|
78
|
+
aontu reaches planet moon model.aon # does one entity reach another, at any remove?
|
|
79
|
+
aontu trim --check model.aon # entries whose removal changes nothing
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Check code against a model
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
aontu render --check src/ --profile go.aon model.aon
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`render` writes code from the model; `--check` writes nothing and
|
|
89
|
+
lists what on disk differs from what the model implies. That is the
|
|
90
|
+
gate: the model is the truth, the code is the claim, and drift is a
|
|
91
|
+
finding. `--coverage` reports the other direction — model paths no
|
|
92
|
+
output consumed, and rendered declarations no rule produced.
|
|
93
|
+
|
|
94
|
+
## Ask what a model says, and why
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
aontu get $.entity.planet.table model.aon
|
|
98
|
+
aontu why $.entity.planet.table model.aon
|
|
99
|
+
aontu get $.entity --keys model.aon
|
|
100
|
+
aontu get $.entity --types model.aon
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`why` names **every** contribution to a value, with the file and line
|
|
104
|
+
each was written on. It is the first thing to run when a value is not
|
|
105
|
+
what you expected, and the second thing to run when `vet` refuses.
|
|
106
|
+
|
|
107
|
+
## Change a value without editing the file
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
aontu set '$.entity.planet.table=planet_v2' --entry model.aon --overlay local.aon
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The change is checked before it is written; a change that contradicts
|
|
114
|
+
a pinned value is refused, and the file is left alone.
|
|
115
|
+
|
|
116
|
+
## Gate a change to the model itself
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
aontu subsume old.aon new.aon # does the general admit every specific?
|
|
120
|
+
aontu breaking --against git#HEAD~1 model.aon
|
|
121
|
+
aontu hash model.aon # a pin that survives reformatting
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Hand a model to another agent
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
aontu agentsmd --write AGENTS.md model.aon
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Splices a derived stanza — the pin, the root keys, the shape, and the
|
|
131
|
+
commands spelled with paths that exist — between two markers, and
|
|
132
|
+
leaves the rest of the file alone. Re-run it in the commit that
|
|
133
|
+
changes the model.
|
|
134
|
+
|
|
135
|
+
## When something refuses
|
|
136
|
+
|
|
137
|
+
Read the code in the brackets, then look it up:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
aontu explain no_scalar_unify
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every finding carries a `code`, a `class`, a `path` and the **two
|
|
144
|
+
sites** that disagree — the value and the constraint it failed, each
|
|
145
|
+
with its file and line. The class says what kind of answer it is:
|
|
146
|
+
`conflict` (two things cannot both hold), `incomplete` (nothing is
|
|
147
|
+
wrong yet, something is missing), `parse`, `reference`, `budget`.
|
|
148
|
+
|
|
149
|
+
Full index: [`error-codes.md`](error-codes.md). The language on one
|
|
150
|
+
page: [`grammar-card.md`](grammar-card.md). The worked ladder from
|
|
151
|
+
plain JSON upward: [`examples.md`](examples.md).
|
package/src/agentsmd.ts
CHANGED
|
@@ -34,6 +34,12 @@ export type AgentsMdReport = {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export type AgentsMdOptions = {
|
|
37
|
+
// How deep the SHAPE line projects, default 2 (G11 phase 7). Two
|
|
38
|
+
// levels name the root keys and say `top` under them, which tells an
|
|
39
|
+
// agent what the document is ABOUT and nothing it can act on; a
|
|
40
|
+
// caller that wants the fields asks for them. The default is
|
|
41
|
+
// unchanged, because the stanza is spliced into a file people read.
|
|
42
|
+
depth?: number
|
|
37
43
|
// The name the stanza should call the document. The engine never
|
|
38
44
|
// reads a file; the CLI passes what the author typed.
|
|
39
45
|
name?: string
|
|
@@ -70,8 +76,10 @@ export function agentsMd(
|
|
|
70
76
|
// arrive through a `--text-ext` include listed those keys and then
|
|
71
77
|
// reported an EMPTY shape, because the read the shape came from
|
|
72
78
|
// refused the include the read above it had just honoured.
|
|
73
|
-
const shape = get(src, '$',
|
|
74
|
-
|
|
79
|
+
const shape = get(src, '$', {
|
|
80
|
+
view: 'types', depth: options.depth ?? 2,
|
|
81
|
+
path: options.path, ...includeOpts(options),
|
|
82
|
+
})
|
|
75
83
|
|
|
76
84
|
// A REAL path, so the example command works as written: the first
|
|
77
85
|
// root key when there is one, the root itself when there is not.
|
|
@@ -107,6 +115,9 @@ export function agentsMd(
|
|
|
107
115
|
'# change it without editing it',
|
|
108
116
|
'aontu set ' + example + '=<value> --entry ' + name +
|
|
109
117
|
' --overlay overlay.aon',
|
|
118
|
+
'',
|
|
119
|
+
'# the language itself, offline: the whole grammar on one page',
|
|
120
|
+
'aontu help language',
|
|
110
121
|
'```',
|
|
111
122
|
'',
|
|
112
123
|
'Regenerate this section with `aontu agentsmd ' + name + '`.',
|
package/src/allow.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// THE ROLE GATE: may an agent operating under a ROLE modify a SUBTREE?
|
|
4
|
+
//
|
|
5
|
+
// An agent about to change a model (`aontu set`, an overlay, a rewrite)
|
|
6
|
+
// asks first, and the answer comes from a ROLE MODEL that is itself an
|
|
7
|
+
// aontu document: one entry per role, each naming the subtrees the
|
|
8
|
+
// role may modify and, optionally, the ones it may not. Roles are
|
|
9
|
+
// arbitrary identifier strings -- `admin`, `dev`, `product`, `qa` --
|
|
10
|
+
// and the model is whatever its author writes, so the rules of the
|
|
11
|
+
// language (spreads, references, includes, `close()`) compose role
|
|
12
|
+
// models the way they compose everything else.
|
|
13
|
+
//
|
|
14
|
+
// The shape of a role is aontu too. It is conjoined with the model at
|
|
15
|
+
// evaluation, as a spread template over the roles map:
|
|
16
|
+
//
|
|
17
|
+
// roles: { &: { allow: [&: Entry] deny?: [&: Entry] } }
|
|
18
|
+
// Entry = string & re("^[$]") & re("[^.]$")
|
|
19
|
+
//
|
|
20
|
+
// so a malformed role -- `allow: "$.a"`, `deny: [1]`, an entry that is
|
|
21
|
+
// empty or does not start at the root or ends in a dot, a roles map
|
|
22
|
+
// that is a number -- is refused by the engine with the engine's own
|
|
23
|
+
// code and site, and this module never invents a finding shape of its
|
|
24
|
+
// own. The shape is a VALUE the model meets, not text appended to it:
|
|
25
|
+
// a model whose last line is an unclosed map or a dangling key would
|
|
26
|
+
// swallow appended text, and the shape would then apply to nothing.
|
|
27
|
+
// A role with no `allow` list allows nothing, because the template's
|
|
28
|
+
// empty list is what the tree holds for it.
|
|
29
|
+
//
|
|
30
|
+
// The lists are read from the WRITTEN tree, not from the generated
|
|
31
|
+
// document: a `hide()` mark keeps a list out of the output, and a gate
|
|
32
|
+
// that read the output would let a hidden `deny` vanish -- the wrong
|
|
33
|
+
// direction to be wrong in. Each entry must still be one concrete
|
|
34
|
+
// string: a kind (`string`) or anything else that does not generate is
|
|
35
|
+
// refused with the engine's `no_gen`.
|
|
36
|
+
//
|
|
37
|
+
// The rule is deliberately small, and it errs towards refusal:
|
|
38
|
+
//
|
|
39
|
+
// - A path is ALLOWED when some `allow` entry is an ancestor of it or
|
|
40
|
+
// equal to it. Being allowed `$.services` allows `$.services.auth`
|
|
41
|
+
// and `$.services.auth.replicas`; it does not allow `$` -- a change
|
|
42
|
+
// at the root reaches every sibling too.
|
|
43
|
+
// - A path is REFUSED when any `deny` entry INTERSECTS it: an
|
|
44
|
+
// ancestor, itself, or a descendant. Denied `$.services.*.tier`
|
|
45
|
+
// refuses `$.services.auth.tier` (the denied node), and it refuses
|
|
46
|
+
// `$.services.auth` and `$.services` too, because a change at
|
|
47
|
+
// either could rewrite the tier. Deny wins over allow whatever the
|
|
48
|
+
// order the entries were written in.
|
|
49
|
+
// - `*` in an entry matches exactly one segment, any key. It is the
|
|
50
|
+
// only pattern character; everything else is a key or a list index
|
|
51
|
+
// compared for equality, as a reference compares them.
|
|
52
|
+
// - A role is ONE KEY of the roles map, looked up as written, and a
|
|
53
|
+
// role the map does not declare may modify nothing.
|
|
54
|
+
//
|
|
55
|
+
// The answer names the entry that decided it, as a path INTO THE ROLE
|
|
56
|
+
// MODEL (`$.roles.dev.deny.0`), so `aontu why` can say who wrote the
|
|
57
|
+
// rule and where.
|
|
58
|
+
|
|
59
|
+
import { Aontu } from './aontu'
|
|
60
|
+
import { ConjunctVal } from './val/ConjunctVal'
|
|
61
|
+
import { anchorAt } from './vet'
|
|
62
|
+
import type { VetFinding } from './vet'
|
|
63
|
+
import { evalFailure, nearestKey, pathParts } from './query'
|
|
64
|
+
import { includeOpts } from './utility'
|
|
65
|
+
import type { IncludeOptions } from './utility'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Where the roles map lives when the caller does not say.
|
|
69
|
+
export const ALLOW_AT = '$.roles'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export type AllowVerdict = 'allowed' | 'refused' | 'error'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// What decided one path: the entry that covers it, the entry that
|
|
76
|
+
// intersects it, nothing at all, or a role the model does not declare.
|
|
77
|
+
export type AllowReason = 'allow' | 'deny' | 'uncovered' | 'no_role'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
export type AllowDecision = {
|
|
81
|
+
path: string // the asked path, normalised: `$.a.b`
|
|
82
|
+
allowed: boolean
|
|
83
|
+
reason: AllowReason
|
|
84
|
+
by?: string // the deciding entry's path in the role model
|
|
85
|
+
pattern?: string // that entry's text, as the author wrote it
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
export type AllowReport = {
|
|
90
|
+
verdict: AllowVerdict
|
|
91
|
+
role: string
|
|
92
|
+
paths: AllowDecision[]
|
|
93
|
+
// G2's finding shape, as every verb reports: empty when the verdict
|
|
94
|
+
// is `allowed`, the engine's own failure when it is `error`, and
|
|
95
|
+
// the `no_path` of an undeclared role beside the refusals.
|
|
96
|
+
findings: VetFinding[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export type AllowOptions = IncludeOptions & {
|
|
101
|
+
// Where the role model came from: relative loads resolve against
|
|
102
|
+
// its directory, and a finding's site names it.
|
|
103
|
+
path?: string
|
|
104
|
+
// The path of the roles map inside the model (default `$.roles`).
|
|
105
|
+
at?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// One entry: a string that starts at the root and does not end in a
|
|
110
|
+
// dot. `re()` refuses a nested quantifier, so the two conditions are
|
|
111
|
+
// two patterns rather than one grammar; an empty segment in the middle
|
|
112
|
+
// is harmless, because the path split drops it as a reference does.
|
|
113
|
+
const ENTRY = 'string & re("^[$]") & re("[^.]$")'
|
|
114
|
+
|
|
115
|
+
// The shape every role must satisfy, in the language: a list of
|
|
116
|
+
// subtree entries to allow, and optionally one to deny.
|
|
117
|
+
const ROLE_SHAPE = `{ allow: [&: ${ENTRY}] deny?: [&: ${ENTRY}] }`
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
// The shape document: a spread over the roles map at `at`, or -- when
|
|
121
|
+
// the roles map IS the document -- a top-level spread. Keys are quoted
|
|
122
|
+
// the way `aontu set` quotes an overlay line, so a segment may be a
|
|
123
|
+
// word the grammar spells otherwise.
|
|
124
|
+
function shapeSource(at: string): string {
|
|
125
|
+
const keys = pathParts(at).map((p) => JSON.stringify(p))
|
|
126
|
+
return 0 === keys.length
|
|
127
|
+
? '&: ' + ROLE_SHAPE
|
|
128
|
+
: keys.join(': ') + ': { &: ' + ROLE_SHAPE + ' }'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// `$.a.b` for a segment list, `$` for none: the spelling every report
|
|
133
|
+
// uses for a path, whatever the caller wrote.
|
|
134
|
+
function pathText(parts: string[]): string {
|
|
135
|
+
return '$' + (0 < parts.length ? '.' + parts.join('.') : '')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
// The finding shape `get` reports with, deliberately: the gate invents
|
|
140
|
+
// no error format of its own.
|
|
141
|
+
function finding(
|
|
142
|
+
code: string, path: string, message: string, note?: string): VetFinding {
|
|
143
|
+
return {
|
|
144
|
+
code,
|
|
145
|
+
class: 'reference',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
path,
|
|
148
|
+
message,
|
|
149
|
+
sites: [],
|
|
150
|
+
...(null == note ? {} : { note }),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
type Entry = {
|
|
156
|
+
parts: string[]
|
|
157
|
+
text: string
|
|
158
|
+
by: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
// The entries of one list of the role, read from the tree. A concrete
|
|
163
|
+
// string is taken as written, hidden or not; anything else is asked
|
|
164
|
+
// to generate, which is where a kind or a hidden kind fails with the
|
|
165
|
+
// engine's own code, and where a preference answers with its default.
|
|
166
|
+
function readEntries(
|
|
167
|
+
list: any, by: string, ctx: any,
|
|
168
|
+
): { entries: Entry[], finding?: VetFinding } {
|
|
169
|
+
const entries: Entry[] = []
|
|
170
|
+
for (let i = 0; i < list.peg.length; i++) {
|
|
171
|
+
const el: any = list.peg[i]
|
|
172
|
+
const before = ctx.err.length
|
|
173
|
+
const text: string = true === el.isString ? el.peg : el.gen(ctx)
|
|
174
|
+
if (before < ctx.err.length) {
|
|
175
|
+
const err: any = ctx.err[before]
|
|
176
|
+
return { entries, finding: finding(err.why, `${by}.${i}`, err.msg) }
|
|
177
|
+
}
|
|
178
|
+
entries.push({ parts: pathParts(text), text, by: `${by}.${i}` })
|
|
179
|
+
}
|
|
180
|
+
return { entries }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// One segment of an entry against one segment of a path: `*` matches
|
|
185
|
+
// any key, anything else matches itself.
|
|
186
|
+
function segmentMatches(pattern: string, segment: string): boolean {
|
|
187
|
+
return '*' === pattern || pattern === segment
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// The two subtrees share a node: one is an ancestor of the other, or
|
|
192
|
+
// they are the same. Checked over the shorter of the two, because the
|
|
193
|
+
// longer one only says where inside the shared subtree it goes on.
|
|
194
|
+
function intersects(entry: string[], path: string[]): boolean {
|
|
195
|
+
const n = Math.min(entry.length, path.length)
|
|
196
|
+
for (let i = 0; i < n; i++) {
|
|
197
|
+
if (!segmentMatches(entry[i], path[i])) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
// The entry is an ancestor of the path, or the path itself. A longer
|
|
206
|
+
// entry names something INSIDE the asked subtree, and a change to the
|
|
207
|
+
// subtree reaches its siblings, so it does not cover.
|
|
208
|
+
function covers(entry: string[], path: string[]): boolean {
|
|
209
|
+
return entry.length <= path.length && intersects(entry, path)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
function decide(asked: string, allows: Entry[], denies: Entry[]): AllowDecision {
|
|
214
|
+
const parts = pathParts(asked)
|
|
215
|
+
const path = pathText(parts)
|
|
216
|
+
|
|
217
|
+
// Deny first, and any intersection refuses: an entry above the path
|
|
218
|
+
// forbids the whole subtree it is in, and one below it forbids the
|
|
219
|
+
// change that would rewrite it from above.
|
|
220
|
+
for (const d of denies) {
|
|
221
|
+
if (intersects(d.parts, parts)) {
|
|
222
|
+
return { path, allowed: false, reason: 'deny', by: d.by, pattern: d.text }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const a of allows) {
|
|
226
|
+
if (covers(a.parts, parts)) {
|
|
227
|
+
return { path, allowed: true, reason: 'allow', by: a.by, pattern: a.text }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { path, allowed: false, reason: 'uncovered' }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
function errorReport(role: string, f: VetFinding): AllowReport {
|
|
235
|
+
return { verdict: 'error', role, paths: [], findings: [f] }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
// Evaluate the role model, select the role, and decide every path.
|
|
240
|
+
export function allow(
|
|
241
|
+
src: string, role: string, paths: string[], opts?: AllowOptions,
|
|
242
|
+
): AllowReport {
|
|
243
|
+
const options = opts ?? {}
|
|
244
|
+
const at = pathText(pathParts(options.at ?? ALLOW_AT))
|
|
245
|
+
|
|
246
|
+
const aontu = new Aontu(includeOpts(options))
|
|
247
|
+
const ctx = aontu.ctx({ collect: true })
|
|
248
|
+
const parseOpts = null == options.path ? undefined : { path: options.path }
|
|
249
|
+
|
|
250
|
+
// The model MEETS the shape as data meets a schema under vet: both
|
|
251
|
+
// parsed, conjoined, and unified ONCE. A parsed tree is single-use,
|
|
252
|
+
// and a model evaluated on its own and then met again has already
|
|
253
|
+
// resolved its references against itself, so a registry written
|
|
254
|
+
// `roles: close({ &: $.Role ... })` would fail its second pass with
|
|
255
|
+
// a `$.Role` it cannot find. The shape is parsed FIRST: the context
|
|
256
|
+
// takes the last parsed document as its root and as the text an
|
|
257
|
+
// error frame excerpts, and both must be the model's.
|
|
258
|
+
const shape = aontu.parse(shapeSource(at), undefined, ctx)
|
|
259
|
+
const model = aontu.parse(src, parseOpts, ctx)
|
|
260
|
+
if (0 < ctx.err.length) {
|
|
261
|
+
return errorReport(role, evalFailure(ctx))
|
|
262
|
+
}
|
|
263
|
+
const root: any = aontu.unify(
|
|
264
|
+
new ConjunctVal({ peg: [model, shape] }, ctx), undefined, ctx)
|
|
265
|
+
if (0 < ctx.err.length || true === root.isNil) {
|
|
266
|
+
return errorReport(role, evalFailure(ctx))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// The shape made the roles map exist, so what can be missing is the
|
|
270
|
+
// ROLE -- and an undeclared role may modify nothing. That is the
|
|
271
|
+
// question's answer (refused), not a broken model (error), and the
|
|
272
|
+
// finding carries the nearest declared name. The role is one key,
|
|
273
|
+
// looked up as written: not a path, so a name may hold a dot, and
|
|
274
|
+
// an own key only, so a name the prototype has is not a role.
|
|
275
|
+
const roles: any = anchorAt(root, at)
|
|
276
|
+
const atRole = `${at}.${role}`
|
|
277
|
+
if (!Object.prototype.hasOwnProperty.call(roles.peg, role)) {
|
|
278
|
+
const near = nearestKey(role, Object.keys(roles.peg))
|
|
279
|
+
return {
|
|
280
|
+
verdict: 'refused',
|
|
281
|
+
role,
|
|
282
|
+
paths: paths.map((p) => ({
|
|
283
|
+
path: pathText(pathParts(p)), allowed: false, reason: 'no_role',
|
|
284
|
+
})),
|
|
285
|
+
findings: [finding(
|
|
286
|
+
'no_path',
|
|
287
|
+
atRole,
|
|
288
|
+
`The role ${role} is not declared at ${at} in this document.`,
|
|
289
|
+
null == near ? undefined : `did you mean ${near}?`)],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const node: any = roles.peg[role]
|
|
293
|
+
|
|
294
|
+
const allows = readEntries(node.peg.allow, `${atRole}.allow`, ctx)
|
|
295
|
+
if (null != allows.finding) {
|
|
296
|
+
return errorReport(role, allows.finding)
|
|
297
|
+
}
|
|
298
|
+
const denies = readEntries(node.peg.deny, `${atRole}.deny`, ctx)
|
|
299
|
+
if (null != denies.finding) {
|
|
300
|
+
return errorReport(role, denies.finding)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const decisions = paths.map((p) => decide(p, allows.entries, denies.entries))
|
|
304
|
+
|
|
305
|
+
// Nothing asked is nothing allowed: a gate that answered `allowed`
|
|
306
|
+
// to an empty question would let a caller that dropped its
|
|
307
|
+
// arguments through.
|
|
308
|
+
const allowed = 0 < decisions.length && decisions.every((d) => d.allowed)
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
verdict: allowed ? 'allowed' : 'refused',
|
|
312
|
+
role,
|
|
313
|
+
paths: decisions,
|
|
314
|
+
findings: [],
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/aontu.ts
CHANGED
|
@@ -22,6 +22,10 @@ import { get, why } from './query'
|
|
|
22
22
|
import { patch } from './patch'
|
|
23
23
|
import { diff } from './diff'
|
|
24
24
|
import { agentsMd } from './agentsmd'
|
|
25
|
+
import { allow } from './allow'
|
|
26
|
+
export type {
|
|
27
|
+
AllowDecision, AllowOptions, AllowReason, AllowReport, AllowVerdict,
|
|
28
|
+
} from './allow'
|
|
25
29
|
import { graphOf } from './graph'
|
|
26
30
|
import { relationCheck, relationErrors } from './relation'
|
|
27
31
|
import { view, viewSet, viewTree } from './view'
|
|
@@ -38,7 +42,7 @@ export type { LintFinding, FormatReport, FormatOptions } from './format'
|
|
|
38
42
|
// Kept in step with package.json by the `version` npm lifecycle script,
|
|
39
43
|
// which runs on `npm version` / `npm run repo-bump`. version.test.ts
|
|
40
44
|
// fails if the two ever drift.
|
|
41
|
-
const VERSION = '0.
|
|
45
|
+
const VERSION = '0.62.0'
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
// A module file's VALUE, as far as it goes. COLLECTED, not raised: a
|
|
@@ -475,6 +479,11 @@ export {
|
|
|
475
479
|
patch,
|
|
476
480
|
diff,
|
|
477
481
|
agentsMd,
|
|
482
|
+
|
|
483
|
+
// The role gate (docs/design/ALLOW.0.md): may a role modify a
|
|
484
|
+
// subtree, by a role model that is itself an aontu document. The
|
|
485
|
+
// question an agent asks before `set`.
|
|
486
|
+
allow,
|
|
478
487
|
graphOf,
|
|
479
488
|
relationCheck,
|
|
480
489
|
view,
|