aontu 0.65.0 → 0.67.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 +4 -1
- package/dist/allow.js +2 -2
- package/dist/allow.js.map +1 -1
- package/dist/aontu.d.ts +1 -1
- package/dist/aontu.js +1 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +344 -23
- package/dist/cli.js.map +1 -1
- package/dist/helpdoc.js +1 -1
- package/dist/helpdoc.js.map +1 -1
- package/dist/hints.js +2 -1
- package/dist/hints.js.map +1 -1
- package/dist/lang.js +6 -3
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/mcp-server.js +1 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +7 -9
- package/dist/query.js.map +1 -1
- package/dist/vet.d.ts +1 -0
- package/dist/vet.js +39 -9
- package/dist/vet.js.map +1 -1
- package/dist/view.js +2 -4
- package/dist/view.js.map +1 -1
- package/grammar/aontu.gbnf +8 -18
- package/grammar/aontu.lark +6 -14
- package/grammar/aontu.tmLanguage.json +4 -6
- package/package.json +4 -3
- package/skill/tasks.md +12 -7
- package/src/allow.ts +3 -3
- package/src/aontu.ts +1 -1
- package/src/cli.ts +380 -24
- package/src/helpdoc.ts +1 -1
- package/src/hints.ts +2 -1
- package/src/lang.ts +7 -3
- package/src/mcp-server.ts +1 -1
- package/src/mcp.ts +1 -1
- package/src/query.ts +8 -13
- package/src/tsconfig.json +2 -1
- package/src/vet.ts +47 -9
- package/src/view.ts +3 -7
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -14,16 +14,14 @@
|
|
|
14
14
|
"rejects AS THOUGH it were valid, which is why the builtin list below is",
|
|
15
15
|
"the engine's own set and not a hand-picked sample of it.",
|
|
16
16
|
"",
|
|
17
|
-
"THE BUILTIN LIST IS A COPY, AND A COPY DRIFTS
|
|
18
|
-
"`same` — never a builtin — and was missing `acyclic`, `inverse` and",
|
|
19
|
-
"`rel` for two releases, with every test green. So the same guard the",
|
|
17
|
+
"THE BUILTIN LIST IS A COPY, AND A COPY DRIFTS, so the same guard the",
|
|
20
18
|
"other two grammars have covers this file: ts/test/grammar.test.ts,",
|
|
21
19
|
"`the-textmate-grammar-names-exactly-the-engine-builtins`, asserts this",
|
|
22
20
|
"list against BUILTIN_FUNCS in BOTH directions. Add a builtin to the",
|
|
23
21
|
"engine without adding it here and that test fails.",
|
|
24
22
|
"",
|
|
25
|
-
"No `|>`:
|
|
26
|
-
"
|
|
23
|
+
"No `|>`: the pipe is not in the language, and all three published",
|
|
24
|
+
"grammars agree about that."
|
|
27
25
|
],
|
|
28
26
|
"name": "aontu",
|
|
29
27
|
"displayName": "Aontu",
|
|
@@ -130,7 +128,7 @@
|
|
|
130
128
|
"match": "\\b(true|false|null)\\b"
|
|
131
129
|
},
|
|
132
130
|
{
|
|
133
|
-
"comment": "`_` is a literal of the language
|
|
131
|
+
"comment": "`_` is a literal of the language, not text.",
|
|
134
132
|
"name": "constant.language.placeholder.aontu",
|
|
135
133
|
"match": "(?<![A-Za-z0-9_])_(?![A-Za-z0-9_])"
|
|
136
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aontu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"main": "dist/aontu.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"types": "dist/aontu.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"watch": "tsc --build src test -w",
|
|
39
39
|
"build": "tsc --build src test",
|
|
40
40
|
"prepack": "node scripts/prepack.js",
|
|
41
|
-
"clean": "rm -rf dist dist-test node_modules yarn.lock package-lock.json",
|
|
41
|
+
"clean": "rm -rf dist dist-test .tsbuild node_modules yarn.lock package-lock.json",
|
|
42
42
|
"reset": "npm run clean && npm i && npm run build && npm test",
|
|
43
43
|
"version": "node -e \"const fs=require('fs');const v=require('./package.json').version;const f='./src/aontu.ts';const s=fs.readFileSync(f,'utf8');const re=/^const VERSION = '.*?'/m;if(!re.test(s)){console.error('version: no VERSION constant in '+f);process.exit(1)}const n=s.replace(re,'const VERSION = \\''+v+'\\'');if(n!==s){fs.writeFileSync(f,n);console.log('version: src/aontu.ts VERSION -> '+v)}\"",
|
|
44
44
|
"repo-bump": "npm version patch --no-git-tag-version && node -e \"console.log('bumped to v' + require('./package.json').version + ' (package.json only - commit via repo-tag / repo-release)')\"",
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
"@tabnas/parser": "0.9.7",
|
|
79
79
|
"@tabnas/path": "0.3.2",
|
|
80
80
|
"@tabnas/toml": "0.5.7",
|
|
81
|
-
"@tabnas/yaml": "0.5.7"
|
|
81
|
+
"@tabnas/yaml": "0.5.7",
|
|
82
|
+
"jostraca": "0.38.0"
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"@tabnas/railroad": "0.3.6",
|
package/skill/tasks.md
CHANGED
|
@@ -82,16 +82,21 @@ aontu trim --check model.aon # entries whose removal changes nothing
|
|
|
82
82
|
## Generate code from a model, and hold it
|
|
83
83
|
|
|
84
84
|
```
|
|
85
|
-
aontu
|
|
86
|
-
aontu
|
|
85
|
+
aontu render gen.aon build # write the files below build/
|
|
86
|
+
aontu render --check gen.aon build # write nothing; exit 1 when build/ has drifted
|
|
87
|
+
aontu render gen/ build # every generator directly in gen/, as one run
|
|
88
|
+
aontu model get out gen.aon # the component tree: files, and the lines in them
|
|
89
|
+
aontu trace gen.aon # what rule wrote each line
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
A generator answers a **component tree** -- `file(name, [lines])` and
|
|
90
|
-
its neighbours -- which
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
its neighbours -- which `aontu render` hands to jostraca, the generator
|
|
94
|
+
runtime, to write. A tree that is one file is written to the path
|
|
95
|
+
itself, and a folder of generators is one run. `--check` compares with
|
|
96
|
+
what is committed: the model is the
|
|
97
|
+
truth, the code is the claim, and drift is a finding. `aontu trace`
|
|
98
|
+
answers the other direction: for a line of the output, the model node
|
|
99
|
+
and the rule behind it.
|
|
95
100
|
|
|
96
101
|
## Ask what a model says, and why
|
|
97
102
|
|
package/src/allow.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { Aontu } from './aontu'
|
|
5
5
|
import { ConjunctVal } from './val/ConjunctVal'
|
|
6
|
-
import { anchorAt } from './vet'
|
|
6
|
+
import { anchorAt, engineFinding } from './vet'
|
|
7
7
|
import type { VetFinding } from './vet'
|
|
8
8
|
import { evalFailure, nearestKey, pathParts } from './query'
|
|
9
9
|
import { includeOpts } from './utility'
|
|
@@ -101,7 +101,7 @@ function readEntries(
|
|
|
101
101
|
const text: string = true === el.isString ? el.peg : el.gen(ctx)
|
|
102
102
|
if (before < ctx.err.length) {
|
|
103
103
|
const err: any = ctx.err[before]
|
|
104
|
-
return { entries, finding:
|
|
104
|
+
return { entries, finding: engineFinding(err, ctx, `${by}.${i}`) }
|
|
105
105
|
}
|
|
106
106
|
entries.push({ parts: pathParts(text), text, by: `${by}.${i}` })
|
|
107
107
|
}
|
|
@@ -174,7 +174,7 @@ export function allow(
|
|
|
174
174
|
const root: any = aontu.unify(
|
|
175
175
|
new ConjunctVal({ peg: [model, shape] }, ctx), undefined, ctx)
|
|
176
176
|
if (0 < ctx.err.length || true === root.isNil) {
|
|
177
|
-
return errorReport(role, evalFailure(ctx))
|
|
177
|
+
return errorReport(role, evalFailure(ctx, root))
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
const roles: any = anchorAt(root, at)
|
package/src/aontu.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
import { evalFailure } from './query'
|
|
6
6
|
// __importStar downlevel helper, whose branches no supported Node takes.
|
|
7
7
|
import {
|
|
8
|
-
existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync,
|
|
8
|
+
existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync,
|
|
9
|
+
writeFileSync,
|
|
9
10
|
} from 'node:fs'
|
|
11
|
+
import type { Dirent } from 'node:fs'
|
|
10
12
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
11
13
|
import { tmpdir } from 'node:os'
|
|
12
14
|
import { createInterface } from 'node:readline'
|
|
@@ -97,6 +99,9 @@ const HELP = `Usage: aontu [options] [file]
|
|
|
97
99
|
[--profile <file>] <file>
|
|
98
100
|
aontu trace [--at <path>] [--format json] [--marker <token>]
|
|
99
101
|
[--profile <file>] <file>
|
|
102
|
+
aontu render [--check] [--at <path>] [--format json]
|
|
103
|
+
[--marker <token>] [--profile <file>]
|
|
104
|
+
<file|folder> <path>
|
|
100
105
|
aontu hash [options] <file>
|
|
101
106
|
aontu sync [--frozen] [options] [dir]
|
|
102
107
|
aontu add <pkg>[@<version>] [options] [dir]
|
|
@@ -161,22 +166,30 @@ query between a document and its own earlier versions.
|
|
|
161
166
|
|
|
162
167
|
Options:
|
|
163
168
|
-c, --canon Print the canonical form instead of generated JSON
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
(the bare command's, as --jsonl is; model get has
|
|
170
|
+
its own)
|
|
171
|
+
--format <f> text (default) or json, on every verb that answers a
|
|
172
|
+
report. The json form is one object opening with an
|
|
173
|
+
aontu block; the bare command's carries findings, ok
|
|
174
|
+
and out
|
|
167
175
|
-h, --help Show this help and exit (the verbs and their flags);
|
|
168
176
|
aontu help is the LANGUAGE, and lists its own topics
|
|
169
177
|
--jsonl REPL: answer every command as one JSON line
|
|
170
178
|
-v, --version Print the version and exit
|
|
171
|
-
--trust <t> Include capability: system
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
--trust <t> Include capability: system, none, or root[:dir] to
|
|
180
|
+
confine @"..." below a directory. A bare root means
|
|
181
|
+
the entry root: the document's own directory, or the
|
|
182
|
+
project's for the package verbs. Every verb takes it
|
|
183
|
+
but help, explain, init and lsp, which read no
|
|
184
|
+
document, and mcp, which is the npm build's server
|
|
185
|
+
and confines with --root. Unset, a run behaves as
|
|
186
|
+
system, and the bare command warns once for an
|
|
187
|
+
include that leaves the entry root
|
|
175
188
|
--include-root <dir> Shorthand for --trust root:<dir>
|
|
176
|
-
--text-ext <e> Read these extensions as text too, comma-separated
|
|
177
|
-
|
|
189
|
+
--text-ext <e> Read these extensions as text too, comma-separated,
|
|
190
|
+
with or without dots (md,sql). .txt needs no flag; a
|
|
178
191
|
named format keeps its meaning, and .js stays
|
|
179
|
-
refused.
|
|
192
|
+
refused. The verbs that take --trust take it too
|
|
180
193
|
|
|
181
194
|
Package verbs (a module is imported; a package is published):
|
|
182
195
|
sync Make the project correct: resolve by minimum version
|
|
@@ -411,6 +424,31 @@ every other line is a line of output.
|
|
|
411
424
|
|
|
412
425
|
Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
|
|
413
426
|
|
|
427
|
+
Render options:
|
|
428
|
+
--check Compare what the generator writes with what <path>
|
|
429
|
+
holds, write nothing, and exit 1 on drift
|
|
430
|
+
--at <path> Where the component tree lives in the document
|
|
431
|
+
(default $.out)
|
|
432
|
+
--format json Print the files written, or the drift, as JSON
|
|
433
|
+
--marker <t> The file is a generator, and this is its marker
|
|
434
|
+
(default //-, and #- --- /*- <!--- by extension)
|
|
435
|
+
--profile <f> A profile file, whose template.ext names the
|
|
436
|
+
extensions it marks and template.marker the marker
|
|
437
|
+
|
|
438
|
+
The render verb writes the component tree a generator answers. The
|
|
439
|
+
tree is handed to jostraca, the generator runtime, which writes the
|
|
440
|
+
files: a tree that is one file is written to <path> itself, unless
|
|
441
|
+
<path> is a directory, and any other tree is written below <path>.
|
|
442
|
+
With --check nothing is written and <path> is compared with what the
|
|
443
|
+
generator writes, one "kind: file" line per difference.
|
|
444
|
+
A folder as the generator is a set: every regular file directly in
|
|
445
|
+
it, dotfiles aside, in name order, and their trees are written below
|
|
446
|
+
<path> as one run, so a path two of them claim is refused. A file with
|
|
447
|
+
no marker line in it is refused by name.
|
|
448
|
+
|
|
449
|
+
Render exit codes: 0 written or clean, 1 --check drift, 2 usage or
|
|
450
|
+
I/O, 4 the document does not stand up, or --at names nothing.
|
|
451
|
+
|
|
414
452
|
Model set options:
|
|
415
453
|
--entry <file> The document the change is checked against
|
|
416
454
|
--overlay <file> The file the change is appended to (created if
|
|
@@ -678,7 +716,7 @@ function takeTrust(argv: string[], io: Io = PROCESS_IO):
|
|
|
678
716
|
}
|
|
679
717
|
else if ('--include-root' === arg) {
|
|
680
718
|
const dir = argv[++i]
|
|
681
|
-
if (null == dir) {
|
|
719
|
+
if (null == dir || '' === dir) {
|
|
682
720
|
io.err('aontu: --include-root needs a directory\n')
|
|
683
721
|
return undefined
|
|
684
722
|
}
|
|
@@ -2674,20 +2712,42 @@ function pkgNetText(verb: string, report: any): string {
|
|
|
2674
2712
|
const MODEL_HELP = 'aontu model get|why|set ... (try --help)'
|
|
2675
2713
|
|
|
2676
2714
|
// One document, interrogated or edited (ADR-039 part 5).
|
|
2715
|
+
// Where the subcommand is, past any global flag that precedes it.
|
|
2716
|
+
// `takeTrust` strips those anywhere in a tail, so the subcommand has
|
|
2717
|
+
// to be found past a flag AND past its value: in `--text-ext get` the
|
|
2718
|
+
// `get` is the extension list, not the subcommand.
|
|
2719
|
+
const MODEL_SUBS = ['get', 'why', 'set']
|
|
2720
|
+
const GLOBAL_VALUED = ['--trust', '--include-root', '--text-ext']
|
|
2721
|
+
|
|
2722
|
+
|
|
2723
|
+
function modelSubAt(argv: string[]): number {
|
|
2724
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2725
|
+
if (GLOBAL_VALUED.includes(argv[i])) {
|
|
2726
|
+
i++
|
|
2727
|
+
continue
|
|
2728
|
+
}
|
|
2729
|
+
return MODEL_SUBS.includes(argv[i]) ? i : -1
|
|
2730
|
+
}
|
|
2731
|
+
return -1
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
|
|
2677
2735
|
function runModel(argv: string[]): number {
|
|
2678
2736
|
const sub = argv[0]
|
|
2679
2737
|
if ('-h' === sub || '--help' === sub) {
|
|
2680
2738
|
process.stdout.write(HELP)
|
|
2681
2739
|
return 0
|
|
2682
2740
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2741
|
+
const at = modelSubAt(argv)
|
|
2742
|
+
if (0 <= at) {
|
|
2743
|
+
const tail = argv.slice(0, at).concat(argv.slice(at + 1))
|
|
2744
|
+
if ('get' === argv[at]) {
|
|
2745
|
+
return runGet(tail)
|
|
2746
|
+
}
|
|
2747
|
+
if ('why' === argv[at]) {
|
|
2748
|
+
return runWhy(tail)
|
|
2749
|
+
}
|
|
2750
|
+
return runSet(tail)
|
|
2691
2751
|
}
|
|
2692
2752
|
process.stderr.write(`aontu: model needs get, why or set\n${MODEL_HELP}\n`)
|
|
2693
2753
|
return 2
|
|
@@ -2880,6 +2940,297 @@ function runTrace(argv: string[]): number {
|
|
|
2880
2940
|
}
|
|
2881
2941
|
|
|
2882
2942
|
|
|
2943
|
+
// ---------------------------------------------------------------------
|
|
2944
|
+
// The writer (ADR-040). A generator answers a component tree; this verb
|
|
2945
|
+
// hands it to jostraca, which writes the files below a path or holds
|
|
2946
|
+
// them to it.
|
|
2947
|
+
|
|
2948
|
+
const RENDER_HELP =
|
|
2949
|
+
'aontu render [--check] [--at <path>] [--format json] ' +
|
|
2950
|
+
'[--marker <token>] [--profile <file>] <file|folder> <path> (try --help)'
|
|
2951
|
+
|
|
2952
|
+
|
|
2953
|
+
// Loaded at the call, so every other verb starts without it.
|
|
2954
|
+
function generatorRuntime(): any {
|
|
2955
|
+
return require('jostraca')
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
|
|
2959
|
+
function isDirectory(path: string): boolean {
|
|
2960
|
+
return true === statSync(path, { throwIfNoEntry: false })?.isDirectory()
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
|
|
2964
|
+
// A `File` the write path skips, RENAMED rather than removed: dropping
|
|
2965
|
+
// the node would take its children's claims with it and hide drift the
|
|
2966
|
+
// write path does make. The skip is the File's own save alone, so what
|
|
2967
|
+
// separates the runs is its output path, composed by the runtime rather
|
|
2968
|
+
// than here.
|
|
2969
|
+
const EXCLUDED_NAME = '.aontu-check-excluded-'
|
|
2970
|
+
|
|
2971
|
+
|
|
2972
|
+
// `exclude` as the runtime reads it: `true`, or a string or list member
|
|
2973
|
+
// equal to the node's COMPONENT path, the chain of `name` props above
|
|
2974
|
+
// it, which a Project's `folder` is not part of. The Go runtime honours
|
|
2975
|
+
// the boolean alone, so the ports' `--check` answers differ for the
|
|
2976
|
+
// path forms (test/spec/divergent.tsv).
|
|
2977
|
+
function excludedFile(exclude: any, at: string[]): boolean {
|
|
2978
|
+
if (true === exclude) {
|
|
2979
|
+
return true
|
|
2980
|
+
}
|
|
2981
|
+
const path = at.join('/')
|
|
2982
|
+
if ('string' === typeof exclude) {
|
|
2983
|
+
return exclude === path
|
|
2984
|
+
}
|
|
2985
|
+
return Array.isArray(exclude) && exclude.includes(path)
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
function renameExcluded(node: any, at: string[], cut: number[]): any {
|
|
2990
|
+
if (Array.isArray(node)) {
|
|
2991
|
+
return node.map((child) => renameExcluded(child, at, cut))
|
|
2992
|
+
}
|
|
2993
|
+
// A hand-written tree carries nodes with no `props` and nodes with
|
|
2994
|
+
// no `children`, and neither needs an arm of its own.
|
|
2995
|
+
const props: any = node.props ?? {}
|
|
2996
|
+
const below = 'string' === typeof props.name ? at.concat(props.name) : at
|
|
2997
|
+
if ('File' === node.cmp && excludedFile(props.exclude, below)) {
|
|
2998
|
+
cut.push(1)
|
|
2999
|
+
return { ...node, props: { ...props, name: EXCLUDED_NAME + cut.length } }
|
|
3000
|
+
}
|
|
3001
|
+
if (!Array.isArray(node.children)) {
|
|
3002
|
+
return node
|
|
3003
|
+
}
|
|
3004
|
+
return { ...node, children: renameExcluded(node.children, below, cut) }
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
|
|
3008
|
+
// A file the runtime declined to touch, its copy on disk carrying the
|
|
3009
|
+
// protect marker, is in none of the lists it answers with: they hold
|
|
3010
|
+
// what was DONE to a file, and nothing was. The run's own record names
|
|
3011
|
+
// it. `since` drops what earlier runs left.
|
|
3012
|
+
function renderSkipped(folder: string, since: number): string[] {
|
|
3013
|
+
const at = join(folder, '.jostraca', 'jostraca.meta.log')
|
|
3014
|
+
let meta: any
|
|
3015
|
+
try {
|
|
3016
|
+
meta = JSON.parse(readFileSync(at, 'utf8'))
|
|
3017
|
+
}
|
|
3018
|
+
catch (err: any) {
|
|
3019
|
+
return []
|
|
3020
|
+
}
|
|
3021
|
+
return Object.entries(meta?.files ?? {})
|
|
3022
|
+
.filter(([_, f]: [string, any]) => 'skip' === f?.action && since <= f?.when)
|
|
3023
|
+
.map(([path]) => path)
|
|
3024
|
+
.sort(cmpCodePoint)
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
|
|
3028
|
+
async function runRender(argv: string[]): Promise<number> {
|
|
3029
|
+
const trusted = takeTrust(argv)
|
|
3030
|
+
if (null == trusted) {
|
|
3031
|
+
return 2
|
|
3032
|
+
}
|
|
3033
|
+
argv = trusted.argv
|
|
3034
|
+
const trust = trusted.trust
|
|
3035
|
+
const rest: string[] = []
|
|
3036
|
+
const profileFiles: string[] = []
|
|
3037
|
+
let format: SubsumeFormat = 'text'
|
|
3038
|
+
let at: string | undefined = undefined
|
|
3039
|
+
let marker: string | undefined = undefined
|
|
3040
|
+
let check = false
|
|
3041
|
+
|
|
3042
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3043
|
+
const arg = argv[i]
|
|
3044
|
+
if ('-h' === arg || '--help' === arg) {
|
|
3045
|
+
process.stdout.write(HELP)
|
|
3046
|
+
return 0
|
|
3047
|
+
}
|
|
3048
|
+
if ('--check' === arg) {
|
|
3049
|
+
check = true
|
|
3050
|
+
}
|
|
3051
|
+
else if ('--format' === arg) {
|
|
3052
|
+
const f = argv[++i]
|
|
3053
|
+
if ('text' !== f && 'json' !== f) {
|
|
3054
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
3055
|
+
return 2
|
|
3056
|
+
}
|
|
3057
|
+
format = f
|
|
3058
|
+
}
|
|
3059
|
+
else if ('--at' === arg) {
|
|
3060
|
+
at = argv[++i]
|
|
3061
|
+
if (null == at || '' === at) {
|
|
3062
|
+
process.stderr.write('aontu: --at needs a path\n')
|
|
3063
|
+
return 2
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
else if ('--marker' === arg) {
|
|
3067
|
+
marker = argv[++i]
|
|
3068
|
+
if (null == marker || '' === marker) {
|
|
3069
|
+
process.stderr.write('aontu: --marker needs a token\n')
|
|
3070
|
+
return 2
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
else if ('--profile' === arg) {
|
|
3074
|
+
const pf = argv[++i]
|
|
3075
|
+
if (null == pf || '' === pf) {
|
|
3076
|
+
process.stderr.write('aontu: --profile needs a file\n')
|
|
3077
|
+
return 2
|
|
3078
|
+
}
|
|
3079
|
+
profileFiles.push(pf)
|
|
3080
|
+
}
|
|
3081
|
+
else if (arg.startsWith('-')) {
|
|
3082
|
+
process.stderr.write(`aontu: unknown render option ${arg} (try --help)\n`)
|
|
3083
|
+
return 2
|
|
3084
|
+
}
|
|
3085
|
+
else {
|
|
3086
|
+
rest.push(arg)
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
if (2 !== rest.length) {
|
|
3091
|
+
process.stderr.write(`aontu: render needs a file and a path\n${RENDER_HELP}\n`)
|
|
3092
|
+
return 2
|
|
3093
|
+
}
|
|
3094
|
+
const [file, dest] = rest
|
|
3095
|
+
|
|
3096
|
+
const declared = loadProfiles(profileFiles, trust)
|
|
3097
|
+
if ('number' === typeof declared) {
|
|
3098
|
+
return declared
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// A FOLDER IS A SET OF GENERATORS: every regular file directly in it,
|
|
3102
|
+
// dotfiles aside, in code-point order, written as one tree.
|
|
3103
|
+
let entries: Dirent[] | undefined
|
|
3104
|
+
try {
|
|
3105
|
+
entries = readdirSync(file, { withFileTypes: true })
|
|
3106
|
+
}
|
|
3107
|
+
catch (err: any) {
|
|
3108
|
+
entries = undefined
|
|
3109
|
+
}
|
|
3110
|
+
const set = undefined !== entries
|
|
3111
|
+
const files = undefined === entries ? [file] :
|
|
3112
|
+
entries.filter((e) => e.isFile() && !e.name.startsWith('.'))
|
|
3113
|
+
.map((e) => e.name).sort(cmpCodePoint).map((n) => join(file, n))
|
|
3114
|
+
if (0 === files.length) {
|
|
3115
|
+
process.stderr.write(`aontu: ${file} holds no generator\n`)
|
|
3116
|
+
return 2
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
const trees: any[] = []
|
|
3120
|
+
for (const f of files) {
|
|
3121
|
+
let src: string
|
|
3122
|
+
try {
|
|
3123
|
+
src = readFileSync(f, 'utf8')
|
|
3124
|
+
}
|
|
3125
|
+
catch (err: any) {
|
|
3126
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3127
|
+
return 2
|
|
3128
|
+
}
|
|
3129
|
+
if (!/[.](aon|aontu)$/.test(f)) {
|
|
3130
|
+
const mark = marker ?? markerFromProfiles(declared, f) ?? markerFor(f)
|
|
3131
|
+
if (!templateOutputs(src, mark).some((out) => !out)) {
|
|
3132
|
+
process.stderr.write(`aontu: ${f} carries no ${mark} marker line, ` +
|
|
3133
|
+
'so there is no aontu in it to render\n')
|
|
3134
|
+
return 2
|
|
3135
|
+
}
|
|
3136
|
+
src = desugarTemplate(src, mark)
|
|
3137
|
+
}
|
|
3138
|
+
const report = get(src, at ?? '$.out', {
|
|
3139
|
+
view: 'json', path: f, ...verbOpts(trust, entryRootOf(f)),
|
|
3140
|
+
})
|
|
3141
|
+
if (!report.ok) {
|
|
3142
|
+
process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n')
|
|
3143
|
+
return 4
|
|
3144
|
+
}
|
|
3145
|
+
const tree = JSON.parse(report.out)
|
|
3146
|
+
// A `File` without a name is refused: the runtime ports disagree
|
|
3147
|
+
// about it.
|
|
3148
|
+
if ('File' === tree?.cmp && 'string' !== typeof tree.props?.name) {
|
|
3149
|
+
process.stderr.write(`aontu: ${f}: the file at ${at ?? '$.out'} has no name\n`)
|
|
3150
|
+
return 4
|
|
3151
|
+
}
|
|
3152
|
+
trees.push(tree)
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// ONE FILE GOES TO THE PATH ITSELF, unless the path is a directory; a
|
|
3156
|
+
// set is written below the path whatever its trees are.
|
|
3157
|
+
let folder = dest
|
|
3158
|
+
const tree: any = set ? trees.flatMap((t) => Array.isArray(t) ? t : [t]) : trees[0]
|
|
3159
|
+
if (!set && 'File' === tree?.cmp && !isDirectory(dest)) {
|
|
3160
|
+
tree.props.name = basename(dest)
|
|
3161
|
+
folder = dirname(dest)
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
const { cmpTree, Jostraca } = generatorRuntime()
|
|
3165
|
+
let root: any
|
|
3166
|
+
try {
|
|
3167
|
+
root = cmpTree(tree, { raw: true })
|
|
3168
|
+
}
|
|
3169
|
+
catch (err: any) {
|
|
3170
|
+
process.stderr.write(`aontu: ${file}: ${err.message}\n`)
|
|
3171
|
+
return 4
|
|
3172
|
+
}
|
|
3173
|
+
const runtime = Jostraca()
|
|
3174
|
+
|
|
3175
|
+
try {
|
|
3176
|
+
if (check) {
|
|
3177
|
+
const res = await runtime.check({ folder }, root)
|
|
3178
|
+
let drift = res.drift.map((d: any) => ({ kind: d.kind, path: d.path }))
|
|
3179
|
+
// `--check` answers "would `render` change anything", so a file
|
|
3180
|
+
// the write path leaves alone is not held to the generator's
|
|
3181
|
+
// bytes. The skip is gated on the target BEING there -- `render`
|
|
3182
|
+
// writes an absent one -- so drift at a path with nothing at it
|
|
3183
|
+
// survives, which is the `missing` a deleted file reports.
|
|
3184
|
+
const cut: number[] = []
|
|
3185
|
+
const renamed = renameExcluded(tree, [], cut)
|
|
3186
|
+
if (0 < cut.length) {
|
|
3187
|
+
const kept = new Set<string>((await Jostraca().check(
|
|
3188
|
+
{ folder }, cmpTree(renamed, { raw: true }))).checked)
|
|
3189
|
+
const skipped = new Set<string>(
|
|
3190
|
+
res.checked.filter((p: string) => !kept.has(p)))
|
|
3191
|
+
drift = drift.filter((d: any) => !skipped.has(d.path) ||
|
|
3192
|
+
!existsSync(join(folder, d.path)))
|
|
3193
|
+
}
|
|
3194
|
+
if ('json' === format) {
|
|
3195
|
+
process.stdout.write(exactJSON({
|
|
3196
|
+
aontu: { version: version(), verb: 'render' },
|
|
3197
|
+
verdict: 0 === drift.length ? 'ok' : 'drift',
|
|
3198
|
+
checked: res.checked,
|
|
3199
|
+
drift,
|
|
3200
|
+
}, 2) + '\n')
|
|
3201
|
+
}
|
|
3202
|
+
else {
|
|
3203
|
+
for (const d of drift) {
|
|
3204
|
+
process.stdout.write(`${d.kind}: ${d.path}\n`)
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
return 0 === drift.length ? 0 : 1
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
const since = Date.now()
|
|
3211
|
+
const res = await runtime.generate({ folder }, root)
|
|
3212
|
+
const skipped = renderSkipped(folder, since)
|
|
3213
|
+
if ('json' === format) {
|
|
3214
|
+
process.stdout.write(exactJSON({
|
|
3215
|
+
aontu: { version: version(), verb: 'render' },
|
|
3216
|
+
verdict: 'ok',
|
|
3217
|
+
files: { ...res.files, skipped },
|
|
3218
|
+
}, 2) + '\n')
|
|
3219
|
+
}
|
|
3220
|
+
else {
|
|
3221
|
+
for (const path of skipped) {
|
|
3222
|
+
process.stdout.write(`skipped: ${path}\n`)
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return 0
|
|
3226
|
+
}
|
|
3227
|
+
catch (err: any) {
|
|
3228
|
+
process.stderr.write(`aontu: ${err.message}\n`)
|
|
3229
|
+
return 2
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
|
|
2883
3234
|
function runReaches(argv: string[]): number {
|
|
2884
3235
|
const trusted = takeTrust(argv)
|
|
2885
3236
|
if (null == trusted) {
|
|
@@ -3697,7 +4048,7 @@ function runHash(argv: string[]): number {
|
|
|
3697
4048
|
if (0 < ctx.err.length || true === v?.isNil) {
|
|
3698
4049
|
process.stderr.write(
|
|
3699
4050
|
`aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
|
|
3700
|
-
renderFinding(evalFailure(ctx)) + '\n')
|
|
4051
|
+
renderFinding(evalFailure(ctx, v)) + '\n')
|
|
3701
4052
|
return 4
|
|
3702
4053
|
}
|
|
3703
4054
|
|
|
@@ -4880,8 +5231,8 @@ function runInit(argv: string[]): number {
|
|
|
4880
5231
|
const KNOWN_VERBS = [
|
|
4881
5232
|
'add', 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
|
|
4882
5233
|
'help', 'init', 'jsonschema', 'lsp', 'mcp', 'model', 'pkg', 'publish',
|
|
4883
|
-
'reaches', 'relations', 'remove', '
|
|
4884
|
-
'trim', 'vet', 'view', 'why',
|
|
5234
|
+
'reaches', 'relations', 'remove', 'render', 'subsume', 'sync', 'template',
|
|
5235
|
+
'trace', 'trim', 'vet', 'view', 'why',
|
|
4885
5236
|
]
|
|
4886
5237
|
|
|
4887
5238
|
|
|
@@ -5029,6 +5380,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
5029
5380
|
return finish(runTrace(argv.slice(3)))
|
|
5030
5381
|
}
|
|
5031
5382
|
|
|
5383
|
+
if ('render' === argv[2]) {
|
|
5384
|
+
return void runRender(argv.slice(3)).then(finish)
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5032
5387
|
if ('reaches' === argv[2]) {
|
|
5033
5388
|
return finish(runReaches(argv.slice(3)))
|
|
5034
5389
|
}
|
|
@@ -5083,7 +5438,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
5083
5438
|
}
|
|
5084
5439
|
else if ('--include-root' === arg) {
|
|
5085
5440
|
const dir = args[++i]
|
|
5086
|
-
if (null == dir) {
|
|
5441
|
+
if (null == dir || '' === dir) {
|
|
5087
5442
|
process.stderr.write('aontu: --include-root needs a directory\n')
|
|
5088
5443
|
return finish(2)
|
|
5089
5444
|
}
|
|
@@ -5146,6 +5501,7 @@ export {
|
|
|
5146
5501
|
runJsonSchema,
|
|
5147
5502
|
runTemplate,
|
|
5148
5503
|
runTrace,
|
|
5504
|
+
runRender, renderSkipped,
|
|
5149
5505
|
runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
|
|
5150
5506
|
runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
|
|
5151
5507
|
looksLikeVerb,
|
package/src/helpdoc.ts
CHANGED
|
@@ -17,7 +17,7 @@ const HELPDOC: HelpTopic[] = [
|
|
|
17
17
|
"topic": "tasks",
|
|
18
18
|
"summary": "what do you want to do? — the verb for a job",
|
|
19
19
|
"source": "docs/skill/tasks.md",
|
|
20
|
-
"text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n## Generate code from a model, and hold it\n\n```\naontu model get out gen.aon
|
|
20
|
+
"text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n## Generate code from a model, and hold it\n\n```\naontu render gen.aon build # write the files below build/\naontu render --check gen.aon build # write nothing; exit 1 when build/ has drifted\naontu render gen/ build # every generator directly in gen/, as one run\naontu model get out gen.aon # the component tree: files, and the lines in them\naontu trace gen.aon # what rule wrote each line\n```\n\nA generator answers a **component tree** -- `file(name, [lines])` and\nits neighbours -- which `aontu render` hands to jostraca, the generator\nruntime, to write. A tree that is one file is written to the path\nitself, and a folder of generators is one run. `--check` compares with\nwhat is committed: the model is the\ntruth, the code is the claim, and drift is a finding. `aontu trace`\nanswers the other direction: for a line of the output, the model node\nand the rule behind it.\n\n## Ask what a model says, and why\n\n```\naontu model get $.entity.planet.table model.aon\naontu model why $.entity.planet.table model.aon\naontu model get $.entity --keys model.aon\naontu model get $.entity --types model.aon\n```\n\n`why` names **every** contribution to a value, with the file and line\neach was written on. It is the first thing to run when a value is not\nwhat you expected, and the second thing to run when `vet` refuses.\n\n## Change a value without editing the file\n\n```\naontu model set '$.entity.planet.table=planet_v2' --entry model.aon --overlay local.aon\n```\n\nThe change is checked before it is written; a change that contradicts\na pinned value is refused, and the file is left alone.\n\n## Gate a change to the model itself\n\n```\naontu subsume old.aon new.aon # does the general admit every specific?\naontu breaking --against git#HEAD~1 model.aon\naontu hash model.aon # a pin that survives reformatting\n```\n\n## Hand a model to another agent\n\n```\naontu agentsmd --write AGENTS.md model.aon\n```\n\nSplices a derived stanza — the pin, the root keys, the shape, and the\ncommands spelled with paths that exist — between two markers, and\nleaves the rest of the file alone. Re-run it in the commit that\nchanges the model.\n\n## When something refuses\n\nRead the code in the brackets, then look it up:\n\n```\naontu explain no_scalar_unify\n```\n\nEvery finding carries a `code`, a `class`, a `path` and the **two\nsites** that disagree — the value and the constraint it failed, each\nwith its file and line. The class says what kind of answer it is:\n`conflict` (two things cannot both hold), `incomplete` (nothing is\nwrong yet, something is missing), `parse`, `reference`, `budget`.\n\nFull index: [`error-codes.md`](error-codes.md). The language on one\npage: [`grammar-card.md`](grammar-card.md). The worked ladder from\nplain JSON upward: [`examples.md`](examples.md).\n"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"topic": "language",
|