aontu 0.61.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 +941 -24
- package/dist/cli.js.map +1 -1
- package/dist/format.js +57 -1
- 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 +104 -69
- package/dist/std.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 +1066 -28
- package/src/format.ts +75 -3
- 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 +104 -69
- 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
package/src/cli.ts
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
exactJSON, vet, subsume, trimCheck, relationCheck,
|
|
24
24
|
hcanon, canonHash,
|
|
25
25
|
get, why, patch, agentsMd,
|
|
26
|
+
allow,
|
|
26
27
|
render,
|
|
27
28
|
renderProfile,
|
|
28
29
|
} from './aontu'
|
|
30
|
+
import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
|
|
29
31
|
import type { RenderCoverage, RenderReport } from './render'
|
|
30
32
|
import { desugarTemplate, resugarTemplate, templateOutputs, markerFor } from './template'
|
|
31
33
|
import { outsideRoot } from './mcp'
|
|
@@ -39,7 +41,9 @@ import type {
|
|
|
39
41
|
} from './mod-tool'
|
|
40
42
|
import { modCacheDir } from './mod'
|
|
41
43
|
import { VET_MAX_ERRORS } from './vet'
|
|
42
|
-
import type {
|
|
44
|
+
import type {
|
|
45
|
+
VetReport, VetFinding, VetVerdict, VetCoverage,
|
|
46
|
+
} from './vet'
|
|
43
47
|
import type {
|
|
44
48
|
SubsumeReport, SubsumeVerdict, SubsumeProfile,
|
|
45
49
|
} from './subsume'
|
|
@@ -57,11 +61,21 @@ import type { WhyRecord } from './provenance'
|
|
|
57
61
|
import { agentsMdSplice } from './agentsmd'
|
|
58
62
|
import { format, unifiedDiff } from './format'
|
|
59
63
|
import { includeOpts } from './utility'
|
|
64
|
+
import { HELPDOC, INITDOC } from './helpdoc'
|
|
65
|
+
import type { HelpTopic } from './helpdoc'
|
|
66
|
+
import { hints, codeClasses, codeClass } from './hints'
|
|
67
|
+
import { cmpCodePoint } from './keyorder'
|
|
60
68
|
import type { IncludeOptions } from './utility'
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
type Mode = 'json' | 'canon'
|
|
64
72
|
|
|
73
|
+
// The REPORT form of the bare command (G11 phase 7), which is a
|
|
74
|
+
// separate axis from Mode: `--canon` chooses what the answer IS,
|
|
75
|
+
// `--format` chooses how the answer is WRAPPED. Every other verb
|
|
76
|
+
// spells the second one this way.
|
|
77
|
+
type EvalFormat = 'text' | 'json'
|
|
78
|
+
|
|
65
79
|
|
|
66
80
|
const HELP = `Usage: aontu [options] [file]
|
|
67
81
|
aontu vet [options] <schema> <data> [more-data...]
|
|
@@ -82,8 +96,12 @@ const HELP = `Usage: aontu [options] [file]
|
|
|
82
96
|
aontu get <path> [options] <file>
|
|
83
97
|
aontu why <path> [options] <file>
|
|
84
98
|
aontu set <path>=<value>... --entry <file> --overlay <file>
|
|
85
|
-
aontu
|
|
99
|
+
aontu allow --role <role> [--at <path>] <roles-file> <path>...
|
|
100
|
+
aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
|
|
86
101
|
aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
|
|
102
|
+
aontu help [topic] [--format text|json]
|
|
103
|
+
aontu explain <code> | --list [--format text|json]
|
|
104
|
+
aontu init [dir]
|
|
87
105
|
aontu lsp
|
|
88
106
|
aontu mcp [--root <dir>]
|
|
89
107
|
|
|
@@ -91,6 +109,34 @@ Evaluate an aontu source file and print the result as JSON.
|
|
|
91
109
|
With no file on an interactive terminal, start a REPL.
|
|
92
110
|
With no file and piped input, read the source from stdin.
|
|
93
111
|
|
|
112
|
+
NEW TO THE LANGUAGE? This page documents the TOOL. The documentation
|
|
113
|
+
of the LANGUAGE travels inside this binary, and this is how to reach it:
|
|
114
|
+
|
|
115
|
+
aontu help List the topics this binary carries
|
|
116
|
+
aontu help tasks Which verb does the job you have
|
|
117
|
+
aontu help language The whole grammar, on one page
|
|
118
|
+
aontu help examples The ladder, from plain JSON upward
|
|
119
|
+
aontu help codes What a refusal means
|
|
120
|
+
aontu help grammar The published ABNF
|
|
121
|
+
aontu explain <code> What one error code a report carries means
|
|
122
|
+
aontu explain --list Every registered code with its class
|
|
123
|
+
|
|
124
|
+
Every one of those answers with no network and no checkout. The
|
|
125
|
+
long-form documentation -- the tutorial, the language and API
|
|
126
|
+
references, the how-to guides -- is in docs/ of the repository, which
|
|
127
|
+
is where to go when the topics above are not enough; the contributor
|
|
128
|
+
and agent guide is AGENTS.md beside it.
|
|
129
|
+
|
|
130
|
+
NOTHING TO EDIT YET? aontu init [dir] writes a working model, an
|
|
131
|
+
instance of it, and the four checks to run -- so the first
|
|
132
|
+
document is an edit of something that already holds, rather than an
|
|
133
|
+
invention. It refuses to overwrite.
|
|
134
|
+
|
|
135
|
+
The one construct to know before writing anything: &: inside a map is
|
|
136
|
+
a TEMPLATE that every key of that map must satisfy. A quoted "*" is a
|
|
137
|
+
key named *, not a wildcard, and a schema written that way constrains
|
|
138
|
+
nothing while still reporting valid.
|
|
139
|
+
|
|
94
140
|
The vet verb validates data documents against a schema document and
|
|
95
141
|
reports what does not hold, as text or as a machine-readable object.
|
|
96
142
|
|
|
@@ -100,7 +146,11 @@ query between a document and its own earlier versions.
|
|
|
100
146
|
|
|
101
147
|
Options:
|
|
102
148
|
-c, --canon Print the canonical form instead of generated JSON
|
|
103
|
-
|
|
149
|
+
--format <f> text (default) or json. The json form wraps the
|
|
150
|
+
answer as {aontu, findings, ok, out}, so a failure
|
|
151
|
+
here reads like every other verb's
|
|
152
|
+
-h, --help Show this help and exit (the verbs and their flags);
|
|
153
|
+
aontu help is the LANGUAGE, and lists its own topics
|
|
104
154
|
--jsonl REPL: answer every command as one JSON line
|
|
105
155
|
-v, --version Print the version and exit
|
|
106
156
|
--trust <t> Include capability: system (default), none, or
|
|
@@ -131,12 +181,28 @@ Vet options:
|
|
|
131
181
|
--closed Refuse keys the anchor does not declare
|
|
132
182
|
--partial Residue is reported but does not fail the run
|
|
133
183
|
--max-errors <n> Cap the finding list (default 20)
|
|
184
|
+
--coverage Report what the check EXAMINED: how many data
|
|
185
|
+
leaves a schema declaration constrained, the
|
|
186
|
+
shallowest data paths none did, and the
|
|
187
|
+
declarations no data met
|
|
188
|
+
--strict-coverage --coverage, and exit 1 when the run was VACUOUS --
|
|
189
|
+
when no data leaf was constrained at all. The
|
|
190
|
+
verdict word is unchanged, so nothing that passes
|
|
191
|
+
today starts failing without this flag
|
|
192
|
+
--coverage-at <p> Measure coverage under this path of the data only
|
|
134
193
|
--format <f> text (default), json or sarif
|
|
135
194
|
--watch Re-run whenever a watched file changes
|
|
136
195
|
|
|
196
|
+
A check that examined NOTHING and a check that passed answer the same
|
|
197
|
+
without --coverage. The usual cause is a schema written with the
|
|
198
|
+
wildcard other tools use: a quoted "*" is a key NAMED *, not a
|
|
199
|
+
template, so it constrains nothing and the run still reports valid.
|
|
200
|
+
The template is &: -- see aontu help language.
|
|
201
|
+
|
|
137
202
|
Vet exit codes:
|
|
138
203
|
0 valid data unifies, and is concrete (or --partial)
|
|
139
|
-
1 invalid at least one contradiction
|
|
204
|
+
1 invalid at least one contradiction, or a vacuous run under
|
|
205
|
+
--strict-coverage
|
|
140
206
|
2 usage bad option, or a file that cannot be read
|
|
141
207
|
3 incomplete no contradiction, but the truth is not yet satisfied
|
|
142
208
|
4 error the schema is unusable on its own
|
|
@@ -334,14 +400,61 @@ change contradicts a pinned value -- aontu why locates it, and
|
|
|
334
400
|
--in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
|
|
335
401
|
stand up on its own.
|
|
336
402
|
|
|
403
|
+
Allow options:
|
|
404
|
+
--role <role> The role the caller is operating under (required)
|
|
405
|
+
--at <path> Where the roles map lives in the role model
|
|
406
|
+
(default $.roles)
|
|
407
|
+
--format <f> text (default) or json
|
|
408
|
+
|
|
409
|
+
The allow verb asks a role model whether a role may modify every one
|
|
410
|
+
of the given subtrees, and answers before the change is made. The
|
|
411
|
+
role model is an aontu document: one entry per role, each carrying
|
|
412
|
+
allow (the subtrees it may modify) and optionally deny (the ones it
|
|
413
|
+
may not), as path strings starting at $; * in a path matches any one
|
|
414
|
+
key. A path is allowed when an allow entry is at or above it, and
|
|
415
|
+
refused when a deny entry is at, above or below it, whatever the
|
|
416
|
+
order. Every path starts with $, and may be spelled as set's
|
|
417
|
+
assignment, <path>=<value>, whose value must be one value: a value
|
|
418
|
+
carrying a second pair would write a subtree the gate was not asked
|
|
419
|
+
about.
|
|
420
|
+
|
|
421
|
+
Allow exit codes: 0 allowed (every path), 1 refused (at least one
|
|
422
|
+
path, or a role the model does not declare), 2 usage, 4 the role
|
|
423
|
+
model does not stand up on its own.
|
|
424
|
+
|
|
337
425
|
Agentsmd options:
|
|
338
426
|
--write <file> Splice the stanza into this file between the
|
|
339
427
|
aontu:begin and aontu:end markers, appending them
|
|
340
428
|
when they are absent; the rest is left alone
|
|
429
|
+
--depth <n> How deep the shape line projects (default 2). Two
|
|
430
|
+
levels name the root keys and say top under them; a
|
|
431
|
+
caller that wants the fields asks for them
|
|
341
432
|
|
|
342
433
|
Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
|
|
343
434
|
stand up on its own.
|
|
344
435
|
|
|
436
|
+
Help options:
|
|
437
|
+
--format <f> text (default) or json, the topic and its text
|
|
438
|
+
|
|
439
|
+
The help verb prints the embedded teaching pack: the language, not the
|
|
440
|
+
tool. With no topic it lists them. Topics are tasks, language,
|
|
441
|
+
examples, codes and grammar; the corpus is generated from docs/skill/
|
|
442
|
+
and grammar/aontu.abnf, so it cannot drift from those sources.
|
|
443
|
+
|
|
444
|
+
Help exit codes: 0 printed, 2 an unknown topic (the topics are listed)
|
|
445
|
+
or a bad option.
|
|
446
|
+
|
|
447
|
+
Explain options:
|
|
448
|
+
--list Every registered error code with its class
|
|
449
|
+
--format <f> text (default) or json
|
|
450
|
+
|
|
451
|
+
The explain verb answers what one error code means, from the same
|
|
452
|
+
table the engine attaches to a finding. Every registered code has an
|
|
453
|
+
entry, so a code read out of a report always resolves.
|
|
454
|
+
|
|
455
|
+
Explain exit codes: 0 explained, 2 an unknown code (near matches are
|
|
456
|
+
named) or a bad option.
|
|
457
|
+
|
|
345
458
|
Fmt options:
|
|
346
459
|
-w, --write Rewrite each file in place, when its form would change
|
|
347
460
|
-l, --list Print the name of each file whose form would change
|
|
@@ -399,13 +512,55 @@ function version(): string {
|
|
|
399
512
|
}
|
|
400
513
|
|
|
401
514
|
|
|
515
|
+
// The terminal colour escapes the parser puts in its message text. A
|
|
516
|
+
// machine-readable report is no place for them, which is the rule
|
|
517
|
+
// findingOf states in ts/src/vet.ts; the twin here rather than an
|
|
518
|
+
// import because go/cmd/aontu carries its own for the same reason (the
|
|
519
|
+
// engine's is not exported to its command).
|
|
520
|
+
const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g')
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
// THE ENGINE'S DIAGNOSIS AS A FINDING (G11 phase 7). The bare command
|
|
524
|
+
// was the one verb whose failure had no machine-readable form, so the
|
|
525
|
+
// default entry point was the one an agent had to parse with a regular
|
|
526
|
+
// expression.
|
|
527
|
+
//
|
|
528
|
+
// THE HEADLINE ONLY, and no `hint`. Both are parity decisions rather
|
|
529
|
+
// than economies: the frames under the headline are drawn for a person
|
|
530
|
+
// reading a terminal and only the first line is held to byte parity
|
|
531
|
+
// between the ports (the rule findingOf states), and the hint TABLES
|
|
532
|
+
// are deliberately not in parity while the code registry is -- so a
|
|
533
|
+
// hint here would make the two ports answer differently for a code
|
|
534
|
+
// only one of them explains. `aontu explain <code>` is where the hint
|
|
535
|
+
// lives, which is what phase 3 built it for.
|
|
536
|
+
//
|
|
537
|
+
// The CLASS comes from the registry rather than from the nil, because
|
|
538
|
+
// the registry is what both ports hold set-equal
|
|
539
|
+
// (test/spec/errcodes.tsv). Mirrors evalFinding in
|
|
540
|
+
// go/cmd/aontu/main.go.
|
|
541
|
+
function evalFinding(code: string, text: string): VetFinding {
|
|
542
|
+
return {
|
|
543
|
+
class: codeClass(code),
|
|
544
|
+
code,
|
|
545
|
+
message: text.split('\n')[0].replace(EVAL_ANSI, ''),
|
|
546
|
+
path: '$',
|
|
547
|
+
severity: 'error',
|
|
548
|
+
// NO SITE. The bare command's failure is the whole document not
|
|
549
|
+
// standing up, and the two sites a conflict names are in the
|
|
550
|
+
// frames the text form prints; naming one of them here would be a
|
|
551
|
+
// choice the engine has not made.
|
|
552
|
+
sites: [],
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
402
557
|
// Evaluate source, returning either the rendered output or the error
|
|
403
|
-
// message. Never throws.
|
|
558
|
+
// message, and the failure in the finding shape. Never throws.
|
|
404
559
|
function evalSource(
|
|
405
560
|
aontu: Aontu,
|
|
406
561
|
src: string,
|
|
407
562
|
mode: Mode,
|
|
408
|
-
): { ok: boolean; text: string } {
|
|
563
|
+
): { ok: boolean; text: string; findings: VetFinding[] } {
|
|
409
564
|
try {
|
|
410
565
|
// exactJSON, not JSON.stringify: a document using the `0d` exact
|
|
411
566
|
// leaves generates bigints and Decimals, which JSON.stringify cannot
|
|
@@ -416,14 +571,51 @@ function evalSource(
|
|
|
416
571
|
const text = 'canon' === mode
|
|
417
572
|
? aontu.unify(src).canon
|
|
418
573
|
: exactJSON(aontu.generate(src), 2)
|
|
419
|
-
return { ok: true, text }
|
|
574
|
+
return { ok: true, text, findings: [] }
|
|
420
575
|
}
|
|
421
576
|
catch (err: any) {
|
|
422
577
|
const msg = (err instanceof AontuError || true === err?.aontu)
|
|
423
578
|
? err.message
|
|
424
579
|
: String(err?.message ?? err)
|
|
425
|
-
|
|
580
|
+
// WHAT THE ENGINE COLLECTED, when it collected anything: an
|
|
581
|
+
// AontuError carries the NilVals the run failed on, already
|
|
582
|
+
// materialised (handleErrors in ts/src/aontu.ts), and their first
|
|
583
|
+
// is the diagnosis every other verb reports. An error raised
|
|
584
|
+
// outside the engine's own collection -- exactJSON's circular
|
|
585
|
+
// refusal, a foreign object claiming to be one -- carries none,
|
|
586
|
+
// and answers with the text alone rather than an invented code.
|
|
587
|
+
const errs: any[] = 'function' === typeof err?.errs ? err.errs() : []
|
|
588
|
+
const first: any = errs[0]
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
text: msg,
|
|
592
|
+
findings: null == first ? [] : [evalFinding(first.why, msg)],
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
// The bare command's answer, in the form the caller asked for. The
|
|
599
|
+
// text form is what it has always printed, on the stream the verdict
|
|
600
|
+
// chooses; `--format json` is the same answer as one object, on
|
|
601
|
+
// stdout, so a harness reads one stream and one shape either way.
|
|
602
|
+
// Mirrors emit in go/cmd/aontu/main.go.
|
|
603
|
+
function emitEval(
|
|
604
|
+
res: { ok: boolean; text: string; findings: VetFinding[] },
|
|
605
|
+
format: EvalFormat,
|
|
606
|
+
): number {
|
|
607
|
+
if ('json' === format) {
|
|
608
|
+
process.stdout.write(exactJSON({
|
|
609
|
+
aontu: { version: version(), verb: 'eval' },
|
|
610
|
+
findings: res.findings,
|
|
611
|
+
ok: res.ok,
|
|
612
|
+
out: res.ok ? res.text : '',
|
|
613
|
+
}, 2) + '\n')
|
|
426
614
|
}
|
|
615
|
+
else {
|
|
616
|
+
;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
|
|
617
|
+
}
|
|
618
|
+
return res.ok ? 0 : 1
|
|
427
619
|
}
|
|
428
620
|
|
|
429
621
|
|
|
@@ -600,12 +792,35 @@ function entryRootOf(file: string | undefined): string {
|
|
|
600
792
|
}
|
|
601
793
|
|
|
602
794
|
|
|
603
|
-
function runFile(
|
|
795
|
+
function runFile(
|
|
796
|
+
file: string, mode: Mode, format: EvalFormat, trust: TrustArg): number {
|
|
604
797
|
let src: string
|
|
605
798
|
try {
|
|
606
799
|
src = readFileSync(file, 'utf8')
|
|
607
800
|
}
|
|
608
801
|
catch (err: any) {
|
|
802
|
+
// A MISTYPED VERB READS AS A FILE NAME, and until G11 phase 2 that
|
|
803
|
+
// was only said when there were TWO of them. The one-argument case
|
|
804
|
+
// is the one an agent actually produces -- `aontu help`, `aontu
|
|
805
|
+
// init`, `aontu ontology` -- and it answered `cannot read help:
|
|
806
|
+
// ...`, which describes the symptom and hides the cause.
|
|
807
|
+
//
|
|
808
|
+
// The test is SHAPE, not existence: a bare word (no separator, no
|
|
809
|
+
// extension) that cannot be read was meant as a verb, while
|
|
810
|
+
// `./help`, `help.aon` and `/tmp/help` were meant as paths and keep
|
|
811
|
+
// the file diagnosis and its exit 1. That is the same escape hatch
|
|
812
|
+
// the subcommand dispatch documents. Mirrors go/cmd/aontu/main.go.
|
|
813
|
+
if (looksLikeVerb(file)) {
|
|
814
|
+
process.stderr.write(
|
|
815
|
+
`aontu: \`${file}\` is not a file, and not a verb this port knows\n`)
|
|
816
|
+
const near = nearestVerb(file, KNOWN_VERBS)
|
|
817
|
+
if ('' !== near) {
|
|
818
|
+
process.stderr.write(`aontu: did you mean \`aontu ${near}\`?\n`)
|
|
819
|
+
}
|
|
820
|
+
process.stderr.write(
|
|
821
|
+
'aontu: `aontu --help` lists the verbs, `aontu help` the topics\n')
|
|
822
|
+
return 2
|
|
823
|
+
}
|
|
609
824
|
process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`)
|
|
610
825
|
return 1
|
|
611
826
|
}
|
|
@@ -622,13 +837,12 @@ function runFile(file: string, mode: Mode, trust: TrustArg): number {
|
|
|
622
837
|
errfs: { existsSync, readFileSync },
|
|
623
838
|
...trustOpts(trust, dirname(path)),
|
|
624
839
|
})
|
|
625
|
-
|
|
626
|
-
;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
|
|
627
|
-
return res.ok ? 0 : 1
|
|
840
|
+
return emitEval(evalSource(aontu, src, mode), format)
|
|
628
841
|
}
|
|
629
842
|
|
|
630
843
|
|
|
631
|
-
function runStdin(
|
|
844
|
+
function runStdin(
|
|
845
|
+
mode: Mode, format: EvalFormat, trust: TrustArg): Promise<number> {
|
|
632
846
|
return new Promise((resolve) => {
|
|
633
847
|
let src = ''
|
|
634
848
|
process.stdin.setEncoding('utf8')
|
|
@@ -636,8 +850,7 @@ function runStdin(mode: Mode, trust: TrustArg): Promise<number> {
|
|
|
636
850
|
process.stdin.on('end', () => {
|
|
637
851
|
const res = evalSource(
|
|
638
852
|
new Aontu(trustOpts(trust, process.cwd())), src, mode)
|
|
639
|
-
|
|
640
|
-
resolve(res.ok ? 0 : 1)
|
|
853
|
+
resolve(emitEval(res, format))
|
|
641
854
|
})
|
|
642
855
|
})
|
|
643
856
|
}
|
|
@@ -870,6 +1083,11 @@ type VetArgs = {
|
|
|
870
1083
|
partial?: boolean
|
|
871
1084
|
maxErrors?: number
|
|
872
1085
|
watch?: boolean
|
|
1086
|
+
// G11 phase 5. `strictCoverage` implies `coverage`; `coverageAt`
|
|
1087
|
+
// narrows the data side and implies it too.
|
|
1088
|
+
coverage?: boolean
|
|
1089
|
+
strictCoverage?: boolean
|
|
1090
|
+
coverageAt?: string
|
|
873
1091
|
}
|
|
874
1092
|
|
|
875
1093
|
|
|
@@ -883,6 +1101,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
883
1101
|
let partial = false
|
|
884
1102
|
let maxErrors: number | undefined
|
|
885
1103
|
let watch = false
|
|
1104
|
+
let coverage = false
|
|
1105
|
+
let strictCoverage = false
|
|
1106
|
+
let coverageAt: string | undefined
|
|
886
1107
|
|
|
887
1108
|
for (let i = 0; i < argv.length; i++) {
|
|
888
1109
|
const arg = argv[i]
|
|
@@ -929,6 +1150,24 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
929
1150
|
else if ('--partial' === arg) {
|
|
930
1151
|
partial = true
|
|
931
1152
|
}
|
|
1153
|
+
else if ('--coverage' === arg) {
|
|
1154
|
+
coverage = true
|
|
1155
|
+
}
|
|
1156
|
+
else if ('--strict-coverage' === arg) {
|
|
1157
|
+
// IMPLIES THE ACCOUNTING, because a gate cannot fire on what was
|
|
1158
|
+
// never measured. Asking for the strict form and having to
|
|
1159
|
+
// remember `--coverage` beside it is a usage trap with one
|
|
1160
|
+
// correct answer, so the flag takes it.
|
|
1161
|
+
coverage = true
|
|
1162
|
+
strictCoverage = true
|
|
1163
|
+
}
|
|
1164
|
+
else if ('--coverage-at' === arg) {
|
|
1165
|
+
coverageAt = argv[++i]
|
|
1166
|
+
if (null == coverageAt) {
|
|
1167
|
+
return { err: 'aontu: --coverage-at needs a path' }
|
|
1168
|
+
}
|
|
1169
|
+
coverage = true
|
|
1170
|
+
}
|
|
932
1171
|
else if ('--watch' === arg) {
|
|
933
1172
|
watch = true
|
|
934
1173
|
}
|
|
@@ -954,6 +1193,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
954
1193
|
partial,
|
|
955
1194
|
maxErrors,
|
|
956
1195
|
watch,
|
|
1196
|
+
coverage,
|
|
1197
|
+
strictCoverage,
|
|
1198
|
+
coverageAt,
|
|
957
1199
|
},
|
|
958
1200
|
}
|
|
959
1201
|
}
|
|
@@ -993,14 +1235,53 @@ function renderVetText(report: VetReport): string {
|
|
|
993
1235
|
const head = `verdict: ${report.verdict}` +
|
|
994
1236
|
(report.truncated ? ' (findings truncated)' : '')
|
|
995
1237
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1238
|
+
const body = 0 === report.findings.length ? []
|
|
1239
|
+
: ['', ...report.findings.map(renderFinding)]
|
|
1240
|
+
const cover = null == report.coverage ? []
|
|
1241
|
+
: ['', ...renderVetCoverage(report.coverage)]
|
|
999
1242
|
|
|
1000
|
-
return [head,
|
|
1243
|
+
return [head, ...body, ...cover].join('\n')
|
|
1001
1244
|
}
|
|
1002
1245
|
|
|
1003
1246
|
|
|
1247
|
+
// The coverage block (G11 phase 5). VACUOUS FIRST and in the
|
|
1248
|
+
// imperative, because it is the one line that changes what the reader
|
|
1249
|
+
// should do: a `valid` verdict above it means nothing.
|
|
1250
|
+
function renderVetCoverage(c: VetCoverage): string[] {
|
|
1251
|
+
const out: string[] = []
|
|
1252
|
+
if (c.vacuous) {
|
|
1253
|
+
out.push('coverage: VACUOUS — no data leaf was constrained' +
|
|
1254
|
+
' by the schema; this run checked nothing')
|
|
1255
|
+
}
|
|
1256
|
+
out.push(`coverage: ${c.checked}/${c.leaves} data leaves checked,` +
|
|
1257
|
+
` ${c.declared} schema declarations`)
|
|
1258
|
+
// The lists are the SHALLOWEST paths, so each names a subtree rather
|
|
1259
|
+
// than every leaf under it, and both are capped: a report a reader
|
|
1260
|
+
// scrolls past is a report nobody reads.
|
|
1261
|
+
for (const [label, paths] of [
|
|
1262
|
+
['unchecked', c.unchecked], ['unused', c.unused],
|
|
1263
|
+
] as [string, string[]][]) {
|
|
1264
|
+
if (0 === paths.length) {
|
|
1265
|
+
continue
|
|
1266
|
+
}
|
|
1267
|
+
const shown = paths.slice(0, COVERAGE_LIST_MAX)
|
|
1268
|
+
for (const p of shown) {
|
|
1269
|
+
out.push(` ${label}: ${p}`)
|
|
1270
|
+
}
|
|
1271
|
+
if (shown.length < paths.length) {
|
|
1272
|
+
out.push(` ${label}: … and ${paths.length - shown.length} more`)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return out
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
// How many coverage paths the TEXT form prints per list. The JSON form
|
|
1280
|
+
// carries every one: a machine reads the whole list, a person reads the
|
|
1281
|
+
// first few and the count.
|
|
1282
|
+
const COVERAGE_LIST_MAX = 10
|
|
1283
|
+
|
|
1284
|
+
|
|
1004
1285
|
// The machine-readable form. `aontu` names the producer, so a report
|
|
1005
1286
|
// read from a file or a pipe says which version and which verb made it
|
|
1006
1287
|
// without the consumer having to know.
|
|
@@ -1010,6 +1291,7 @@ function renderVetJson(report: VetReport): string {
|
|
|
1010
1291
|
verdict: report.verdict,
|
|
1011
1292
|
truncated: report.truncated,
|
|
1012
1293
|
findings: report.findings,
|
|
1294
|
+
...(null == report.coverage ? {} : { coverage: report.coverage }),
|
|
1013
1295
|
}, 2)
|
|
1014
1296
|
}
|
|
1015
1297
|
|
|
@@ -1057,6 +1339,20 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1057
1339
|
let verdict: VetVerdict = 'valid'
|
|
1058
1340
|
let truncated = false
|
|
1059
1341
|
const findings: VetFinding[] = []
|
|
1342
|
+
// COVERAGE ACROSS SEVERAL DATA FILES (G11 phase 5). Two data files
|
|
1343
|
+
// are two candidates for one truth, so the schema side is the SAME
|
|
1344
|
+
// for each: `declared` is taken once, and a declaration is unused
|
|
1345
|
+
// only when NO file met it -- the intersection, because a
|
|
1346
|
+
// declaration one file exercised is exercised. The data side adds
|
|
1347
|
+
// up: leaves and checked leaves sum, and `unchecked` is the union.
|
|
1348
|
+
let cov: VetCoverage | undefined
|
|
1349
|
+
// Initialised rather than left undefined: it is filled in the same
|
|
1350
|
+
// block that sets `cov`, so a fallback at the read below would be an
|
|
1351
|
+
// arm nothing can take. The FIRST file replaces it wholesale, which
|
|
1352
|
+
// is what makes the fold an intersection rather than an empty set.
|
|
1353
|
+
let unusedEvery = new Set<string>()
|
|
1354
|
+
let unusedSeen = false
|
|
1355
|
+
const uncheckedAll = new Set<string>()
|
|
1060
1356
|
|
|
1061
1357
|
for (const source of sources) {
|
|
1062
1358
|
const report = vet(schemaSrc, source.src, {
|
|
@@ -1075,6 +1371,8 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1075
1371
|
// absolute one would name the same file two ways.
|
|
1076
1372
|
schemaPath: args.schema,
|
|
1077
1373
|
dataPath: source.file,
|
|
1374
|
+
coverage: args.coverage,
|
|
1375
|
+
coverageAt: args.coverageAt,
|
|
1078
1376
|
})
|
|
1079
1377
|
|
|
1080
1378
|
if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
|
|
@@ -1083,6 +1381,25 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1083
1381
|
truncated = truncated || report.truncated
|
|
1084
1382
|
findings.push(...report.findings)
|
|
1085
1383
|
|
|
1384
|
+
if (null != report.coverage) {
|
|
1385
|
+
const c = report.coverage
|
|
1386
|
+
cov = null == cov ? { ...c } : {
|
|
1387
|
+
checked: cov.checked + c.checked,
|
|
1388
|
+
declared: c.declared,
|
|
1389
|
+
leaves: cov.leaves + c.leaves,
|
|
1390
|
+
unchecked: [],
|
|
1391
|
+
unused: [],
|
|
1392
|
+
vacuous: false,
|
|
1393
|
+
}
|
|
1394
|
+
for (const p of c.unchecked) {
|
|
1395
|
+
uncheckedAll.add(p)
|
|
1396
|
+
}
|
|
1397
|
+
const mine = new Set(c.unused)
|
|
1398
|
+
unusedEvery = unusedSeen
|
|
1399
|
+
? new Set([...unusedEvery].filter((u) => mine.has(u))) : mine
|
|
1400
|
+
unusedSeen = true
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1086
1403
|
// A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
|
|
1087
1404
|
// is reported ONCE. `error` means exactly that -- the run could not
|
|
1088
1405
|
// be set up from the truth's side, never the data's (the exit table
|
|
@@ -1107,16 +1424,38 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1107
1424
|
const cap = args.maxErrors ?? VET_MAX_ERRORS
|
|
1108
1425
|
const kept = cap < findings.length ? findings.slice(0, cap) : findings
|
|
1109
1426
|
|
|
1427
|
+
if (null != cov) {
|
|
1428
|
+
cov.unchecked = [...uncheckedAll].sort(cmpCodePoint)
|
|
1429
|
+
cov.unused = [...unusedEvery].sort(cmpCodePoint)
|
|
1430
|
+
cov.vacuous = 0 === cov.checked && 0 < cov.leaves
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1110
1433
|
const report: VetReport = {
|
|
1111
1434
|
verdict,
|
|
1112
1435
|
truncated: truncated || cap < findings.length,
|
|
1113
1436
|
findings: kept,
|
|
1437
|
+
...(null == cov ? {} : { coverage: cov }),
|
|
1114
1438
|
}
|
|
1115
1439
|
const text = 'json' === args.format ? renderVetJson(report) :
|
|
1116
1440
|
'sarif' === args.format ? renderVetSarif(report) :
|
|
1117
1441
|
renderVetText(report)
|
|
1118
1442
|
|
|
1119
1443
|
process.stdout.write(text + '\n')
|
|
1444
|
+
|
|
1445
|
+
// A VACUOUS CHECK IS A FAILED GATE UNDER `--strict-coverage`, and
|
|
1446
|
+
// only under it: the verdict WORD is unchanged, so nothing that
|
|
1447
|
+
// passes today starts failing, and a caller who wants the stronger
|
|
1448
|
+
// gate asks for it. The reason goes to stderr, because stdout is a
|
|
1449
|
+
// report contract -- a JSON consumer reads `coverage.vacuous` and a
|
|
1450
|
+
// person reads this.
|
|
1451
|
+
if (true === args.strictCoverage && true === report.coverage?.vacuous) {
|
|
1452
|
+
process.stderr.write(
|
|
1453
|
+
'aontu: no data leaf was constrained by the schema:' +
|
|
1454
|
+
' this run checked nothing\n' +
|
|
1455
|
+
'aontu: `aontu help language` — a map template is `&:`,' +
|
|
1456
|
+
' and a quoted "*" is a key named *\n')
|
|
1457
|
+
return 1
|
|
1458
|
+
}
|
|
1120
1459
|
return VET_EXIT[verdict]
|
|
1121
1460
|
}
|
|
1122
1461
|
|
|
@@ -2235,6 +2574,31 @@ function modText(sub: string, report: any): string {
|
|
|
2235
2574
|
}
|
|
2236
2575
|
|
|
2237
2576
|
|
|
2577
|
+
// VACUITY SIGNALS (G11 phase 4,
|
|
2578
|
+
// docs/capability-review/g11-agent-onramp.md).
|
|
2579
|
+
//
|
|
2580
|
+
// The same principle phase 5 applied to `vet`: a verb that did NOTHING
|
|
2581
|
+
// and a verb that did its job answer the same. `aontu view tree` over a
|
|
2582
|
+
// document declaring no relations printed one newline and exited 0;
|
|
2583
|
+
// `aontu render` with no profile printed nothing and exited 0; `aontu
|
|
2584
|
+
// relations` over a document declaring none answered `verdict: pass`.
|
|
2585
|
+
// For a person at a terminal that is a shrug. For an unattended agent
|
|
2586
|
+
// it is a green check mark on an empty box.
|
|
2587
|
+
//
|
|
2588
|
+
// ON STDERR, ALWAYS. stdout is a report contract -- a `--format json`
|
|
2589
|
+
// consumer parses it -- and the exit code is a verdict class that
|
|
2590
|
+
// callers already branch on. Neither changes here: what changes is
|
|
2591
|
+
// that the caller is TOLD. A caller who wants it to be fatal has
|
|
2592
|
+
// `vet --strict-coverage`, and the same argument would give the other
|
|
2593
|
+
// verbs a flag of their own if one is ever asked for.
|
|
2594
|
+
//
|
|
2595
|
+
// The repository already ruled this for one verb, in G8 phase 6 on
|
|
2596
|
+
// `trim`: "doing something else silently is worse than refusing".
|
|
2597
|
+
function vacuous(what: string, why: string): void {
|
|
2598
|
+
process.stderr.write(`aontu: ${what}: ${why}\n`)
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
|
|
2238
2602
|
function runRelations(argv: string[]): number {
|
|
2239
2603
|
const trusted = takeTrust(argv)
|
|
2240
2604
|
if (null == trusted) {
|
|
@@ -2283,12 +2647,21 @@ function runRelations(argv: string[]): number {
|
|
|
2283
2647
|
}
|
|
2284
2648
|
|
|
2285
2649
|
const report = relationCheck(src, {
|
|
2286
|
-
path: files[0],
|
|
2650
|
+
path: files[0], count: true,
|
|
2651
|
+
...verbOpts(trust, entryRootOf(files[0])),
|
|
2287
2652
|
})
|
|
2288
2653
|
const text = 'json' === format
|
|
2289
2654
|
? renderRelationsJson(report)
|
|
2290
2655
|
: renderRelationsText(report)
|
|
2291
2656
|
process.stdout.write(text + '\n')
|
|
2657
|
+
// `pass` over NO declarations is the vacuous case, and the engine
|
|
2658
|
+
// knows it exactly: `_reldecls` is empty. The count is asked for
|
|
2659
|
+
// here rather than derived, so the answer costs no second
|
|
2660
|
+
// evaluation.
|
|
2661
|
+
if (0 === report.declared) {
|
|
2662
|
+
vacuous('this document declares no relations',
|
|
2663
|
+
'`pass` means nothing was checked, not that the graph is sound')
|
|
2664
|
+
}
|
|
2292
2665
|
return RELATIONS_EXIT[report.verdict]
|
|
2293
2666
|
}
|
|
2294
2667
|
|
|
@@ -2591,7 +2964,7 @@ function runView(argv: string[]): number {
|
|
|
2591
2964
|
}
|
|
2592
2965
|
}
|
|
2593
2966
|
|
|
2594
|
-
const
|
|
2967
|
+
const viewOpts = {
|
|
2595
2968
|
...opts,
|
|
2596
2969
|
style: viewStyleOf(style, opts.as ?? viewDefaultProfile(kind)),
|
|
2597
2970
|
kind,
|
|
@@ -2599,7 +2972,28 @@ function runView(argv: string[]): number {
|
|
|
2599
2972
|
roots,
|
|
2600
2973
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
2601
2974
|
docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
|
|
2602
|
-
}
|
|
2975
|
+
}
|
|
2976
|
+
const report = view(srcs[0], viewOpts)
|
|
2977
|
+
|
|
2978
|
+
// AN EMPTY FIGURE IS THE SAME BYTES AS A DRAWN ONE MINUS ITS
|
|
2979
|
+
// CONTENT, and every profile spells "empty" differently: text draws
|
|
2980
|
+
// nothing at all, mermaid still draws its `flowchart LR` header, the
|
|
2981
|
+
// matrix still prints its count line. Rather than teach this one
|
|
2982
|
+
// place each of those spellings -- a list that goes stale the first
|
|
2983
|
+
// time a profile gains a header -- ASK THE SAME KIND TO DRAW AN
|
|
2984
|
+
// EMPTY DOCUMENT and compare. Equal texts mean this document
|
|
2985
|
+
// contributed nothing to the figure, whatever the profile.
|
|
2986
|
+
//
|
|
2987
|
+
// It costs one drawing of `{}`, which is the cheapest document
|
|
2988
|
+
// there is, and only on a run that produced a figure at all.
|
|
2989
|
+
if ('error' !== report.verdict && null != report.text) {
|
|
2990
|
+
const bare = view('{}', viewOpts)
|
|
2991
|
+
if ('error' !== bare.verdict && bare.text === report.text) {
|
|
2992
|
+
vacuous('nothing to draw',
|
|
2993
|
+
'this figure is what the same view draws for an empty document' +
|
|
2994
|
+
' — the model declares nothing this kind can show')
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2603
2997
|
|
|
2604
2998
|
if ('json' === format) {
|
|
2605
2999
|
process.stdout.write(renderViewJson(report) + '\n')
|
|
@@ -3117,6 +3511,11 @@ function runRender(argv: string[]): number {
|
|
|
3117
3511
|
profiles.push(profile)
|
|
3118
3512
|
}
|
|
3119
3513
|
|
|
3514
|
+
// A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
|
|
3515
|
+
// bytes and exit 0. The profile is what maps a model onto a
|
|
3516
|
+
// language, so without one there is nothing for the renderer to
|
|
3517
|
+
// write -- which is a usable answer only if the caller is told.
|
|
3518
|
+
const noProfiles = 0 === profiles.length
|
|
3120
3519
|
const report = render(src, {
|
|
3121
3520
|
at, unit, strict, profiles, path: files[0],
|
|
3122
3521
|
coverage, coverageAt,
|
|
@@ -3127,6 +3526,15 @@ function runRender(argv: string[]): number {
|
|
|
3127
3526
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
3128
3527
|
})
|
|
3129
3528
|
|
|
3529
|
+
// Said once, whatever the format: stdout stays the report.
|
|
3530
|
+
if ('error' !== report.verdict && 0 === report.units.length) {
|
|
3531
|
+
vacuous('nothing was rendered',
|
|
3532
|
+
noProfiles
|
|
3533
|
+
? 'no profile was given, and the document declares none' +
|
|
3534
|
+
' (see aontu help tasks)'
|
|
3535
|
+
: 'the document produced no units under this profile')
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3130
3538
|
if ('json' === format) {
|
|
3131
3539
|
process.stdout.write(exactJSON({
|
|
3132
3540
|
aontu: { version: version(), verb: 'render' },
|
|
@@ -3838,6 +4246,188 @@ function runSet(argv: string[]): number {
|
|
|
3838
4246
|
}
|
|
3839
4247
|
|
|
3840
4248
|
|
|
4249
|
+
// ---------------------------------------------------------------------
|
|
4250
|
+
// The role gate (docs/design/ALLOW.0.md): may the role the caller is
|
|
4251
|
+
// operating under modify these subtrees? Asked before `set`, by an
|
|
4252
|
+
// agent whose skill names its role, and answered from a role model
|
|
4253
|
+
// that is itself an aontu document. The verdict is the exit code, as
|
|
4254
|
+
// it is for every gate here: 0 is yes, 1 is no, 4 is "the model that
|
|
4255
|
+
// was to decide does not stand up", and an agent branches on nothing
|
|
4256
|
+
// else.
|
|
4257
|
+
|
|
4258
|
+
const ALLOW_HELP =
|
|
4259
|
+
'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)'
|
|
4260
|
+
|
|
4261
|
+
const ALLOW_EXIT: Record<AllowVerdict, number> = {
|
|
4262
|
+
allowed: 0,
|
|
4263
|
+
refused: 1,
|
|
4264
|
+
error: 4,
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
|
|
4268
|
+
// One line per asked path: the answer, and the entry that gave it, as
|
|
4269
|
+
// a path into the role model so `aontu why` can locate the rule.
|
|
4270
|
+
function renderAllowDecision(d: AllowDecision, role: string): string {
|
|
4271
|
+
const head = `${d.path}: ${d.allowed ? 'allowed' : 'refused'}`
|
|
4272
|
+
switch (d.reason) {
|
|
4273
|
+
case 'allow':
|
|
4274
|
+
case 'deny':
|
|
4275
|
+
return `${head} by ${d.by} (${d.pattern})`
|
|
4276
|
+
case 'uncovered':
|
|
4277
|
+
return `${head} (no allow entry of ${role} covers it)`
|
|
4278
|
+
default:
|
|
4279
|
+
return `${head} (role ${role} is not declared)`
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
|
|
4284
|
+
function renderAllowText(report: AllowReport): string {
|
|
4285
|
+
const lines = [`verdict: ${report.verdict}`, `role: ${report.role}`]
|
|
4286
|
+
.concat(report.paths.map((d) => renderAllowDecision(d, report.role)))
|
|
4287
|
+
if (0 === report.findings.length) {
|
|
4288
|
+
return lines.join('\n')
|
|
4289
|
+
}
|
|
4290
|
+
return lines.concat('', report.findings.map(renderFinding)).join('\n')
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
|
|
4294
|
+
// Does the text after `=` parse as exactly one value? Parsed, never
|
|
4295
|
+
// evaluated, with loads denied: the question is the shape of the
|
|
4296
|
+
// argument, and reading a file to answer it would be the write the
|
|
4297
|
+
// gate exists to precede.
|
|
4298
|
+
function oneValue(value: string): boolean {
|
|
4299
|
+
try {
|
|
4300
|
+
const probe: any = new Aontu({ trust: { include: 'none' } })
|
|
4301
|
+
.parse('v: ' + value)
|
|
4302
|
+
return 1 === Object.keys(probe.peg).length
|
|
4303
|
+
}
|
|
4304
|
+
catch {
|
|
4305
|
+
return false
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
|
|
4310
|
+
function runAllow(argv: string[]): number {
|
|
4311
|
+
const trusted = takeTrust(argv)
|
|
4312
|
+
if (null == trusted) {
|
|
4313
|
+
return 2
|
|
4314
|
+
}
|
|
4315
|
+
argv = trusted.argv
|
|
4316
|
+
const trust = trusted.trust
|
|
4317
|
+
const rest: string[] = []
|
|
4318
|
+
let role: string | undefined
|
|
4319
|
+
let at: string | undefined
|
|
4320
|
+
let format: SubsumeFormat = 'text'
|
|
4321
|
+
|
|
4322
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4323
|
+
const arg = argv[i]
|
|
4324
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4325
|
+
process.stdout.write(HELP)
|
|
4326
|
+
return 0
|
|
4327
|
+
}
|
|
4328
|
+
if ('--role' === arg) {
|
|
4329
|
+
role = argv[++i]
|
|
4330
|
+
if (null == role) {
|
|
4331
|
+
process.stderr.write('aontu: --role needs a role name\n')
|
|
4332
|
+
return 2
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
else if ('--at' === arg) {
|
|
4336
|
+
at = argv[++i]
|
|
4337
|
+
if (null == at) {
|
|
4338
|
+
process.stderr.write('aontu: --at needs a path\n')
|
|
4339
|
+
return 2
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
else if ('--format' === arg) {
|
|
4343
|
+
const f = argv[++i]
|
|
4344
|
+
if ('text' !== f && 'json' !== f) {
|
|
4345
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
4346
|
+
return 2
|
|
4347
|
+
}
|
|
4348
|
+
format = f
|
|
4349
|
+
}
|
|
4350
|
+
else if (arg.startsWith('-')) {
|
|
4351
|
+
process.stderr.write(`aontu: unknown allow option ${arg} (try --help)\n`)
|
|
4352
|
+
return 2
|
|
4353
|
+
}
|
|
4354
|
+
else {
|
|
4355
|
+
rest.push(arg)
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
if (null == role || rest.length < 2) {
|
|
4360
|
+
process.stderr.write(
|
|
4361
|
+
`aontu: allow needs --role, a role model and at least one path\n` +
|
|
4362
|
+
`${ALLOW_HELP}\n`)
|
|
4363
|
+
return 2
|
|
4364
|
+
}
|
|
4365
|
+
const [file, ...asked] = rest
|
|
4366
|
+
|
|
4367
|
+
// A role is ONE KEY of the roles map. A dotted name would be read as
|
|
4368
|
+
// a path by `why` when it follows the entry the report names, and an
|
|
4369
|
+
// empty one names the map itself.
|
|
4370
|
+
if ('' === role || role.includes('.')) {
|
|
4371
|
+
process.stderr.write('aontu: --role needs one key, without dots\n')
|
|
4372
|
+
return 2
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
// A path may arrive in `set`'s spelling, `$.a.b=1`, so a skill can
|
|
4376
|
+
// hand the gate the very arguments the write will get. The text up
|
|
4377
|
+
// to the first `=` is the path, and it starts with `$`: an empty
|
|
4378
|
+
// argument, or a second file name, would otherwise read as a path
|
|
4379
|
+
// and be answered. The VALUE is checked to be one value. `set`
|
|
4380
|
+
// appends it as source after the flattened path, so a value carrying
|
|
4381
|
+
// a second pair -- `3 secrets: key: "x"` -- writes a sibling of the
|
|
4382
|
+
// overlay root, a subtree the gate was never asked about.
|
|
4383
|
+
const paths: string[] = []
|
|
4384
|
+
for (const arg of asked) {
|
|
4385
|
+
const eq = arg.indexOf('=')
|
|
4386
|
+
const path = eq < 0 ? arg : arg.slice(0, eq)
|
|
4387
|
+
if (!path.startsWith('$')) {
|
|
4388
|
+
process.stderr.write(
|
|
4389
|
+
`aontu: a path starts with $ (got ${JSON.stringify(arg)})\n`)
|
|
4390
|
+
return 2
|
|
4391
|
+
}
|
|
4392
|
+
if (0 <= eq && !oneValue(arg.slice(eq + 1))) {
|
|
4393
|
+
process.stderr.write(
|
|
4394
|
+
`aontu: the value of ${path} is not one value\n`)
|
|
4395
|
+
return 2
|
|
4396
|
+
}
|
|
4397
|
+
paths.push(path)
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
let src: string
|
|
4401
|
+
try {
|
|
4402
|
+
src = readFileSync(file, 'utf8')
|
|
4403
|
+
}
|
|
4404
|
+
catch (err: any) {
|
|
4405
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
4406
|
+
return 2
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
const report = allow(src, role, paths, {
|
|
4410
|
+
at, path: file, ...verbOpts(trust, entryRootOf(file)),
|
|
4411
|
+
})
|
|
4412
|
+
|
|
4413
|
+
const text = 'json' === format ?
|
|
4414
|
+
exactJSON({
|
|
4415
|
+
aontu: { version: version(), verb: 'allow' },
|
|
4416
|
+
findings: report.findings,
|
|
4417
|
+
paths: report.paths,
|
|
4418
|
+
role: report.role,
|
|
4419
|
+
verdict: report.verdict,
|
|
4420
|
+
}, 2) :
|
|
4421
|
+
renderAllowText(report)
|
|
4422
|
+
|
|
4423
|
+
// The report IS the answer, refused or not, so it goes to stdout as
|
|
4424
|
+
// vet's does; the exit code carries the verdict for a caller that
|
|
4425
|
+
// reads nothing else.
|
|
4426
|
+
process.stdout.write(text + '\n')
|
|
4427
|
+
return ALLOW_EXIT[report.verdict]
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
|
|
3841
4431
|
// ---------------------------------------------------------------------
|
|
3842
4432
|
// The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
|
|
3843
4433
|
// derived from the definition, so it cannot drift from the formal
|
|
@@ -3854,6 +4444,10 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3854
4444
|
const trust = trusted.trust
|
|
3855
4445
|
const files: string[] = []
|
|
3856
4446
|
let write: string | undefined
|
|
4447
|
+
// The SHAPE's depth (G11 phase 7). Default 2, unchanged: the stanza
|
|
4448
|
+
// is spliced into a file people read, and a deeper shape is a
|
|
4449
|
+
// question the caller asks rather than one it is handed.
|
|
4450
|
+
let depth = 2
|
|
3857
4451
|
|
|
3858
4452
|
for (let i = 0; i < argv.length; i++) {
|
|
3859
4453
|
const arg = argv[i]
|
|
@@ -3868,6 +4462,14 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3868
4462
|
return 2
|
|
3869
4463
|
}
|
|
3870
4464
|
}
|
|
4465
|
+
else if ('--depth' === arg) {
|
|
4466
|
+
const n = Number(argv[++i])
|
|
4467
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
4468
|
+
process.stderr.write('aontu: --depth needs a positive integer\n')
|
|
4469
|
+
return 2
|
|
4470
|
+
}
|
|
4471
|
+
depth = n
|
|
4472
|
+
}
|
|
3871
4473
|
else if (arg.startsWith('-')) {
|
|
3872
4474
|
process.stderr.write(
|
|
3873
4475
|
`aontu: unknown agentsmd option ${arg} (try --help)\n`)
|
|
@@ -3894,7 +4496,7 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3894
4496
|
}
|
|
3895
4497
|
|
|
3896
4498
|
const report = agentsMd(src, {
|
|
3897
|
-
name: files[0], path: files[0],
|
|
4499
|
+
depth, name: files[0], path: files[0],
|
|
3898
4500
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
3899
4501
|
})
|
|
3900
4502
|
if (!report.ok) {
|
|
@@ -4202,6 +4804,410 @@ function parseTrustArg(value: string): TrustArg | undefined {
|
|
|
4202
4804
|
}
|
|
4203
4805
|
|
|
4204
4806
|
|
|
4807
|
+
// THE TEACHING PACK, SERVED FROM THE COMMAND (G11 phase 1,
|
|
4808
|
+
// docs/capability-review/g11-agent-onramp.md; mirrors
|
|
4809
|
+
// go/cmd/aontu/help.go).
|
|
4810
|
+
//
|
|
4811
|
+
// HELP documents the TOOLCHAIN and says nothing about the LANGUAGE:
|
|
4812
|
+
// `&`, the map template and the one construct an ontology cannot be
|
|
4813
|
+
// written without, occurs zero times in it, while `template` occurs
|
|
4814
|
+
// fourteen times and names an unrelated verb every time. docs/skill/
|
|
4815
|
+
// was already the right content and already gated; the gap was
|
|
4816
|
+
// DELIVERY, since it reached an installation as
|
|
4817
|
+
// node_modules/aontu/skill/ where nothing looks. ts/src/helpdoc.ts is
|
|
4818
|
+
// generated from those sources by ts/scripts/helpdoc.cjs and asserted
|
|
4819
|
+
// byte-identical with them by ts/test/helpdoc.test.ts.
|
|
4820
|
+
|
|
4821
|
+
const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)'
|
|
4822
|
+
const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)'
|
|
4823
|
+
|
|
4824
|
+
|
|
4825
|
+
function helpIndexText(index: HelpTopic[]): string {
|
|
4826
|
+
const width = index.reduce((w, t) => Math.max(w, t.topic.length), 0)
|
|
4827
|
+
return 'aontu help <topic> — the language, offline.\n\n' +
|
|
4828
|
+
index.map((t) =>
|
|
4829
|
+
' ' + t.topic.padEnd(width) + ' ' + t.summary).join('\n') +
|
|
4830
|
+
'\n\n' +
|
|
4831
|
+
'`aontu --help` documents the verbs, their flags and their exit\n' +
|
|
4832
|
+
'codes. `aontu explain <code>` explains one error code.\n' +
|
|
4833
|
+
'Start at `aontu help tasks` if you know the job but not the verb.'
|
|
4834
|
+
}
|
|
4835
|
+
|
|
4836
|
+
|
|
4837
|
+
function runHelp(argv: string[]): number {
|
|
4838
|
+
let format: SubsumeFormat = 'text'
|
|
4839
|
+
const topics: string[] = []
|
|
4840
|
+
|
|
4841
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4842
|
+
const arg = argv[i]
|
|
4843
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4844
|
+
process.stdout.write(HELP)
|
|
4845
|
+
return 0
|
|
4846
|
+
}
|
|
4847
|
+
else if ('--format' === arg) {
|
|
4848
|
+
const f = argv[++i]
|
|
4849
|
+
if ('text' !== f && 'json' !== f) {
|
|
4850
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
4851
|
+
return 2
|
|
4852
|
+
}
|
|
4853
|
+
format = f
|
|
4854
|
+
}
|
|
4855
|
+
else if (arg.startsWith('-')) {
|
|
4856
|
+
process.stderr.write(`aontu: unknown help option ${arg} (try --help)\n`)
|
|
4857
|
+
return 2
|
|
4858
|
+
}
|
|
4859
|
+
else {
|
|
4860
|
+
topics.push(arg)
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
if (1 < topics.length) {
|
|
4865
|
+
process.stderr.write(`aontu: help takes one topic\n${HELP_VERB_HELP}\n`)
|
|
4866
|
+
return 2
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
if (0 === topics.length) {
|
|
4870
|
+
process.stdout.write(('json' === format
|
|
4871
|
+
? exactJSON({
|
|
4872
|
+
aontu: { version: version(), verb: 'help' },
|
|
4873
|
+
topics: HELPDOC.map(
|
|
4874
|
+
(t) => ({ topic: t.topic, summary: t.summary, source: t.source })),
|
|
4875
|
+
}, 2)
|
|
4876
|
+
: helpIndexText(HELPDOC)) + '\n')
|
|
4877
|
+
return 0
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
const found = HELPDOC.find((t) => topics[0] === t.topic)
|
|
4881
|
+
if (null != found) {
|
|
4882
|
+
if ('json' === format) {
|
|
4883
|
+
process.stdout.write(exactJSON({
|
|
4884
|
+
aontu: { version: version(), verb: 'help' },
|
|
4885
|
+
topic: found.topic,
|
|
4886
|
+
summary: found.summary,
|
|
4887
|
+
source: found.source,
|
|
4888
|
+
text: found.text,
|
|
4889
|
+
}, 2) + '\n')
|
|
4890
|
+
return 0
|
|
4891
|
+
}
|
|
4892
|
+
process.stdout.write(found.text)
|
|
4893
|
+
return 0
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
// AN UNKNOWN TOPIC IS A USAGE ERROR AND NAMES THE ALTERNATIVES,
|
|
4897
|
+
// because the caller who typed it has no other way to find out what
|
|
4898
|
+
// exists -- that is the whole condition this verb was added for.
|
|
4899
|
+
process.stderr.write(
|
|
4900
|
+
`aontu: no help topic \`${topics[0]}\`\n` +
|
|
4901
|
+
`aontu: topics are ${HELPDOC.map((t) => t.topic).join(', ')}\n`)
|
|
4902
|
+
return 2
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
|
|
4906
|
+
// `aontu explain <code>` (G11 phase 3; mirrors
|
|
4907
|
+
// go/cmd/aontu/explain.go).
|
|
4908
|
+
//
|
|
4909
|
+
// THE REGISTRY IS THE LIST, NOT THE HINT TABLE. test/spec/errcodes.tsv
|
|
4910
|
+
// registers 157 codes and the spec suite asserts set equality between
|
|
4911
|
+
// the file and codeClasses IN BOTH PORTS, so listing from codeClasses
|
|
4912
|
+
// is listing the shared contract. The hint tables are smaller and are
|
|
4913
|
+
// NOT in parity -- 130 entries here against 131 in Go, the extra being
|
|
4914
|
+
// decimal_syntax, which this port never raises -- so listing from them
|
|
4915
|
+
// would make `aontu explain --list` differ between ports over a
|
|
4916
|
+
// difference that is not about what either port can report.
|
|
4917
|
+
//
|
|
4918
|
+
// A REGISTERED CODE WITH NO HINT ANSWERS WITH ITS CLASS AND SAYS SO.
|
|
4919
|
+
// Twenty-seven registered codes carry no explanation text here; before
|
|
4920
|
+
// this verb their absence was invisible, because a hint is only ever
|
|
4921
|
+
// seen beside the error that raises it.
|
|
4922
|
+
|
|
4923
|
+
// The dynamic prefixes a generated code extends (`func:upper`,
|
|
4924
|
+
// `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
|
|
4925
|
+
// exported; a code that extends one is registered through its prefix
|
|
4926
|
+
// and carries that prefix's hint.
|
|
4927
|
+
const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref[']
|
|
4928
|
+
|
|
4929
|
+
|
|
4930
|
+
function explainCode(
|
|
4931
|
+
code: string): { cls: string, hint: string, registered: boolean } {
|
|
4932
|
+
const cls = codeClass(code)
|
|
4933
|
+
let hint = hints[code] ?? ''
|
|
4934
|
+
let registered = null != codeClasses[code]
|
|
4935
|
+
if (!registered) {
|
|
4936
|
+
for (const prefix of EXPLAIN_PREFIXES) {
|
|
4937
|
+
if (code.startsWith(prefix)) {
|
|
4938
|
+
// No guard on `hint` here: every hint key is also a registry
|
|
4939
|
+
// key (the spec suite asserts codeClasses set-equal with
|
|
4940
|
+
// test/spec/errcodes.tsv, and hints is a subset of it), so a
|
|
4941
|
+
// code that reaches this loop is unregistered and therefore
|
|
4942
|
+
// has no hint of its own.
|
|
4943
|
+
registered = true
|
|
4944
|
+
hint = hints[prefix] ?? ''
|
|
4945
|
+
break
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
return { cls, hint, registered }
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
|
|
4953
|
+
// Every code in the shared registry, sorted by code point so both
|
|
4954
|
+
// ports list them in the same order.
|
|
4955
|
+
function explainCodes(): string[] {
|
|
4956
|
+
return Object.keys(codeClasses).sort(cmpCodePoint)
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
|
|
4960
|
+
function explainListText(format: SubsumeFormat): string {
|
|
4961
|
+
const codes = explainCodes()
|
|
4962
|
+
if ('json' === format) {
|
|
4963
|
+
return exactJSON({
|
|
4964
|
+
aontu: { version: version(), verb: 'explain' },
|
|
4965
|
+
codes: codes.map((code) => ({
|
|
4966
|
+
code,
|
|
4967
|
+
class: codeClass(code),
|
|
4968
|
+
// Whether this port carries explanation text for the code. The
|
|
4969
|
+
// registry is in parity; the hint tables are not, so a consumer
|
|
4970
|
+
// that wants only explained codes can filter rather than guess.
|
|
4971
|
+
explained: '' !== explainCode(code).hint,
|
|
4972
|
+
})),
|
|
4973
|
+
}, 2)
|
|
4974
|
+
}
|
|
4975
|
+
const width = codes.reduce((w, c) => Math.max(w, c.length), 0)
|
|
4976
|
+
return codes.map((c) =>
|
|
4977
|
+
c.padEnd(width) + ' ' + codeClass(c) +
|
|
4978
|
+
('' === explainCode(c).hint ? ' (no text)' : '')).join('\n')
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
|
|
4982
|
+
function runExplain(argv: string[]): number {
|
|
4983
|
+
let format: SubsumeFormat = 'text'
|
|
4984
|
+
let list = false
|
|
4985
|
+
const codes: string[] = []
|
|
4986
|
+
|
|
4987
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4988
|
+
const arg = argv[i]
|
|
4989
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4990
|
+
process.stdout.write(HELP)
|
|
4991
|
+
return 0
|
|
4992
|
+
}
|
|
4993
|
+
else if ('--list' === arg) {
|
|
4994
|
+
list = true
|
|
4995
|
+
}
|
|
4996
|
+
else if ('--format' === arg) {
|
|
4997
|
+
const f = argv[++i]
|
|
4998
|
+
if ('text' !== f && 'json' !== f) {
|
|
4999
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
5000
|
+
return 2
|
|
5001
|
+
}
|
|
5002
|
+
format = f
|
|
5003
|
+
}
|
|
5004
|
+
else if (arg.startsWith('-')) {
|
|
5005
|
+
process.stderr.write(
|
|
5006
|
+
`aontu: unknown explain option ${arg} (try --help)\n`)
|
|
5007
|
+
return 2
|
|
5008
|
+
}
|
|
5009
|
+
else {
|
|
5010
|
+
codes.push(arg)
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
if (list) {
|
|
5015
|
+
if (0 < codes.length) {
|
|
5016
|
+
process.stderr.write(`aontu: --list takes no code\n${EXPLAIN_HELP}\n`)
|
|
5017
|
+
return 2
|
|
5018
|
+
}
|
|
5019
|
+
process.stdout.write(explainListText(format) + '\n')
|
|
5020
|
+
return 0
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
if (1 !== codes.length) {
|
|
5024
|
+
process.stderr.write(`aontu: explain needs one code\n${EXPLAIN_HELP}\n`)
|
|
5025
|
+
return 2
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
const code = codes[0]
|
|
5029
|
+
const { cls, hint, registered } = explainCode(code)
|
|
5030
|
+
if (!registered) {
|
|
5031
|
+
// AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
|
|
5032
|
+
// caller reading a code out of a report has almost certainly typed
|
|
5033
|
+
// it correctly, so the likely cause is a code from another tool or
|
|
5034
|
+
// a truncated one, and the near matches say which.
|
|
5035
|
+
process.stderr.write(`aontu: no such error code \`${code}\`\n`)
|
|
5036
|
+
const near = nearestVerb(code, explainCodes())
|
|
5037
|
+
if ('' !== near) {
|
|
5038
|
+
process.stderr.write(`aontu: did you mean \`${near}\`?\n`)
|
|
5039
|
+
}
|
|
5040
|
+
process.stderr.write(
|
|
5041
|
+
'aontu: `aontu explain --list` lists every registered code\n')
|
|
5042
|
+
return 2
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
if ('json' === format) {
|
|
5046
|
+
process.stdout.write(exactJSON({
|
|
5047
|
+
aontu: { version: version(), verb: 'explain' },
|
|
5048
|
+
code,
|
|
5049
|
+
class: cls,
|
|
5050
|
+
hint,
|
|
5051
|
+
}, 2) + '\n')
|
|
5052
|
+
return 0
|
|
5053
|
+
}
|
|
5054
|
+
// A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
|
|
5055
|
+
// empty block, which would read as an explanation that happened to
|
|
5056
|
+
// be blank.
|
|
5057
|
+
const body = '' === hint
|
|
5058
|
+
? '(no explanation text is registered for this code)'
|
|
5059
|
+
: hint
|
|
5060
|
+
process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`)
|
|
5061
|
+
return 0
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
|
|
5065
|
+
// `aontu init` (G11 phase 6,
|
|
5066
|
+
// docs/capability-review/g11-agent-onramp.md).
|
|
5067
|
+
//
|
|
5068
|
+
// NOT SCAFFOLDING CONVENIENCE. The agent's most expensive failure is
|
|
5069
|
+
// writing a FIRST document at all: the measurement that opened G11
|
|
5070
|
+
// found one reaching for the wildcard its neighbours use and getting
|
|
5071
|
+
// `verdict: valid` over data that violates it. A known-good starting
|
|
5072
|
+
// document turns generation into editing, which is the operation a
|
|
5073
|
+
// model is reliably good at.
|
|
5074
|
+
//
|
|
5075
|
+
// The trio is real, runnable and tested where it lives
|
|
5076
|
+
// (docs/skill/init/, run by ts/test/helpdoc.test.ts), and staged into
|
|
5077
|
+
// both ports by the same generator that stages the teaching pack, so
|
|
5078
|
+
// the two write the same bytes.
|
|
5079
|
+
|
|
5080
|
+
const INIT_HELP = 'aontu init [dir] (try --help)'
|
|
5081
|
+
|
|
5082
|
+
|
|
5083
|
+
function runInit(argv: string[]): number {
|
|
5084
|
+
const dirs: string[] = []
|
|
5085
|
+
|
|
5086
|
+
for (const arg of argv) {
|
|
5087
|
+
if ('-h' === arg || '--help' === arg) {
|
|
5088
|
+
process.stdout.write(HELP)
|
|
5089
|
+
return 0
|
|
5090
|
+
}
|
|
5091
|
+
if (arg.startsWith('-')) {
|
|
5092
|
+
process.stderr.write(`aontu: unknown init option ${arg} (try --help)\n`)
|
|
5093
|
+
return 2
|
|
5094
|
+
}
|
|
5095
|
+
dirs.push(arg)
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
if (1 < dirs.length) {
|
|
5099
|
+
process.stderr.write(`aontu: init takes one directory\n${INIT_HELP}\n`)
|
|
5100
|
+
return 2
|
|
5101
|
+
}
|
|
5102
|
+
const dir = dirs[0] ?? '.'
|
|
5103
|
+
|
|
5104
|
+
// REFUSES TO OVERWRITE, and checks every member BEFORE writing any of
|
|
5105
|
+
// them: a scaffold that wrote two files and then refused the third
|
|
5106
|
+
// would leave a directory in a state neither the caller nor a re-run
|
|
5107
|
+
// can reason about.
|
|
5108
|
+
const standing = INITDOC.filter((f) => existsSync(join(dir, f.name)))
|
|
5109
|
+
if (0 < standing.length) {
|
|
5110
|
+
process.stderr.write(
|
|
5111
|
+
`aontu: ${dir} already holds ${standing.map((f) => f.name).join(', ')}\n` +
|
|
5112
|
+
'aontu: init never overwrites; move them aside or name an' +
|
|
5113
|
+
' empty directory\n')
|
|
5114
|
+
return 2
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
try {
|
|
5118
|
+
mkdirSync(dir, { recursive: true })
|
|
5119
|
+
for (const f of INITDOC) {
|
|
5120
|
+
writeFileSync(join(dir, f.name), f.text, { mode: f.mode })
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
catch (err: any) {
|
|
5124
|
+
process.stderr.write(`aontu: cannot write in ${dir}: ${err.message}\n`)
|
|
5125
|
+
return 2
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
process.stdout.write(
|
|
5129
|
+
INITDOC.map((f) => join(dir, f.name)).join('\n') + '\n' +
|
|
5130
|
+
'\nA model, an instance of it, and the four questions to ask.\n' +
|
|
5131
|
+
'Run the checks: sh ' + join(dir, 'check.sh') + '\n' +
|
|
5132
|
+
'Learn the language: aontu help language\n')
|
|
5133
|
+
return 0
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
|
|
5137
|
+
// EVERY VERB THIS PORT DISPATCHES, for the nearest-verb suggestion
|
|
5138
|
+
// G11 phase 2 prints. A separate list from the if-chain in main()
|
|
5139
|
+
// because the chain's arms have three different shapes and cannot be
|
|
5140
|
+
// a table; ts/test/cli-help.test.ts keeps the two from drifting by
|
|
5141
|
+
// running each name and requiring it not to fall through to the bare
|
|
5142
|
+
// command.
|
|
5143
|
+
const KNOWN_VERBS = [
|
|
5144
|
+
'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
|
|
5145
|
+
'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
|
|
5146
|
+
'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
|
|
5147
|
+
'view', 'why',
|
|
5148
|
+
]
|
|
5149
|
+
|
|
5150
|
+
|
|
5151
|
+
// looksLikeVerb reports whether an unreadable argument was meant as a
|
|
5152
|
+
// verb rather than as a path. A bare word has no separator and no
|
|
5153
|
+
// extension; `./help`, `help.aon`, `/tmp/help` and `sub/dir` are paths
|
|
5154
|
+
// and keep the file diagnosis. Mirrors go/cmd/aontu/main.go.
|
|
5155
|
+
function looksLikeVerb(arg: string): boolean {
|
|
5156
|
+
return '' !== arg &&
|
|
5157
|
+
!/[/\\.]/.test(arg) &&
|
|
5158
|
+
!arg.startsWith('-')
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
|
|
5162
|
+
// NEAREST-VERB SUGGESTION (G11 phase 2). Restricted
|
|
5163
|
+
// Damerau-Levenshtein with a cap that grows with the word and stops at
|
|
5164
|
+
// three: one edit is a convincing suggestion on any length, three is
|
|
5165
|
+
// the most that can be believed on a long one, and an UNCAPPED
|
|
5166
|
+
// nearest match on a three-letter typo names something unrelated with
|
|
5167
|
+
// confidence. Mirrors go/cmd/aontu/help.go, including the sort, so
|
|
5168
|
+
// the two ports suggest the same verb on a tie.
|
|
5169
|
+
function nearestVerb(word: string, verbs: string[]): string {
|
|
5170
|
+
let best = ''
|
|
5171
|
+
let bestDist = Infinity
|
|
5172
|
+
const limit = Math.min(3, 1 + Math.floor(word.length / 4))
|
|
5173
|
+
for (const v of [...verbs].sort(cmpCodePoint)) {
|
|
5174
|
+
const d = editDistance(word.toLowerCase(), v)
|
|
5175
|
+
if (d < bestDist) {
|
|
5176
|
+
best = v
|
|
5177
|
+
bestDist = d
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
return bestDist > limit ? '' : best
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
|
|
5184
|
+
// Levenshtein with a transposition, iterative over two rows. Mirrors
|
|
5185
|
+
// editDistance in go/cmd/aontu/help.go exactly.
|
|
5186
|
+
function editDistance(a: string, b: string): number {
|
|
5187
|
+
const ar = [...a]
|
|
5188
|
+
const br = [...b]
|
|
5189
|
+
let prev2 = new Array(br.length + 1).fill(0)
|
|
5190
|
+
let prev = new Array(br.length + 1).fill(0).map((_, j) => j)
|
|
5191
|
+
let cur = new Array(br.length + 1).fill(0)
|
|
5192
|
+
for (let i = 1; i <= ar.length; i++) {
|
|
5193
|
+
cur[0] = i
|
|
5194
|
+
for (let j = 1; j <= br.length; j++) {
|
|
5195
|
+
const cost = ar[i - 1] === br[j - 1] ? 0 : 1
|
|
5196
|
+
let m = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
|
|
5197
|
+
if (1 < i && 1 < j &&
|
|
5198
|
+
ar[i - 1] === br[j - 2] && ar[i - 2] === br[j - 1] &&
|
|
5199
|
+
prev2[j - 2] + 1 < m) {
|
|
5200
|
+
m = prev2[j - 2] + 1
|
|
5201
|
+
}
|
|
5202
|
+
cur[j] = m
|
|
5203
|
+
}
|
|
5204
|
+
prev2 = [...prev]
|
|
5205
|
+
prev = [...cur]
|
|
5206
|
+
}
|
|
5207
|
+
return prev[br.length]
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
|
|
4205
5211
|
function main(argv: string[], servers: Servers = SERVERS): void {
|
|
4206
5212
|
// COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
|
|
4207
5213
|
// hardcoded their ANSI escapes, so a piped report and a `--jsonl`
|
|
@@ -4212,6 +5218,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4212
5218
|
setColor(true === process.stderr.isTTY ? undefined : false)
|
|
4213
5219
|
|
|
4214
5220
|
let mode: Mode = 'json'
|
|
5221
|
+
// THE REPORT FORM (G11 phase 7), default text: every existing caller
|
|
5222
|
+
// reads exactly what it always read, and a caller that asks for json
|
|
5223
|
+
// gets the failure in the finding shape every other verb reports.
|
|
5224
|
+
let format: EvalFormat = 'text'
|
|
4215
5225
|
// A LIST, though the bare command evaluates exactly one document.
|
|
4216
5226
|
// It used to be one variable and the last argument won, which made a
|
|
4217
5227
|
// MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
|
|
@@ -4264,6 +5274,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4264
5274
|
return finish(runSet(argv.slice(3)))
|
|
4265
5275
|
}
|
|
4266
5276
|
|
|
5277
|
+
if ('allow' === argv[2]) {
|
|
5278
|
+
return finish(runAllow(argv.slice(3)))
|
|
5279
|
+
}
|
|
5280
|
+
|
|
4267
5281
|
if ('why' === argv[2]) {
|
|
4268
5282
|
return finish(runWhy(argv.slice(3)))
|
|
4269
5283
|
}
|
|
@@ -4276,6 +5290,20 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4276
5290
|
return finish(runHash(argv.slice(3)))
|
|
4277
5291
|
}
|
|
4278
5292
|
|
|
5293
|
+
// G11 phases 1 and 3. Dispatched with the rest, so `aontu ./help`
|
|
5294
|
+
// still reads a file named help exactly as `aontu ./vet` does.
|
|
5295
|
+
if ('help' === argv[2]) {
|
|
5296
|
+
return finish(runHelp(argv.slice(3)))
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
if ('explain' === argv[2]) {
|
|
5300
|
+
return finish(runExplain(argv.slice(3)))
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
if ('init' === argv[2]) {
|
|
5304
|
+
return finish(runInit(argv.slice(3)))
|
|
5305
|
+
}
|
|
5306
|
+
|
|
4279
5307
|
if ('mod' === argv[2]) {
|
|
4280
5308
|
return finish(runMod(argv.slice(3)))
|
|
4281
5309
|
}
|
|
@@ -4331,6 +5359,14 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4331
5359
|
}
|
|
4332
5360
|
trust = parsed
|
|
4333
5361
|
}
|
|
5362
|
+
else if ('--format' === arg) {
|
|
5363
|
+
const f = args[++i]
|
|
5364
|
+
if ('text' !== f && 'json' !== f) {
|
|
5365
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
5366
|
+
return finish(2)
|
|
5367
|
+
}
|
|
5368
|
+
format = f
|
|
5369
|
+
}
|
|
4334
5370
|
else if ('--jsonl' === arg) {
|
|
4335
5371
|
jsonl = true
|
|
4336
5372
|
// A JSONL answer is machine-read by definition, even when the
|
|
@@ -4390,7 +5426,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4390
5426
|
|
|
4391
5427
|
const file = files[0]
|
|
4392
5428
|
if (null != file) {
|
|
4393
|
-
finish(runFile(file, mode, trust))
|
|
5429
|
+
finish(runFile(file, mode, format, trust))
|
|
4394
5430
|
}
|
|
4395
5431
|
// `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
|
|
4396
5432
|
// a harness over a pipe, so gating it on an interactive terminal
|
|
@@ -4400,9 +5436,9 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4400
5436
|
runRepl(mode, jsonl, trust)
|
|
4401
5437
|
}
|
|
4402
5438
|
else {
|
|
4403
|
-
runStdin(mode, trust).then((code) => finish(code))
|
|
5439
|
+
runStdin(mode, format, trust).then((code) => finish(code))
|
|
4404
5440
|
}
|
|
4405
|
-
} /* node:coverage ignore next
|
|
5441
|
+
} /* node:coverage ignore next 20 */
|
|
4406
5442
|
|
|
4407
5443
|
|
|
4408
5444
|
// No require.main guard here: bin/aontu.js is the executable entry and
|
|
@@ -4417,7 +5453,9 @@ export {
|
|
|
4417
5453
|
runRender,
|
|
4418
5454
|
runTemplate,
|
|
4419
5455
|
runMod,
|
|
4420
|
-
runHash, runGet,
|
|
4421
|
-
|
|
5456
|
+
runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
|
|
5457
|
+
looksLikeVerb,
|
|
5458
|
+
KNOWN_VERBS,
|
|
5459
|
+
runWhy, renderWhyText, runSet, runAllow, runAgentsMd, runFmt,
|
|
4422
5460
|
watchChange, watchSignature, vetWaiter, deprecatedAt,
|
|
4423
5461
|
}
|