aontu 0.61.0 → 0.63.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 -4
- package/dist/agentsmd.d.ts +1 -0
- package/dist/agentsmd.js +7 -28
- package/dist/agentsmd.js.map +1 -1
- package/dist/alias.js.map +1 -1
- package/dist/allow.d.ts +23 -0
- package/dist/allow.js +138 -0
- package/dist/allow.js.map +1 -0
- package/dist/aontu.d.ts +4 -2
- package/dist/aontu.js +4 -80
- package/dist/aontu.js.map +1 -1
- package/dist/aontumodel.d.ts +4 -0
- package/dist/aontumodel.js +33 -0
- package/dist/aontumodel.js.map +1 -0
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +877 -479
- package/dist/cli.js.map +1 -1
- package/dist/ctx.js +0 -48
- package/dist/ctx.js.map +1 -1
- package/dist/diff.js +0 -32
- package/dist/diff.js.map +1 -1
- package/dist/err.js +0 -40
- package/dist/err.js.map +1 -1
- package/dist/escape.js +0 -45
- package/dist/escape.js.map +1 -1
- package/dist/exactjson.d.ts +0 -35
- package/dist/exactjson.js +0 -131
- package/dist/exactjson.js.map +1 -1
- package/dist/format.js +55 -189
- package/dist/format.js.map +1 -1
- package/dist/grammar.d.ts +9 -0
- package/dist/grammar.js +54 -0
- package/dist/grammar.js.map +1 -0
- package/dist/graph.js +0 -26
- package/dist/graph.js.map +1 -1
- package/dist/hcanon.js +0 -82
- package/dist/hcanon.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.d.ts +0 -6
- package/dist/hints.js +59 -47
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.js +0 -114
- package/dist/jsonschema.js.map +1 -1
- package/dist/keyorder.d.ts +0 -7
- package/dist/keyorder.js +0 -41
- package/dist/keyorder.js.map +1 -1
- package/dist/lang.js +32 -877
- package/dist/lang.js.map +1 -1
- package/dist/lower.d.ts +3 -0
- package/dist/lower.js +14 -61
- package/dist/lower.js.map +1 -1
- package/dist/lsp-server.js +0 -16
- package/dist/lsp-server.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.js +12 -159
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.js +0 -26
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp.js +0 -113
- package/dist/mcp.js.map +1 -1
- package/dist/mod-tool.js +0 -130
- package/dist/mod-tool.js.map +1 -1
- package/dist/mod.js +0 -162
- package/dist/mod.js.map +1 -1
- package/dist/patch.js +0 -217
- package/dist/patch.js.map +1 -1
- package/dist/provenance.js +0 -140
- package/dist/provenance.js.map +1 -1
- package/dist/query.js +0 -75
- package/dist/query.js.map +1 -1
- package/dist/reach.js +0 -43
- package/dist/reach.js.map +1 -1
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +3 -58
- package/dist/relation.js.map +1 -1
- package/dist/render.js +33 -143
- package/dist/render.js.map +1 -1
- package/dist/report-sarif.d.ts +0 -11
- package/dist/report-sarif.js +0 -28
- package/dist/report-sarif.js.map +1 -1
- package/dist/sig.js +0 -35
- package/dist/sig.js.map +1 -1
- package/dist/sigdecl.js +1 -1
- package/dist/sigdecl.js.map +1 -1
- package/dist/siggate.js +0 -4
- package/dist/siggate.js.map +1 -1
- package/dist/site.js +3 -29
- package/dist/site.js.map +1 -1
- package/dist/subsume.d.ts +0 -10
- package/dist/subsume.js +0 -137
- package/dist/subsume.js.map +1 -1
- package/dist/template.d.ts +2 -1
- package/dist/template.js +58 -138
- package/dist/template.js.map +1 -1
- package/dist/trim.js +0 -41
- package/dist/trim.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.js.map +1 -1
- package/dist/unify.js +12 -242
- package/dist/unify.js.map +1 -1
- package/dist/utility.js +0 -22
- package/dist/utility.js.map +1 -1
- package/dist/val/AbnfFuncVal.d.ts +18 -0
- package/dist/val/AbnfFuncVal.js +132 -0
- package/dist/val/AbnfFuncVal.js.map +1 -0
- package/dist/val/AbsentVal.d.ts +11 -0
- package/dist/val/AbsentVal.js +30 -0
- package/dist/val/AbsentVal.js.map +1 -0
- package/dist/val/AggFuncVal.d.ts +10 -1
- package/dist/val/AggFuncVal.js +104 -116
- package/dist/val/AggFuncVal.js.map +1 -1
- package/dist/val/ArithFuncVal.js +0 -12
- package/dist/val/ArithFuncVal.js.map +1 -1
- package/dist/val/BagVal.js +1 -78
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/BigDecimalVal.js +0 -16
- package/dist/val/BigDecimalVal.js.map +1 -1
- package/dist/val/BigIntegerVal.js +0 -16
- package/dist/val/BigIntegerVal.js.map +1 -1
- package/dist/val/CloseFuncVal.js +0 -9
- package/dist/val/CloseFuncVal.js.map +1 -1
- package/dist/val/CmpFuncVal.d.ts +20 -0
- package/dist/val/CmpFuncVal.js +188 -0
- package/dist/val/CmpFuncVal.js.map +1 -0
- package/dist/val/ConjunctVal.js +0 -29
- package/dist/val/ConjunctVal.js.map +1 -1
- package/dist/val/ConstraintVal.js +0 -500
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/ContainerKindVal.js +0 -2
- package/dist/val/ContainerKindVal.js.map +1 -1
- package/dist/val/CopyFuncVal.js +0 -3
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/Decimal.js +0 -179
- package/dist/val/Decimal.js.map +1 -1
- package/dist/val/DeprecateFuncVal.js.map +1 -1
- package/dist/val/DisjunctVal.js +0 -152
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/EachFuncVal.d.ts +1 -2
- package/dist/val/EachFuncVal.js +12 -29
- package/dist/val/EachFuncVal.js.map +1 -1
- package/dist/val/EmitFuncVal.d.ts +1 -1
- package/dist/val/EmitFuncVal.js +6 -119
- package/dist/val/EmitFuncVal.js.map +1 -1
- package/dist/val/ExpectVal.js +0 -62
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FilterFuncVal.js +0 -25
- package/dist/val/FilterFuncVal.js.map +1 -1
- package/dist/val/FuncBaseVal.d.ts +1 -0
- package/dist/val/FuncBaseVal.js +7 -127
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/GraphAtomVal.js +0 -15
- package/dist/val/GraphAtomVal.js.map +1 -1
- package/dist/val/HideFuncVal.js +0 -13
- package/dist/val/HideFuncVal.js.map +1 -1
- package/dist/val/IntegerVal.js +0 -61
- package/dist/val/IntegerVal.js.map +1 -1
- package/dist/val/JunctionVal.js +0 -20
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/KeyFuncVal.js +0 -46
- package/dist/val/KeyFuncVal.js.map +1 -1
- package/dist/val/ListVal.js +0 -57
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js +11 -6
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +0 -151
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.js +0 -27
- package/dist/val/MatchFuncVal.js.map +1 -1
- package/dist/val/{FormFuncVal.d.ts → MaybeFuncVal.d.ts} +5 -5
- package/dist/val/MaybeFuncVal.js +50 -0
- package/dist/val/MaybeFuncVal.js.map +1 -0
- package/dist/val/MoveFuncVal.js +0 -18
- package/dist/val/MoveFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +2 -36
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/NomFuncVal.d.ts +12 -0
- package/dist/val/NomFuncVal.js +153 -0
- package/dist/val/NomFuncVal.js.map +1 -0
- package/dist/val/NumberVal.js +0 -15
- package/dist/val/NumberVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -0
- package/dist/val/OpBaseVal.js +3 -15
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/PackFuncVal.js +0 -34
- package/dist/val/PackFuncVal.js.map +1 -1
- package/dist/val/PathFuncVal.js +0 -6
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PathVal.js +0 -41
- package/dist/val/PathVal.js.map +1 -1
- package/dist/val/PlaceVal.js +0 -25
- package/dist/val/PlaceVal.js.map +1 -1
- package/dist/val/PlusOpVal.d.ts +1 -7
- package/dist/val/PlusOpVal.js +13 -74
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefFuncVal.js +0 -1
- package/dist/val/PrefFuncVal.js.map +1 -1
- package/dist/val/PrefVal.js +0 -167
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RecurseVal.js +0 -55
- package/dist/val/RecurseVal.js.map +1 -1
- package/dist/val/RefVal.js +0 -282
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.js +0 -232
- package/dist/val/ReferFuncVal.js.map +1 -1
- package/dist/val/ScalarKindVal.js +0 -49
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/ScalarVal.js +0 -11
- package/dist/val/ScalarVal.js.map +1 -1
- package/dist/val/StrFuncVal.js +0 -18
- package/dist/val/StrFuncVal.js.map +1 -1
- package/dist/val/SuperFuncVal.js +0 -32
- package/dist/val/SuperFuncVal.js.map +1 -1
- package/dist/val/TopVal.js +0 -1
- package/dist/val/TopVal.js.map +1 -1
- package/dist/val/TranslateFuncVal.d.ts +12 -0
- package/dist/val/TranslateFuncVal.js +99 -0
- package/dist/val/TranslateFuncVal.js.map +1 -0
- package/dist/val/UpperFuncVal.js +11 -6
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/Val.d.ts +1 -0
- package/dist/val/Val.js +2 -133
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js +0 -12
- package/dist/val/VarVal.js.map +1 -1
- package/dist/val/arith.js +0 -37
- package/dist/val/arith.js.map +1 -1
- package/dist/val/caserange.d.ts +3 -0
- package/dist/val/caserange.js +49 -0
- package/dist/val/caserange.js.map +1 -0
- package/dist/val/members.js +0 -6
- package/dist/val/members.js.map +1 -1
- package/dist/val/numcmp.js +0 -11
- package/dist/val/numcmp.js.map +1 -1
- package/dist/val/numkind.js +0 -145
- package/dist/val/numkind.js.map +1 -1
- package/dist/val/valutil.js +0 -16
- package/dist/val/valutil.js.map +1 -1
- package/dist/vet.d.ts +12 -0
- package/dist/vet.js +159 -412
- package/dist/vet.js.map +1 -1
- package/dist/view.js +0 -414
- package/dist/view.js.map +1 -1
- package/dist/walk.js +0 -41
- package/dist/walk.js.map +1 -1
- package/grammar/aontu.abnf +9 -7
- package/grammar/aontu.gbnf +5 -5
- package/grammar/aontu.lark +5 -5
- package/grammar/aontu.tmLanguage.json +1 -1
- package/package.json +4 -2
- 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 +8 -32
- package/src/alias.ts +0 -39
- package/src/allow.ts +221 -0
- package/src/aontu.ts +10 -108
- package/src/aontumodel.ts +32 -0
- package/src/cli.ts +1009 -540
- package/src/ctx.ts +0 -103
- package/src/diff.ts +0 -40
- package/src/err.ts +0 -40
- package/src/escape.ts +0 -46
- package/src/exactjson.ts +0 -131
- package/src/format.ts +63 -234
- package/src/grammar.ts +72 -0
- package/src/graph.ts +0 -61
- package/src/hcanon.ts +0 -82
- package/src/helpdoc.ts +77 -0
- package/src/hints.ts +72 -49
- package/src/jsonschema.ts +0 -123
- package/src/keyorder.ts +0 -42
- package/src/lang.ts +39 -895
- package/src/lower.ts +15 -65
- package/src/lsp-server.ts +0 -16
- package/src/lsp.ts +12 -180
- package/src/mcp-server.ts +0 -31
- package/src/mcp.ts +0 -130
- package/src/mod-tool.ts +0 -158
- package/src/mod.ts +0 -178
- package/src/patch.ts +0 -232
- package/src/provenance.ts +0 -183
- package/src/query.ts +0 -84
- package/src/reach.ts +0 -53
- package/src/relation.ts +7 -71
- package/src/render.ts +33 -180
- package/src/report-sarif.ts +0 -48
- package/src/sig.ts +0 -35
- package/src/sigdecl.ts +1 -1
- package/src/siggate.ts +0 -30
- package/src/site.ts +3 -29
- package/src/subsume.ts +1 -161
- package/src/template.ts +69 -140
- package/src/trim.ts +0 -53
- package/src/type.ts +2 -45
- package/src/unify.ts +13 -251
- package/src/utility.ts +0 -31
- package/src/val/AbnfFuncVal.ts +181 -0
- package/src/val/AbsentVal.ts +54 -0
- package/src/val/AggFuncVal.ts +152 -188
- package/src/val/ArithFuncVal.ts +0 -20
- package/src/val/BagVal.ts +1 -78
- package/src/val/BigDecimalVal.ts +0 -16
- package/src/val/BigIntegerVal.ts +0 -16
- package/src/val/CloseFuncVal.ts +0 -9
- package/src/val/CmpFuncVal.ts +249 -0
- package/src/val/ConjunctVal.ts +0 -33
- package/src/val/ConstraintVal.ts +2 -537
- package/src/val/ContainerKindVal.ts +0 -18
- package/src/val/CopyFuncVal.ts +0 -5
- package/src/val/Decimal.ts +1 -185
- package/src/val/DeprecateFuncVal.ts +0 -10
- package/src/val/DisjunctVal.ts +0 -157
- package/src/val/EachFuncVal.ts +12 -53
- package/src/val/EmitFuncVal.ts +8 -208
- package/src/val/ExpectVal.ts +0 -62
- package/src/val/FilterFuncVal.ts +0 -55
- package/src/val/FuncBaseVal.ts +9 -130
- package/src/val/GraphAtomVal.ts +0 -42
- package/src/val/HideFuncVal.ts +0 -15
- package/src/val/IntegerVal.ts +0 -61
- package/src/val/JunctionVal.ts +0 -20
- package/src/val/KeyFuncVal.ts +0 -48
- package/src/val/ListVal.ts +0 -59
- package/src/val/LowerFuncVal.ts +12 -7
- package/src/val/MapVal.ts +0 -151
- package/src/val/MatchFuncVal.ts +0 -59
- package/src/val/MaybeFuncVal.ts +86 -0
- package/src/val/MoveFuncVal.ts +0 -20
- package/src/val/NilVal.ts +2 -36
- package/src/val/NomFuncVal.ts +200 -0
- package/src/val/NumberVal.ts +0 -16
- package/src/val/OpBaseVal.ts +4 -17
- package/src/val/PackFuncVal.ts +0 -63
- package/src/val/PathFuncVal.ts +0 -32
- package/src/val/PathVal.ts +0 -66
- package/src/val/PlaceVal.ts +0 -45
- package/src/val/PlusOpVal.ts +18 -75
- package/src/val/PrefFuncVal.ts +0 -1
- package/src/val/PrefVal.ts +0 -179
- package/src/val/RecurseVal.ts +0 -81
- package/src/val/RefVal.ts +1 -285
- package/src/val/ReferFuncVal.ts +0 -255
- package/src/val/ScalarKindVal.ts +0 -50
- package/src/val/ScalarVal.ts +0 -12
- package/src/val/StrFuncVal.ts +0 -44
- package/src/val/SuperFuncVal.ts +0 -42
- package/src/val/TopVal.ts +0 -1
- package/src/val/TranslateFuncVal.ts +132 -0
- package/src/val/UpperFuncVal.ts +12 -7
- package/src/val/Val.ts +3 -192
- package/src/val/VarVal.ts +0 -15
- package/src/val/arith.ts +0 -92
- package/src/val/caserange.ts +53 -0
- package/src/val/members.ts +0 -23
- package/src/val/numcmp.ts +1 -27
- package/src/val/numkind.ts +0 -149
- package/src/val/valutil.ts +0 -16
- package/src/vet.ts +209 -504
- package/src/view.ts +0 -507
- package/src/walk.ts +0 -41
- package/dist/std.d.ts +0 -3
- package/dist/std.js +0 -637
- package/dist/std.js.map +0 -1
- package/dist/val/FormFuncVal.js +0 -55
- package/dist/val/FormFuncVal.js.map +0 -1
- package/src/std.ts +0 -648
- package/src/val/FormFuncVal.ts +0 -119
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
2
|
|
|
3
|
-
// Command-line interface for Aontu.
|
|
4
|
-
//
|
|
5
|
-
// aontu [options] [file]
|
|
6
|
-
//
|
|
7
|
-
// With a file argument, the file is evaluated and the result printed.
|
|
8
|
-
// With no file on an interactive terminal, a REPL is started. With no
|
|
9
|
-
// file and piped input, the source is read from stdin. See HELP below.
|
|
10
3
|
|
|
11
4
|
// Named imports, not `import * as`: the namespace form makes tsc emit the
|
|
12
5
|
import { evalFailure } from './query'
|
|
@@ -23,11 +16,16 @@ import {
|
|
|
23
16
|
exactJSON, vet, subsume, trimCheck, relationCheck,
|
|
24
17
|
hcanon, canonHash,
|
|
25
18
|
get, why, patch, agentsMd,
|
|
19
|
+
allow,
|
|
26
20
|
render,
|
|
27
21
|
renderProfile,
|
|
28
22
|
} from './aontu'
|
|
23
|
+
import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
|
|
29
24
|
import type { RenderCoverage, RenderReport } from './render'
|
|
30
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
desugarTemplate, resugarTemplate, templateOutputs, markerFor,
|
|
27
|
+
markerFromProfiles,
|
|
28
|
+
} from './template'
|
|
31
29
|
import { outsideRoot } from './mcp'
|
|
32
30
|
import { sarifReport } from './report-sarif'
|
|
33
31
|
import { main as lspMain } from './lsp-server'
|
|
@@ -39,7 +37,9 @@ import type {
|
|
|
39
37
|
} from './mod-tool'
|
|
40
38
|
import { modCacheDir } from './mod'
|
|
41
39
|
import { VET_MAX_ERRORS } from './vet'
|
|
42
|
-
import type {
|
|
40
|
+
import type {
|
|
41
|
+
VetReport, VetFinding, VetVerdict, VetCoverage,
|
|
42
|
+
} from './vet'
|
|
43
43
|
import type {
|
|
44
44
|
SubsumeReport, SubsumeVerdict, SubsumeProfile,
|
|
45
45
|
} from './subsume'
|
|
@@ -57,11 +57,21 @@ import type { WhyRecord } from './provenance'
|
|
|
57
57
|
import { agentsMdSplice } from './agentsmd'
|
|
58
58
|
import { format, unifiedDiff } from './format'
|
|
59
59
|
import { includeOpts } from './utility'
|
|
60
|
+
import { HELPDOC, INITDOC } from './helpdoc'
|
|
61
|
+
import type { HelpTopic } from './helpdoc'
|
|
62
|
+
import { hints, codeClasses, codeClass } from './hints'
|
|
63
|
+
import { cmpCodePoint } from './keyorder'
|
|
60
64
|
import type { IncludeOptions } from './utility'
|
|
61
65
|
|
|
62
66
|
|
|
63
67
|
type Mode = 'json' | 'canon'
|
|
64
68
|
|
|
69
|
+
// The REPORT form of the bare command (G11 phase 7), which is a
|
|
70
|
+
// separate axis from Mode: `--canon` chooses what the answer IS,
|
|
71
|
+
// `--format` chooses how the answer is WRAPPED. Every other verb
|
|
72
|
+
// spells the second one this way.
|
|
73
|
+
type EvalFormat = 'text' | 'json'
|
|
74
|
+
|
|
65
75
|
|
|
66
76
|
const HELP = `Usage: aontu [options] [file]
|
|
67
77
|
aontu vet [options] <schema> <data> [more-data...]
|
|
@@ -76,14 +86,20 @@ const HELP = `Usage: aontu [options] [file]
|
|
|
76
86
|
aontu render [--at <path>] [--profile <file>]... [--unit <path>]
|
|
77
87
|
[--stdout | --out <dir> | --check <dir> | --coverage]
|
|
78
88
|
[--coverage-at <path>] [--strict] <file>
|
|
79
|
-
aontu template [--resugar] [--check] [--marker <token>]
|
|
89
|
+
aontu template [--resugar] [--check] [--marker <token>]
|
|
90
|
+
[--profile <file>] <file>
|
|
80
91
|
aontu hash [options] <file>
|
|
81
92
|
aontu mod tidy|verify|vendor|manifest [options] [dir]
|
|
82
93
|
aontu get <path> [options] <file>
|
|
83
94
|
aontu why <path> [options] <file>
|
|
84
95
|
aontu set <path>=<value>... --entry <file> --overlay <file>
|
|
85
|
-
aontu
|
|
86
|
-
aontu
|
|
96
|
+
aontu allow --role <role> [--at <path>] <roles-file> <path>...
|
|
97
|
+
aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
|
|
98
|
+
aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
|
|
99
|
+
[--profile <file>] <file>...
|
|
100
|
+
aontu help [topic] [--format text|json]
|
|
101
|
+
aontu explain <code> | --list [--format text|json]
|
|
102
|
+
aontu init [dir]
|
|
87
103
|
aontu lsp
|
|
88
104
|
aontu mcp [--root <dir>]
|
|
89
105
|
|
|
@@ -91,6 +107,34 @@ Evaluate an aontu source file and print the result as JSON.
|
|
|
91
107
|
With no file on an interactive terminal, start a REPL.
|
|
92
108
|
With no file and piped input, read the source from stdin.
|
|
93
109
|
|
|
110
|
+
NEW TO THE LANGUAGE? This page documents the TOOL. The documentation
|
|
111
|
+
of the LANGUAGE travels inside this binary, and this is how to reach it:
|
|
112
|
+
|
|
113
|
+
aontu help List the topics this binary carries
|
|
114
|
+
aontu help tasks Which verb does the job you have
|
|
115
|
+
aontu help language The whole grammar, on one page
|
|
116
|
+
aontu help examples The ladder, from plain JSON upward
|
|
117
|
+
aontu help codes What a refusal means
|
|
118
|
+
aontu help grammar The published ABNF
|
|
119
|
+
aontu explain <code> What one error code a report carries means
|
|
120
|
+
aontu explain --list Every registered code with its class
|
|
121
|
+
|
|
122
|
+
Every one of those answers with no network and no checkout. The
|
|
123
|
+
long-form documentation -- the tutorial, the language and API
|
|
124
|
+
references, the how-to guides -- is in docs/ of the repository, which
|
|
125
|
+
is where to go when the topics above are not enough; the contributor
|
|
126
|
+
and agent guide is AGENTS.md beside it.
|
|
127
|
+
|
|
128
|
+
NOTHING TO EDIT YET? aontu init [dir] writes a working model, an
|
|
129
|
+
instance of it, and the four checks to run -- so the first
|
|
130
|
+
document is an edit of something that already holds, rather than an
|
|
131
|
+
invention. It refuses to overwrite.
|
|
132
|
+
|
|
133
|
+
The one construct to know before writing anything: &: inside a map is
|
|
134
|
+
a TEMPLATE that every key of that map must satisfy. A quoted "*" is a
|
|
135
|
+
key named *, not a wildcard, and a schema written that way constrains
|
|
136
|
+
nothing while still reporting valid.
|
|
137
|
+
|
|
94
138
|
The vet verb validates data documents against a schema document and
|
|
95
139
|
reports what does not hold, as text or as a machine-readable object.
|
|
96
140
|
|
|
@@ -100,7 +144,11 @@ query between a document and its own earlier versions.
|
|
|
100
144
|
|
|
101
145
|
Options:
|
|
102
146
|
-c, --canon Print the canonical form instead of generated JSON
|
|
103
|
-
|
|
147
|
+
--format <f> text (default) or json. The json form wraps the
|
|
148
|
+
answer as {aontu, findings, ok, out}, so a failure
|
|
149
|
+
here reads like every other verb's
|
|
150
|
+
-h, --help Show this help and exit (the verbs and their flags);
|
|
151
|
+
aontu help is the LANGUAGE, and lists its own topics
|
|
104
152
|
--jsonl REPL: answer every command as one JSON line
|
|
105
153
|
-v, --version Print the version and exit
|
|
106
154
|
--trust <t> Include capability: system (default), none, or
|
|
@@ -131,12 +179,28 @@ Vet options:
|
|
|
131
179
|
--closed Refuse keys the anchor does not declare
|
|
132
180
|
--partial Residue is reported but does not fail the run
|
|
133
181
|
--max-errors <n> Cap the finding list (default 20)
|
|
182
|
+
--coverage Report what the check EXAMINED: how many data
|
|
183
|
+
leaves a schema declaration constrained, the
|
|
184
|
+
shallowest data paths none did, and the
|
|
185
|
+
declarations no data met
|
|
186
|
+
--strict-coverage --coverage, and exit 1 when the run was VACUOUS --
|
|
187
|
+
when no data leaf was constrained at all. The
|
|
188
|
+
verdict word is unchanged, so nothing that passes
|
|
189
|
+
today starts failing without this flag
|
|
190
|
+
--coverage-at <p> Measure coverage under this path of the data only
|
|
134
191
|
--format <f> text (default), json or sarif
|
|
135
192
|
--watch Re-run whenever a watched file changes
|
|
136
193
|
|
|
194
|
+
A check that examined NOTHING and a check that passed answer the same
|
|
195
|
+
without --coverage. The usual cause is a schema written with the
|
|
196
|
+
wildcard other tools use: a quoted "*" is a key NAMED *, not a
|
|
197
|
+
template, so it constrains nothing and the run still reports valid.
|
|
198
|
+
The template is &: -- see aontu help language.
|
|
199
|
+
|
|
137
200
|
Vet exit codes:
|
|
138
201
|
0 valid data unifies, and is concrete (or --partial)
|
|
139
|
-
1 invalid at least one contradiction
|
|
202
|
+
1 invalid at least one contradiction, or a vacuous run under
|
|
203
|
+
--strict-coverage
|
|
140
204
|
2 usage bad option, or a file that cannot be read
|
|
141
205
|
3 incomplete no contradiction, but the truth is not yet satisfied
|
|
142
206
|
4 error the schema is unusable on its own
|
|
@@ -272,8 +336,9 @@ up on its own, or a relation, root or path that names nothing.
|
|
|
272
336
|
Render options:
|
|
273
337
|
--at <path> Render the value at this path ($.a.b); the root by
|
|
274
338
|
default
|
|
275
|
-
--profile <file> A profile document,
|
|
276
|
-
against aontu:
|
|
339
|
+
--profile <file> A profile document, aontu: render: Lang: {lang,
|
|
340
|
+
...}, vetted against aontu:render; repeatable,
|
|
341
|
+
one per language
|
|
277
342
|
--unit <path> Render only the unit with this path
|
|
278
343
|
--stdout One unit's bytes and nothing else (with --unit when
|
|
279
344
|
the instance has several)
|
|
@@ -297,8 +362,8 @@ does not stand up or the instance is not aontu:code.
|
|
|
297
362
|
A render entry file whose extension is not .aon is a TEMPLATE: a
|
|
298
363
|
generator in the target's own syntax, whose marker lines carry aontu
|
|
299
364
|
and whose other lines are output. It is desugared before it is
|
|
300
|
-
evaluated, and
|
|
301
|
-
|
|
365
|
+
evaluated, and a language the table does not know names its marker with
|
|
366
|
+
--marker, or declares it once in a profile file that --profile reads.
|
|
302
367
|
|
|
303
368
|
Template options:
|
|
304
369
|
--resugar The file is the canonical aontu; print the template
|
|
@@ -306,7 +371,9 @@ Template options:
|
|
|
306
371
|
--check Desugar and resugar, and exit 1 if the file is not
|
|
307
372
|
what the round trip answers
|
|
308
373
|
--marker <t> The marker, when the extension does not name it
|
|
309
|
-
(default //-, and #- --- /*- by extension)
|
|
374
|
+
(default //-, and #- --- /*- <!--- by extension)
|
|
375
|
+
--profile <f> A profile file, whose template.ext names the
|
|
376
|
+
extensions it marks and template.marker the marker
|
|
310
377
|
|
|
311
378
|
The template verb prints the canonical aontu form of a generator
|
|
312
379
|
written in the target's own syntax: a marked line is aontu source, and
|
|
@@ -334,14 +401,61 @@ change contradicts a pinned value -- aontu why locates it, and
|
|
|
334
401
|
--in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
|
|
335
402
|
stand up on its own.
|
|
336
403
|
|
|
404
|
+
Allow options:
|
|
405
|
+
--role <role> The role the caller is operating under (required)
|
|
406
|
+
--at <path> Where the roles map lives in the role model
|
|
407
|
+
(default $.roles)
|
|
408
|
+
--format <f> text (default) or json
|
|
409
|
+
|
|
410
|
+
The allow verb asks a role model whether a role may modify every one
|
|
411
|
+
of the given subtrees, and answers before the change is made. The
|
|
412
|
+
role model is an aontu document: one entry per role, each carrying
|
|
413
|
+
allow (the subtrees it may modify) and optionally deny (the ones it
|
|
414
|
+
may not), as path strings starting at $; * in a path matches any one
|
|
415
|
+
key. A path is allowed when an allow entry is at or above it, and
|
|
416
|
+
refused when a deny entry is at, above or below it, whatever the
|
|
417
|
+
order. Every path starts with $, and may be spelled as set's
|
|
418
|
+
assignment, <path>=<value>, whose value must be one value: a value
|
|
419
|
+
carrying a second pair would write a subtree the gate was not asked
|
|
420
|
+
about.
|
|
421
|
+
|
|
422
|
+
Allow exit codes: 0 allowed (every path), 1 refused (at least one
|
|
423
|
+
path, or a role the model does not declare), 2 usage, 4 the role
|
|
424
|
+
model does not stand up on its own.
|
|
425
|
+
|
|
337
426
|
Agentsmd options:
|
|
338
427
|
--write <file> Splice the stanza into this file between the
|
|
339
428
|
aontu:begin and aontu:end markers, appending them
|
|
340
429
|
when they are absent; the rest is left alone
|
|
430
|
+
--depth <n> How deep the shape line projects (default 2). Two
|
|
431
|
+
levels name the root keys and say top under them; a
|
|
432
|
+
caller that wants the fields asks for them
|
|
341
433
|
|
|
342
434
|
Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
|
|
343
435
|
stand up on its own.
|
|
344
436
|
|
|
437
|
+
Help options:
|
|
438
|
+
--format <f> text (default) or json, the topic and its text
|
|
439
|
+
|
|
440
|
+
The help verb prints the embedded teaching pack: the language, not the
|
|
441
|
+
tool. With no topic it lists them. Topics are tasks, language,
|
|
442
|
+
examples, codes and grammar; the corpus is generated from docs/skill/
|
|
443
|
+
and grammar/aontu.abnf, so it cannot drift from those sources.
|
|
444
|
+
|
|
445
|
+
Help exit codes: 0 printed, 2 an unknown topic (the topics are listed)
|
|
446
|
+
or a bad option.
|
|
447
|
+
|
|
448
|
+
Explain options:
|
|
449
|
+
--list Every registered error code with its class
|
|
450
|
+
--format <f> text (default) or json
|
|
451
|
+
|
|
452
|
+
The explain verb answers what one error code means, from the same
|
|
453
|
+
table the engine attaches to a finding. Every registered code has an
|
|
454
|
+
entry, so a code read out of a report always resolves.
|
|
455
|
+
|
|
456
|
+
Explain exit codes: 0 explained, 2 an unknown code (near matches are
|
|
457
|
+
named) or a bad option.
|
|
458
|
+
|
|
345
459
|
Fmt options:
|
|
346
460
|
-w, --write Rewrite each file in place, when its form would change
|
|
347
461
|
-l, --list Print the name of each file whose form would change
|
|
@@ -352,7 +466,9 @@ Fmt options:
|
|
|
352
466
|
shapes, on standard error, and print nothing else
|
|
353
467
|
--strict With --lint, and exit 1 when there is a finding
|
|
354
468
|
--marker <t> The file is a generator, and this is its marker
|
|
355
|
-
(default //-, and #- --- /*- by extension)
|
|
469
|
+
(default //-, and #- --- /*- <!--- by extension)
|
|
470
|
+
--profile <f> A profile file, whose template.ext names the
|
|
471
|
+
extensions it marks and template.marker the marker
|
|
356
472
|
|
|
357
473
|
The fmt verb prints one document in the agreed form; with no file it
|
|
358
474
|
reads standard input. Several files need one of the options above.
|
|
@@ -399,51 +515,78 @@ function version(): string {
|
|
|
399
515
|
}
|
|
400
516
|
|
|
401
517
|
|
|
518
|
+
const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g')
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
function evalFinding(code: string, text: string): VetFinding {
|
|
522
|
+
return {
|
|
523
|
+
class: codeClass(code),
|
|
524
|
+
code,
|
|
525
|
+
message: text.split('\n')[0].replace(EVAL_ANSI, ''),
|
|
526
|
+
path: '$',
|
|
527
|
+
severity: 'error',
|
|
528
|
+
sites: [],
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
|
|
402
533
|
// Evaluate source, returning either the rendered output or the error
|
|
403
|
-
// message. Never throws.
|
|
534
|
+
// message, and the failure in the finding shape. Never throws.
|
|
404
535
|
function evalSource(
|
|
405
536
|
aontu: Aontu,
|
|
406
537
|
src: string,
|
|
407
538
|
mode: Mode,
|
|
408
|
-
): { ok: boolean; text: string } {
|
|
539
|
+
): { ok: boolean; text: string; findings: VetFinding[] } {
|
|
409
540
|
try {
|
|
410
|
-
// exactJSON, not JSON.stringify: a document using the `0d` exact
|
|
411
|
-
// leaves generates bigints and Decimals, which JSON.stringify cannot
|
|
412
|
-
// write (D9). The CLI prints INDENTED JSON and the shared suite's
|
|
413
|
-
// `gens` mode prints COMPACT JSON, but both go through this one
|
|
414
|
-
// emitter -- an indent argument rather than a second implementation,
|
|
415
|
-
// so the two cannot drift from each other or from the Go port.
|
|
416
541
|
const text = 'canon' === mode
|
|
417
542
|
? aontu.unify(src).canon
|
|
418
543
|
: exactJSON(aontu.generate(src), 2)
|
|
419
|
-
return { ok: true, text }
|
|
544
|
+
return { ok: true, text, findings: [] }
|
|
420
545
|
}
|
|
421
546
|
catch (err: any) {
|
|
422
547
|
const msg = (err instanceof AontuError || true === err?.aontu)
|
|
423
548
|
? err.message
|
|
424
549
|
: String(err?.message ?? err)
|
|
425
|
-
|
|
550
|
+
const errs: any[] = 'function' === typeof err?.errs ? err.errs() : []
|
|
551
|
+
const first: any = errs[0]
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
text: msg,
|
|
555
|
+
findings: null == first ? [] : [evalFinding(first.why, msg)],
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
// The bare command's answer, in the form the caller asked for. The
|
|
562
|
+
// text form is what it has always printed, on the stream the verdict
|
|
563
|
+
// chooses; `--format json` is the same answer as one object, on
|
|
564
|
+
// stdout, so a harness reads one stream and one shape either way.
|
|
565
|
+
// Mirrors emit in go/cmd/aontu/main.go.
|
|
566
|
+
function emitEval(
|
|
567
|
+
res: { ok: boolean; text: string; findings: VetFinding[] },
|
|
568
|
+
format: EvalFormat,
|
|
569
|
+
): number {
|
|
570
|
+
if ('json' === format) {
|
|
571
|
+
process.stdout.write(exactJSON({
|
|
572
|
+
aontu: { version: version(), verb: 'eval' },
|
|
573
|
+
findings: res.findings,
|
|
574
|
+
ok: res.ok,
|
|
575
|
+
out: res.ok ? res.text : '',
|
|
576
|
+
}, 2) + '\n')
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
|
|
426
580
|
}
|
|
581
|
+
return res.ok ? 0 : 1
|
|
427
582
|
}
|
|
428
583
|
|
|
429
584
|
|
|
430
|
-
// The include capability the main verb runs with (G5, docs/trust.md).
|
|
431
|
-
// `--trust` and `--include-root` set it explicitly; the default is
|
|
432
|
-
// 'system' WITH the warning window: every resolution that escapes the
|
|
433
|
-
// entry root or goes through package resolution prints a one-line
|
|
434
|
-
// stderr warning naming the flag a future default will require
|
|
435
|
-
// (phase 6, the staged flip).
|
|
436
585
|
type TrustArg = (
|
|
437
586
|
| { kind: 'system-warn' }
|
|
438
587
|
| { kind: 'system' }
|
|
439
588
|
| { kind: 'none' }
|
|
440
589
|
| { kind: 'root', dir?: string }
|
|
441
|
-
// EXTENSIONS READ AS TEXT ride with the capability rather than
|
|
442
|
-
// beside it: both answer "what may an include read", both are
|
|
443
|
-
// stripped by takeTrust before a verb parses its own tail, and a
|
|
444
|
-
// verb that threads one and not the other is the G5 defect again --
|
|
445
|
-
// `aontu vet` running under a flag the bare command honoured and it
|
|
446
|
-
// did not.
|
|
447
590
|
) & { textExt: string[] }
|
|
448
591
|
|
|
449
592
|
|
|
@@ -487,16 +630,6 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
|
|
|
487
630
|
}
|
|
488
631
|
|
|
489
632
|
|
|
490
|
-
// EVERY VERB honours the include capability, not just the bare
|
|
491
|
-
// command. G5 wired `--trust`/`--include-root` to `aontu <file>` alone,
|
|
492
|
-
// so `aontu vet schema.aon data.json` -- the surface an agent actually
|
|
493
|
-
// scripts -- ran the full system resolver with no flag to confine it
|
|
494
|
-
// and no warning (use-cases/REVIEW.md finding G). The flags are
|
|
495
|
-
// stripped here, before each verb parses its own tail, so a verb only
|
|
496
|
-
// has to pass the profile on to its engine.
|
|
497
|
-
//
|
|
498
|
-
// Returns undefined when the spelling is wrong, with the message
|
|
499
|
-
// already printed: the caller answers the usage class.
|
|
500
633
|
function takeTrust(argv: string[]):
|
|
501
634
|
{ argv: string[], trust: TrustArg } | undefined {
|
|
502
635
|
const rest: string[] = []
|
|
@@ -600,35 +733,40 @@ function entryRootOf(file: string | undefined): string {
|
|
|
600
733
|
}
|
|
601
734
|
|
|
602
735
|
|
|
603
|
-
function runFile(
|
|
736
|
+
function runFile(
|
|
737
|
+
file: string, mode: Mode, format: EvalFormat, trust: TrustArg): number {
|
|
604
738
|
let src: string
|
|
605
739
|
try {
|
|
606
740
|
src = readFileSync(file, 'utf8')
|
|
607
741
|
}
|
|
608
742
|
catch (err: any) {
|
|
743
|
+
if (looksLikeVerb(file)) {
|
|
744
|
+
process.stderr.write(
|
|
745
|
+
`aontu: \`${file}\` is not a file, and not a verb this port knows\n`)
|
|
746
|
+
const near = nearestVerb(file, KNOWN_VERBS)
|
|
747
|
+
if ('' !== near) {
|
|
748
|
+
process.stderr.write(`aontu: did you mean \`aontu ${near}\`?\n`)
|
|
749
|
+
}
|
|
750
|
+
process.stderr.write(
|
|
751
|
+
'aontu: `aontu --help` lists the verbs, `aontu help` the topics\n')
|
|
752
|
+
return 2
|
|
753
|
+
}
|
|
609
754
|
process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`)
|
|
610
755
|
return 1
|
|
611
756
|
}
|
|
612
757
|
|
|
613
758
|
const path = resolve(file)
|
|
614
|
-
// `fs` IS WHAT MAKES A FRAME EXCERPT THE FILE IT NAMES. Without it,
|
|
615
|
-
// err.ts's resolveSrc falls back to the ENTRY text, so a frame whose
|
|
616
|
-
// arrow says `lib/types.aon:2:6` printed the entry's line 2 under it
|
|
617
|
-
// -- a real file name over another file's line, which
|
|
618
|
-
// docs/reference-api.md forbids in the same words it uses to require
|
|
619
|
-
// the name.
|
|
620
759
|
const aontu = new Aontu({
|
|
621
760
|
path,
|
|
622
761
|
errfs: { existsSync, readFileSync },
|
|
623
762
|
...trustOpts(trust, dirname(path)),
|
|
624
763
|
})
|
|
625
|
-
|
|
626
|
-
;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
|
|
627
|
-
return res.ok ? 0 : 1
|
|
764
|
+
return emitEval(evalSource(aontu, src, mode), format)
|
|
628
765
|
}
|
|
629
766
|
|
|
630
767
|
|
|
631
|
-
function runStdin(
|
|
768
|
+
function runStdin(
|
|
769
|
+
mode: Mode, format: EvalFormat, trust: TrustArg): Promise<number> {
|
|
632
770
|
return new Promise((resolve) => {
|
|
633
771
|
let src = ''
|
|
634
772
|
process.stdin.setEncoding('utf8')
|
|
@@ -636,22 +774,12 @@ function runStdin(mode: Mode, trust: TrustArg): Promise<number> {
|
|
|
636
774
|
process.stdin.on('end', () => {
|
|
637
775
|
const res = evalSource(
|
|
638
776
|
new Aontu(trustOpts(trust, process.cwd())), src, mode)
|
|
639
|
-
|
|
640
|
-
resolve(res.ok ? 0 : 1)
|
|
777
|
+
resolve(emitEval(res, format))
|
|
641
778
|
})
|
|
642
779
|
})
|
|
643
780
|
}
|
|
644
781
|
|
|
645
782
|
|
|
646
|
-
// THE REPL AS AN INSPECTION TOOL (G7 phase 7): `:load` holds a
|
|
647
|
-
// document, and `:get`, `:keys` and `:why` ask the query and
|
|
648
|
-
// provenance surfaces about it, so the session is a place to
|
|
649
|
-
// INTERROGATE a definition rather than only to evaluate snippets.
|
|
650
|
-
//
|
|
651
|
-
// The command handler is a PURE FUNCTION of (state, line): a readline
|
|
652
|
-
// loop is untestable, and every answer this REPL gives has to be as
|
|
653
|
-
// checkable as the CLI's. File reading is injected for the same
|
|
654
|
-
// reason.
|
|
655
783
|
export type ReplState = {
|
|
656
784
|
// How a value renders: the `:canon` / `:json` toggle.
|
|
657
785
|
mode: Mode
|
|
@@ -660,11 +788,6 @@ export type ReplState = {
|
|
|
660
788
|
jsonl: boolean
|
|
661
789
|
name?: string
|
|
662
790
|
src?: string
|
|
663
|
-
// The include capability the session evaluates under. `--trust` and
|
|
664
|
-
// `--include-root` were parsed and then DROPPED on the way to the
|
|
665
|
-
// REPL, so `--jsonl` -- the surface built to be driven by a harness
|
|
666
|
-
// -- ran unconfined however it was invoked (use-cases/REVIEW.md
|
|
667
|
-
// finding G). The state carries it, so every line honours it.
|
|
668
791
|
trust?: TrustArg
|
|
669
792
|
}
|
|
670
793
|
|
|
@@ -819,14 +942,6 @@ function runRepl(initialMode: Mode, jsonl: boolean, trust: TrustArg): void {
|
|
|
819
942
|
})
|
|
820
943
|
|
|
821
944
|
rl.on('close', () => {
|
|
822
|
-
// The closing newline is for a HUMAN, so it is written only for
|
|
823
|
-
// one: it moves the terminal off the prompt line that `rl` left
|
|
824
|
-
// hanging. In `--jsonl` there is no prompt, every answer already
|
|
825
|
-
// ends in its own newline, and this one appended a bare empty line
|
|
826
|
-
// to the stream -- a record that is not JSON, at the end of a
|
|
827
|
-
// protocol whose whole contract is one JSON object per line. A
|
|
828
|
-
// harness parsing every line it receives failed on it, after the
|
|
829
|
-
// commands had all succeeded. Mirrors go/cmd/aontu/repl.go.
|
|
830
945
|
if (!jsonl) {
|
|
831
946
|
process.stdout.write('\n')
|
|
832
947
|
}
|
|
@@ -838,16 +953,6 @@ function runRepl(initialMode: Mode, jsonl: boolean, trust: TrustArg): void {
|
|
|
838
953
|
}
|
|
839
954
|
|
|
840
955
|
|
|
841
|
-
|
|
842
|
-
// THE VET VERB (G2 phase 3).
|
|
843
|
-
//
|
|
844
|
-
// Exit codes are VERDICT CLASSES, not a pass/fail bit: an agent loop
|
|
845
|
-
// branches on "the data contradicts the truth" (1) differently from
|
|
846
|
-
// "the data has not supplied everything the truth requires" (3), and
|
|
847
|
-
// differently again from "the schema itself is broken" (4), which is
|
|
848
|
-
// never the data's fault. 2 stays what it already was for this CLI --
|
|
849
|
-
// the caller got the invocation wrong -- which is why an unreadable
|
|
850
|
-
// file is a 2 rather than a 4.
|
|
851
956
|
const VET_EXIT: Record<VetVerdict, number> = {
|
|
852
957
|
valid: 0,
|
|
853
958
|
invalid: 1,
|
|
@@ -870,6 +975,11 @@ type VetArgs = {
|
|
|
870
975
|
partial?: boolean
|
|
871
976
|
maxErrors?: number
|
|
872
977
|
watch?: boolean
|
|
978
|
+
// G11 phase 5. `strictCoverage` implies `coverage`; `coverageAt`
|
|
979
|
+
// narrows the data side and implies it too.
|
|
980
|
+
coverage?: boolean
|
|
981
|
+
strictCoverage?: boolean
|
|
982
|
+
coverageAt?: string
|
|
873
983
|
}
|
|
874
984
|
|
|
875
985
|
|
|
@@ -883,6 +993,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
883
993
|
let partial = false
|
|
884
994
|
let maxErrors: number | undefined
|
|
885
995
|
let watch = false
|
|
996
|
+
let coverage = false
|
|
997
|
+
let strictCoverage = false
|
|
998
|
+
let coverageAt: string | undefined
|
|
886
999
|
|
|
887
1000
|
for (let i = 0; i < argv.length; i++) {
|
|
888
1001
|
const arg = argv[i]
|
|
@@ -909,14 +1022,6 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
909
1022
|
format = f
|
|
910
1023
|
}
|
|
911
1024
|
else if ('--max-errors' === arg) {
|
|
912
|
-
// ONE GRAMMAR, spelled the same way in both ports: decimal
|
|
913
|
-
// digits, one to nine of them, at least 1. `Number()` alone
|
|
914
|
-
// accepted `1.0`, `1e2`, `0x10` and ` 3`, which Go's parser
|
|
915
|
-
// refuses -- so the same documented invocation meant different
|
|
916
|
-
// things in the two shipped commands. The nine-digit ceiling is
|
|
917
|
-
// where the ports would part company again: beyond it Go's
|
|
918
|
-
// integer conversion saturates, and a cap nobody can reach is
|
|
919
|
-
// not worth a divergence.
|
|
920
1025
|
const raw = argv[++i]
|
|
921
1026
|
if (!/^[0-9]{1,9}$/.test(raw ?? '') || 1 > Number(raw)) {
|
|
922
1027
|
return { err: 'aontu: --max-errors needs a positive whole number' }
|
|
@@ -929,6 +1034,24 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
929
1034
|
else if ('--partial' === arg) {
|
|
930
1035
|
partial = true
|
|
931
1036
|
}
|
|
1037
|
+
else if ('--coverage' === arg) {
|
|
1038
|
+
coverage = true
|
|
1039
|
+
}
|
|
1040
|
+
else if ('--strict-coverage' === arg) {
|
|
1041
|
+
// IMPLIES THE ACCOUNTING, because a gate cannot fire on what was
|
|
1042
|
+
// never measured. Asking for the strict form and having to
|
|
1043
|
+
// remember `--coverage` beside it is a usage trap with one
|
|
1044
|
+
// correct answer, so the flag takes it.
|
|
1045
|
+
coverage = true
|
|
1046
|
+
strictCoverage = true
|
|
1047
|
+
}
|
|
1048
|
+
else if ('--coverage-at' === arg) {
|
|
1049
|
+
coverageAt = argv[++i]
|
|
1050
|
+
if (null == coverageAt) {
|
|
1051
|
+
return { err: 'aontu: --coverage-at needs a path' }
|
|
1052
|
+
}
|
|
1053
|
+
coverage = true
|
|
1054
|
+
}
|
|
932
1055
|
else if ('--watch' === arg) {
|
|
933
1056
|
watch = true
|
|
934
1057
|
}
|
|
@@ -954,6 +1077,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
|
|
|
954
1077
|
partial,
|
|
955
1078
|
maxErrors,
|
|
956
1079
|
watch,
|
|
1080
|
+
coverage,
|
|
1081
|
+
strictCoverage,
|
|
1082
|
+
coverageAt,
|
|
957
1083
|
},
|
|
958
1084
|
}
|
|
959
1085
|
}
|
|
@@ -978,10 +1104,6 @@ function renderFinding(f: VetFinding): string {
|
|
|
978
1104
|
out.push(` actual: ${f.actual}`)
|
|
979
1105
|
}
|
|
980
1106
|
for (const s of f.sites) {
|
|
981
|
-
// Every site carries the canon of the value it stands for: that is
|
|
982
|
-
// what makes the two sides of a conflict readable side by side. A
|
|
983
|
-
// site's file is always a string -- empty when the value belongs to
|
|
984
|
-
// neither document -- so there is nothing to coalesce here.
|
|
985
1107
|
out.push(` ${s.role}: ${s.file}:${s.row}:${s.col} (${s.value})`)
|
|
986
1108
|
}
|
|
987
1109
|
|
|
@@ -993,14 +1115,53 @@ function renderVetText(report: VetReport): string {
|
|
|
993
1115
|
const head = `verdict: ${report.verdict}` +
|
|
994
1116
|
(report.truncated ? ' (findings truncated)' : '')
|
|
995
1117
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1118
|
+
const body = 0 === report.findings.length ? []
|
|
1119
|
+
: ['', ...report.findings.map(renderFinding)]
|
|
1120
|
+
const cover = null == report.coverage ? []
|
|
1121
|
+
: ['', ...renderVetCoverage(report.coverage)]
|
|
999
1122
|
|
|
1000
|
-
return [head,
|
|
1123
|
+
return [head, ...body, ...cover].join('\n')
|
|
1001
1124
|
}
|
|
1002
1125
|
|
|
1003
1126
|
|
|
1127
|
+
// The coverage block (G11 phase 5). VACUOUS FIRST and in the
|
|
1128
|
+
// imperative, because it is the one line that changes what the reader
|
|
1129
|
+
// should do: a `valid` verdict above it means nothing.
|
|
1130
|
+
function renderVetCoverage(c: VetCoverage): string[] {
|
|
1131
|
+
const out: string[] = []
|
|
1132
|
+
if (c.vacuous) {
|
|
1133
|
+
out.push('coverage: VACUOUS — no data leaf was constrained' +
|
|
1134
|
+
' by the schema; this run checked nothing')
|
|
1135
|
+
}
|
|
1136
|
+
out.push(`coverage: ${c.checked}/${c.leaves} data leaves checked,` +
|
|
1137
|
+
` ${c.declared} schema declarations`)
|
|
1138
|
+
// The lists are the SHALLOWEST paths, so each names a subtree rather
|
|
1139
|
+
// than every leaf under it, and both are capped: a report a reader
|
|
1140
|
+
// scrolls past is a report nobody reads.
|
|
1141
|
+
for (const [label, paths] of [
|
|
1142
|
+
['unchecked', c.unchecked], ['unused', c.unused],
|
|
1143
|
+
] as [string, string[]][]) {
|
|
1144
|
+
if (0 === paths.length) {
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
const shown = paths.slice(0, COVERAGE_LIST_MAX)
|
|
1148
|
+
for (const p of shown) {
|
|
1149
|
+
out.push(` ${label}: ${p}`)
|
|
1150
|
+
}
|
|
1151
|
+
if (shown.length < paths.length) {
|
|
1152
|
+
out.push(` ${label}: … and ${paths.length - shown.length} more`)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return out
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
// How many coverage paths the TEXT form prints per list. The JSON form
|
|
1160
|
+
// carries every one: a machine reads the whole list, a person reads the
|
|
1161
|
+
// first few and the count.
|
|
1162
|
+
const COVERAGE_LIST_MAX = 10
|
|
1163
|
+
|
|
1164
|
+
|
|
1004
1165
|
// The machine-readable form. `aontu` names the producer, so a report
|
|
1005
1166
|
// read from a file or a pipe says which version and which verb made it
|
|
1006
1167
|
// without the consumer having to know.
|
|
@@ -1010,6 +1171,7 @@ function renderVetJson(report: VetReport): string {
|
|
|
1010
1171
|
verdict: report.verdict,
|
|
1011
1172
|
truncated: report.truncated,
|
|
1012
1173
|
findings: report.findings,
|
|
1174
|
+
...(null == report.coverage ? {} : { coverage: report.coverage }),
|
|
1013
1175
|
}, 2)
|
|
1014
1176
|
}
|
|
1015
1177
|
|
|
@@ -1051,12 +1213,17 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1051
1213
|
return 2
|
|
1052
1214
|
}
|
|
1053
1215
|
|
|
1054
|
-
// Each data file is vetted on its own, because a parsed tree is
|
|
1055
|
-
// single-use (docs/reference-api.md) -- and because two data files
|
|
1056
|
-
// are two candidates for the same truth, not one merged candidate.
|
|
1057
1216
|
let verdict: VetVerdict = 'valid'
|
|
1058
1217
|
let truncated = false
|
|
1059
1218
|
const findings: VetFinding[] = []
|
|
1219
|
+
let cov: VetCoverage | undefined
|
|
1220
|
+
// Initialised rather than left undefined: it is filled in the same
|
|
1221
|
+
// block that sets `cov`, so a fallback at the read below would be an
|
|
1222
|
+
// arm nothing can take. The FIRST file replaces it wholesale, which
|
|
1223
|
+
// is what makes the fold an intersection rather than an empty set.
|
|
1224
|
+
let unusedEvery = new Set<string>()
|
|
1225
|
+
let unusedSeen = false
|
|
1226
|
+
const uncheckedAll = new Set<string>()
|
|
1060
1227
|
|
|
1061
1228
|
for (const source of sources) {
|
|
1062
1229
|
const report = vet(schemaSrc, source.src, {
|
|
@@ -1067,14 +1234,10 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1067
1234
|
maxErrors: args.maxErrors,
|
|
1068
1235
|
schemaUrl: args.schema,
|
|
1069
1236
|
dataUrl: source.file,
|
|
1070
|
-
// The paths as well as the labels: a relative `@"file"` load
|
|
1071
|
-
// inside either document resolves from ITS OWN directory, the
|
|
1072
|
-
// way `aontu <file>` already resolves one (runFile above). The
|
|
1073
|
-
// path is passed AS TYPED, not resolved: it doubles as the
|
|
1074
|
-
// label above, and a report that mixed the typed path with an
|
|
1075
|
-
// absolute one would name the same file two ways.
|
|
1076
1237
|
schemaPath: args.schema,
|
|
1077
1238
|
dataPath: source.file,
|
|
1239
|
+
coverage: args.coverage,
|
|
1240
|
+
coverageAt: args.coverageAt,
|
|
1078
1241
|
})
|
|
1079
1242
|
|
|
1080
1243
|
if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
|
|
@@ -1083,40 +1246,59 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
|
|
|
1083
1246
|
truncated = truncated || report.truncated
|
|
1084
1247
|
findings.push(...report.findings)
|
|
1085
1248
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1249
|
+
if (null != report.coverage) {
|
|
1250
|
+
const c = report.coverage
|
|
1251
|
+
cov = null == cov ? { ...c } : {
|
|
1252
|
+
checked: cov.checked + c.checked,
|
|
1253
|
+
declared: c.declared,
|
|
1254
|
+
leaves: cov.leaves + c.leaves,
|
|
1255
|
+
unchecked: [],
|
|
1256
|
+
unused: [],
|
|
1257
|
+
vacuous: false,
|
|
1258
|
+
}
|
|
1259
|
+
for (const p of c.unchecked) {
|
|
1260
|
+
uncheckedAll.add(p)
|
|
1261
|
+
}
|
|
1262
|
+
const mine = new Set(c.unused)
|
|
1263
|
+
unusedEvery = unusedSeen
|
|
1264
|
+
? new Set([...unusedEvery].filter((u) => mine.has(u))) : mine
|
|
1265
|
+
unusedSeen = true
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1096
1268
|
if ('error' === report.verdict) {
|
|
1097
1269
|
break
|
|
1098
1270
|
}
|
|
1099
1271
|
}
|
|
1100
1272
|
|
|
1101
|
-
// The cap is on the REPORT, not on each file. Capping every file's
|
|
1102
|
-
// list and then concatenating them let `--max-errors 1` emit one
|
|
1103
|
-
// finding PER FILE -- and leave `truncated` false while doing it,
|
|
1104
|
-
// because no single file had been cut. The engine still caps each
|
|
1105
|
-
// run, so a pathological file cannot flood the aggregate before it
|
|
1106
|
-
// gets here; this is the second, honest cut.
|
|
1107
1273
|
const cap = args.maxErrors ?? VET_MAX_ERRORS
|
|
1108
1274
|
const kept = cap < findings.length ? findings.slice(0, cap) : findings
|
|
1109
1275
|
|
|
1276
|
+
if (null != cov) {
|
|
1277
|
+
cov.unchecked = [...uncheckedAll].sort(cmpCodePoint)
|
|
1278
|
+
cov.unused = [...unusedEvery].sort(cmpCodePoint)
|
|
1279
|
+
cov.vacuous = 0 === cov.checked && 0 < cov.leaves
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1110
1282
|
const report: VetReport = {
|
|
1111
1283
|
verdict,
|
|
1112
1284
|
truncated: truncated || cap < findings.length,
|
|
1113
1285
|
findings: kept,
|
|
1286
|
+
...(null == cov ? {} : { coverage: cov }),
|
|
1114
1287
|
}
|
|
1115
1288
|
const text = 'json' === args.format ? renderVetJson(report) :
|
|
1116
1289
|
'sarif' === args.format ? renderVetSarif(report) :
|
|
1117
1290
|
renderVetText(report)
|
|
1118
1291
|
|
|
1119
1292
|
process.stdout.write(text + '\n')
|
|
1293
|
+
|
|
1294
|
+
if (true === args.strictCoverage && true === report.coverage?.vacuous) {
|
|
1295
|
+
process.stderr.write(
|
|
1296
|
+
'aontu: no data leaf was constrained by the schema:' +
|
|
1297
|
+
' this run checked nothing\n' +
|
|
1298
|
+
'aontu: `aontu help language` — a map template is `&:`,' +
|
|
1299
|
+
' and a quoted "*" is a key named *\n')
|
|
1300
|
+
return 1
|
|
1301
|
+
}
|
|
1120
1302
|
return VET_EXIT[verdict]
|
|
1121
1303
|
}
|
|
1122
1304
|
|
|
@@ -1144,19 +1326,6 @@ function sleep(ms: number): Promise<void> {
|
|
|
1144
1326
|
}
|
|
1145
1327
|
|
|
1146
1328
|
|
|
1147
|
-
// Resolve true when any watched file's signature moves off `before`.
|
|
1148
|
-
// This is the real waiter: it never resolves false, so a real watch
|
|
1149
|
-
// runs until the process is interrupted; tests inject their own waiter
|
|
1150
|
-
// to bound the loop, and pass a short pollMs when they drive this one
|
|
1151
|
-
// directly. The interval is a required argument (the command passes
|
|
1152
|
-
// WATCH_POLL_MS) so there is no defaulting branch a test could never
|
|
1153
|
-
// take.
|
|
1154
|
-
//
|
|
1155
|
-
// The BASELINE is an argument, not a snapshot taken here: the loop
|
|
1156
|
-
// records it BEFORE each vet run, so a save landing between the run's
|
|
1157
|
-
// reads and the wait still compares as a change. A waiter that
|
|
1158
|
-
// snapshotted on entry would adopt that unvetted save as its baseline
|
|
1159
|
-
// and wait indefinitely on a stale report.
|
|
1160
1329
|
async function watchChange(
|
|
1161
1330
|
files: string[], before: string, pollMs: number): Promise<boolean> {
|
|
1162
1331
|
for (;;) {
|
|
@@ -1196,9 +1365,6 @@ async function watchVet(
|
|
|
1196
1365
|
}
|
|
1197
1366
|
|
|
1198
1367
|
|
|
1199
|
-
// The vet verb. Non-watch runs are synchronous and return the exit
|
|
1200
|
-
// class directly; `--watch` returns a promise that resolves only when
|
|
1201
|
-
// the waiter says stop (never, for the real one).
|
|
1202
1368
|
function runVet(argv: string[], wait?: VetWaiter): number | Promise<number> {
|
|
1203
1369
|
const trusted = takeTrust(argv)
|
|
1204
1370
|
if (null == trusted) {
|
|
@@ -1376,14 +1542,6 @@ type BreakingArgs = {
|
|
|
1376
1542
|
file: string
|
|
1377
1543
|
against: string[]
|
|
1378
1544
|
mode?: BreakingMode
|
|
1379
|
-
// `--at`: compare a SUBTREE of both versions. The gate's own
|
|
1380
|
-
// sub-question, and the one a real repository needs -- a document's
|
|
1381
|
-
// top level carries the module's version string and its policy
|
|
1382
|
-
// block, which are supposed to change between releases and which
|
|
1383
|
-
// make the whole-document comparison answer about them rather than
|
|
1384
|
-
// about the contract (use-cases/REVIEW.md finding D). `subsume` has
|
|
1385
|
-
// taken it since G3; `breaking` did not, so the only way to gate a
|
|
1386
|
-
// subtree was to split the file.
|
|
1387
1545
|
at?: string
|
|
1388
1546
|
allowUndecided: boolean
|
|
1389
1547
|
allowDeprecatedRemoval: boolean
|
|
@@ -1467,36 +1625,10 @@ function parseBreakingArgs(
|
|
|
1467
1625
|
}
|
|
1468
1626
|
}
|
|
1469
1627
|
|
|
1470
|
-
// One resolved `--against` spelling: the old document's text, the path
|
|
1471
|
-
// its own relative includes must resolve from, and (for a git spelling)
|
|
1472
|
-
// the temporary tree to remove when the run is done.
|
|
1473
1628
|
type OldVersion = { src: string, path: string, temp?: string }
|
|
1474
1629
|
|
|
1475
|
-
// A source file the include resolver can actually load. `git#<rev>`
|
|
1476
|
-
// materialises these and nothing else: an include names an Aontu
|
|
1477
|
-
// document (`.aon`/`.aontu`, the two extensions `@"foo"` tries) or a
|
|
1478
|
-
// JSON one, so the rest of a revision's tree cannot be part of any
|
|
1479
|
-
// include closure and copying it would be pure cost.
|
|
1480
1630
|
const INCLUDABLE = /\.(aon|aontu|jsonic|json)$/
|
|
1481
1631
|
|
|
1482
|
-
// Resolve one --against spelling to an old version.
|
|
1483
|
-
//
|
|
1484
|
-
// A `git#<rev>` spelling is the old version of the WHOLE TREE, not of
|
|
1485
|
-
// the entry file alone. It used to be `git show <rev>:./<file>`, whose
|
|
1486
|
-
// text was then evaluated with `generalPath`/`specificPath` pointing at
|
|
1487
|
-
// the WORKING file -- so every `@"..."` include in the old document
|
|
1488
|
-
// resolved against the working tree, and the "old" side was old entry
|
|
1489
|
-
// text meeting new includes. A breaking change inside an included file
|
|
1490
|
-
// therefore compared against itself and answered `compatible`: the
|
|
1491
|
-
// documented CI gate silently un-gated every non-entry file of the
|
|
1492
|
-
// multi-file layout real models use (use-cases/BUGS.md §26). The old
|
|
1493
|
-
// tree's includable sources are copied into a temporary directory and
|
|
1494
|
-
// the old document is evaluated from THERE.
|
|
1495
|
-
//
|
|
1496
|
-
// Sources outside the revision -- package includes under node_modules,
|
|
1497
|
-
// the bundled `std/system` -- still resolve as they do today: they are
|
|
1498
|
-
// not in the tree, and their versions travel with the lockfile rather
|
|
1499
|
-
// than with this comparison.
|
|
1500
1632
|
function oldVersion(spec: string, file: string): OldVersion | undefined {
|
|
1501
1633
|
if (!spec.startsWith('git#')) {
|
|
1502
1634
|
try {
|
|
@@ -1528,24 +1660,10 @@ function oldVersion(spec: string, file: string): OldVersion | undefined {
|
|
|
1528
1660
|
// that only some failures take.
|
|
1529
1661
|
const temp = mkdtempSync(join(tmpdir(), 'aontu-against-'))
|
|
1530
1662
|
try {
|
|
1531
|
-
// THE REPO-RELATIVE PATH COMES FROM GIT, not from path arithmetic.
|
|
1532
|
-
// Relativising `rev-parse --show-toplevel` against `resolve(file)`
|
|
1533
|
-
// puts two DIFFERENT COORDINATE SYSTEMS on either side of the
|
|
1534
|
-
// subtraction: git prints the real path, while the caller's is
|
|
1535
|
-
// whatever they typed. On macOS a temp file under /var is
|
|
1536
|
-
// /private/var to git, and on Windows a TMP short name
|
|
1537
|
-
// (RUNNER~1) is the long form to git -- so the subtraction gave a
|
|
1538
|
-
// `../..` climb, the entry was "not in that revision", and the
|
|
1539
|
-
// documented CI spelling failed on both platforms while passing on
|
|
1540
|
-
// Linux (this PR's own CI). `--show-prefix` is the same question
|
|
1541
|
-
// asked in git's coordinates: the repo-relative directory of the
|
|
1542
|
-
// cwd, already slash-separated and already normalised.
|
|
1543
1663
|
const prefix = git(['rev-parse', '--show-prefix'], dir).trim()
|
|
1544
1664
|
const entryRel = prefix + basename(file)
|
|
1545
1665
|
const top = git(['rev-parse', '--show-toplevel'], dir).trim()
|
|
1546
1666
|
|
|
1547
|
-
// `-z` so a path with a newline or a quote cannot be mistaken for
|
|
1548
|
-
// two paths (git otherwise quotes such names).
|
|
1549
1667
|
const listed = git(['ls-tree', '-r', '-z', '--name-only', rev], top)
|
|
1550
1668
|
.split('\0').filter((p) => '' !== p)
|
|
1551
1669
|
if (!listed.includes(entryRel)) {
|
|
@@ -1580,13 +1698,6 @@ function policyCompat(
|
|
|
1580
1698
|
): BreakingMode | undefined {
|
|
1581
1699
|
const aontu = new Aontu()
|
|
1582
1700
|
const ctx = aontu.ctx({ collect: true })
|
|
1583
|
-
// The declaration is read by EVALUATING the document, so this leg
|
|
1584
|
-
// runs the include resolver too and has to run it under BOTH of the
|
|
1585
|
-
// verb's include options -- a `breaking --trust none` that read its
|
|
1586
|
-
// own mode through an unconfined resolver would confine the
|
|
1587
|
-
// comparison and not the question (use-cases/REVIEW.md finding G),
|
|
1588
|
-
// and one that took the capability alone read no mode at all when
|
|
1589
|
-
// the declaration arrived through a `--text-ext` include.
|
|
1590
1701
|
const v: any = aontu.unify(newSrc, { path, ...includeOpts(include) }, ctx)
|
|
1591
1702
|
if (0 < ctx.err.length || true === v?.isNil) {
|
|
1592
1703
|
return undefined
|
|
@@ -1606,11 +1717,6 @@ function policyCompat(
|
|
|
1606
1717
|
? m : undefined
|
|
1607
1718
|
}
|
|
1608
1719
|
|
|
1609
|
-
// Is the evaluated old version's value at the finding path deprecated?
|
|
1610
|
-
// The --allow-deprecated-removal downgrade (G3 phase 4): removing (or
|
|
1611
|
-
// otherwise changing) a value the old version already deprecated warns
|
|
1612
|
-
// instead of breaking. The Go port exports the same reader as
|
|
1613
|
-
// aontu.DeprecatedAt.
|
|
1614
1720
|
function deprecatedAt(oldSrc: string, path: string, filePath: string): boolean {
|
|
1615
1721
|
const aontu = new Aontu()
|
|
1616
1722
|
const ctx = aontu.ctx({ collect: true })
|
|
@@ -1685,9 +1791,6 @@ function runBreaking(argv: string[]): number {
|
|
|
1685
1791
|
return 2
|
|
1686
1792
|
}
|
|
1687
1793
|
|
|
1688
|
-
// The declared mode: --mode overrides the document's own policy;
|
|
1689
|
-
// neither means backward, the index's framing (v1-valid documents
|
|
1690
|
-
// stay valid).
|
|
1691
1794
|
const mode: BreakingMode =
|
|
1692
1795
|
args.mode ??
|
|
1693
1796
|
policyCompat(newSrc, args.file,
|
|
@@ -1727,8 +1830,6 @@ function runBreaking(argv: string[]): number {
|
|
|
1727
1830
|
temps.push(old.temp)
|
|
1728
1831
|
}
|
|
1729
1832
|
|
|
1730
|
-
// backward: the NEW document is the general side — every old
|
|
1731
|
-
// instance must still be admitted. forward: the old one is.
|
|
1732
1833
|
const checks: Array<{ general: [string, string], specific: [string, string] }> = []
|
|
1733
1834
|
if ('backward' === mode || 'full' === mode) {
|
|
1734
1835
|
checks.push({ general: [newSrc, args.file], specific: [oldSrc, spec] })
|
|
@@ -1745,18 +1846,10 @@ function runBreaking(argv: string[]): number {
|
|
|
1745
1846
|
at: args.at,
|
|
1746
1847
|
generalUrl: check.general[1],
|
|
1747
1848
|
specificUrl: check.specific[1],
|
|
1748
|
-
// The old side's relative loads resolve from ITS own tree --
|
|
1749
|
-
// the materialised revision for a git spelling, the named
|
|
1750
|
-
// file's directory otherwise -- so an included file's change
|
|
1751
|
-
// is part of the comparison rather than invisible to it.
|
|
1752
1849
|
generalPath: check.general[1] === spec ? oldPath : args.file,
|
|
1753
1850
|
specificPath: check.specific[1] === spec ? oldPath : args.file,
|
|
1754
1851
|
})
|
|
1755
1852
|
|
|
1756
|
-
// The deprecated-removal downgrade: a finding about a value the
|
|
1757
|
-
// OLD version already deprecated becomes a warning, and warnings
|
|
1758
|
-
// do not move the verdict. Deprecate-then-remove is the
|
|
1759
|
-
// supported rename path (the design's own sequencing).
|
|
1760
1853
|
let verdict = report.verdict
|
|
1761
1854
|
if (args.allowDeprecatedRemoval) {
|
|
1762
1855
|
let liveFindings = 0
|
|
@@ -1814,13 +1907,6 @@ function renderBreakingJson(report: SubsumeReport, mode: string): string {
|
|
|
1814
1907
|
}
|
|
1815
1908
|
|
|
1816
1909
|
|
|
1817
|
-
// ---------------------------------------------------------------------
|
|
1818
|
-
// The trim reporter (G3 phase 6): report redundant entries as paths.
|
|
1819
|
-
// Report-only — REWRITING needs G7's format-preserving patch surface —
|
|
1820
|
-
// which is why --check is REQUIRED rather than defaulted: `aontu trim
|
|
1821
|
-
// f.aon` reads as "trim this file", and doing something else silently
|
|
1822
|
-
// is worse than saying so.
|
|
1823
|
-
|
|
1824
1910
|
const TRIM_HELP = 'aontu trim --check <file> (try --help)'
|
|
1825
1911
|
|
|
1826
1912
|
const TRIM_EXIT: Record<TrimVerdict, number> = {
|
|
@@ -1898,8 +1984,6 @@ function runTrim(argv: string[]): number {
|
|
|
1898
1984
|
|
|
1899
1985
|
function renderTrimText(report: TrimReport): string {
|
|
1900
1986
|
const head = `verdict: ${report.verdict}`
|
|
1901
|
-
// WHY, when the document could not be evaluated at all: rendered as
|
|
1902
|
-
// vet renders a finding, because it IS one (the review's finding F).
|
|
1903
1987
|
const errors = report.errors ?? []
|
|
1904
1988
|
if (0 < errors.length) {
|
|
1905
1989
|
return [head, ''].concat(errors.map(renderFinding)).join('\n')
|
|
@@ -1964,29 +2048,12 @@ const VIEW_EDGES: ViewEdges[] = ['upward', 'all', 'none']
|
|
|
1964
2048
|
// the same division err.ts already draws for the error frames.
|
|
1965
2049
|
const VIEW_STYLES = ['auto', 'none', 'ansi', 'css']
|
|
1966
2050
|
|
|
1967
|
-
// `--style auto` resolved, which only the CLI can do. The mechanism is
|
|
1968
|
-
// the PROFILE's and the library knows it -- an SVG carries its
|
|
1969
|
-
// stylesheet unless told not to, which is what makes a figure stand
|
|
1970
|
-
// alone. What the library cannot know is whether the DESTINATION is a
|
|
1971
|
-
// terminal, so that is the only thing decided here: escapes on the
|
|
1972
|
-
// text profile when stdout is a terminal and NO_COLOR is unset, the
|
|
1973
|
-
// same two conditions the error frames use. `undefined` leaves the
|
|
1974
|
-
// profile's own default in place.
|
|
1975
2051
|
function viewStyleOf(
|
|
1976
2052
|
asked: string | undefined, as: ViewProfile | undefined
|
|
1977
2053
|
): ViewStyle | undefined {
|
|
1978
2054
|
if (undefined !== asked && 'auto' !== asked) {
|
|
1979
2055
|
return asked as ViewStyle
|
|
1980
2056
|
}
|
|
1981
|
-
// STDOUT'S OWN TERMINAL-NESS, and NO_COLOR read here rather than
|
|
1982
|
-
// through colorActive(). The figure goes to STDOUT and the error
|
|
1983
|
-
// frames go to STDERR, and they are not the same destination: main()
|
|
1984
|
-
// has already called setColor for stderr, so asking colorActive()
|
|
1985
|
-
// would answer the wrong question twice --- no escapes for
|
|
1986
|
-
// `aontu view tree m.aon 2>/dev/null` at a terminal, and escapes
|
|
1987
|
-
// into the pipe for `aontu view tree m.aon | less`. The NO_COLOR
|
|
1988
|
-
// rule is the one no-color.org states and err.ts implements:
|
|
1989
|
-
// set, to anything but empty, means no colour.
|
|
1990
2057
|
const no = process.env.NO_COLOR
|
|
1991
2058
|
return 'text' === as && true === process.stdout.isTTY
|
|
1992
2059
|
&& (null == no || '' === no) ? 'ansi' : undefined
|
|
@@ -2012,24 +2079,6 @@ const VIEW_USAGE_CODES = [
|
|
|
2012
2079
|
|
|
2013
2080
|
const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
|
|
2014
2081
|
|
|
2015
|
-
// The module tooling (G6 phase 3, ts/src/mod-tool.ts). All LOCAL:
|
|
2016
|
-
// `tidy` resolves the closure from what is in the stores and rewrites
|
|
2017
|
-
// the lockfile, `verify` asks whether the stores still mean what the
|
|
2018
|
-
// lockfile pins and changes nothing, `vendor` materialises the locked
|
|
2019
|
-
// closure into the project, `manifest` prints what a publish would
|
|
2020
|
-
// push.
|
|
2021
|
-
//
|
|
2022
|
-
// TIDY AND VERIFY ARE DIFFERENT QUESTIONS, and that is why both exist.
|
|
2023
|
-
// Tidy recomputes and rewrites by design -- a pin is what a module
|
|
2024
|
-
// means NOW -- so it makes the lockfile agree with whatever the store
|
|
2025
|
-
// holds, tampering included. Verify is the gate: a CI job runs it
|
|
2026
|
-
// BEFORE tidy, or instead of it.
|
|
2027
|
-
//
|
|
2028
|
-
// `get` and `publish` are the NETWORK half of the design and are not in
|
|
2029
|
-
// this build. They are named here rather than left to fall out as an
|
|
2030
|
-
// unknown subcommand, because a reader of the design will type them and
|
|
2031
|
-
// deserves to be told which half is missing rather than that the word
|
|
2032
|
-
// is wrong.
|
|
2033
2082
|
function runMod(argv: string[]): number {
|
|
2034
2083
|
const rest: string[] = []
|
|
2035
2084
|
let format: SubsumeFormat = 'text'
|
|
@@ -2083,10 +2132,6 @@ function runMod(argv: string[]): number {
|
|
|
2083
2132
|
return 2
|
|
2084
2133
|
}
|
|
2085
2134
|
|
|
2086
|
-
// THE OLD LAYOUT IS NAMED, NOT READ. The lockfile and the vendored
|
|
2087
|
-
// closure moved under aontu_meta/; a project that still carries them
|
|
2088
|
-
// at its root would otherwise look untouched by any of these verbs,
|
|
2089
|
-
// which is the one silence worth breaking.
|
|
2090
2135
|
if (existsSync(join(dir, 'aon_vendor')) || existsSync(join(dir, 'mod-lock.aon'))) {
|
|
2091
2136
|
process.stderr.write(
|
|
2092
2137
|
'aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
|
|
@@ -2130,9 +2175,6 @@ type ModVerdict =
|
|
|
2130
2175
|
const MOD_EXIT: Record<ModVerdict, number> = {
|
|
2131
2176
|
ok: 0,
|
|
2132
2177
|
missing: 1,
|
|
2133
|
-
// A REFUSED GATE, with `breaking`: a store that no longer means what
|
|
2134
|
-
// the lockfile pins is the integrity check saying no, and a CI job
|
|
2135
|
-
// reading exit codes should not have to learn a third class for it.
|
|
2136
2178
|
mismatch: 1,
|
|
2137
2179
|
// Likewise a lockfile that does not cover the project: the gate has
|
|
2138
2180
|
// nothing to check, which is a refusal and not a pass.
|
|
@@ -2183,11 +2225,6 @@ function modText(sub: string, report: any): string {
|
|
|
2183
2225
|
for (const f of report.findings) {
|
|
2184
2226
|
lines.push(f.path + ': ' + f.message)
|
|
2185
2227
|
}
|
|
2186
|
-
// What a manifest lacks is a declaration the module does not make
|
|
2187
|
-
// or an entry file that is not there, and neither is something a
|
|
2188
|
-
// fetch would supply -- so this is not the tail the other two
|
|
2189
|
-
// subcommands share. The name says which kind it is: `mod.version`
|
|
2190
|
-
// is a declaration, `service.aon` is a file.
|
|
2191
2228
|
for (const miss of report.missing) {
|
|
2192
2229
|
lines.push(miss + ': missing')
|
|
2193
2230
|
}
|
|
@@ -2198,8 +2235,6 @@ function modText(sub: string, report: any): string {
|
|
|
2198
2235
|
for (const mod of report.verified) {
|
|
2199
2236
|
lines.push(mod + ': verified')
|
|
2200
2237
|
}
|
|
2201
|
-
// BOTH HASHES, because the useful question is which way it moved:
|
|
2202
|
-
// an empty `got` is a module that no longer stands up at all.
|
|
2203
2238
|
for (const m of report.mismatched) {
|
|
2204
2239
|
lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
|
|
2205
2240
|
('' === m.got ? 'nothing (it does not evaluate)' : m.got))
|
|
@@ -2235,6 +2270,11 @@ function modText(sub: string, report: any): string {
|
|
|
2235
2270
|
}
|
|
2236
2271
|
|
|
2237
2272
|
|
|
2273
|
+
function vacuous(what: string, why: string): void {
|
|
2274
|
+
process.stderr.write(`aontu: ${what}: ${why}\n`)
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
|
|
2238
2278
|
function runRelations(argv: string[]): number {
|
|
2239
2279
|
const trusted = takeTrust(argv)
|
|
2240
2280
|
if (null == trusted) {
|
|
@@ -2283,12 +2323,21 @@ function runRelations(argv: string[]): number {
|
|
|
2283
2323
|
}
|
|
2284
2324
|
|
|
2285
2325
|
const report = relationCheck(src, {
|
|
2286
|
-
path: files[0],
|
|
2326
|
+
path: files[0], count: true,
|
|
2327
|
+
...verbOpts(trust, entryRootOf(files[0])),
|
|
2287
2328
|
})
|
|
2288
2329
|
const text = 'json' === format
|
|
2289
2330
|
? renderRelationsJson(report)
|
|
2290
2331
|
: renderRelationsText(report)
|
|
2291
2332
|
process.stdout.write(text + '\n')
|
|
2333
|
+
// `pass` over NO declarations is the vacuous case, and the engine
|
|
2334
|
+
// knows it exactly: `_reldecls` is empty. The count is asked for
|
|
2335
|
+
// here rather than derived, so the answer costs no second
|
|
2336
|
+
// evaluation.
|
|
2337
|
+
if (0 === report.declared) {
|
|
2338
|
+
vacuous('this document declares no relations',
|
|
2339
|
+
'`pass` means nothing was checked, not that the graph is sound')
|
|
2340
|
+
}
|
|
2292
2341
|
return RELATIONS_EXIT[report.verdict]
|
|
2293
2342
|
}
|
|
2294
2343
|
|
|
@@ -2508,12 +2557,6 @@ function runView(argv: string[]): number {
|
|
|
2508
2557
|
}
|
|
2509
2558
|
}
|
|
2510
2559
|
|
|
2511
|
-
// ESCAPES NEVER GO INTO A FILE. A pinned golden holding terminal
|
|
2512
|
-
// control codes is not a golden anybody can read, and a byte
|
|
2513
|
-
// comparison against one would fail on the reader's terminal
|
|
2514
|
-
// settings. `auto` resolves to `none` there on its own; asking for
|
|
2515
|
-
// `ansi` explicitly is a usage error rather than a silent downgrade,
|
|
2516
|
-
// so a script that wanted colour is told where it went.
|
|
2517
2560
|
if ('ansi' === style && (undefined !== out || undefined !== opts.views)) {
|
|
2518
2561
|
process.stderr.write(
|
|
2519
2562
|
'aontu: --style ansi writes to a terminal, not to a file\n')
|
|
@@ -2523,10 +2566,6 @@ function runView(argv: string[]): number {
|
|
|
2523
2566
|
// THE VIEW DOCUMENT draws every figure a document declares, so it
|
|
2524
2567
|
// names no kind: the declarations do, one each.
|
|
2525
2568
|
if (undefined !== opts.views) {
|
|
2526
|
-
// A declaration names its own profile, so the style is left to
|
|
2527
|
-
// each figure's own default; `--style none` still reaches every
|
|
2528
|
-
// one of them, which is how a host page that binds the CSS
|
|
2529
|
-
// variables asks for eight figures without eight stylesheets.
|
|
2530
2569
|
opts.style = viewStyleOf(style, undefined)
|
|
2531
2570
|
return runViewSet(rest, opts, trust, { format, check, strict, out })
|
|
2532
2571
|
}
|
|
@@ -2591,7 +2630,7 @@ function runView(argv: string[]): number {
|
|
|
2591
2630
|
}
|
|
2592
2631
|
}
|
|
2593
2632
|
|
|
2594
|
-
const
|
|
2633
|
+
const viewOpts = {
|
|
2595
2634
|
...opts,
|
|
2596
2635
|
style: viewStyleOf(style, opts.as ?? viewDefaultProfile(kind)),
|
|
2597
2636
|
kind,
|
|
@@ -2599,7 +2638,17 @@ function runView(argv: string[]): number {
|
|
|
2599
2638
|
roots,
|
|
2600
2639
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
2601
2640
|
docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
|
|
2602
|
-
}
|
|
2641
|
+
}
|
|
2642
|
+
const report = view(srcs[0], viewOpts)
|
|
2643
|
+
|
|
2644
|
+
if ('error' !== report.verdict && null != report.text) {
|
|
2645
|
+
const bare = view('{}', viewOpts)
|
|
2646
|
+
if ('error' !== bare.verdict && bare.text === report.text) {
|
|
2647
|
+
vacuous('nothing to draw',
|
|
2648
|
+
'this figure is what the same view draws for an empty document' +
|
|
2649
|
+
' — the model declares nothing this kind can show')
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2603
2652
|
|
|
2604
2653
|
if ('json' === format) {
|
|
2605
2654
|
process.stdout.write(renderViewJson(report) + '\n')
|
|
@@ -2645,13 +2694,6 @@ function runView(argv: string[]): number {
|
|
|
2645
2694
|
return strict && 'lossy' === report.verdict ? 1 : VIEW_EXIT[report.verdict]
|
|
2646
2695
|
}
|
|
2647
2696
|
|
|
2648
|
-
// `aontu view --views <path> <file>`: every figure the document
|
|
2649
|
-
// declares, from one evaluation, all or nothing.
|
|
2650
|
-
//
|
|
2651
|
-
// A declared `out` is resolved against the DOCUMENT's own directory,
|
|
2652
|
-
// not the caller's: a view document is committed beside the figures it
|
|
2653
|
-
// gates, and a gate that only passes from one working directory is not
|
|
2654
|
-
// a gate.
|
|
2655
2697
|
function runViewSet(
|
|
2656
2698
|
rest: string[], opts: ViewOptions, trust: TrustArg,
|
|
2657
2699
|
how: { format: SubsumeFormat, check: boolean, strict: boolean, out?: string }
|
|
@@ -2792,8 +2834,6 @@ function renderViewJson(report: ViewReport): string {
|
|
|
2792
2834
|
|
|
2793
2835
|
function renderRelationsText(report: RelationReport): string {
|
|
2794
2836
|
const head = `verdict: ${report.verdict}`
|
|
2795
|
-
// WHY, when the document could not be evaluated at all: rendered as
|
|
2796
|
-
// vet renders a finding, because it IS one (the review's finding F).
|
|
2797
2837
|
const errors = report.errors ?? []
|
|
2798
2838
|
if (0 < errors.length) {
|
|
2799
2839
|
return [head, ''].concat(errors.map(renderFinding)).join('\n')
|
|
@@ -2819,21 +2859,6 @@ function renderRelationsJson(report: RelationReport): string {
|
|
|
2819
2859
|
}
|
|
2820
2860
|
|
|
2821
2861
|
|
|
2822
|
-
|
|
2823
|
-
// ---------------------------------------------------------------------
|
|
2824
|
-
// JSON SCHEMA EXPORT (SUPPORT.md act 2, the review's finding I): the
|
|
2825
|
-
// bridge to every structured-output API, which constrains generation to
|
|
2826
|
-
// JSON Schema and nothing else. Export the model, let the provider
|
|
2827
|
-
// generate under it, then `vet` the result against the model itself --
|
|
2828
|
-
// the hybrid an enterprise actually deploys, and impossible without
|
|
2829
|
-
// this verb.
|
|
2830
|
-
//
|
|
2831
|
-
// THE SCHEMA GOES TO STDOUT AND THE LOSSES TO STDERR, so `aontu
|
|
2832
|
-
// jsonschema x.aon > schema.json` writes a schema and still tells the
|
|
2833
|
-
// reader what it could not carry. `--strict` makes a loss a refusal,
|
|
2834
|
-
// for the CI job that would rather fail than ship a schema weaker than
|
|
2835
|
-
// its model.
|
|
2836
|
-
|
|
2837
2862
|
const JSONSCHEMA_HELP =
|
|
2838
2863
|
'aontu jsonschema [--at <path>] [--strict] <file> (try --help)'
|
|
2839
2864
|
|
|
@@ -2929,14 +2954,6 @@ function runJsonSchema(argv: string[]): number {
|
|
|
2929
2954
|
strict && 'lossy' === report.verdict ? 1 : 0
|
|
2930
2955
|
}
|
|
2931
2956
|
|
|
2932
|
-
// ---------------------------------------------------------------------
|
|
2933
|
-
// THE RENDER VERB (docs/design/RENDER.0.md D8): evaluate a document,
|
|
2934
|
-
// vet the value at --at against aontu:code, fold code.units into bytes,
|
|
2935
|
-
// and put them where the flag says -- one unit on stdout, every unit
|
|
2936
|
-
// below --out (all or nothing), or compared against --check. Exit codes
|
|
2937
|
-
// mirror jsonschema's: 0 ok; 1 lossy under --strict or drift under
|
|
2938
|
-
// --check; 2 usage or I/O, a refused unit path included; 4 the
|
|
2939
|
-
// document does not stand up or the instance is not aontu:code.
|
|
2940
2957
|
|
|
2941
2958
|
const RENDER_HELP =
|
|
2942
2959
|
'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
|
|
@@ -3074,49 +3091,22 @@ function runRender(argv: string[]): number {
|
|
|
3074
3091
|
return 2
|
|
3075
3092
|
}
|
|
3076
3093
|
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
// carries the target's extension and never `.aon`. Desugared here
|
|
3081
|
-
// rather than anywhere deeper, because a template is an entry
|
|
3082
|
-
// spelling and not a value: an include is still aontu.
|
|
3083
|
-
if (!files[0].endsWith('.aon')) {
|
|
3084
|
-
src = desugarTemplate(src, marker ?? markerFor(files[0]))
|
|
3094
|
+
const loadedProfiles = loadProfiles(profileFiles, trust)
|
|
3095
|
+
if ('number' === typeof loadedProfiles) {
|
|
3096
|
+
return loadedProfiles
|
|
3085
3097
|
}
|
|
3098
|
+
const profiles = loadedProfiles
|
|
3086
3099
|
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
// (renderProfile, which also fills the defaults). Two files claiming
|
|
3091
|
-
// one lang is a usage error: the fold could not choose.
|
|
3092
|
-
const profiles: any[] = []
|
|
3093
|
-
const langs = new Map<string, string>()
|
|
3094
|
-
for (const pf of profileFiles) {
|
|
3095
|
-
let text: string
|
|
3096
|
-
try {
|
|
3097
|
-
text = readFileSync(pf, 'utf8')
|
|
3098
|
-
}
|
|
3099
|
-
catch (err: any) {
|
|
3100
|
-
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3101
|
-
return 2
|
|
3102
|
-
}
|
|
3103
|
-
const loaded = renderProfile(text,
|
|
3104
|
-
{ path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
|
|
3105
|
-
if (undefined !== loaded.errors) {
|
|
3106
|
-
process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
|
|
3107
|
-
return 4
|
|
3108
|
-
}
|
|
3109
|
-
const profile = loaded.profile
|
|
3110
|
-
const prev = langs.get(profile.lang)
|
|
3111
|
-
if (undefined !== prev) {
|
|
3112
|
-
process.stderr.write(
|
|
3113
|
-
`aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`)
|
|
3114
|
-
return 2
|
|
3115
|
-
}
|
|
3116
|
-
langs.set(profile.lang, pf)
|
|
3117
|
-
profiles.push(profile)
|
|
3100
|
+
if (!files[0].endsWith('.aon')) {
|
|
3101
|
+
src = desugarTemplate(src, marker ??
|
|
3102
|
+
markerFromProfiles(profiles, files[0]) ?? markerFor(files[0]))
|
|
3118
3103
|
}
|
|
3119
3104
|
|
|
3105
|
+
// A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
|
|
3106
|
+
// bytes and exit 0. The profile is what maps a model onto a
|
|
3107
|
+
// language, so without one there is nothing for the renderer to
|
|
3108
|
+
// write -- which is a usable answer only if the caller is told.
|
|
3109
|
+
const noProfiles = 0 === profiles.length
|
|
3120
3110
|
const report = render(src, {
|
|
3121
3111
|
at, unit, strict, profiles, path: files[0],
|
|
3122
3112
|
coverage, coverageAt,
|
|
@@ -3127,6 +3117,15 @@ function runRender(argv: string[]): number {
|
|
|
3127
3117
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
3128
3118
|
})
|
|
3129
3119
|
|
|
3120
|
+
// Said once, whatever the format: stdout stays the report.
|
|
3121
|
+
if ('error' !== report.verdict && 0 === report.units.length) {
|
|
3122
|
+
vacuous('nothing was rendered',
|
|
3123
|
+
noProfiles
|
|
3124
|
+
? 'no profile was given, and the document declares none' +
|
|
3125
|
+
' (see aontu help tasks)'
|
|
3126
|
+
: 'the document produced no units under this profile')
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3130
3129
|
if ('json' === format) {
|
|
3131
3130
|
process.stdout.write(exactJSON({
|
|
3132
3131
|
aontu: { version: version(), verb: 'render' },
|
|
@@ -3252,21 +3251,21 @@ function renderExit(report: RenderReport, drift: number): number {
|
|
|
3252
3251
|
}
|
|
3253
3252
|
|
|
3254
3253
|
|
|
3255
|
-
// ---------------------------------------------------------------------
|
|
3256
|
-
// THE TEMPLATE SURFACE (docs/design/TEMPLATE.0.md; RENDER.0.md P8): the
|
|
3257
|
-
// two transforms and the round trip between them. `render` reads a
|
|
3258
|
-
// template directly, by its extension; this verb is for seeing the
|
|
3259
|
-
// canonical form, for writing one by hand and sugaring it, and for the
|
|
3260
|
-
// check that keeps a committed template and its meaning in agreement.
|
|
3261
|
-
|
|
3262
3254
|
const TEMPLATE_HELP =
|
|
3263
3255
|
'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
|
|
3264
3256
|
|
|
3265
3257
|
function runTemplate(argv: string[]): number {
|
|
3258
|
+
const trusted = takeTrust(argv)
|
|
3259
|
+
if (null == trusted) {
|
|
3260
|
+
return 2
|
|
3261
|
+
}
|
|
3262
|
+
argv = trusted.argv
|
|
3263
|
+
const trust = trusted.trust
|
|
3266
3264
|
const files: string[] = []
|
|
3267
3265
|
let resugar = false
|
|
3268
3266
|
let check = false
|
|
3269
3267
|
let marker: string | undefined = undefined
|
|
3268
|
+
const profileFiles: string[] = []
|
|
3270
3269
|
|
|
3271
3270
|
for (let i = 0; i < argv.length; i++) {
|
|
3272
3271
|
const arg = argv[i]
|
|
@@ -3287,6 +3286,14 @@ function runTemplate(argv: string[]): number {
|
|
|
3287
3286
|
return 2
|
|
3288
3287
|
}
|
|
3289
3288
|
}
|
|
3289
|
+
else if ('--profile' === arg) {
|
|
3290
|
+
const pf = argv[++i]
|
|
3291
|
+
if (null == pf) {
|
|
3292
|
+
process.stderr.write('aontu: --profile needs a file\n')
|
|
3293
|
+
return 2
|
|
3294
|
+
}
|
|
3295
|
+
profileFiles.push(pf)
|
|
3296
|
+
}
|
|
3290
3297
|
else if (arg.startsWith('-')) {
|
|
3291
3298
|
process.stderr.write(
|
|
3292
3299
|
`aontu: unknown template option ${arg} (try --help)\n`)
|
|
@@ -3301,10 +3308,6 @@ function runTemplate(argv: string[]): number {
|
|
|
3301
3308
|
process.stderr.write(`aontu: template needs one file\n${TEMPLATE_HELP}\n`)
|
|
3302
3309
|
return 2
|
|
3303
3310
|
}
|
|
3304
|
-
// THE TWO ARE DIRECTIONS, NOT MODES THAT COMPOSE: `--check` reads a
|
|
3305
|
-
// template and asks whether the round trip answers it back, and
|
|
3306
|
-
// `--resugar` reads the canonical form instead. A run cannot be both
|
|
3307
|
-
// at once, because the file is one thing or the other.
|
|
3308
3311
|
if (resugar && check) {
|
|
3309
3312
|
process.stderr.write(
|
|
3310
3313
|
'aontu: template takes one of --resugar or --check\n')
|
|
@@ -3320,19 +3323,15 @@ function runTemplate(argv: string[]): number {
|
|
|
3320
3323
|
return 2
|
|
3321
3324
|
}
|
|
3322
3325
|
|
|
3323
|
-
const
|
|
3326
|
+
const declared = loadProfiles(profileFiles, trust)
|
|
3327
|
+
if ('number' === typeof declared) {
|
|
3328
|
+
return declared
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
const mark = marker ?? markerFromProfiles(declared, files[0]) ??
|
|
3332
|
+
markerFor(files[0])
|
|
3324
3333
|
|
|
3325
3334
|
if (check) {
|
|
3326
|
-
// THE ROUND TRIP IS THE CHECK (D6): the file held to the spelling
|
|
3327
|
-
// the two transforms answer. What that names is a marker line the
|
|
3328
|
-
// transform would not have written -- one without its space, or one
|
|
3329
|
-
// whose aontu is indented after the marker rather than before it,
|
|
3330
|
-
// since the marker keeps its own indentation. It does NOT name a
|
|
3331
|
-
// changed body line: a template's whitespace is output, so a
|
|
3332
|
-
// trimmed trailing space is still a valid template and it is
|
|
3333
|
-
// `render --check` against the committed files that catches it.
|
|
3334
|
-
// The first line that differs is the report, since a whole diff of
|
|
3335
|
-
// a generator is the file again.
|
|
3336
3335
|
const back = resugarTemplate(desugarTemplate(src, mark), mark)
|
|
3337
3336
|
if (back === src) {
|
|
3338
3337
|
return 0
|
|
@@ -3343,12 +3342,6 @@ function runTemplate(argv: string[]): number {
|
|
|
3343
3342
|
while (n < want.length && n < have.length && want[n] === have[n]) {
|
|
3344
3343
|
n++
|
|
3345
3344
|
}
|
|
3346
|
-
// THE TWO ARE THE SAME LENGTH, always: each transform maps one
|
|
3347
|
-
// line to one line and applies the same trailing-newline rule, so
|
|
3348
|
-
// `back` has as many lines as `src`. The loop above therefore stops
|
|
3349
|
-
// at a real difference rather than by running out of either -- an
|
|
3350
|
-
// equal prefix all the way to the end IS `back === src`, which
|
|
3351
|
-
// returned above. So both indexes are in range here.
|
|
3352
3345
|
process.stderr.write(
|
|
3353
3346
|
`aontu: ${files[0]}:${n + 1} is not what the round trip answers\n` +
|
|
3354
3347
|
` have: ${JSON.stringify(have[n])}\n` +
|
|
@@ -3362,13 +3355,43 @@ function runTemplate(argv: string[]): number {
|
|
|
3362
3355
|
}
|
|
3363
3356
|
|
|
3364
3357
|
|
|
3365
|
-
//
|
|
3366
|
-
//
|
|
3367
|
-
//
|
|
3368
|
-
//
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3358
|
+
// The profiles named by --profile, vetted, or the exit code that says
|
|
3359
|
+
// why not. A profile is a language declared as data: `render` matches
|
|
3360
|
+
// one to a unit by `lang`, and `template` and `fmt` match one to a file
|
|
3361
|
+
// by the extensions its `template.ext` names.
|
|
3362
|
+
function loadProfiles(
|
|
3363
|
+
profileFiles: string[], trust: TrustArg
|
|
3364
|
+
): any[] | number {
|
|
3365
|
+
const profiles: any[] = []
|
|
3366
|
+
const langs = new Map<string, string>()
|
|
3367
|
+
for (const pf of profileFiles) {
|
|
3368
|
+
let text: string
|
|
3369
|
+
try {
|
|
3370
|
+
text = readFileSync(pf, 'utf8')
|
|
3371
|
+
}
|
|
3372
|
+
catch (err: any) {
|
|
3373
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3374
|
+
return 2
|
|
3375
|
+
}
|
|
3376
|
+
const loaded = renderProfile(text,
|
|
3377
|
+
{ path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
|
|
3378
|
+
if (undefined !== loaded.errors) {
|
|
3379
|
+
process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
|
|
3380
|
+
return 4
|
|
3381
|
+
}
|
|
3382
|
+
const profile = loaded.profile
|
|
3383
|
+
const prev = langs.get(profile.lang)
|
|
3384
|
+
if (undefined !== prev) {
|
|
3385
|
+
process.stderr.write(
|
|
3386
|
+
`aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`)
|
|
3387
|
+
return 2
|
|
3388
|
+
}
|
|
3389
|
+
langs.set(profile.lang, pf)
|
|
3390
|
+
profiles.push(profile)
|
|
3391
|
+
}
|
|
3392
|
+
return profiles
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3372
3395
|
|
|
3373
3396
|
const HASH_HELP = 'aontu hash <file> (try --help)'
|
|
3374
3397
|
|
|
@@ -3429,14 +3452,6 @@ function runHash(argv: string[]): number {
|
|
|
3429
3452
|
const ctx = aontu.ctx({ collect: true })
|
|
3430
3453
|
const v: any = aontu.unify(src, { path: files[0] }, ctx)
|
|
3431
3454
|
if (0 < ctx.err.length || true === v?.isNil) {
|
|
3432
|
-
// A document that does not stand up on its own has no meaning to
|
|
3433
|
-
// pin, and a hash of a broken evaluation would be a pin that
|
|
3434
|
-
// silently agrees with every other broken evaluation.
|
|
3435
|
-
// WHY it does not stand up, not just that it does not: the same
|
|
3436
|
-
// diagnosis `aontu <file>` prints (the review's finding F).
|
|
3437
|
-
// evalFailure unconditionally, as every other call site does: it
|
|
3438
|
-
// owns the "ctx.err is never empty here" contract, and a guard
|
|
3439
|
-
// that pretends otherwise is a dead arm asserting nothing.
|
|
3440
3455
|
process.stderr.write(
|
|
3441
3456
|
`aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
|
|
3442
3457
|
renderFinding(evalFailure(ctx)) + '\n')
|
|
@@ -3455,13 +3470,6 @@ function runHash(argv: string[]): number {
|
|
|
3455
3470
|
}
|
|
3456
3471
|
|
|
3457
3472
|
|
|
3458
|
-
// ---------------------------------------------------------------------
|
|
3459
|
-
// The query surface (G7 phase 1): one node of an evaluated document,
|
|
3460
|
-
// selected by path and rendered. Evaluation is still GLOBAL -- what
|
|
3461
|
-
// `get` buys is the size of the ANSWER, not the cost of producing it --
|
|
3462
|
-
// and the projections are lattice abstractions, each a valid Aontu
|
|
3463
|
-
// document that subsumes the truth it summarises.
|
|
3464
|
-
|
|
3465
3473
|
const GET_HELP = 'aontu get <path> <file> (try --help)'
|
|
3466
3474
|
|
|
3467
3475
|
function runGet(argv: string[]): number {
|
|
@@ -3649,11 +3657,6 @@ function runWhy(argv: string[]): number {
|
|
|
3649
3657
|
}
|
|
3650
3658
|
|
|
3651
3659
|
|
|
3652
|
-
// One contribution per line, numbered in source order, each with what
|
|
3653
|
-
// was written, where, and how it got here. A siteless contribution
|
|
3654
|
-
// prints no location rather than a `-1:-1` that means nothing —
|
|
3655
|
-
// exported for the direct test, because the site SHAPE allows one
|
|
3656
|
-
// while no document has yet produced one (ADR-002).
|
|
3657
3660
|
function renderWhyText(record: WhyRecord): string {
|
|
3658
3661
|
const head = `${record.path} = ${record.value}`
|
|
3659
3662
|
if (0 === record.conjuncts.length) {
|
|
@@ -3671,13 +3674,6 @@ function renderWhyText(record: WhyRecord): string {
|
|
|
3671
3674
|
}
|
|
3672
3675
|
|
|
3673
3676
|
|
|
3674
|
-
// ---------------------------------------------------------------------
|
|
3675
|
-
// The overlay patch verb (G7 phase 5): change a document by APPENDING
|
|
3676
|
-
// to an overlay, not by rewriting it. An overlay entry is just another
|
|
3677
|
-
// conjunct and unification is order-independent, so this needs no
|
|
3678
|
-
// rewriter — the format-preserving in-place edit is stage 2, and needs
|
|
3679
|
-
// a comment-preserving CST the parser stack does not have.
|
|
3680
|
-
|
|
3681
3677
|
const SET_HELP =
|
|
3682
3678
|
'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)'
|
|
3683
3679
|
|
|
@@ -3794,29 +3790,12 @@ function runSet(argv: string[]): number {
|
|
|
3794
3790
|
}, 2) + '\n')
|
|
3795
3791
|
}
|
|
3796
3792
|
else {
|
|
3797
|
-
// A replacement is REPORTED as the edit it is, not left for the
|
|
3798
|
-
// reader to infer from a changed file: `where: what -> what`, in
|
|
3799
|
-
// source spelling, because the spelling is what changed.
|
|
3800
|
-
//
|
|
3801
|
-
// PAST TENSE ONLY WHERE IT HAPPENED. A refused write leaves the
|
|
3802
|
-
// file exactly as it was, and one assignment can be replaceable
|
|
3803
|
-
// while another makes the whole run invalid — so `replaced:` there
|
|
3804
|
-
// tells an operator the pin was changed when it was not, and unlike
|
|
3805
|
-
// `--dry-run` there is nothing else on the line to say otherwise.
|
|
3806
3793
|
const verb = wrote ? 'replaced' : 'would replace'
|
|
3807
3794
|
const edits = report.replaced.map((r) =>
|
|
3808
3795
|
`${verb}: ${r.file}:${r.row}:${r.col} ${r.from} -> ${r.to}`)
|
|
3809
3796
|
const head = [`verdict: ${report.verdict}`].concat(edits).join('\n') +
|
|
3810
3797
|
(wrote ? `\nwrote: ${overlayFile}` : dryRun ? '\n(dry run)' : '')
|
|
3811
3798
|
|
|
3812
|
-
// A SUCCESSFUL COMMAND WRITES ITS STATUS TO STDOUT, findings or
|
|
3813
|
-
// not. Routing on `findings.length` was right while every finding
|
|
3814
|
-
// this verb could produce was an ERROR; `--in-place` made a WARNING
|
|
3815
|
-
// possible, and a run that held, wrote the file and exited 0 then
|
|
3816
|
-
// sent its whole report to stderr — leaving stdout empty, so
|
|
3817
|
-
// `$(aontu set ...)` captured nothing and only the JSON form
|
|
3818
|
-
// behaved like a success. The verdict decides the stream; warnings
|
|
3819
|
-
// are diagnostics and go to stderr beside it.
|
|
3820
3799
|
const failed = 'invalid' === report.verdict || 'error' === report.verdict
|
|
3821
3800
|
const findingText = report.findings.map(renderFinding)
|
|
3822
3801
|
if (failed) {
|
|
@@ -3838,6 +3817,171 @@ function runSet(argv: string[]): number {
|
|
|
3838
3817
|
}
|
|
3839
3818
|
|
|
3840
3819
|
|
|
3820
|
+
const ALLOW_HELP =
|
|
3821
|
+
'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)'
|
|
3822
|
+
|
|
3823
|
+
const ALLOW_EXIT: Record<AllowVerdict, number> = {
|
|
3824
|
+
allowed: 0,
|
|
3825
|
+
refused: 1,
|
|
3826
|
+
error: 4,
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
|
|
3830
|
+
// One line per asked path: the answer, and the entry that gave it, as
|
|
3831
|
+
// a path into the role model so `aontu why` can locate the rule.
|
|
3832
|
+
function renderAllowDecision(d: AllowDecision, role: string): string {
|
|
3833
|
+
const head = `${d.path}: ${d.allowed ? 'allowed' : 'refused'}`
|
|
3834
|
+
switch (d.reason) {
|
|
3835
|
+
case 'allow':
|
|
3836
|
+
case 'deny':
|
|
3837
|
+
return `${head} by ${d.by} (${d.pattern})`
|
|
3838
|
+
case 'uncovered':
|
|
3839
|
+
return `${head} (no allow entry of ${role} covers it)`
|
|
3840
|
+
default:
|
|
3841
|
+
return `${head} (role ${role} is not declared)`
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
|
|
3846
|
+
function renderAllowText(report: AllowReport): string {
|
|
3847
|
+
const lines = [`verdict: ${report.verdict}`, `role: ${report.role}`]
|
|
3848
|
+
.concat(report.paths.map((d) => renderAllowDecision(d, report.role)))
|
|
3849
|
+
if (0 === report.findings.length) {
|
|
3850
|
+
return lines.join('\n')
|
|
3851
|
+
}
|
|
3852
|
+
return lines.concat('', report.findings.map(renderFinding)).join('\n')
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
|
|
3856
|
+
// Does the text after `=` parse as exactly one value? Parsed, never
|
|
3857
|
+
// evaluated, with loads denied: the question is the shape of the
|
|
3858
|
+
// argument, and reading a file to answer it would be the write the
|
|
3859
|
+
// gate exists to precede.
|
|
3860
|
+
function oneValue(value: string): boolean {
|
|
3861
|
+
try {
|
|
3862
|
+
const probe: any = new Aontu({ trust: { include: 'none' } })
|
|
3863
|
+
.parse('v: ' + value)
|
|
3864
|
+
return 1 === Object.keys(probe.peg).length
|
|
3865
|
+
}
|
|
3866
|
+
catch {
|
|
3867
|
+
return false
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
|
|
3872
|
+
function runAllow(argv: string[]): number {
|
|
3873
|
+
const trusted = takeTrust(argv)
|
|
3874
|
+
if (null == trusted) {
|
|
3875
|
+
return 2
|
|
3876
|
+
}
|
|
3877
|
+
argv = trusted.argv
|
|
3878
|
+
const trust = trusted.trust
|
|
3879
|
+
const rest: string[] = []
|
|
3880
|
+
let role: string | undefined
|
|
3881
|
+
let at: string | undefined
|
|
3882
|
+
let format: SubsumeFormat = 'text'
|
|
3883
|
+
|
|
3884
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3885
|
+
const arg = argv[i]
|
|
3886
|
+
if ('-h' === arg || '--help' === arg) {
|
|
3887
|
+
process.stdout.write(HELP)
|
|
3888
|
+
return 0
|
|
3889
|
+
}
|
|
3890
|
+
if ('--role' === arg) {
|
|
3891
|
+
role = argv[++i]
|
|
3892
|
+
if (null == role) {
|
|
3893
|
+
process.stderr.write('aontu: --role needs a role name\n')
|
|
3894
|
+
return 2
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
else if ('--at' === arg) {
|
|
3898
|
+
at = argv[++i]
|
|
3899
|
+
if (null == at) {
|
|
3900
|
+
process.stderr.write('aontu: --at needs a path\n')
|
|
3901
|
+
return 2
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
else if ('--format' === arg) {
|
|
3905
|
+
const f = argv[++i]
|
|
3906
|
+
if ('text' !== f && 'json' !== f) {
|
|
3907
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
3908
|
+
return 2
|
|
3909
|
+
}
|
|
3910
|
+
format = f
|
|
3911
|
+
}
|
|
3912
|
+
else if (arg.startsWith('-')) {
|
|
3913
|
+
process.stderr.write(`aontu: unknown allow option ${arg} (try --help)\n`)
|
|
3914
|
+
return 2
|
|
3915
|
+
}
|
|
3916
|
+
else {
|
|
3917
|
+
rest.push(arg)
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
if (null == role || rest.length < 2) {
|
|
3922
|
+
process.stderr.write(
|
|
3923
|
+
`aontu: allow needs --role, a role model and at least one path\n` +
|
|
3924
|
+
`${ALLOW_HELP}\n`)
|
|
3925
|
+
return 2
|
|
3926
|
+
}
|
|
3927
|
+
const [file, ...asked] = rest
|
|
3928
|
+
|
|
3929
|
+
// A role is ONE KEY of the roles map. A dotted name would be read as
|
|
3930
|
+
// a path by `why` when it follows the entry the report names, and an
|
|
3931
|
+
// empty one names the map itself.
|
|
3932
|
+
if ('' === role || role.includes('.')) {
|
|
3933
|
+
process.stderr.write('aontu: --role needs one key, without dots\n')
|
|
3934
|
+
return 2
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const paths: string[] = []
|
|
3938
|
+
for (const arg of asked) {
|
|
3939
|
+
const eq = arg.indexOf('=')
|
|
3940
|
+
const path = eq < 0 ? arg : arg.slice(0, eq)
|
|
3941
|
+
if (!path.startsWith('$')) {
|
|
3942
|
+
process.stderr.write(
|
|
3943
|
+
`aontu: a path starts with $ (got ${JSON.stringify(arg)})\n`)
|
|
3944
|
+
return 2
|
|
3945
|
+
}
|
|
3946
|
+
if (0 <= eq && !oneValue(arg.slice(eq + 1))) {
|
|
3947
|
+
process.stderr.write(
|
|
3948
|
+
`aontu: the value of ${path} is not one value\n`)
|
|
3949
|
+
return 2
|
|
3950
|
+
}
|
|
3951
|
+
paths.push(path)
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
let src: string
|
|
3955
|
+
try {
|
|
3956
|
+
src = readFileSync(file, 'utf8')
|
|
3957
|
+
}
|
|
3958
|
+
catch (err: any) {
|
|
3959
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
3960
|
+
return 2
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
const report = allow(src, role, paths, {
|
|
3964
|
+
at, path: file, ...verbOpts(trust, entryRootOf(file)),
|
|
3965
|
+
})
|
|
3966
|
+
|
|
3967
|
+
const text = 'json' === format ?
|
|
3968
|
+
exactJSON({
|
|
3969
|
+
aontu: { version: version(), verb: 'allow' },
|
|
3970
|
+
findings: report.findings,
|
|
3971
|
+
paths: report.paths,
|
|
3972
|
+
role: report.role,
|
|
3973
|
+
verdict: report.verdict,
|
|
3974
|
+
}, 2) :
|
|
3975
|
+
renderAllowText(report)
|
|
3976
|
+
|
|
3977
|
+
// The report IS the answer, refused or not, so it goes to stdout as
|
|
3978
|
+
// vet's does; the exit code carries the verdict for a caller that
|
|
3979
|
+
// reads nothing else.
|
|
3980
|
+
process.stdout.write(text + '\n')
|
|
3981
|
+
return ALLOW_EXIT[report.verdict]
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
|
|
3841
3985
|
// ---------------------------------------------------------------------
|
|
3842
3986
|
// The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
|
|
3843
3987
|
// derived from the definition, so it cannot drift from the formal
|
|
@@ -3854,6 +3998,10 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3854
3998
|
const trust = trusted.trust
|
|
3855
3999
|
const files: string[] = []
|
|
3856
4000
|
let write: string | undefined
|
|
4001
|
+
// The SHAPE's depth (G11 phase 7). Default 2, unchanged: the stanza
|
|
4002
|
+
// is spliced into a file people read, and a deeper shape is a
|
|
4003
|
+
// question the caller asks rather than one it is handed.
|
|
4004
|
+
let depth = 2
|
|
3857
4005
|
|
|
3858
4006
|
for (let i = 0; i < argv.length; i++) {
|
|
3859
4007
|
const arg = argv[i]
|
|
@@ -3868,6 +4016,14 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3868
4016
|
return 2
|
|
3869
4017
|
}
|
|
3870
4018
|
}
|
|
4019
|
+
else if ('--depth' === arg) {
|
|
4020
|
+
const n = Number(argv[++i])
|
|
4021
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
4022
|
+
process.stderr.write('aontu: --depth needs a positive integer\n')
|
|
4023
|
+
return 2
|
|
4024
|
+
}
|
|
4025
|
+
depth = n
|
|
4026
|
+
}
|
|
3871
4027
|
else if (arg.startsWith('-')) {
|
|
3872
4028
|
process.stderr.write(
|
|
3873
4029
|
`aontu: unknown agentsmd option ${arg} (try --help)\n`)
|
|
@@ -3894,7 +4050,7 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3894
4050
|
}
|
|
3895
4051
|
|
|
3896
4052
|
const report = agentsMd(src, {
|
|
3897
|
-
name: files[0], path: files[0],
|
|
4053
|
+
depth, name: files[0], path: files[0],
|
|
3898
4054
|
...verbOpts(trust, entryRootOf(files[0])),
|
|
3899
4055
|
})
|
|
3900
4056
|
if (!report.ok) {
|
|
@@ -3933,35 +4089,23 @@ function runAgentsMd(argv: string[]): number {
|
|
|
3933
4089
|
}
|
|
3934
4090
|
|
|
3935
4091
|
|
|
3936
|
-
// Exit without truncating output.
|
|
3937
|
-
//
|
|
3938
|
-
// process.exit() terminates immediately, discarding anything still
|
|
3939
|
-
// queued on stdout. A write to a PIPE is asynchronous once it exceeds
|
|
3940
|
-
// the pipe buffer, so `write(big); exit(0)` silently truncated output at
|
|
3941
|
-
// 65536 bytes — while a write to a TTY or a file, being synchronous,
|
|
3942
|
-
// looked fine. Setting exitCode instead lets the process end naturally,
|
|
3943
|
-
// after the queue drains.
|
|
3944
|
-
//
|
|
3945
|
-
// This predates the exact leaves but they make it trivially reachable
|
|
3946
|
-
// (one long biginteger canon exceeds the buffer), and it lands squarely
|
|
3947
|
-
// on the parity-probe discipline in AGENTS.md, which derives expected
|
|
3948
|
-
// spec values by piping BOTH CLIs and comparing. A truncated pipe there
|
|
3949
|
-
// reads as a port divergence.
|
|
3950
|
-
// ---------------------------------------------------------------------
|
|
3951
|
-
// The source formatter (docs/design/FMT.0.md): one agreed form, in the
|
|
3952
|
-
// tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
|
|
3953
|
-
// the form itself is the library's (ts/src/format.ts), and the two
|
|
3954
|
-
// ports agree on it row by row in test/spec/fmt.tsv.
|
|
3955
|
-
|
|
3956
4092
|
const FMT_HELP =
|
|
3957
|
-
'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
|
|
4093
|
+
'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] ' +
|
|
4094
|
+
'[--profile <file>] <file>... (try --help)'
|
|
3958
4095
|
|
|
3959
4096
|
type FmtFlags = {
|
|
3960
4097
|
write: boolean, list: boolean, check: boolean, diff: boolean, lint: boolean, strict: boolean,
|
|
3961
4098
|
}
|
|
3962
4099
|
|
|
3963
4100
|
function runFmt(argv: string[]): number | Promise<number> {
|
|
4101
|
+
const trusted = takeTrust(argv)
|
|
4102
|
+
if (null == trusted) {
|
|
4103
|
+
return 2
|
|
4104
|
+
}
|
|
4105
|
+
argv = trusted.argv
|
|
4106
|
+
const trust = trusted.trust
|
|
3964
4107
|
const files: string[] = []
|
|
4108
|
+
const profileFiles: string[] = []
|
|
3965
4109
|
let marker: string | undefined = undefined
|
|
3966
4110
|
const flags: FmtFlags = {
|
|
3967
4111
|
write: false, list: false, check: false, diff: false, lint: false, strict: false,
|
|
@@ -4002,6 +4146,14 @@ function runFmt(argv: string[]): number | Promise<number> {
|
|
|
4002
4146
|
return 2
|
|
4003
4147
|
}
|
|
4004
4148
|
}
|
|
4149
|
+
else if ('--profile' === arg) {
|
|
4150
|
+
const pf = argv[++i]
|
|
4151
|
+
if (null == pf) {
|
|
4152
|
+
process.stderr.write('aontu: --profile needs a file\n')
|
|
4153
|
+
return 2
|
|
4154
|
+
}
|
|
4155
|
+
profileFiles.push(pf)
|
|
4156
|
+
}
|
|
4005
4157
|
else if (arg.startsWith('-')) {
|
|
4006
4158
|
process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
|
|
4007
4159
|
return 2
|
|
@@ -4027,6 +4179,11 @@ function runFmt(argv: string[]): number | Promise<number> {
|
|
|
4027
4179
|
})
|
|
4028
4180
|
}
|
|
4029
4181
|
|
|
4182
|
+
const declared = loadProfiles(profileFiles, trust)
|
|
4183
|
+
if ('number' === typeof declared) {
|
|
4184
|
+
return declared
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4030
4187
|
// Several files onto standard output would be one stream nobody can
|
|
4031
4188
|
// split again (the note's X-6): the verb refuses unless an option
|
|
4032
4189
|
// says what to do with each.
|
|
@@ -4047,13 +4204,14 @@ function runFmt(argv: string[]): number | Promise<number> {
|
|
|
4047
4204
|
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
|
|
4048
4205
|
return 2
|
|
4049
4206
|
}
|
|
4050
|
-
const mark = fmtMarker(file, src,
|
|
4207
|
+
const mark = fmtMarker(file, src,
|
|
4208
|
+
marker ?? markerFromProfiles(declared, file))
|
|
4051
4209
|
if (false === mark) {
|
|
4052
4210
|
process.stderr.write(
|
|
4053
4211
|
`aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
|
|
4054
4212
|
`${markerFor(file)} marker line, so there is no aontu in it to ` +
|
|
4055
|
-
'format; --marker names the marker for a language the table
|
|
4056
|
-
'not know\n')
|
|
4213
|
+
'format; --marker names the marker for a language the table ' +
|
|
4214
|
+
'does not know, and --profile reads one that declares it\n')
|
|
4057
4215
|
return 2
|
|
4058
4216
|
}
|
|
4059
4217
|
worst = Math.max(worst, fmtOne(file, src, flags, mark))
|
|
@@ -4061,20 +4219,6 @@ function runFmt(argv: string[]): number | Promise<number> {
|
|
|
4061
4219
|
return worst
|
|
4062
4220
|
}
|
|
4063
4221
|
|
|
4064
|
-
// WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
|
|
4065
|
-
// `render` reads an entry by): `.aon` and `.aontu` are aontu source,
|
|
4066
|
-
// and anything else is a GENERATOR written in the target's own syntax
|
|
4067
|
-
// (docs/design/TEMPLATE.0.md), whose marker lines carry the document
|
|
4068
|
-
// this formats and whose other lines are output. `undefined` is aontu,
|
|
4069
|
-
// a string is the generator's marker, and `false` is neither.
|
|
4070
|
-
//
|
|
4071
|
-
// A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
|
|
4072
|
-
// FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
|
|
4073
|
-
// include is another language's file, and reading one as a generator
|
|
4074
|
-
// would answer it back unchanged having understood none of it. The
|
|
4075
|
-
// marker is the evidence that a file was written to carry aontu at
|
|
4076
|
-
// all. `--marker` says so outright, and then the file is a generator
|
|
4077
|
-
// whatever it is called.
|
|
4078
4222
|
function fmtMarker(
|
|
4079
4223
|
file: string, src: string, marker: string | undefined): string | undefined | false {
|
|
4080
4224
|
if (undefined !== marker) {
|
|
@@ -4136,26 +4280,13 @@ function fmtOne(
|
|
|
4136
4280
|
}
|
|
4137
4281
|
|
|
4138
4282
|
|
|
4139
|
-
// ---------------------------------------------------------------------
|
|
4140
|
-
// The servers as verbs: `aontu lsp` runs the language server and
|
|
4141
|
-
// `aontu mcp` the MCP server over the CLI's own streams, so the one
|
|
4142
|
-
// command on PATH is the editor's and the agent's server too, and a
|
|
4143
|
-
// version manager (docs/design/ENV.0.md) has one thing to resolve. The
|
|
4144
|
-
// standalone bins (aontu-lsp, aontu-mcp) run the same functions. Each
|
|
4145
|
-
// server owns its exit; the CLI only dispatches. The pair is a
|
|
4146
|
-
// parameter of main so that a test can see the dispatch without a
|
|
4147
|
-
// server taking the test's own stdin.
|
|
4148
|
-
|
|
4149
4283
|
type Servers = {
|
|
4150
4284
|
lsp: () => void
|
|
4151
4285
|
mcp: (argv: string[]) => void
|
|
4152
4286
|
}
|
|
4153
4287
|
|
|
4154
|
-
//
|
|
4155
|
-
//
|
|
4156
|
-
// cli.test.ts run each through a child process instead, so these two
|
|
4157
|
-
// lines are excluded from the in-process count, as the stdio wiring
|
|
4158
|
-
// of lsp-server.ts is.
|
|
4288
|
+
// Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
|
|
4289
|
+
// drives each server through a child process instead.
|
|
4159
4290
|
/* node:coverage ignore next 4 */
|
|
4160
4291
|
const SERVERS: Servers = {
|
|
4161
4292
|
lsp: () => void lspMain(),
|
|
@@ -4202,23 +4333,353 @@ function parseTrustArg(value: string): TrustArg | undefined {
|
|
|
4202
4333
|
}
|
|
4203
4334
|
|
|
4204
4335
|
|
|
4336
|
+
const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)'
|
|
4337
|
+
const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)'
|
|
4338
|
+
|
|
4339
|
+
|
|
4340
|
+
function helpIndexText(index: HelpTopic[]): string {
|
|
4341
|
+
const width = index.reduce((w, t) => Math.max(w, t.topic.length), 0)
|
|
4342
|
+
return 'aontu help <topic> — the language, offline.\n\n' +
|
|
4343
|
+
index.map((t) =>
|
|
4344
|
+
' ' + t.topic.padEnd(width) + ' ' + t.summary).join('\n') +
|
|
4345
|
+
'\n\n' +
|
|
4346
|
+
'`aontu --help` documents the verbs, their flags and their exit\n' +
|
|
4347
|
+
'codes. `aontu explain <code>` explains one error code.\n' +
|
|
4348
|
+
'Start at `aontu help tasks` if you know the job but not the verb.'
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
|
|
4352
|
+
function runHelp(argv: string[]): number {
|
|
4353
|
+
let format: SubsumeFormat = 'text'
|
|
4354
|
+
const topics: string[] = []
|
|
4355
|
+
|
|
4356
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4357
|
+
const arg = argv[i]
|
|
4358
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4359
|
+
process.stdout.write(HELP)
|
|
4360
|
+
return 0
|
|
4361
|
+
}
|
|
4362
|
+
else if ('--format' === arg) {
|
|
4363
|
+
const f = argv[++i]
|
|
4364
|
+
if ('text' !== f && 'json' !== f) {
|
|
4365
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
4366
|
+
return 2
|
|
4367
|
+
}
|
|
4368
|
+
format = f
|
|
4369
|
+
}
|
|
4370
|
+
else if (arg.startsWith('-')) {
|
|
4371
|
+
process.stderr.write(`aontu: unknown help option ${arg} (try --help)\n`)
|
|
4372
|
+
return 2
|
|
4373
|
+
}
|
|
4374
|
+
else {
|
|
4375
|
+
topics.push(arg)
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
if (1 < topics.length) {
|
|
4380
|
+
process.stderr.write(`aontu: help takes one topic\n${HELP_VERB_HELP}\n`)
|
|
4381
|
+
return 2
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
if (0 === topics.length) {
|
|
4385
|
+
process.stdout.write(('json' === format
|
|
4386
|
+
? exactJSON({
|
|
4387
|
+
aontu: { version: version(), verb: 'help' },
|
|
4388
|
+
topics: HELPDOC.map(
|
|
4389
|
+
(t) => ({ topic: t.topic, summary: t.summary, source: t.source })),
|
|
4390
|
+
}, 2)
|
|
4391
|
+
: helpIndexText(HELPDOC)) + '\n')
|
|
4392
|
+
return 0
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
const found = HELPDOC.find((t) => topics[0] === t.topic)
|
|
4396
|
+
if (null != found) {
|
|
4397
|
+
if ('json' === format) {
|
|
4398
|
+
process.stdout.write(exactJSON({
|
|
4399
|
+
aontu: { version: version(), verb: 'help' },
|
|
4400
|
+
topic: found.topic,
|
|
4401
|
+
summary: found.summary,
|
|
4402
|
+
source: found.source,
|
|
4403
|
+
text: found.text,
|
|
4404
|
+
}, 2) + '\n')
|
|
4405
|
+
return 0
|
|
4406
|
+
}
|
|
4407
|
+
process.stdout.write(found.text)
|
|
4408
|
+
return 0
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
// AN UNKNOWN TOPIC IS A USAGE ERROR AND NAMES THE ALTERNATIVES,
|
|
4412
|
+
// because the caller who typed it has no other way to find out what
|
|
4413
|
+
// exists -- that is the whole condition this verb was added for.
|
|
4414
|
+
process.stderr.write(
|
|
4415
|
+
`aontu: no help topic \`${topics[0]}\`\n` +
|
|
4416
|
+
`aontu: topics are ${HELPDOC.map((t) => t.topic).join(', ')}\n`)
|
|
4417
|
+
return 2
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
|
|
4421
|
+
// The dynamic prefixes a generated code extends (`func:upper`,
|
|
4422
|
+
// `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
|
|
4423
|
+
// exported; a code that extends one is registered through its prefix
|
|
4424
|
+
// and carries that prefix's hint.
|
|
4425
|
+
const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref[']
|
|
4426
|
+
|
|
4427
|
+
|
|
4428
|
+
function explainCode(
|
|
4429
|
+
code: string): { cls: string, hint: string, registered: boolean } {
|
|
4430
|
+
const cls = codeClass(code)
|
|
4431
|
+
let hint = hints[code] ?? ''
|
|
4432
|
+
let registered = null != codeClasses[code]
|
|
4433
|
+
if (!registered) {
|
|
4434
|
+
for (const prefix of EXPLAIN_PREFIXES) {
|
|
4435
|
+
if (code.startsWith(prefix)) {
|
|
4436
|
+
// No guard on `hint` here: every hint key is also a registry
|
|
4437
|
+
// key (the spec suite asserts codeClasses set-equal with
|
|
4438
|
+
// test/spec/errcodes.tsv, and hints is a subset of it), so a
|
|
4439
|
+
// code that reaches this loop is unregistered and therefore
|
|
4440
|
+
// has no hint of its own.
|
|
4441
|
+
registered = true
|
|
4442
|
+
hint = hints[prefix] ?? ''
|
|
4443
|
+
break
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
return { cls, hint, registered }
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
|
|
4451
|
+
// Every code in the shared registry, sorted by code point so both
|
|
4452
|
+
// ports list them in the same order.
|
|
4453
|
+
function explainCodes(): string[] {
|
|
4454
|
+
return Object.keys(codeClasses).sort(cmpCodePoint)
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
|
|
4458
|
+
function explainListText(format: SubsumeFormat): string {
|
|
4459
|
+
const codes = explainCodes()
|
|
4460
|
+
if ('json' === format) {
|
|
4461
|
+
return exactJSON({
|
|
4462
|
+
aontu: { version: version(), verb: 'explain' },
|
|
4463
|
+
codes: codes.map((code) => ({
|
|
4464
|
+
code,
|
|
4465
|
+
class: codeClass(code),
|
|
4466
|
+
// Whether this port carries explanation text for the code. The
|
|
4467
|
+
// registry is in parity; the hint tables are not, so a consumer
|
|
4468
|
+
// that wants only explained codes can filter rather than guess.
|
|
4469
|
+
explained: '' !== explainCode(code).hint,
|
|
4470
|
+
})),
|
|
4471
|
+
}, 2)
|
|
4472
|
+
}
|
|
4473
|
+
const width = codes.reduce((w, c) => Math.max(w, c.length), 0)
|
|
4474
|
+
return codes.map((c) =>
|
|
4475
|
+
c.padEnd(width) + ' ' + codeClass(c) +
|
|
4476
|
+
('' === explainCode(c).hint ? ' (no text)' : '')).join('\n')
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
|
|
4480
|
+
function runExplain(argv: string[]): number {
|
|
4481
|
+
let format: SubsumeFormat = 'text'
|
|
4482
|
+
let list = false
|
|
4483
|
+
const codes: string[] = []
|
|
4484
|
+
|
|
4485
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4486
|
+
const arg = argv[i]
|
|
4487
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4488
|
+
process.stdout.write(HELP)
|
|
4489
|
+
return 0
|
|
4490
|
+
}
|
|
4491
|
+
else if ('--list' === arg) {
|
|
4492
|
+
list = true
|
|
4493
|
+
}
|
|
4494
|
+
else if ('--format' === arg) {
|
|
4495
|
+
const f = argv[++i]
|
|
4496
|
+
if ('text' !== f && 'json' !== f) {
|
|
4497
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
4498
|
+
return 2
|
|
4499
|
+
}
|
|
4500
|
+
format = f
|
|
4501
|
+
}
|
|
4502
|
+
else if (arg.startsWith('-')) {
|
|
4503
|
+
process.stderr.write(
|
|
4504
|
+
`aontu: unknown explain option ${arg} (try --help)\n`)
|
|
4505
|
+
return 2
|
|
4506
|
+
}
|
|
4507
|
+
else {
|
|
4508
|
+
codes.push(arg)
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
if (list) {
|
|
4513
|
+
if (0 < codes.length) {
|
|
4514
|
+
process.stderr.write(`aontu: --list takes no code\n${EXPLAIN_HELP}\n`)
|
|
4515
|
+
return 2
|
|
4516
|
+
}
|
|
4517
|
+
process.stdout.write(explainListText(format) + '\n')
|
|
4518
|
+
return 0
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
if (1 !== codes.length) {
|
|
4522
|
+
process.stderr.write(`aontu: explain needs one code\n${EXPLAIN_HELP}\n`)
|
|
4523
|
+
return 2
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
const code = codes[0]
|
|
4527
|
+
const { cls, hint, registered } = explainCode(code)
|
|
4528
|
+
if (!registered) {
|
|
4529
|
+
// AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
|
|
4530
|
+
// caller reading a code out of a report has almost certainly typed
|
|
4531
|
+
// it correctly, so the likely cause is a code from another tool or
|
|
4532
|
+
// a truncated one, and the near matches say which.
|
|
4533
|
+
process.stderr.write(`aontu: no such error code \`${code}\`\n`)
|
|
4534
|
+
const near = nearestVerb(code, explainCodes())
|
|
4535
|
+
if ('' !== near) {
|
|
4536
|
+
process.stderr.write(`aontu: did you mean \`${near}\`?\n`)
|
|
4537
|
+
}
|
|
4538
|
+
process.stderr.write(
|
|
4539
|
+
'aontu: `aontu explain --list` lists every registered code\n')
|
|
4540
|
+
return 2
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
if ('json' === format) {
|
|
4544
|
+
process.stdout.write(exactJSON({
|
|
4545
|
+
aontu: { version: version(), verb: 'explain' },
|
|
4546
|
+
code,
|
|
4547
|
+
class: cls,
|
|
4548
|
+
hint,
|
|
4549
|
+
}, 2) + '\n')
|
|
4550
|
+
return 0
|
|
4551
|
+
}
|
|
4552
|
+
// A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
|
|
4553
|
+
// empty block, which would read as an explanation that happened to
|
|
4554
|
+
// be blank.
|
|
4555
|
+
const body = '' === hint
|
|
4556
|
+
? '(no explanation text is registered for this code)'
|
|
4557
|
+
: hint
|
|
4558
|
+
process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`)
|
|
4559
|
+
return 0
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
|
|
4563
|
+
const INIT_HELP = 'aontu init [dir] (try --help)'
|
|
4564
|
+
|
|
4565
|
+
|
|
4566
|
+
function runInit(argv: string[]): number {
|
|
4567
|
+
const dirs: string[] = []
|
|
4568
|
+
|
|
4569
|
+
for (const arg of argv) {
|
|
4570
|
+
if ('-h' === arg || '--help' === arg) {
|
|
4571
|
+
process.stdout.write(HELP)
|
|
4572
|
+
return 0
|
|
4573
|
+
}
|
|
4574
|
+
if (arg.startsWith('-')) {
|
|
4575
|
+
process.stderr.write(`aontu: unknown init option ${arg} (try --help)\n`)
|
|
4576
|
+
return 2
|
|
4577
|
+
}
|
|
4578
|
+
dirs.push(arg)
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
if (1 < dirs.length) {
|
|
4582
|
+
process.stderr.write(`aontu: init takes one directory\n${INIT_HELP}\n`)
|
|
4583
|
+
return 2
|
|
4584
|
+
}
|
|
4585
|
+
const dir = dirs[0] ?? '.'
|
|
4586
|
+
|
|
4587
|
+
const standing = INITDOC.filter((f) => existsSync(join(dir, f.name)))
|
|
4588
|
+
if (0 < standing.length) {
|
|
4589
|
+
process.stderr.write(
|
|
4590
|
+
`aontu: ${dir} already holds ${standing.map((f) => f.name).join(', ')}\n` +
|
|
4591
|
+
'aontu: init never overwrites; move them aside or name an' +
|
|
4592
|
+
' empty directory\n')
|
|
4593
|
+
return 2
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
try {
|
|
4597
|
+
mkdirSync(dir, { recursive: true })
|
|
4598
|
+
for (const f of INITDOC) {
|
|
4599
|
+
writeFileSync(join(dir, f.name), f.text, { mode: f.mode })
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
catch (err: any) {
|
|
4603
|
+
process.stderr.write(`aontu: cannot write in ${dir}: ${err.message}\n`)
|
|
4604
|
+
return 2
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
process.stdout.write(
|
|
4608
|
+
INITDOC.map((f) => join(dir, f.name)).join('\n') + '\n' +
|
|
4609
|
+
'\nA model, an instance of it, and the four questions to ask.\n' +
|
|
4610
|
+
'Run the checks: sh ' + join(dir, 'check.sh') + '\n' +
|
|
4611
|
+
'Learn the language: aontu help language\n')
|
|
4612
|
+
return 0
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
|
|
4616
|
+
const KNOWN_VERBS = [
|
|
4617
|
+
'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
|
|
4618
|
+
'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
|
|
4619
|
+
'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
|
|
4620
|
+
'view', 'why',
|
|
4621
|
+
]
|
|
4622
|
+
|
|
4623
|
+
|
|
4624
|
+
// looksLikeVerb reports whether an unreadable argument was meant as a
|
|
4625
|
+
// verb rather than as a path. A bare word has no separator and no
|
|
4626
|
+
// extension; `./help`, `help.aon`, `/tmp/help` and `sub/dir` are paths
|
|
4627
|
+
// and keep the file diagnosis. Mirrors go/cmd/aontu/main.go.
|
|
4628
|
+
function looksLikeVerb(arg: string): boolean {
|
|
4629
|
+
return '' !== arg &&
|
|
4630
|
+
!/[/\\.]/.test(arg) &&
|
|
4631
|
+
!arg.startsWith('-')
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
|
|
4635
|
+
function nearestVerb(word: string, verbs: string[]): string {
|
|
4636
|
+
let best = ''
|
|
4637
|
+
let bestDist = Infinity
|
|
4638
|
+
const limit = Math.min(3, 1 + Math.floor(word.length / 4))
|
|
4639
|
+
for (const v of [...verbs].sort(cmpCodePoint)) {
|
|
4640
|
+
const d = editDistance(word.toLowerCase(), v)
|
|
4641
|
+
if (d < bestDist) {
|
|
4642
|
+
best = v
|
|
4643
|
+
bestDist = d
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
return bestDist > limit ? '' : best
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
|
|
4650
|
+
function editDistance(a: string, b: string): number {
|
|
4651
|
+
const ar = [...a]
|
|
4652
|
+
const br = [...b]
|
|
4653
|
+
let prev2 = new Array(br.length + 1).fill(0)
|
|
4654
|
+
let prev = new Array(br.length + 1).fill(0).map((_, j) => j)
|
|
4655
|
+
let cur = new Array(br.length + 1).fill(0)
|
|
4656
|
+
for (let i = 1; i <= ar.length; i++) {
|
|
4657
|
+
cur[0] = i
|
|
4658
|
+
for (let j = 1; j <= br.length; j++) {
|
|
4659
|
+
const cost = ar[i - 1] === br[j - 1] ? 0 : 1
|
|
4660
|
+
let m = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
|
|
4661
|
+
if (1 < i && 1 < j &&
|
|
4662
|
+
ar[i - 1] === br[j - 2] && ar[i - 2] === br[j - 1] &&
|
|
4663
|
+
prev2[j - 2] + 1 < m) {
|
|
4664
|
+
m = prev2[j - 2] + 1
|
|
4665
|
+
}
|
|
4666
|
+
cur[j] = m
|
|
4667
|
+
}
|
|
4668
|
+
prev2 = [...prev]
|
|
4669
|
+
prev = [...cur]
|
|
4670
|
+
}
|
|
4671
|
+
return prev[br.length]
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
|
|
4205
4675
|
function main(argv: string[], servers: Servers = SERVERS): void {
|
|
4206
|
-
// COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
|
|
4207
|
-
// hardcoded their ANSI escapes, so a piped report and a `--jsonl`
|
|
4208
|
-
// answer carried terminal control codes into whatever read them (the
|
|
4209
|
-
// review's finding F). `NO_COLOR` is honoured by the library itself;
|
|
4210
|
-
// only the CLI can see whether its stderr is a terminal, so only the
|
|
4211
|
-
// CLI can make this call. `undefined` means "leave it to NO_COLOR".
|
|
4212
4676
|
setColor(true === process.stderr.isTTY ? undefined : false)
|
|
4213
4677
|
|
|
4214
4678
|
let mode: Mode = 'json'
|
|
4215
|
-
//
|
|
4216
|
-
//
|
|
4217
|
-
//
|
|
4218
|
-
|
|
4219
|
-
// subcommand, fell through to this loop as a file name, and was
|
|
4220
|
-
// overwritten twice. In a tool loop that reads as a passing
|
|
4221
|
-
// validation. Counting them is what lets the refusal below happen.
|
|
4679
|
+
// THE REPORT FORM (G11 phase 7), default text: every existing caller
|
|
4680
|
+
// reads exactly what it always read, and a caller that asks for json
|
|
4681
|
+
// gets the failure in the finding shape every other verb reports.
|
|
4682
|
+
let format: EvalFormat = 'text'
|
|
4222
4683
|
const files: string[] = []
|
|
4223
4684
|
let trust: TrustArg = { kind: 'system-warn', textExt: [] }
|
|
4224
4685
|
let textExt: string[] = []
|
|
@@ -4228,15 +4689,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4228
4689
|
// mode the REPL already has.
|
|
4229
4690
|
let jsonl = false
|
|
4230
4691
|
|
|
4231
|
-
// Subcommand dispatch, and deliberately only for a FIRST argument:
|
|
4232
|
-
// `aontu vet` is the verb, while `aontu somefile vet` keeps meaning
|
|
4233
|
-
// what it always did. A file named `vet` is still reachable as
|
|
4234
|
-
// `aontu ./vet`.
|
|
4235
|
-
//
|
|
4236
|
-
// Promise.resolve either way: a non-watch run returns its exit class
|
|
4237
|
-
// synchronously (and has already written its report), while `--watch`
|
|
4238
|
-
// resolves only when the watch ends — so one await-shaped line serves
|
|
4239
|
-
// both without a branch to keep covered.
|
|
4240
4692
|
if ('vet' === argv[2]) {
|
|
4241
4693
|
return void Promise.resolve(runVet(argv.slice(3))).then(finish)
|
|
4242
4694
|
}
|
|
@@ -4264,6 +4716,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4264
4716
|
return finish(runSet(argv.slice(3)))
|
|
4265
4717
|
}
|
|
4266
4718
|
|
|
4719
|
+
if ('allow' === argv[2]) {
|
|
4720
|
+
return finish(runAllow(argv.slice(3)))
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4267
4723
|
if ('why' === argv[2]) {
|
|
4268
4724
|
return finish(runWhy(argv.slice(3)))
|
|
4269
4725
|
}
|
|
@@ -4276,6 +4732,20 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4276
4732
|
return finish(runHash(argv.slice(3)))
|
|
4277
4733
|
}
|
|
4278
4734
|
|
|
4735
|
+
// G11 phases 1 and 3. Dispatched with the rest, so `aontu ./help`
|
|
4736
|
+
// still reads a file named help exactly as `aontu ./vet` does.
|
|
4737
|
+
if ('help' === argv[2]) {
|
|
4738
|
+
return finish(runHelp(argv.slice(3)))
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
if ('explain' === argv[2]) {
|
|
4742
|
+
return finish(runExplain(argv.slice(3)))
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
if ('init' === argv[2]) {
|
|
4746
|
+
return finish(runInit(argv.slice(3)))
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4279
4749
|
if ('mod' === argv[2]) {
|
|
4280
4750
|
return finish(runMod(argv.slice(3)))
|
|
4281
4751
|
}
|
|
@@ -4331,6 +4801,14 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4331
4801
|
}
|
|
4332
4802
|
trust = parsed
|
|
4333
4803
|
}
|
|
4804
|
+
else if ('--format' === arg) {
|
|
4805
|
+
const f = args[++i]
|
|
4806
|
+
if ('text' !== f && 'json' !== f) {
|
|
4807
|
+
process.stderr.write('aontu: --format needs text or json\n')
|
|
4808
|
+
return finish(2)
|
|
4809
|
+
}
|
|
4810
|
+
format = f
|
|
4811
|
+
}
|
|
4334
4812
|
else if ('--jsonl' === arg) {
|
|
4335
4813
|
jsonl = true
|
|
4336
4814
|
// A JSONL answer is machine-read by definition, even when the
|
|
@@ -4367,14 +4845,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4367
4845
|
}
|
|
4368
4846
|
}
|
|
4369
4847
|
|
|
4370
|
-
// ONE DOCUMENT. The bare form has always been `aontu [options]
|
|
4371
|
-
// [file]`, singular, and anything past the first was silently
|
|
4372
|
-
// discarded rather than refused -- so every way of getting the verb
|
|
4373
|
-
// wrong (a typo, a verb this port does not have, a verb spelled for
|
|
4374
|
-
// another tool) ended in a plausible answer about the wrong file.
|
|
4375
|
-
// Exit 2, the usage class, and the message names the cause rather
|
|
4376
|
-
// than the symptom: nothing here can tell a mistyped verb from a
|
|
4377
|
-
// second file, but the reader can.
|
|
4378
4848
|
if (1 < files.length) {
|
|
4379
4849
|
process.stderr.write(
|
|
4380
4850
|
`aontu: the bare command evaluates one document, and ${files.length}` +
|
|
@@ -4383,14 +4853,11 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4383
4853
|
return finish(2)
|
|
4384
4854
|
}
|
|
4385
4855
|
|
|
4386
|
-
// The extensions ride with the capability from here on, so the three
|
|
4387
|
-
// entry shapes below (file, REPL, stdin) each get them by threading
|
|
4388
|
-
// the one value they already thread.
|
|
4389
4856
|
trust = { ...trust, textExt }
|
|
4390
4857
|
|
|
4391
4858
|
const file = files[0]
|
|
4392
4859
|
if (null != file) {
|
|
4393
|
-
finish(runFile(file, mode, trust))
|
|
4860
|
+
finish(runFile(file, mode, format, trust))
|
|
4394
4861
|
}
|
|
4395
4862
|
// `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
|
|
4396
4863
|
// a harness over a pipe, so gating it on an interactive terminal
|
|
@@ -4400,9 +4867,9 @@ function main(argv: string[], servers: Servers = SERVERS): void {
|
|
|
4400
4867
|
runRepl(mode, jsonl, trust)
|
|
4401
4868
|
}
|
|
4402
4869
|
else {
|
|
4403
|
-
runStdin(mode, trust).then((code) => finish(code))
|
|
4870
|
+
runStdin(mode, format, trust).then((code) => finish(code))
|
|
4404
4871
|
}
|
|
4405
|
-
} /* node:coverage ignore next
|
|
4872
|
+
} /* node:coverage ignore next 20 */
|
|
4406
4873
|
|
|
4407
4874
|
|
|
4408
4875
|
// No require.main guard here: bin/aontu.js is the executable entry and
|
|
@@ -4417,7 +4884,9 @@ export {
|
|
|
4417
4884
|
runRender,
|
|
4418
4885
|
runTemplate,
|
|
4419
4886
|
runMod,
|
|
4420
|
-
runHash, runGet,
|
|
4421
|
-
|
|
4887
|
+
runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
|
|
4888
|
+
looksLikeVerb,
|
|
4889
|
+
KNOWN_VERBS,
|
|
4890
|
+
runWhy, renderWhyText, runSet, runAllow, runAgentsMd, runFmt,
|
|
4422
4891
|
watchChange, watchSignature, vetWaiter, deprecatedAt,
|
|
4423
4892
|
}
|