aontu 0.52.0 → 0.53.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 +88 -0
- package/bin/aontu-mcp.js +4 -0
- package/dist/agentsmd.d.ts +16 -0
- package/dist/agentsmd.js +107 -0
- package/dist/agentsmd.js.map +1 -0
- package/dist/aontu.d.ts +14 -3
- package/dist/aontu.js +145 -4
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +44 -1
- package/dist/cli.js +2401 -44
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +16 -0
- package/dist/ctx.js +44 -0
- package/dist/ctx.js.map +1 -1
- package/dist/diff.d.ts +22 -0
- package/dist/diff.js +141 -0
- package/dist/diff.js.map +1 -0
- package/dist/err.d.ts +3 -1
- package/dist/err.js +48 -8
- package/dist/err.js.map +1 -1
- package/dist/graph.d.ts +16 -0
- package/dist/graph.js +73 -0
- package/dist/graph.js.map +1 -0
- package/dist/hcanon.d.ts +3 -0
- package/dist/hcanon.js +146 -0
- package/dist/hcanon.js.map +1 -0
- package/dist/hints.js +223 -5
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.d.ts +20 -0
- package/dist/jsonschema.js +391 -0
- package/dist/jsonschema.js.map +1 -0
- package/dist/lang.js +698 -35
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +9 -2
- package/dist/lsp.js +262 -46
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.d.ts +20 -0
- package/dist/mcp-server.js +147 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.js +814 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mod-tool.d.ts +58 -0
- package/dist/mod-tool.js +498 -0
- package/dist/mod-tool.js.map +1 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +250 -0
- package/dist/mod.js.map +1 -0
- package/dist/patch.d.ts +44 -0
- package/dist/patch.js +506 -0
- package/dist/patch.js.map +1 -0
- package/dist/provenance.d.ts +40 -0
- package/dist/provenance.js +335 -0
- package/dist/provenance.js.map +1 -0
- package/dist/query.d.ts +27 -0
- package/dist/query.js +294 -0
- package/dist/query.js.map +1 -0
- package/dist/reach.d.ts +14 -0
- package/dist/reach.js +140 -0
- package/dist/reach.js.map +1 -0
- package/dist/relation.d.ts +19 -0
- package/dist/relation.js +305 -0
- package/dist/relation.js.map +1 -0
- package/dist/report-sarif.d.ts +14 -0
- package/dist/report-sarif.js +102 -0
- package/dist/report-sarif.js.map +1 -0
- package/dist/site.d.ts +4 -0
- package/dist/site.js +31 -0
- package/dist/site.js.map +1 -1
- package/dist/std.d.ts +1 -0
- package/dist/std.js +73 -0
- package/dist/std.js.map +1 -0
- package/dist/subsume.d.ts +39 -0
- package/dist/subsume.js +526 -0
- package/dist/subsume.js.map +1 -0
- package/dist/trim.d.ts +19 -0
- package/dist/trim.js +155 -0
- package/dist/trim.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +17 -1
- package/dist/type.js.map +1 -1
- package/dist/unify.d.ts +3 -1
- package/dist/unify.js +287 -18
- package/dist/unify.js.map +1 -1
- package/dist/utility.d.ts +9 -1
- package/dist/utility.js +122 -1
- package/dist/utility.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +33 -0
- package/dist/val/AggFuncVal.js +202 -0
- package/dist/val/AggFuncVal.js.map +1 -0
- package/dist/val/ArithFuncVal.d.ts +31 -0
- package/dist/val/ArithFuncVal.js +62 -0
- package/dist/val/ArithFuncVal.js.map +1 -0
- package/dist/val/BagVal.d.ts +5 -0
- package/dist/val/BagVal.js +96 -5
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/CloseFuncVal.js +9 -1
- package/dist/val/CloseFuncVal.js.map +1 -1
- package/dist/val/ConjunctVal.d.ts +1 -1
- package/dist/val/ConjunctVal.js +19 -0
- package/dist/val/ConjunctVal.js.map +1 -1
- package/dist/val/ConstraintVal.d.ts +48 -1
- package/dist/val/ConstraintVal.js +1501 -110
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/CopyFuncVal.d.ts +1 -2
- package/dist/val/CopyFuncVal.js +7 -0
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/Decimal.d.ts +1 -0
- package/dist/val/Decimal.js +13 -0
- package/dist/val/Decimal.js.map +1 -1
- package/dist/val/DeprecateFuncVal.d.ts +11 -0
- package/dist/val/DeprecateFuncVal.js +47 -0
- package/dist/val/DeprecateFuncVal.js.map +1 -0
- package/dist/val/DisjunctVal.js +130 -21
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/EachFuncVal.d.ts +15 -0
- package/dist/val/EachFuncVal.js +75 -0
- package/dist/val/EachFuncVal.js.map +1 -0
- package/dist/val/ExpectVal.d.ts +1 -0
- package/dist/val/ExpectVal.js +41 -4
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FeatureVal.js +1 -1
- package/dist/val/FeatureVal.js.map +1 -1
- package/dist/val/FilterFuncVal.d.ts +15 -0
- package/dist/val/FilterFuncVal.js +91 -0
- package/dist/val/FilterFuncVal.js.map +1 -0
- package/dist/val/FuncBaseVal.d.ts +6 -1
- package/dist/val/FuncBaseVal.js +178 -2
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/HideFuncVal.js.map +1 -1
- package/dist/val/IdFuncVal.d.ts +13 -0
- package/dist/val/IdFuncVal.js +54 -0
- package/dist/val/IdFuncVal.js.map +1 -0
- package/dist/val/JunctionVal.js +7 -1
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/KeyFuncVal.d.ts +1 -1
- package/dist/val/KeyFuncVal.js +38 -30
- package/dist/val/KeyFuncVal.js.map +1 -1
- package/dist/val/ListVal.js +117 -17
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +102 -8
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.d.ts +15 -0
- package/dist/val/MatchFuncVal.js +107 -0
- package/dist/val/MatchFuncVal.js.map +1 -0
- package/dist/val/MoveFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +24 -0
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -1
- package/dist/val/OpBaseVal.js +24 -2
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/OpenFuncVal.js +4 -1
- package/dist/val/OpenFuncVal.js.map +1 -1
- package/dist/val/PackFuncVal.d.ts +15 -0
- package/dist/val/PackFuncVal.js +108 -0
- package/dist/val/PackFuncVal.js.map +1 -0
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +13 -0
- package/dist/val/PlaceVal.js +131 -0
- package/dist/val/PlaceVal.js.map +1 -0
- package/dist/val/PlusOpVal.js +11 -2
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefFuncVal.js.map +1 -1
- package/dist/val/PrefVal.d.ts +2 -2
- package/dist/val/PrefVal.js +78 -23
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -1
- package/dist/val/RefVal.js +158 -30
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.d.ts +36 -0
- package/dist/val/ReferFuncVal.js +303 -0
- package/dist/val/ReferFuncVal.js.map +1 -0
- package/dist/val/ScalarKindVal.d.ts +1 -2
- package/dist/val/ScalarKindVal.js +0 -11
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/TopVal.js.map +1 -1
- package/dist/val/TypeFuncVal.js.map +1 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/Val.d.ts +9 -2
- package/dist/val/Val.js +150 -4
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js.map +1 -1
- package/dist/val/arith.d.ts +6 -0
- package/dist/val/arith.js +170 -0
- package/dist/val/arith.js.map +1 -0
- package/dist/vet.d.ts +45 -0
- package/dist/vet.js +776 -0
- package/dist/vet.js.map +1 -0
- package/dist/walk.d.ts +2 -0
- package/dist/walk.js +91 -0
- package/dist/walk.js.map +1 -0
- package/grammar/aontu.gbnf +130 -0
- package/grammar/aontu.lark +113 -0
- package/package.json +30 -15
- package/skill/SKILL.md +37 -0
- package/skill/error-codes.md +62 -0
- package/skill/examples.md +99 -0
- package/skill/grammar-card.md +57 -0
- package/src/agentsmd.ts +135 -0
- package/src/aontu.ts +192 -4
- package/src/cli.ts +2858 -71
- package/src/ctx.ts +81 -0
- package/src/diff.ts +196 -0
- package/src/err.ts +52 -8
- package/src/graph.ts +135 -0
- package/src/hcanon.ts +169 -0
- package/src/hints.ts +271 -5
- package/src/jsonschema.ts +511 -0
- package/src/lang.ts +779 -37
- package/src/lsp.ts +281 -47
- package/src/mcp-server.ts +187 -0
- package/src/mcp.ts +993 -0
- package/src/mod-tool.ts +679 -0
- package/src/mod.ts +344 -0
- package/src/patch.ts +624 -0
- package/src/provenance.ts +430 -0
- package/src/query.ts +379 -0
- package/src/reach.ts +184 -0
- package/src/relation.ts +395 -0
- package/src/report-sarif.ts +137 -0
- package/src/site.ts +36 -1
- package/src/std.ts +73 -0
- package/src/subsume.ts +690 -0
- package/src/trim.ts +195 -0
- package/src/tsconfig.json +10 -4
- package/src/type.ts +51 -2
- package/src/unify.ts +311 -16
- package/src/utility.ts +139 -1
- package/src/val/AggFuncVal.ts +319 -0
- package/src/val/ArithFuncVal.ts +108 -0
- package/src/val/BagVal.ts +101 -4
- package/src/val/CloseFuncVal.ts +9 -1
- package/src/val/ConjunctVal.ts +20 -0
- package/src/val/ConstraintVal.ts +1699 -116
- package/src/val/CopyFuncVal.ts +7 -1
- package/src/val/Decimal.ts +15 -0
- package/src/val/DeprecateFuncVal.ts +84 -0
- package/src/val/DisjunctVal.ts +139 -28
- package/src/val/EachFuncVal.ts +133 -0
- package/src/val/ExpectVal.ts +43 -6
- package/src/val/FeatureVal.ts +1 -1
- package/src/val/FilterFuncVal.ts +154 -0
- package/src/val/FuncBaseVal.ts +200 -3
- package/src/val/HideFuncVal.ts +0 -2
- package/src/val/IdFuncVal.ts +91 -0
- package/src/val/JunctionVal.ts +7 -1
- package/src/val/KeyFuncVal.ts +39 -35
- package/src/val/ListVal.ts +125 -18
- package/src/val/LowerFuncVal.ts +0 -1
- package/src/val/MapVal.ts +110 -8
- package/src/val/MatchFuncVal.ts +176 -0
- package/src/val/MoveFuncVal.ts +0 -2
- package/src/val/NilVal.ts +25 -0
- package/src/val/OpBaseVal.ts +26 -3
- package/src/val/OpenFuncVal.ts +4 -2
- package/src/val/PackFuncVal.ts +175 -0
- package/src/val/PathFuncVal.ts +0 -1
- package/src/val/PlaceVal.ts +193 -0
- package/src/val/PlusOpVal.ts +11 -2
- package/src/val/PrefFuncVal.ts +0 -1
- package/src/val/PrefVal.ts +79 -36
- package/src/val/RefVal.ts +163 -30
- package/src/val/ReferFuncVal.ts +387 -0
- package/src/val/ScalarKindVal.ts +0 -13
- package/src/val/TopVal.ts +0 -1
- package/src/val/TypeFuncVal.ts +0 -2
- package/src/val/UpperFuncVal.ts +0 -1
- package/src/val/Val.ts +213 -3
- package/src/val/VarVal.ts +0 -1
- package/src/val/arith.ts +316 -0
- package/src/vet.ts +992 -0
- package/src/walk.ts +99 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.vetWaiter = void 0;
|
|
5
|
+
exports.replCommand = replCommand;
|
|
4
6
|
exports.evalSource = evalSource;
|
|
5
7
|
exports.main = main;
|
|
8
|
+
exports.runVet = runVet;
|
|
9
|
+
exports.runSubsume = runSubsume;
|
|
10
|
+
exports.runBreaking = runBreaking;
|
|
11
|
+
exports.runTrim = runTrim;
|
|
12
|
+
exports.runRelations = runRelations;
|
|
13
|
+
exports.runReaches = runReaches;
|
|
14
|
+
exports.runJsonSchema = runJsonSchema;
|
|
15
|
+
exports.runMod = runMod;
|
|
16
|
+
exports.runHash = runHash;
|
|
17
|
+
exports.runGet = runGet;
|
|
18
|
+
exports.runWhy = runWhy;
|
|
19
|
+
exports.renderWhyText = renderWhyText;
|
|
20
|
+
exports.runSet = runSet;
|
|
21
|
+
exports.runAgentsMd = runAgentsMd;
|
|
22
|
+
exports.watchChange = watchChange;
|
|
23
|
+
exports.watchSignature = watchSignature;
|
|
24
|
+
exports.deprecatedAt = deprecatedAt;
|
|
6
25
|
// Command-line interface for Aontu.
|
|
7
26
|
//
|
|
8
27
|
// aontu [options] [file]
|
|
@@ -11,24 +30,181 @@ exports.main = main;
|
|
|
11
30
|
// With no file on an interactive terminal, a REPL is started. With no
|
|
12
31
|
// file and piped input, the source is read from stdin. See HELP below.
|
|
13
32
|
// Named imports, not `import * as`: the namespace form makes tsc emit the
|
|
33
|
+
const query_1 = require("./query");
|
|
14
34
|
// __importStar downlevel helper, whose branches no supported Node takes.
|
|
15
35
|
const node_fs_1 = require("node:fs");
|
|
16
36
|
const node_path_1 = require("node:path");
|
|
37
|
+
const node_os_1 = require("node:os");
|
|
17
38
|
const node_readline_1 = require("node:readline");
|
|
18
39
|
const aontu_1 = require("./aontu");
|
|
40
|
+
const report_sarif_1 = require("./report-sarif");
|
|
41
|
+
const jsonschema_1 = require("./jsonschema");
|
|
42
|
+
const mod_tool_1 = require("./mod-tool");
|
|
43
|
+
const mod_1 = require("./mod");
|
|
44
|
+
const vet_1 = require("./vet");
|
|
45
|
+
const reach_1 = require("./reach");
|
|
46
|
+
const agentsmd_1 = require("./agentsmd");
|
|
19
47
|
const HELP = `Usage: aontu [options] [file]
|
|
48
|
+
aontu vet [options] <schema> <data> [more-data...]
|
|
49
|
+
aontu subsume [options] <general> <specific>
|
|
50
|
+
aontu breaking --against <file|git#rev> [options] <file>
|
|
51
|
+
aontu trim --check [options] <file>
|
|
52
|
+
aontu relations [options] <file>
|
|
53
|
+
aontu reaches <from> <to> [--relation <name>] [options] <file>
|
|
54
|
+
aontu jsonschema [--at <path>] [--strict] [options] <file>
|
|
55
|
+
aontu hash [options] <file>
|
|
56
|
+
aontu mod tidy|verify|vendor|manifest [options] [dir]
|
|
57
|
+
aontu get <path> [options] <file>
|
|
58
|
+
aontu why <path> [options] <file>
|
|
59
|
+
aontu set <path>=<value>... --entry <file> --overlay <file>
|
|
60
|
+
aontu agentsmd [--write <AGENTS.md>] <file>
|
|
20
61
|
|
|
21
62
|
Evaluate an Aontu source file and print the result as JSON.
|
|
22
63
|
With no file on an interactive terminal, start a REPL.
|
|
23
64
|
With no file and piped input, read the source from stdin.
|
|
24
65
|
|
|
66
|
+
The vet verb validates data documents against a schema document and
|
|
67
|
+
reports what does not hold, as text or as a machine-readable object.
|
|
68
|
+
|
|
69
|
+
The subsume verb asks whether every instance the specific document
|
|
70
|
+
admits, the general document admits too. The breaking verb runs that
|
|
71
|
+
query between a document and its own earlier versions.
|
|
72
|
+
|
|
25
73
|
Options:
|
|
26
74
|
-c, --canon Print the canonical form instead of generated JSON
|
|
27
75
|
-h, --help Show this help and exit
|
|
76
|
+
--jsonl REPL: answer every command as one JSON line
|
|
28
77
|
-v, --version Print the version and exit
|
|
78
|
+
--trust <t> Include capability: system (default), none, or
|
|
79
|
+
root[:dir] to confine @"..." below a directory.
|
|
80
|
+
Every verb takes it too, and a bare root means the
|
|
81
|
+
document's own directory
|
|
82
|
+
--include-root <dir> Shorthand for --trust root:<dir>
|
|
83
|
+
|
|
84
|
+
Mod options:
|
|
85
|
+
--format <f> text (default) or json
|
|
86
|
+
--against <dir> manifest: a prior version's module tree, to gate on
|
|
87
|
+
|
|
88
|
+
Mod subcommands:
|
|
89
|
+
tidy Resolve the module closure by minimum version selection and
|
|
90
|
+
rewrite mod-lock.aon in canonical form
|
|
91
|
+
verify Check every locked module still means what mod-lock.aon
|
|
92
|
+
pins, and change nothing (the CI gate; tidy rewrites)
|
|
93
|
+
vendor Materialise the locked closure into aon_vendor/
|
|
94
|
+
manifest Print the OCI artifact a publish would push, gated on the
|
|
95
|
+
breaking check against --against
|
|
96
|
+
|
|
97
|
+
Vet options:
|
|
98
|
+
--at <path> Validate against this path of the schema ($.a.b)
|
|
99
|
+
--closed Refuse keys the anchor does not declare
|
|
100
|
+
--partial Residue is reported but does not fail the run
|
|
101
|
+
--max-errors <n> Cap the finding list (default 20)
|
|
102
|
+
--format <f> text (default), json or sarif
|
|
103
|
+
--watch Re-run whenever a watched file changes
|
|
104
|
+
|
|
105
|
+
Vet exit codes:
|
|
106
|
+
0 valid data unifies, and is concrete (or --partial)
|
|
107
|
+
1 invalid at least one contradiction
|
|
108
|
+
2 usage bad option, or a file that cannot be read
|
|
109
|
+
3 incomplete no contradiction, but the truth is not yet satisfied
|
|
110
|
+
4 error the schema is unusable on its own
|
|
111
|
+
|
|
112
|
+
Subsume options:
|
|
113
|
+
--profile <p> values, defaults (default) or gen
|
|
114
|
+
--at <path> Compare at this path of both documents ($.a.b)
|
|
115
|
+
--format <f> text (default) or json
|
|
116
|
+
|
|
117
|
+
Subsume exit codes:
|
|
118
|
+
0 subsumes every specific instance is admitted
|
|
119
|
+
1 does_not_subsume a witness exists (see the findings)
|
|
120
|
+
2 usage bad option, or a file that cannot be read
|
|
121
|
+
3 undecided no rule decides (a sub_* reason is reported)
|
|
122
|
+
4 error a document does not stand up on its own
|
|
123
|
+
|
|
124
|
+
Breaking options:
|
|
125
|
+
--against <v> An earlier version: a file path, or git#<rev>
|
|
126
|
+
(resolved by 'git show'); repeatable
|
|
127
|
+
--at <path> Compare this path of both versions ($.a.b), so a
|
|
128
|
+
module's own version string and policy block do
|
|
129
|
+
not decide the verdict
|
|
130
|
+
--mode <m> backward (new admits old, the default), forward
|
|
131
|
+
(old admits new), or full (both); overrides the
|
|
132
|
+
document's own $.aontu_policy.compat declaration
|
|
133
|
+
--allow-undecided Exit 0 on undecided (the report still says so)
|
|
134
|
+
--allow-deprecated-removal
|
|
135
|
+
A finding about a value the old version already
|
|
136
|
+
deprecated warns instead of breaking
|
|
137
|
+
--format <f> text (default) or json
|
|
138
|
+
|
|
139
|
+
Breaking exit codes mirror subsume's: 0 compatible, 1 breaking,
|
|
140
|
+
2 usage, 3 undecided, 4 error.
|
|
141
|
+
|
|
142
|
+
Trim options:
|
|
143
|
+
--check Report redundant entries as paths (required: trim
|
|
144
|
+
only reports for now; rewriting is a future editor)
|
|
145
|
+
--format <f> text (default) or json
|
|
146
|
+
|
|
147
|
+
Trim exit codes: 0 nothing redundant, 1 redundancies reported,
|
|
148
|
+
2 usage, 4 the document does not stand up on its own.
|
|
149
|
+
|
|
150
|
+
Hash options:
|
|
151
|
+
--form Print the hash FORM (the hashed text) instead of the
|
|
152
|
+
hash, which is what to diff when a pin moves
|
|
153
|
+
--format <f> text (default) or json
|
|
154
|
+
|
|
155
|
+
Hash exit codes: 0 hashed, 2 usage, 4 the document does not stand up
|
|
156
|
+
on its own.
|
|
157
|
+
|
|
158
|
+
Get options:
|
|
159
|
+
-c, --canon Canonical-form fragment (default: generated JSON)
|
|
160
|
+
--keys Keys at the node, one per line
|
|
161
|
+
--types Shape view: concrete leaves lifted to their kinds
|
|
162
|
+
--depth <n> Structure to depth n; deeper nodes render as top
|
|
163
|
+
--format <f> text (default) or json
|
|
164
|
+
|
|
165
|
+
Get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4 the
|
|
166
|
+
document does not stand up on its own.
|
|
167
|
+
|
|
168
|
+
Why options:
|
|
169
|
+
--format <f> text (default) or json
|
|
170
|
+
|
|
171
|
+
Why exit codes mirror get's: 0 explained, 1 the path names nothing,
|
|
172
|
+
2 usage, 4 the document does not stand up on its own.
|
|
173
|
+
|
|
174
|
+
Set options:
|
|
175
|
+
--entry <file> The document the change is checked against
|
|
176
|
+
--overlay <file> The file the change is appended to (created if
|
|
177
|
+
absent; not written when the change does not hold)
|
|
178
|
+
--in-place Rewrite a pinned literal where it was written,
|
|
179
|
+
instead of appending a line that contradicts it.
|
|
180
|
+
The span is verified against the source text
|
|
181
|
+
before writing, and where the value is not a
|
|
182
|
+
single editable literal in this overlay the
|
|
183
|
+
assignment is appended as usual with a warning
|
|
184
|
+
saying why
|
|
185
|
+
--dry-run Print the overlay that would be written, write
|
|
186
|
+
nothing
|
|
187
|
+
--format <f> text (default) or json
|
|
188
|
+
|
|
189
|
+
Set exit codes are vet's verdict classes: 0 valid, 1 invalid (the
|
|
190
|
+
change contradicts a pinned value -- aontu why locates it, and
|
|
191
|
+
--in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
|
|
192
|
+
stand up on its own.
|
|
193
|
+
|
|
194
|
+
Agentsmd options:
|
|
195
|
+
--write <file> Splice the stanza into this file between the
|
|
196
|
+
aontu:begin and aontu:end markers, appending them
|
|
197
|
+
when they are absent; the rest is left alone
|
|
198
|
+
|
|
199
|
+
Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
|
|
200
|
+
stand up on its own.
|
|
29
201
|
|
|
30
202
|
REPL commands:
|
|
31
203
|
:help Show REPL help
|
|
204
|
+
:load <file> Evaluate a document and hold it for the commands below
|
|
205
|
+
:get [path] What the held document says at a path
|
|
206
|
+
:keys [path] The keys at a path of the held document
|
|
207
|
+
:why <path> Every contribution to the value at a path
|
|
32
208
|
:canon Switch to canonical-form output
|
|
33
209
|
:json Switch to JSON output
|
|
34
210
|
:quit, :exit Exit the REPL (or press Ctrl-D)
|
|
@@ -64,7 +240,102 @@ function evalSource(aontu, src, mode) {
|
|
|
64
240
|
return { ok: false, text: msg };
|
|
65
241
|
}
|
|
66
242
|
}
|
|
67
|
-
|
|
243
|
+
// The one-line warning of the staged default flip. Once per (kind,
|
|
244
|
+
// path): a fixpoint re-resolves nothing (includes load at parse), but
|
|
245
|
+
// several includes may escape and each deserves exactly one line.
|
|
246
|
+
function makeTrustWarn() {
|
|
247
|
+
const warned = new Set();
|
|
248
|
+
return (kind, path) => {
|
|
249
|
+
const key = kind + ' ' + path;
|
|
250
|
+
if (warned.has(key)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
warned.add(key);
|
|
254
|
+
const how = 'pkg' === kind
|
|
255
|
+
? 'through package resolution'
|
|
256
|
+
: 'outside the entry root';
|
|
257
|
+
process.stderr.write(`aontu: warning: include resolved ${how}: ${path}` +
|
|
258
|
+
` (a future release will deny this by default;` +
|
|
259
|
+
` pass --trust system to keep it, or --include-root to confine)\n`);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Build the evaluator options a TrustArg means, for an entry rooted at
|
|
263
|
+
// entryRoot (the entry file's directory, or the working directory for
|
|
264
|
+
// stdin/REPL).
|
|
265
|
+
function trustOpts(trust, entryRoot) {
|
|
266
|
+
switch (trust.kind) {
|
|
267
|
+
case 'none':
|
|
268
|
+
return { trust: { include: 'none' } };
|
|
269
|
+
case 'root':
|
|
270
|
+
return { trust: { include: { root: trust.dir ?? entryRoot } } };
|
|
271
|
+
case 'system':
|
|
272
|
+
return {};
|
|
273
|
+
default: // system-warn: today's default plus the warning window
|
|
274
|
+
return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// EVERY VERB honours the include capability, not just the bare
|
|
278
|
+
// command. G5 wired `--trust`/`--include-root` to `aontu <file>` alone,
|
|
279
|
+
// so `aontu vet schema.aon data.json` -- the surface an agent actually
|
|
280
|
+
// scripts -- ran the full system resolver with no flag to confine it
|
|
281
|
+
// and no warning (use-cases/REVIEW.md finding G). The flags are
|
|
282
|
+
// stripped here, before each verb parses its own tail, so a verb only
|
|
283
|
+
// has to pass the profile on to its engine.
|
|
284
|
+
//
|
|
285
|
+
// Returns undefined when the spelling is wrong, with the message
|
|
286
|
+
// already printed: the caller answers the usage class.
|
|
287
|
+
function takeTrust(argv) {
|
|
288
|
+
const rest = [];
|
|
289
|
+
let trust = { kind: 'system-warn' };
|
|
290
|
+
for (let i = 0; i < argv.length; i++) {
|
|
291
|
+
const arg = argv[i];
|
|
292
|
+
if ('--trust' === arg) {
|
|
293
|
+
const parsed = null == argv[i + 1] ? undefined : parseTrustArg(argv[++i]);
|
|
294
|
+
if (null == parsed) {
|
|
295
|
+
process.stderr.write('aontu: --trust needs system, none, or root[:dir]\n');
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
trust = parsed;
|
|
299
|
+
}
|
|
300
|
+
else if ('--include-root' === arg) {
|
|
301
|
+
const dir = argv[++i];
|
|
302
|
+
if (null == dir) {
|
|
303
|
+
process.stderr.write('aontu: --include-root needs a directory\n');
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
trust = { kind: 'root', dir };
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
rest.push(arg);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { argv: rest, trust };
|
|
313
|
+
}
|
|
314
|
+
// The evaluator options a REPL session's capability means.
|
|
315
|
+
function replTrust(state, entryRoot) {
|
|
316
|
+
const capability = verbTrust(state.trust ?? { kind: 'system-warn' }, entryRoot);
|
|
317
|
+
return null == capability ? {} : { trust: capability };
|
|
318
|
+
}
|
|
319
|
+
// The capability a verb's engine runs under. `system` and the staged
|
|
320
|
+
// warning default both mean today's behaviour (no option); the warning
|
|
321
|
+
// window itself stays a bare-command nicety, because a verb's report
|
|
322
|
+
// is a machine contract and a stderr line is not part of it.
|
|
323
|
+
function verbTrust(trust, entryRoot) {
|
|
324
|
+
switch (trust.kind) {
|
|
325
|
+
case 'none':
|
|
326
|
+
return { include: 'none' };
|
|
327
|
+
case 'root':
|
|
328
|
+
return { include: { root: trust.dir ?? entryRoot } };
|
|
329
|
+
default:
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// The directory a bare `--trust root` confines to for a verb: the
|
|
334
|
+
// primary document's own, matching the bare command's entry root.
|
|
335
|
+
function entryRootOf(file) {
|
|
336
|
+
return null == file ? process.cwd() : (0, node_path_1.dirname)((0, node_path_1.resolve)(file));
|
|
337
|
+
}
|
|
338
|
+
function runFile(file, mode, trust) {
|
|
68
339
|
let src;
|
|
69
340
|
try {
|
|
70
341
|
src = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
@@ -73,73 +344,2028 @@ function runFile(file, mode) {
|
|
|
73
344
|
process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`);
|
|
74
345
|
return 1;
|
|
75
346
|
}
|
|
76
|
-
const
|
|
347
|
+
const path = (0, node_path_1.resolve)(file);
|
|
348
|
+
const aontu = new aontu_1.Aontu({ path, ...trustOpts(trust, (0, node_path_1.dirname)(path)) });
|
|
77
349
|
const res = evalSource(aontu, src, mode);
|
|
78
350
|
(res.ok ? process.stdout : process.stderr).write(res.text + '\n');
|
|
79
351
|
return res.ok ? 0 : 1;
|
|
80
352
|
}
|
|
81
|
-
function runStdin(mode) {
|
|
353
|
+
function runStdin(mode, trust) {
|
|
82
354
|
return new Promise((resolve) => {
|
|
83
355
|
let src = '';
|
|
84
356
|
process.stdin.setEncoding('utf8');
|
|
85
357
|
process.stdin.on('data', (d) => (src += d));
|
|
86
358
|
process.stdin.on('end', () => {
|
|
87
|
-
const res = evalSource(new aontu_1.Aontu(), src, mode);
|
|
359
|
+
const res = evalSource(new aontu_1.Aontu(trustOpts(trust, process.cwd())), src, mode);
|
|
88
360
|
(res.ok ? process.stdout : process.stderr).write(res.text + '\n');
|
|
89
361
|
resolve(res.ok ? 0 : 1);
|
|
90
362
|
});
|
|
91
363
|
});
|
|
92
364
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
365
|
+
// The loaded document, or the answer to give when there is none.
|
|
366
|
+
function replLoaded(state) {
|
|
367
|
+
return state.src;
|
|
368
|
+
}
|
|
369
|
+
function replCommand(state, line, read) {
|
|
370
|
+
const s = line.trim();
|
|
371
|
+
const answer = (out, next) => {
|
|
372
|
+
const st = { ...state, ...(next ?? {}) };
|
|
373
|
+
return {
|
|
374
|
+
close: false,
|
|
375
|
+
out: st.jsonl ? (0, aontu_1.exactJSON)({ ok: true, out }) : out,
|
|
376
|
+
state: st,
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
const refuse = (out) => ({
|
|
380
|
+
close: false,
|
|
381
|
+
out: state.jsonl ? (0, aontu_1.exactJSON)({ ok: false, out }) : out,
|
|
382
|
+
state,
|
|
383
|
+
});
|
|
384
|
+
if ('' === s) {
|
|
385
|
+
return { close: false, out: '', state };
|
|
386
|
+
}
|
|
387
|
+
if (!s.startsWith(':')) {
|
|
388
|
+
const res = evalSource(new aontu_1.Aontu(replTrust(state, process.cwd())), s, state.mode);
|
|
389
|
+
return res.ok ? answer(res.text) : refuse(res.text);
|
|
390
|
+
}
|
|
391
|
+
const sp = s.indexOf(' ');
|
|
392
|
+
const cmd = sp < 0 ? s : s.slice(0, sp);
|
|
393
|
+
const arg = sp < 0 ? '' : s.slice(sp + 1).trim();
|
|
394
|
+
switch (cmd) {
|
|
395
|
+
case ':help':
|
|
396
|
+
// Trimmed: the loop adds the newline, and the Go REPL answers
|
|
397
|
+
// the same string — a help text that differed by a blank line
|
|
398
|
+
// between the ports would be a parity diff in the one output
|
|
399
|
+
// every user sees first.
|
|
400
|
+
return answer(HELP.replace(/\n$/, ''));
|
|
401
|
+
case ':canon':
|
|
402
|
+
return answer('canon output', { mode: 'canon' });
|
|
403
|
+
case ':json':
|
|
404
|
+
return answer('json output', { mode: 'json' });
|
|
405
|
+
case ':quit':
|
|
406
|
+
case ':exit':
|
|
407
|
+
return { close: true, out: '', state };
|
|
408
|
+
case ':load': {
|
|
409
|
+
if ('' === arg) {
|
|
410
|
+
return refuse(':load needs a file');
|
|
411
|
+
}
|
|
412
|
+
let src;
|
|
413
|
+
try {
|
|
414
|
+
src = read(arg);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return refuse(`cannot read ${arg}: ${err.message}`);
|
|
418
|
+
}
|
|
419
|
+
// Evaluated ONCE, and what is held is the source: parsed trees
|
|
420
|
+
// are single-use, so every later question re-evaluates from the
|
|
421
|
+
// text rather than reusing a tree that has already been spent.
|
|
422
|
+
const res = evalSource(new aontu_1.Aontu({ path: arg, ...replTrust(state, (0, node_path_1.dirname)((0, node_path_1.resolve)(arg))) }), src, state.mode);
|
|
423
|
+
return res.ok
|
|
424
|
+
? answer(`loaded: ${arg}\n${res.text}`, { name: arg, src })
|
|
425
|
+
: refuse(res.text);
|
|
426
|
+
}
|
|
427
|
+
case ':get':
|
|
428
|
+
case ':keys':
|
|
429
|
+
case ':why': {
|
|
430
|
+
const src = replLoaded(state);
|
|
431
|
+
if (null == src) {
|
|
432
|
+
return refuse('nothing loaded (try :load <file>)');
|
|
433
|
+
}
|
|
434
|
+
const path = '' === arg ? '$' : arg;
|
|
435
|
+
if (':why' === cmd) {
|
|
436
|
+
const report = (0, aontu_1.why)(src, path, {
|
|
437
|
+
path: state.name,
|
|
438
|
+
trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
|
|
439
|
+
});
|
|
440
|
+
return report.ok
|
|
441
|
+
? answer(renderWhyText(report.record))
|
|
442
|
+
: refuse(report.findings.map(renderFinding).join('\n'));
|
|
443
|
+
}
|
|
444
|
+
const view = ':keys' === cmd
|
|
445
|
+
? 'keys' : 'canon' === state.mode ? 'canon' : 'json';
|
|
446
|
+
const report = (0, aontu_1.get)(src, path, {
|
|
447
|
+
view, path: state.name,
|
|
448
|
+
trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
|
|
449
|
+
});
|
|
450
|
+
return report.ok
|
|
451
|
+
? answer(report.out)
|
|
452
|
+
: refuse(report.findings.map(renderFinding).join('\n'));
|
|
453
|
+
}
|
|
454
|
+
default:
|
|
455
|
+
return refuse(`unknown command: ${s} (try :help)`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function runRepl(initialMode, jsonl, trust) {
|
|
459
|
+
let state = { mode: initialMode, jsonl, trust };
|
|
96
460
|
const rl = (0, node_readline_1.createInterface)({
|
|
97
461
|
input: process.stdin,
|
|
98
462
|
output: process.stdout,
|
|
99
|
-
prompt: 'aontu> ',
|
|
463
|
+
prompt: jsonl ? '' : 'aontu> ',
|
|
100
464
|
});
|
|
101
|
-
|
|
465
|
+
if (!jsonl) {
|
|
466
|
+
process.stdout.write(`Aontu v${version()} REPL — :help for commands, :quit to exit\n`);
|
|
467
|
+
}
|
|
102
468
|
rl.prompt();
|
|
103
469
|
rl.on('line', (line) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
470
|
+
const res = replCommand(state, line, (f) => (0, node_fs_1.readFileSync)(f, 'utf8'));
|
|
471
|
+
state = res.state;
|
|
472
|
+
if (res.close) {
|
|
473
|
+
rl.close();
|
|
107
474
|
return;
|
|
108
475
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
case ':help':
|
|
112
|
-
process.stdout.write(HELP);
|
|
113
|
-
break;
|
|
114
|
-
case ':canon':
|
|
115
|
-
mode = 'canon';
|
|
116
|
-
process.stdout.write('canon output\n');
|
|
117
|
-
break;
|
|
118
|
-
case ':json':
|
|
119
|
-
mode = 'json';
|
|
120
|
-
process.stdout.write('json output\n');
|
|
121
|
-
break;
|
|
122
|
-
case ':quit':
|
|
123
|
-
case ':exit':
|
|
124
|
-
rl.close();
|
|
125
|
-
return;
|
|
126
|
-
default: process.stdout.write(`unknown command: ${s} (try :help)\n`);
|
|
127
|
-
}
|
|
128
|
-
rl.prompt();
|
|
129
|
-
return;
|
|
476
|
+
if ('' !== res.out) {
|
|
477
|
+
process.stdout.write(res.out + '\n');
|
|
130
478
|
}
|
|
131
|
-
const res = evalSource(aontu, s, mode);
|
|
132
|
-
process.stdout.write(res.text + '\n');
|
|
133
479
|
rl.prompt();
|
|
134
480
|
});
|
|
135
481
|
rl.on('close', () => {
|
|
136
|
-
|
|
482
|
+
// The closing newline is for a HUMAN, so it is written only for
|
|
483
|
+
// one: it moves the terminal off the prompt line that `rl` left
|
|
484
|
+
// hanging. In `--jsonl` there is no prompt, every answer already
|
|
485
|
+
// ends in its own newline, and this one appended a bare empty line
|
|
486
|
+
// to the stream -- a record that is not JSON, at the end of a
|
|
487
|
+
// protocol whose whole contract is one JSON object per line. A
|
|
488
|
+
// harness parsing every line it receives failed on it, after the
|
|
489
|
+
// commands had all succeeded. Mirrors go/cmd/aontu/repl.go.
|
|
490
|
+
if (!jsonl) {
|
|
491
|
+
process.stdout.write('\n');
|
|
492
|
+
}
|
|
137
493
|
// Same reason as finish(): the REPL requires a TTY stdin, but stdout
|
|
138
494
|
// can still be a pipe (`aontu | cat`), so exiting outright could
|
|
139
495
|
// discard queued output here too.
|
|
140
496
|
process.exitCode = 0;
|
|
141
497
|
});
|
|
142
498
|
}
|
|
499
|
+
// THE VET VERB (G2 phase 3).
|
|
500
|
+
//
|
|
501
|
+
// Exit codes are VERDICT CLASSES, not a pass/fail bit: an agent loop
|
|
502
|
+
// branches on "the data contradicts the truth" (1) differently from
|
|
503
|
+
// "the data has not supplied everything the truth requires" (3), and
|
|
504
|
+
// differently again from "the schema itself is broken" (4), which is
|
|
505
|
+
// never the data's fault. 2 stays what it already was for this CLI --
|
|
506
|
+
// the caller got the invocation wrong -- which is why an unreadable
|
|
507
|
+
// file is a 2 rather than a 4.
|
|
508
|
+
const VET_EXIT = {
|
|
509
|
+
valid: 0,
|
|
510
|
+
invalid: 1,
|
|
511
|
+
incomplete: 3,
|
|
512
|
+
error: 4,
|
|
513
|
+
};
|
|
514
|
+
const VET_HELP = 'aontu vet <schema> <data> [more-data...] (try --help)';
|
|
515
|
+
// Parse the verb's argv tail. Returns the error text instead of
|
|
516
|
+
// throwing, so the caller owns the exit code.
|
|
517
|
+
function parseVetArgs(argv) {
|
|
518
|
+
const files = [];
|
|
519
|
+
let format = 'text';
|
|
520
|
+
let at;
|
|
521
|
+
let closed = false;
|
|
522
|
+
let partial = false;
|
|
523
|
+
let maxErrors;
|
|
524
|
+
let watch = false;
|
|
525
|
+
for (let i = 0; i < argv.length; i++) {
|
|
526
|
+
const arg = argv[i];
|
|
527
|
+
// `-h`/`--help` before anything else, INCLUDING the file count:
|
|
528
|
+
// the usage errors below all end with "(try --help)", and a verb
|
|
529
|
+
// that then refused --help as an unknown option was sending the
|
|
530
|
+
// reader in a circle.
|
|
531
|
+
if ('-h' === arg || '--help' === arg) {
|
|
532
|
+
return { args: { help: true, schema: '', data: [], format } };
|
|
533
|
+
}
|
|
534
|
+
if ('--at' === arg) {
|
|
535
|
+
at = argv[++i];
|
|
536
|
+
if (null == at) {
|
|
537
|
+
return { err: 'aontu: --at needs a path' };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if ('--format' === arg) {
|
|
541
|
+
const f = argv[++i];
|
|
542
|
+
if ('text' !== f && 'json' !== f && 'sarif' !== f) {
|
|
543
|
+
return { err: `aontu: --format needs text, json or sarif` };
|
|
544
|
+
}
|
|
545
|
+
format = f;
|
|
546
|
+
}
|
|
547
|
+
else if ('--max-errors' === arg) {
|
|
548
|
+
// ONE GRAMMAR, spelled the same way in both ports: decimal
|
|
549
|
+
// digits, one to nine of them, at least 1. `Number()` alone
|
|
550
|
+
// accepted `1.0`, `1e2`, `0x10` and ` 3`, which Go's parser
|
|
551
|
+
// refuses -- so the same documented invocation meant different
|
|
552
|
+
// things in the two shipped commands. The nine-digit ceiling is
|
|
553
|
+
// where the ports would part company again: beyond it Go's
|
|
554
|
+
// integer conversion saturates, and a cap nobody can reach is
|
|
555
|
+
// not worth a divergence.
|
|
556
|
+
const raw = argv[++i];
|
|
557
|
+
if (!/^[0-9]{1,9}$/.test(raw ?? '') || 1 > Number(raw)) {
|
|
558
|
+
return { err: 'aontu: --max-errors needs a positive whole number' };
|
|
559
|
+
}
|
|
560
|
+
maxErrors = Number(raw);
|
|
561
|
+
}
|
|
562
|
+
else if ('--closed' === arg) {
|
|
563
|
+
closed = true;
|
|
564
|
+
}
|
|
565
|
+
else if ('--partial' === arg) {
|
|
566
|
+
partial = true;
|
|
567
|
+
}
|
|
568
|
+
else if ('--watch' === arg) {
|
|
569
|
+
watch = true;
|
|
570
|
+
}
|
|
571
|
+
else if (arg.startsWith('-')) {
|
|
572
|
+
return { err: `aontu: unknown vet option ${arg} (try --help)` };
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
files.push(arg);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (files.length < 2) {
|
|
579
|
+
return { err: `aontu: vet needs a schema and at least one data file\n${VET_HELP}` };
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
args: {
|
|
583
|
+
schema: files[0],
|
|
584
|
+
data: files.slice(1),
|
|
585
|
+
format,
|
|
586
|
+
at,
|
|
587
|
+
closed,
|
|
588
|
+
partial,
|
|
589
|
+
maxErrors,
|
|
590
|
+
watch,
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// One line per site, so a finding reads as "what is wrong, where the
|
|
595
|
+
// data says it, and where the truth says otherwise". The data site
|
|
596
|
+
// comes first because it is the one to edit.
|
|
597
|
+
function renderFinding(f) {
|
|
598
|
+
const out = [`${f.path}: ${f.code} [${f.class}]`];
|
|
599
|
+
if ('' !== f.message) {
|
|
600
|
+
out.push(` ${f.message}`);
|
|
601
|
+
}
|
|
602
|
+
if (null != f.note) {
|
|
603
|
+
out.push(` note: ${f.note}`);
|
|
604
|
+
}
|
|
605
|
+
if (null != f.expected) {
|
|
606
|
+
out.push(` expected: ${f.expected}`);
|
|
607
|
+
}
|
|
608
|
+
if (null != f.actual) {
|
|
609
|
+
out.push(` actual: ${f.actual}`);
|
|
610
|
+
}
|
|
611
|
+
for (const s of f.sites) {
|
|
612
|
+
// Every site carries the canon of the value it stands for: that is
|
|
613
|
+
// what makes the two sides of a conflict readable side by side. A
|
|
614
|
+
// site's file is always a string -- empty when the value belongs to
|
|
615
|
+
// neither document -- so there is nothing to coalesce here.
|
|
616
|
+
out.push(` ${s.role}: ${s.file}:${s.row}:${s.col} (${s.value})`);
|
|
617
|
+
}
|
|
618
|
+
return out.join('\n');
|
|
619
|
+
}
|
|
620
|
+
function renderVetText(report) {
|
|
621
|
+
const head = `verdict: ${report.verdict}` +
|
|
622
|
+
(report.truncated ? ' (findings truncated)' : '');
|
|
623
|
+
if (0 === report.findings.length) {
|
|
624
|
+
return head;
|
|
625
|
+
}
|
|
626
|
+
return [head, ''].concat(report.findings.map(renderFinding)).join('\n');
|
|
627
|
+
}
|
|
628
|
+
// The machine-readable form. `aontu` names the producer, so a report
|
|
629
|
+
// read from a file or a pipe says which version and which verb made it
|
|
630
|
+
// without the consumer having to know.
|
|
631
|
+
function renderVetJson(report) {
|
|
632
|
+
return (0, aontu_1.exactJSON)({
|
|
633
|
+
aontu: { version: version(), verb: 'vet' },
|
|
634
|
+
verdict: report.verdict,
|
|
635
|
+
truncated: report.truncated,
|
|
636
|
+
findings: report.findings,
|
|
637
|
+
}, 2);
|
|
638
|
+
}
|
|
639
|
+
// The machine-interchange form (G2 phase 5): SARIF 2.1.0, rendered by
|
|
640
|
+
// the library (ts/src/report-sarif.ts) so an embedder gets the same
|
|
641
|
+
// bytes the CLI prints.
|
|
642
|
+
function renderVetSarif(report) {
|
|
643
|
+
return (0, report_sarif_1.sarifReport)(report, version());
|
|
644
|
+
}
|
|
645
|
+
// The worst verdict wins across data files: a run that is invalid
|
|
646
|
+
// anywhere is invalid, and a schema that cannot stand up makes every
|
|
647
|
+
// file's verdict moot.
|
|
648
|
+
const VET_RANK = {
|
|
649
|
+
valid: 0,
|
|
650
|
+
incomplete: 1,
|
|
651
|
+
invalid: 2,
|
|
652
|
+
error: 3,
|
|
653
|
+
};
|
|
654
|
+
// One complete vet run: read every file, vet each data document, print
|
|
655
|
+
// one report, return the exit class. Split from runVet so `--watch` can
|
|
656
|
+
// repeat it — the files are re-read on every run, which is the point of
|
|
657
|
+
// watching them.
|
|
658
|
+
function vetOnce(args, trust) {
|
|
659
|
+
let schemaSrc;
|
|
660
|
+
const sources = [];
|
|
661
|
+
try {
|
|
662
|
+
schemaSrc = (0, node_fs_1.readFileSync)(args.schema, 'utf8');
|
|
663
|
+
for (const file of args.data) {
|
|
664
|
+
sources.push({ file, src: (0, node_fs_1.readFileSync)(file, 'utf8') });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
669
|
+
return 2;
|
|
670
|
+
}
|
|
671
|
+
// Each data file is vetted on its own, because a parsed tree is
|
|
672
|
+
// single-use (docs/reference-api.md) -- and because two data files
|
|
673
|
+
// are two candidates for the same truth, not one merged candidate.
|
|
674
|
+
let verdict = 'valid';
|
|
675
|
+
let truncated = false;
|
|
676
|
+
const findings = [];
|
|
677
|
+
for (const source of sources) {
|
|
678
|
+
const report = (0, aontu_1.vet)(schemaSrc, source.src, {
|
|
679
|
+
trust: verbTrust(trust, entryRootOf(args.schema)),
|
|
680
|
+
at: args.at,
|
|
681
|
+
closed: args.closed,
|
|
682
|
+
partial: args.partial,
|
|
683
|
+
maxErrors: args.maxErrors,
|
|
684
|
+
schemaUrl: args.schema,
|
|
685
|
+
dataUrl: source.file,
|
|
686
|
+
// The paths as well as the labels: a relative `@"file"` load
|
|
687
|
+
// inside either document resolves from ITS OWN directory, the
|
|
688
|
+
// way `aontu <file>` already resolves one (runFile above). The
|
|
689
|
+
// path is passed AS TYPED, not resolved: it doubles as the
|
|
690
|
+
// label above, and a report that mixed the typed path with an
|
|
691
|
+
// absolute one would name the same file two ways.
|
|
692
|
+
schemaPath: args.schema,
|
|
693
|
+
dataPath: source.file,
|
|
694
|
+
});
|
|
695
|
+
if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
|
|
696
|
+
verdict = report.verdict;
|
|
697
|
+
}
|
|
698
|
+
truncated = truncated || report.truncated;
|
|
699
|
+
findings.push(...report.findings);
|
|
700
|
+
// A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
|
|
701
|
+
// is reported ONCE. `error` means exactly that -- the run could not
|
|
702
|
+
// be set up from the truth's side, never the data's (the exit table
|
|
703
|
+
// in docs/reference-api.md) -- so the report the first file
|
|
704
|
+
// produced is the report every later file would produce, character
|
|
705
|
+
// for character. Concatenating them repeated one broken schema N
|
|
706
|
+
// times and, past the cap, marked the report `truncated` over a
|
|
707
|
+
// single underlying fault. It only became visible once the `error`
|
|
708
|
+
// verdict started carrying findings at all: while the list was
|
|
709
|
+
// empty there was nothing to duplicate.
|
|
710
|
+
if ('error' === report.verdict) {
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// The cap is on the REPORT, not on each file. Capping every file's
|
|
715
|
+
// list and then concatenating them let `--max-errors 1` emit one
|
|
716
|
+
// finding PER FILE -- and leave `truncated` false while doing it,
|
|
717
|
+
// because no single file had been cut. The engine still caps each
|
|
718
|
+
// run, so a pathological file cannot flood the aggregate before it
|
|
719
|
+
// gets here; this is the second, honest cut.
|
|
720
|
+
const cap = args.maxErrors ?? vet_1.VET_MAX_ERRORS;
|
|
721
|
+
const kept = cap < findings.length ? findings.slice(0, cap) : findings;
|
|
722
|
+
const report = {
|
|
723
|
+
verdict,
|
|
724
|
+
truncated: truncated || cap < findings.length,
|
|
725
|
+
findings: kept,
|
|
726
|
+
};
|
|
727
|
+
const text = 'json' === args.format ? renderVetJson(report) :
|
|
728
|
+
'sarif' === args.format ? renderVetSarif(report) :
|
|
729
|
+
renderVetText(report);
|
|
730
|
+
process.stdout.write(text + '\n');
|
|
731
|
+
return VET_EXIT[verdict];
|
|
732
|
+
}
|
|
733
|
+
// How often `--watch` polls for a change. Polling by mtime+size rather
|
|
734
|
+
// than fs.watch: the design asks for "re-run on file mtime change", and
|
|
735
|
+
// the native watcher's semantics differ by platform (rename versus
|
|
736
|
+
// change events, editors that replace the inode) in exactly the ways
|
|
737
|
+
// that made every build tool fall back to polling.
|
|
738
|
+
const WATCH_POLL_MS = 100;
|
|
739
|
+
function watchSignature(files) {
|
|
740
|
+
return files.map((f) => {
|
|
741
|
+
// throwIfNoEntry, not try/catch: a file mid-save can be briefly
|
|
742
|
+
// absent, and "gone" is a state to notice, not an error to die on.
|
|
743
|
+
const stat = (0, node_fs_1.statSync)(f, { throwIfNoEntry: false });
|
|
744
|
+
return null == stat ? 'gone' : `${stat.mtimeMs}:${stat.size}`;
|
|
745
|
+
}).join('\n');
|
|
746
|
+
}
|
|
747
|
+
function sleep(ms) {
|
|
748
|
+
return new Promise((done) => setTimeout(done, ms));
|
|
749
|
+
}
|
|
750
|
+
// Resolve true when any watched file's signature moves off `before`.
|
|
751
|
+
// This is the real waiter: it never resolves false, so a real watch
|
|
752
|
+
// runs until the process is interrupted; tests inject their own waiter
|
|
753
|
+
// to bound the loop, and pass a short pollMs when they drive this one
|
|
754
|
+
// directly. The interval is a required argument (the command passes
|
|
755
|
+
// WATCH_POLL_MS) so there is no defaulting branch a test could never
|
|
756
|
+
// take.
|
|
757
|
+
//
|
|
758
|
+
// The BASELINE is an argument, not a snapshot taken here: the loop
|
|
759
|
+
// records it BEFORE each vet run, so a save landing between the run's
|
|
760
|
+
// reads and the wait still compares as a change. A waiter that
|
|
761
|
+
// snapshotted on entry would adopt that unvetted save as its baseline
|
|
762
|
+
// and wait indefinitely on a stale report.
|
|
763
|
+
async function watchChange(files, before, pollMs) {
|
|
764
|
+
for (;;) {
|
|
765
|
+
await sleep(pollMs);
|
|
766
|
+
if (watchSignature(files) !== before) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// The waiter the command runs with: the real change-poller at the real
|
|
772
|
+
// interval. Named (rather than inlined at the runVet call) so the
|
|
773
|
+
// production waiter itself is directly testable.
|
|
774
|
+
const vetWaiter = (files, before) => watchChange(files, before, WATCH_POLL_MS);
|
|
775
|
+
exports.vetWaiter = vetWaiter;
|
|
776
|
+
// The watch loop: one report per run, one run per change, streaming to
|
|
777
|
+
// stdout. An unreadable file mid-watch reports (exit class 2 from
|
|
778
|
+
// vetOnce) and keeps watching — a file being rewritten is briefly
|
|
779
|
+
// unreadable, and dying on it would make the mode useless for the very
|
|
780
|
+
// moment it exists for.
|
|
781
|
+
async function watchVet(args, wait, trust) {
|
|
782
|
+
const files = [args.schema, ...args.data];
|
|
783
|
+
let before = watchSignature(files);
|
|
784
|
+
let code = vetOnce(args, trust);
|
|
785
|
+
while (await wait(files, before)) {
|
|
786
|
+
before = watchSignature(files);
|
|
787
|
+
code = vetOnce(args, trust);
|
|
788
|
+
}
|
|
789
|
+
return code;
|
|
790
|
+
}
|
|
791
|
+
// The vet verb. Non-watch runs are synchronous and return the exit
|
|
792
|
+
// class directly; `--watch` returns a promise that resolves only when
|
|
793
|
+
// the waiter says stop (never, for the real one).
|
|
794
|
+
function runVet(argv, wait) {
|
|
795
|
+
const trusted = takeTrust(argv);
|
|
796
|
+
if (null == trusted) {
|
|
797
|
+
return 2;
|
|
798
|
+
}
|
|
799
|
+
argv = trusted.argv;
|
|
800
|
+
const trust = trusted.trust;
|
|
801
|
+
const parsed = parseVetArgs(argv);
|
|
802
|
+
if (null != parsed.err) {
|
|
803
|
+
process.stderr.write(parsed.err + '\n');
|
|
804
|
+
return 2;
|
|
805
|
+
}
|
|
806
|
+
const args = parsed.args;
|
|
807
|
+
if (true === args.help) {
|
|
808
|
+
process.stdout.write(HELP);
|
|
809
|
+
return 0;
|
|
810
|
+
}
|
|
811
|
+
if (true === args.watch) {
|
|
812
|
+
return watchVet(args, wait ?? vetWaiter, trust);
|
|
813
|
+
}
|
|
814
|
+
return vetOnce(args, trust);
|
|
815
|
+
}
|
|
816
|
+
// ---------------------------------------------------------------------
|
|
817
|
+
// The subsumption verbs (G3 phase 3): `subsume` asks the query once,
|
|
818
|
+
// `breaking` asks it between a document and its own earlier versions.
|
|
819
|
+
const SUBSUME_HELP = 'aontu subsume <general> <specific> (try --help)';
|
|
820
|
+
const BREAKING_HELP = 'aontu breaking --against <file|git#rev> <file> (try --help)';
|
|
821
|
+
// Exit classes mirror vet's convention: 3 is "the truth is not yet
|
|
822
|
+
// settled", which is exactly what undecided means here — and a gate
|
|
823
|
+
// that shrugs is not a gate, so undecided FAILS by default.
|
|
824
|
+
const SUBSUME_EXIT = {
|
|
825
|
+
subsumes: 0,
|
|
826
|
+
does_not_subsume: 1,
|
|
827
|
+
undecided: 3,
|
|
828
|
+
error: 4,
|
|
829
|
+
};
|
|
830
|
+
function parseSubsumeArgs(argv) {
|
|
831
|
+
const files = [];
|
|
832
|
+
let profile;
|
|
833
|
+
let at;
|
|
834
|
+
let format = 'text';
|
|
835
|
+
for (let i = 0; i < argv.length; i++) {
|
|
836
|
+
const arg = argv[i];
|
|
837
|
+
if ('-h' === arg || '--help' === arg) {
|
|
838
|
+
return { args: { help: true, general: '', specific: '', format } };
|
|
839
|
+
}
|
|
840
|
+
if ('--profile' === arg) {
|
|
841
|
+
const p = argv[++i];
|
|
842
|
+
if ('values' !== p && 'defaults' !== p && 'gen' !== p) {
|
|
843
|
+
return { err: 'aontu: --profile needs values, defaults or gen' };
|
|
844
|
+
}
|
|
845
|
+
profile = p;
|
|
846
|
+
}
|
|
847
|
+
else if ('--at' === arg) {
|
|
848
|
+
at = argv[++i];
|
|
849
|
+
if (null == at) {
|
|
850
|
+
return { err: 'aontu: --at needs a path' };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
else if ('--format' === arg) {
|
|
854
|
+
const f = argv[++i];
|
|
855
|
+
if ('text' !== f && 'json' !== f) {
|
|
856
|
+
return { err: 'aontu: --format needs text or json' };
|
|
857
|
+
}
|
|
858
|
+
format = f;
|
|
859
|
+
}
|
|
860
|
+
else if (arg.startsWith('-')) {
|
|
861
|
+
return { err: `aontu: unknown subsume option ${arg} (try --help)` };
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
files.push(arg);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (2 !== files.length) {
|
|
868
|
+
return {
|
|
869
|
+
err: 'aontu: subsume needs a general and a specific file\n' +
|
|
870
|
+
SUBSUME_HELP,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
args: { general: files[0], specific: files[1], profile, at, format },
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function renderSubsumeText(report) {
|
|
878
|
+
const head = `verdict: ${report.verdict}`;
|
|
879
|
+
if (0 === report.findings.length) {
|
|
880
|
+
return head;
|
|
881
|
+
}
|
|
882
|
+
return [head, ''].concat(report.findings.map(renderFinding)).join('\n');
|
|
883
|
+
}
|
|
884
|
+
function renderSubsumeJson(report) {
|
|
885
|
+
return (0, aontu_1.exactJSON)({
|
|
886
|
+
aontu: { version: version(), verb: 'subsume' },
|
|
887
|
+
verdict: report.verdict,
|
|
888
|
+
findings: report.findings,
|
|
889
|
+
}, 2);
|
|
890
|
+
}
|
|
891
|
+
function runSubsume(argv) {
|
|
892
|
+
const trusted = takeTrust(argv);
|
|
893
|
+
if (null == trusted) {
|
|
894
|
+
return 2;
|
|
895
|
+
}
|
|
896
|
+
argv = trusted.argv;
|
|
897
|
+
const trust = trusted.trust;
|
|
898
|
+
const parsed = parseSubsumeArgs(argv);
|
|
899
|
+
if (null != parsed.err) {
|
|
900
|
+
process.stderr.write(parsed.err + '\n');
|
|
901
|
+
return 2;
|
|
902
|
+
}
|
|
903
|
+
const args = parsed.args;
|
|
904
|
+
if (true === args.help) {
|
|
905
|
+
process.stdout.write(HELP);
|
|
906
|
+
return 0;
|
|
907
|
+
}
|
|
908
|
+
let generalSrc, specificSrc;
|
|
909
|
+
try {
|
|
910
|
+
generalSrc = (0, node_fs_1.readFileSync)(args.general, 'utf8');
|
|
911
|
+
specificSrc = (0, node_fs_1.readFileSync)(args.specific, 'utf8');
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
915
|
+
return 2;
|
|
916
|
+
}
|
|
917
|
+
const report = (0, aontu_1.subsume)(generalSrc, specificSrc, {
|
|
918
|
+
trust: verbTrust(trust, entryRootOf(args.general)),
|
|
919
|
+
profile: args.profile,
|
|
920
|
+
at: args.at,
|
|
921
|
+
generalUrl: args.general,
|
|
922
|
+
specificUrl: args.specific,
|
|
923
|
+
generalPath: args.general,
|
|
924
|
+
specificPath: args.specific,
|
|
925
|
+
});
|
|
926
|
+
const text = 'json' === args.format
|
|
927
|
+
? renderSubsumeJson(report)
|
|
928
|
+
: renderSubsumeText(report);
|
|
929
|
+
process.stdout.write(text + '\n');
|
|
930
|
+
return SUBSUME_EXIT[report.verdict];
|
|
931
|
+
}
|
|
932
|
+
function parseBreakingArgs(argv) {
|
|
933
|
+
const files = [];
|
|
934
|
+
const against = [];
|
|
935
|
+
let mode;
|
|
936
|
+
let at;
|
|
937
|
+
let allowUndecided = false;
|
|
938
|
+
let allowDeprecatedRemoval = false;
|
|
939
|
+
let format = 'text';
|
|
940
|
+
for (let i = 0; i < argv.length; i++) {
|
|
941
|
+
const arg = argv[i];
|
|
942
|
+
if ('-h' === arg || '--help' === arg) {
|
|
943
|
+
return {
|
|
944
|
+
args: {
|
|
945
|
+
help: true, file: '', against: [],
|
|
946
|
+
allowUndecided, allowDeprecatedRemoval, format,
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
if ('--against' === arg) {
|
|
951
|
+
const a = argv[++i];
|
|
952
|
+
if (null == a) {
|
|
953
|
+
return { err: 'aontu: --against needs a file path or git#<rev>' };
|
|
954
|
+
}
|
|
955
|
+
against.push(a);
|
|
956
|
+
}
|
|
957
|
+
else if ('--mode' === arg) {
|
|
958
|
+
const m = argv[++i];
|
|
959
|
+
if ('backward' !== m && 'forward' !== m && 'full' !== m) {
|
|
960
|
+
return { err: 'aontu: --mode needs backward, forward or full' };
|
|
961
|
+
}
|
|
962
|
+
mode = m;
|
|
963
|
+
}
|
|
964
|
+
else if ('--at' === arg) {
|
|
965
|
+
const a = argv[++i];
|
|
966
|
+
if (null == a) {
|
|
967
|
+
return { err: 'aontu: --at needs a path' };
|
|
968
|
+
}
|
|
969
|
+
at = a;
|
|
970
|
+
}
|
|
971
|
+
else if ('--allow-undecided' === arg) {
|
|
972
|
+
allowUndecided = true;
|
|
973
|
+
}
|
|
974
|
+
else if ('--allow-deprecated-removal' === arg) {
|
|
975
|
+
allowDeprecatedRemoval = true;
|
|
976
|
+
}
|
|
977
|
+
else if ('--format' === arg) {
|
|
978
|
+
const f = argv[++i];
|
|
979
|
+
if ('text' !== f && 'json' !== f) {
|
|
980
|
+
return { err: 'aontu: --format needs text or json' };
|
|
981
|
+
}
|
|
982
|
+
format = f;
|
|
983
|
+
}
|
|
984
|
+
else if (arg.startsWith('-')) {
|
|
985
|
+
return { err: `aontu: unknown breaking option ${arg} (try --help)` };
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
files.push(arg);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (1 !== files.length || 0 === against.length) {
|
|
992
|
+
return {
|
|
993
|
+
err: 'aontu: breaking needs one file and at least one --against\n' +
|
|
994
|
+
BREAKING_HELP,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
args: {
|
|
999
|
+
file: files[0], against, mode, at,
|
|
1000
|
+
allowUndecided, allowDeprecatedRemoval, format,
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
// A source file the include resolver can actually load. `git#<rev>`
|
|
1005
|
+
// materialises these and nothing else: an include names an Aontu
|
|
1006
|
+
// document (`.aon`/`.aontu`, the two extensions `@"foo"` tries) or a
|
|
1007
|
+
// JSON one, so the rest of a revision's tree cannot be part of any
|
|
1008
|
+
// include closure and copying it would be pure cost.
|
|
1009
|
+
const INCLUDABLE = /\.(aon|aontu|jsonic|json)$/;
|
|
1010
|
+
// Resolve one --against spelling to an old version.
|
|
1011
|
+
//
|
|
1012
|
+
// A `git#<rev>` spelling is the old version of the WHOLE TREE, not of
|
|
1013
|
+
// the entry file alone. It used to be `git show <rev>:./<file>`, whose
|
|
1014
|
+
// text was then evaluated with `generalPath`/`specificPath` pointing at
|
|
1015
|
+
// the WORKING file -- so every `@"..."` include in the old document
|
|
1016
|
+
// resolved against the working tree, and the "old" side was old entry
|
|
1017
|
+
// text meeting new includes. A breaking change inside an included file
|
|
1018
|
+
// therefore compared against itself and answered `compatible`: the
|
|
1019
|
+
// documented CI gate silently un-gated every non-entry file of the
|
|
1020
|
+
// multi-file layout real models use (use-cases/BUGS.md §26). The old
|
|
1021
|
+
// tree's includable sources are copied into a temporary directory and
|
|
1022
|
+
// the old document is evaluated from THERE.
|
|
1023
|
+
//
|
|
1024
|
+
// Sources outside the revision -- package includes under node_modules,
|
|
1025
|
+
// the bundled `std/system` -- still resolve as they do today: they are
|
|
1026
|
+
// not in the tree, and their versions travel with the lockfile rather
|
|
1027
|
+
// than with this comparison.
|
|
1028
|
+
function oldVersion(spec, file) {
|
|
1029
|
+
if (!spec.startsWith('git#')) {
|
|
1030
|
+
try {
|
|
1031
|
+
return { src: (0, node_fs_1.readFileSync)(spec, 'utf8'), path: spec };
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const rev = spec.slice('git#'.length);
|
|
1039
|
+
if ('' === rev) {
|
|
1040
|
+
process.stderr.write('aontu: --against git# needs a revision\n');
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
// Lazy import: the dependency exists only when a git spelling is
|
|
1044
|
+
// actually used, so plain runs never pay for it.
|
|
1045
|
+
const { execFileSync } = require('node:child_process');
|
|
1046
|
+
const dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(file));
|
|
1047
|
+
const git = (args, cwd) => execFileSync('git', args, {
|
|
1048
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
1049
|
+
});
|
|
1050
|
+
// The temporary tree is made BEFORE the first git call, so every
|
|
1051
|
+
// failure below has exactly one cleanup path rather than a branch
|
|
1052
|
+
// that only some failures take.
|
|
1053
|
+
const temp = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'aontu-against-'));
|
|
1054
|
+
try {
|
|
1055
|
+
// THE REPO-RELATIVE PATH COMES FROM GIT, not from path arithmetic.
|
|
1056
|
+
// Relativising `rev-parse --show-toplevel` against `resolve(file)`
|
|
1057
|
+
// puts two DIFFERENT COORDINATE SYSTEMS on either side of the
|
|
1058
|
+
// subtraction: git prints the real path, while the caller's is
|
|
1059
|
+
// whatever they typed. On macOS a temp file under /var is
|
|
1060
|
+
// /private/var to git, and on Windows a TMP short name
|
|
1061
|
+
// (RUNNER~1) is the long form to git -- so the subtraction gave a
|
|
1062
|
+
// `../..` climb, the entry was "not in that revision", and the
|
|
1063
|
+
// documented CI spelling failed on both platforms while passing on
|
|
1064
|
+
// Linux (this PR's own CI). `--show-prefix` is the same question
|
|
1065
|
+
// asked in git's coordinates: the repo-relative directory of the
|
|
1066
|
+
// cwd, already slash-separated and already normalised.
|
|
1067
|
+
const prefix = git(['rev-parse', '--show-prefix'], dir).trim();
|
|
1068
|
+
const entryRel = prefix + (0, node_path_1.basename)(file);
|
|
1069
|
+
const top = git(['rev-parse', '--show-toplevel'], dir).trim();
|
|
1070
|
+
// `-z` so a path with a newline or a quote cannot be mistaken for
|
|
1071
|
+
// two paths (git otherwise quotes such names).
|
|
1072
|
+
const listed = git(['ls-tree', '-r', '-z', '--name-only', rev], top)
|
|
1073
|
+
.split('\0').filter((p) => '' !== p);
|
|
1074
|
+
if (!listed.includes(entryRel)) {
|
|
1075
|
+
throw new Error(`${entryRel} is not in that revision`);
|
|
1076
|
+
}
|
|
1077
|
+
for (const rel of listed) {
|
|
1078
|
+
if (!INCLUDABLE.test(rel)) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const dest = (0, node_path_1.join)(temp, ...rel.split('/'));
|
|
1082
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
|
|
1083
|
+
(0, node_fs_1.writeFileSync)(dest, git(['show', `${rev}:${rel}`], top));
|
|
1084
|
+
}
|
|
1085
|
+
const entry = (0, node_path_1.join)(temp, ...entryRel.split('/'));
|
|
1086
|
+
return { src: (0, node_fs_1.readFileSync)(entry, 'utf8'), path: entry, temp };
|
|
1087
|
+
}
|
|
1088
|
+
catch (err) {
|
|
1089
|
+
(0, node_fs_1.rmSync)(temp, { recursive: true, force: true });
|
|
1090
|
+
const detail = String(err.stderr ?? err.message).trim().split('\n')[0];
|
|
1091
|
+
process.stderr.write(`aontu: cannot resolve ${spec}: ${detail}\n`);
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// The document's own compatibility declaration: `$.aontu_policy.compat`,
|
|
1096
|
+
// a disjunction whose default is the declared mode. Undefined when the
|
|
1097
|
+
// key is absent or does not spell a mode.
|
|
1098
|
+
function policyCompat(newSrc, path, trust) {
|
|
1099
|
+
const aontu = new aontu_1.Aontu();
|
|
1100
|
+
const ctx = aontu.ctx({ collect: true });
|
|
1101
|
+
// The declaration is read by EVALUATING the document, so this leg
|
|
1102
|
+
// runs the include resolver too and has to run it under the verb's
|
|
1103
|
+
// capability -- a `breaking --trust none` that read its own mode
|
|
1104
|
+
// through an unconfined resolver would confine the comparison and
|
|
1105
|
+
// not the question (use-cases/REVIEW.md finding G).
|
|
1106
|
+
const v = aontu.unify(newSrc, { path, ...(null == trust ? {} : { trust }) }, ctx);
|
|
1107
|
+
if (0 < ctx.err.length || true === v?.isNil) {
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
let compat = v?.peg?.aontu_policy?.peg?.compat;
|
|
1111
|
+
if (null == compat) {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
if (true === compat.isDisjunct && Array.isArray(compat.peg)) {
|
|
1115
|
+
compat = compat.peg.find((m) => true === m?.isPref) ?? compat.peg[0];
|
|
1116
|
+
}
|
|
1117
|
+
if (true === compat.isPref) {
|
|
1118
|
+
compat = compat.peg;
|
|
1119
|
+
}
|
|
1120
|
+
const m = true === compat?.isString ? compat.peg : undefined;
|
|
1121
|
+
return 'backward' === m || 'forward' === m || 'full' === m || 'none' === m
|
|
1122
|
+
? m : undefined;
|
|
1123
|
+
}
|
|
1124
|
+
// Is the evaluated old version's value at the finding path deprecated?
|
|
1125
|
+
// The --allow-deprecated-removal downgrade (G3 phase 4): removing (or
|
|
1126
|
+
// otherwise changing) a value the old version already deprecated warns
|
|
1127
|
+
// instead of breaking. The Go port exports the same reader as
|
|
1128
|
+
// aontu.DeprecatedAt.
|
|
1129
|
+
function deprecatedAt(oldSrc, path, filePath) {
|
|
1130
|
+
const aontu = new aontu_1.Aontu();
|
|
1131
|
+
const ctx = aontu.ctx({ collect: true });
|
|
1132
|
+
const v = aontu.unify(oldSrc, { path: filePath }, ctx);
|
|
1133
|
+
if (0 < ctx.err.length || true === v?.isNil) {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
const segs = path.replace(/^\$/, '').split('.').filter((p) => '' !== p);
|
|
1137
|
+
let node = v;
|
|
1138
|
+
for (const seg of segs) {
|
|
1139
|
+
if (true === node?.isMap) {
|
|
1140
|
+
node = node.peg?.[seg];
|
|
1141
|
+
}
|
|
1142
|
+
else if (true === node?.isList) {
|
|
1143
|
+
node = node.peg?.[Number(seg)];
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
if (null == node) {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return null != node?.deprecation;
|
|
1153
|
+
}
|
|
1154
|
+
// Verdict aggregation for breaking: an error anywhere makes the run an
|
|
1155
|
+
// error; otherwise a witness anywhere makes it breaking; otherwise an
|
|
1156
|
+
// open question anywhere leaves it undecided.
|
|
1157
|
+
const BREAKING_RANK = {
|
|
1158
|
+
subsumes: 0,
|
|
1159
|
+
undecided: 1,
|
|
1160
|
+
does_not_subsume: 2,
|
|
1161
|
+
error: 3,
|
|
1162
|
+
};
|
|
1163
|
+
const BREAKING_EXIT = SUBSUME_EXIT;
|
|
1164
|
+
const BREAKING_VERDICT = {
|
|
1165
|
+
subsumes: 'compatible',
|
|
1166
|
+
does_not_subsume: 'breaking',
|
|
1167
|
+
undecided: 'undecided',
|
|
1168
|
+
error: 'error',
|
|
1169
|
+
};
|
|
1170
|
+
function runBreaking(argv) {
|
|
1171
|
+
const trusted = takeTrust(argv);
|
|
1172
|
+
if (null == trusted) {
|
|
1173
|
+
return 2;
|
|
1174
|
+
}
|
|
1175
|
+
argv = trusted.argv;
|
|
1176
|
+
const trust = trusted.trust;
|
|
1177
|
+
const parsed = parseBreakingArgs(argv);
|
|
1178
|
+
if (null != parsed.err) {
|
|
1179
|
+
process.stderr.write(parsed.err + '\n');
|
|
1180
|
+
return 2;
|
|
1181
|
+
}
|
|
1182
|
+
const args = parsed.args;
|
|
1183
|
+
if (true === args.help) {
|
|
1184
|
+
process.stdout.write(HELP);
|
|
1185
|
+
return 0;
|
|
1186
|
+
}
|
|
1187
|
+
let newSrc;
|
|
1188
|
+
try {
|
|
1189
|
+
newSrc = (0, node_fs_1.readFileSync)(args.file, 'utf8');
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1193
|
+
return 2;
|
|
1194
|
+
}
|
|
1195
|
+
// The declared mode: --mode overrides the document's own policy;
|
|
1196
|
+
// neither means backward, the index's framing (v1-valid documents
|
|
1197
|
+
// stay valid).
|
|
1198
|
+
const mode = args.mode ??
|
|
1199
|
+
policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
|
|
1200
|
+
'backward';
|
|
1201
|
+
if ('none' === mode) {
|
|
1202
|
+
// The document declares no compatibility promise: nothing to check.
|
|
1203
|
+
const report = { verdict: 'subsumes', findings: [] };
|
|
1204
|
+
const text = 'json' === args.format
|
|
1205
|
+
? renderBreakingJson(report, mode)
|
|
1206
|
+
: renderBreakingText(report);
|
|
1207
|
+
process.stdout.write(text + '\n');
|
|
1208
|
+
return 0;
|
|
1209
|
+
}
|
|
1210
|
+
let worst = 'subsumes';
|
|
1211
|
+
const findings = [];
|
|
1212
|
+
// Temporary trees materialised for `git#<rev>` spellings, removed
|
|
1213
|
+
// once every check that reads them has run.
|
|
1214
|
+
const temps = [];
|
|
1215
|
+
const sweep = () => {
|
|
1216
|
+
for (const t of temps) {
|
|
1217
|
+
(0, node_fs_1.rmSync)(t, { recursive: true, force: true });
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
try {
|
|
1221
|
+
for (const spec of args.against) {
|
|
1222
|
+
const old = oldVersion(spec, args.file);
|
|
1223
|
+
if (null == old) {
|
|
1224
|
+
return 2;
|
|
1225
|
+
}
|
|
1226
|
+
const oldSrc = old.src;
|
|
1227
|
+
if (null != old.temp) {
|
|
1228
|
+
temps.push(old.temp);
|
|
1229
|
+
}
|
|
1230
|
+
// backward: the NEW document is the general side — every old
|
|
1231
|
+
// instance must still be admitted. forward: the old one is.
|
|
1232
|
+
const checks = [];
|
|
1233
|
+
if ('backward' === mode || 'full' === mode) {
|
|
1234
|
+
checks.push({ general: [newSrc, args.file], specific: [oldSrc, spec] });
|
|
1235
|
+
}
|
|
1236
|
+
if ('forward' === mode || 'full' === mode) {
|
|
1237
|
+
checks.push({ general: [oldSrc, spec], specific: [newSrc, args.file] });
|
|
1238
|
+
}
|
|
1239
|
+
const oldPath = old.path;
|
|
1240
|
+
for (const check of checks) {
|
|
1241
|
+
const report = (0, aontu_1.subsume)(check.general[0], check.specific[0], {
|
|
1242
|
+
trust: verbTrust(trust, entryRootOf(args.file)),
|
|
1243
|
+
at: args.at,
|
|
1244
|
+
generalUrl: check.general[1],
|
|
1245
|
+
specificUrl: check.specific[1],
|
|
1246
|
+
// The old side's relative loads resolve from ITS own tree --
|
|
1247
|
+
// the materialised revision for a git spelling, the named
|
|
1248
|
+
// file's directory otherwise -- so an included file's change
|
|
1249
|
+
// is part of the comparison rather than invisible to it.
|
|
1250
|
+
generalPath: check.general[1] === spec ? oldPath : args.file,
|
|
1251
|
+
specificPath: check.specific[1] === spec ? oldPath : args.file,
|
|
1252
|
+
});
|
|
1253
|
+
// The deprecated-removal downgrade: a finding about a value the
|
|
1254
|
+
// OLD version already deprecated becomes a warning, and warnings
|
|
1255
|
+
// do not move the verdict. Deprecate-then-remove is the
|
|
1256
|
+
// supported rename path (the design's own sequencing).
|
|
1257
|
+
let verdict = report.verdict;
|
|
1258
|
+
if (args.allowDeprecatedRemoval) {
|
|
1259
|
+
let liveFindings = 0;
|
|
1260
|
+
for (const f of report.findings) {
|
|
1261
|
+
if ('error' === f.severity &&
|
|
1262
|
+
deprecatedAt(oldSrc, f.path, oldPath)) {
|
|
1263
|
+
f.severity = 'warning';
|
|
1264
|
+
}
|
|
1265
|
+
if ('error' === f.severity) {
|
|
1266
|
+
liveFindings++;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if ('does_not_subsume' === verdict && 0 === liveFindings) {
|
|
1270
|
+
verdict = 'subsumes';
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (BREAKING_RANK[worst] < BREAKING_RANK[verdict]) {
|
|
1274
|
+
worst = verdict;
|
|
1275
|
+
}
|
|
1276
|
+
findings.push(...report.findings);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
sweep();
|
|
1282
|
+
}
|
|
1283
|
+
const report = { verdict: worst, findings };
|
|
1284
|
+
const text = 'json' === args.format
|
|
1285
|
+
? renderBreakingJson(report, mode)
|
|
1286
|
+
: renderBreakingText(report);
|
|
1287
|
+
process.stdout.write(text + '\n');
|
|
1288
|
+
if ('undecided' === worst && args.allowUndecided) {
|
|
1289
|
+
return 0;
|
|
1290
|
+
}
|
|
1291
|
+
return BREAKING_EXIT[worst];
|
|
1292
|
+
}
|
|
1293
|
+
function renderBreakingText(report) {
|
|
1294
|
+
const head = `verdict: ${BREAKING_VERDICT[report.verdict]}`;
|
|
1295
|
+
if (0 === report.findings.length) {
|
|
1296
|
+
return head;
|
|
1297
|
+
}
|
|
1298
|
+
return [head, ''].concat(report.findings.map(renderFinding)).join('\n');
|
|
1299
|
+
}
|
|
1300
|
+
function renderBreakingJson(report, mode) {
|
|
1301
|
+
return (0, aontu_1.exactJSON)({
|
|
1302
|
+
aontu: { version: version(), verb: 'breaking', mode },
|
|
1303
|
+
verdict: BREAKING_VERDICT[report.verdict],
|
|
1304
|
+
findings: report.findings,
|
|
1305
|
+
}, 2);
|
|
1306
|
+
}
|
|
1307
|
+
// ---------------------------------------------------------------------
|
|
1308
|
+
// The trim reporter (G3 phase 6): report redundant entries as paths.
|
|
1309
|
+
// Report-only — REWRITING needs G7's format-preserving patch surface —
|
|
1310
|
+
// which is why --check is REQUIRED rather than defaulted: `aontu trim
|
|
1311
|
+
// f.aon` reads as "trim this file", and doing something else silently
|
|
1312
|
+
// is worse than saying so.
|
|
1313
|
+
const TRIM_HELP = 'aontu trim --check <file> (try --help)';
|
|
1314
|
+
const TRIM_EXIT = {
|
|
1315
|
+
clean: 0,
|
|
1316
|
+
redundant: 1,
|
|
1317
|
+
error: 4,
|
|
1318
|
+
};
|
|
1319
|
+
function runTrim(argv) {
|
|
1320
|
+
const trusted = takeTrust(argv);
|
|
1321
|
+
if (null == trusted) {
|
|
1322
|
+
return 2;
|
|
1323
|
+
}
|
|
1324
|
+
argv = trusted.argv;
|
|
1325
|
+
const trust = trusted.trust;
|
|
1326
|
+
const files = [];
|
|
1327
|
+
let check = false;
|
|
1328
|
+
let format = 'text';
|
|
1329
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1330
|
+
const arg = argv[i];
|
|
1331
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1332
|
+
process.stdout.write(HELP);
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
if ('--check' === arg) {
|
|
1336
|
+
check = true;
|
|
1337
|
+
}
|
|
1338
|
+
else if ('--format' === arg) {
|
|
1339
|
+
const f = argv[++i];
|
|
1340
|
+
if ('text' !== f && 'json' !== f) {
|
|
1341
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1342
|
+
return 2;
|
|
1343
|
+
}
|
|
1344
|
+
format = f;
|
|
1345
|
+
}
|
|
1346
|
+
else if (arg.startsWith('-')) {
|
|
1347
|
+
process.stderr.write(`aontu: unknown trim option ${arg} (try --help)\n`);
|
|
1348
|
+
return 2;
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
files.push(arg);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (1 !== files.length) {
|
|
1355
|
+
process.stderr.write(`aontu: trim needs one file\n${TRIM_HELP}\n`);
|
|
1356
|
+
return 2;
|
|
1357
|
+
}
|
|
1358
|
+
if (!check) {
|
|
1359
|
+
process.stderr.write('aontu: trim only reports for now — rewriting needs a format-' +
|
|
1360
|
+
'preserving editor (G7); pass --check\n');
|
|
1361
|
+
return 2;
|
|
1362
|
+
}
|
|
1363
|
+
let src;
|
|
1364
|
+
try {
|
|
1365
|
+
src = (0, node_fs_1.readFileSync)(files[0], 'utf8');
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1369
|
+
return 2;
|
|
1370
|
+
}
|
|
1371
|
+
const report = (0, aontu_1.trimCheck)(src, {
|
|
1372
|
+
path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
|
|
1373
|
+
});
|
|
1374
|
+
const text = 'json' === format
|
|
1375
|
+
? renderTrimJson(report)
|
|
1376
|
+
: renderTrimText(report);
|
|
1377
|
+
process.stdout.write(text + '\n');
|
|
1378
|
+
return TRIM_EXIT[report.verdict];
|
|
1379
|
+
}
|
|
1380
|
+
function renderTrimText(report) {
|
|
1381
|
+
const head = `verdict: ${report.verdict}`;
|
|
1382
|
+
// WHY, when the document could not be evaluated at all: rendered as
|
|
1383
|
+
// vet renders a finding, because it IS one (the review's finding F).
|
|
1384
|
+
const errors = report.errors ?? [];
|
|
1385
|
+
if (0 < errors.length) {
|
|
1386
|
+
return [head, ''].concat(errors.map(renderFinding)).join('\n');
|
|
1387
|
+
}
|
|
1388
|
+
if (0 === report.redundant.length) {
|
|
1389
|
+
return head;
|
|
1390
|
+
}
|
|
1391
|
+
return [head, ''].concat(report.redundant).join('\n');
|
|
1392
|
+
}
|
|
1393
|
+
function renderTrimJson(report) {
|
|
1394
|
+
return (0, aontu_1.exactJSON)({
|
|
1395
|
+
aontu: { version: version(), verb: 'trim' },
|
|
1396
|
+
verdict: report.verdict,
|
|
1397
|
+
redundant: report.redundant,
|
|
1398
|
+
...(null == report.errors ? {} : { errors: report.errors }),
|
|
1399
|
+
}, 2);
|
|
1400
|
+
}
|
|
1401
|
+
// The relation reporter (G4 phase 5): acyclicity and inverse
|
|
1402
|
+
// consistency over the edge set. A verb of its own rather than a leg of
|
|
1403
|
+
// `vet`, for the reason `trim` is one: vet answers "does this DOCUMENT
|
|
1404
|
+
// satisfy that SCHEMA", and these are facts about one finished model,
|
|
1405
|
+
// with no schema on the other side of the question.
|
|
1406
|
+
const RELATIONS_HELP = 'aontu relations <file> (try --help)';
|
|
1407
|
+
const RELATIONS_EXIT = {
|
|
1408
|
+
pass: 0,
|
|
1409
|
+
fail: 1,
|
|
1410
|
+
error: 4,
|
|
1411
|
+
};
|
|
1412
|
+
const REACHES_HELP = 'aontu reaches <from> <to> [--relation <name>] <file> (try --help)';
|
|
1413
|
+
// Same three-way shape every check verb here uses: the check held (0),
|
|
1414
|
+
// the check failed (1), the document could not be checked (4). An
|
|
1415
|
+
// unreachable pair is a FAILED CHECK and not an error: the question was
|
|
1416
|
+
// answered, and the answer was no.
|
|
1417
|
+
const REACHES_EXIT = {
|
|
1418
|
+
reaches: 0,
|
|
1419
|
+
unreachable: 1,
|
|
1420
|
+
error: 4,
|
|
1421
|
+
};
|
|
1422
|
+
const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)';
|
|
1423
|
+
// The module tooling (G6 phase 3, ts/src/mod-tool.ts). All LOCAL:
|
|
1424
|
+
// `tidy` resolves the closure from what is in the stores and rewrites
|
|
1425
|
+
// the lockfile, `verify` asks whether the stores still mean what the
|
|
1426
|
+
// lockfile pins and changes nothing, `vendor` materialises the locked
|
|
1427
|
+
// closure into the project, `manifest` prints what a publish would
|
|
1428
|
+
// push.
|
|
1429
|
+
//
|
|
1430
|
+
// TIDY AND VERIFY ARE DIFFERENT QUESTIONS, and that is why both exist.
|
|
1431
|
+
// Tidy recomputes and rewrites by design -- a pin is what a module
|
|
1432
|
+
// means NOW -- so it makes the lockfile agree with whatever the store
|
|
1433
|
+
// holds, tampering included. Verify is the gate: a CI job runs it
|
|
1434
|
+
// BEFORE tidy, or instead of it.
|
|
1435
|
+
//
|
|
1436
|
+
// `get` and `publish` are the NETWORK half of the design and are not in
|
|
1437
|
+
// this build. They are named here rather than left to fall out as an
|
|
1438
|
+
// unknown subcommand, because a reader of the design will type them and
|
|
1439
|
+
// deserves to be told which half is missing rather than that the word
|
|
1440
|
+
// is wrong.
|
|
1441
|
+
function runMod(argv) {
|
|
1442
|
+
const rest = [];
|
|
1443
|
+
let format = 'text';
|
|
1444
|
+
let against;
|
|
1445
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1446
|
+
const arg = argv[i];
|
|
1447
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1448
|
+
process.stdout.write(HELP);
|
|
1449
|
+
return 0;
|
|
1450
|
+
}
|
|
1451
|
+
if ('--format' === arg) {
|
|
1452
|
+
const f = argv[++i];
|
|
1453
|
+
if ('text' !== f && 'json' !== f) {
|
|
1454
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1455
|
+
return 2;
|
|
1456
|
+
}
|
|
1457
|
+
format = f;
|
|
1458
|
+
}
|
|
1459
|
+
else if ('--against' === arg) {
|
|
1460
|
+
const a = argv[++i];
|
|
1461
|
+
if (null == a) {
|
|
1462
|
+
process.stderr.write('aontu: --against needs a module directory\n');
|
|
1463
|
+
return 2;
|
|
1464
|
+
}
|
|
1465
|
+
against = a;
|
|
1466
|
+
}
|
|
1467
|
+
else if (arg.startsWith('-')) {
|
|
1468
|
+
process.stderr.write(`aontu: unknown mod option ${arg} (try --help)\n`);
|
|
1469
|
+
return 2;
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
rest.push(arg);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const sub = rest[0];
|
|
1476
|
+
const dir = rest[1] ?? '.';
|
|
1477
|
+
if ('get' === sub || 'publish' === sub) {
|
|
1478
|
+
process.stderr.write('aontu: mod ' + sub + ' needs a registry client, which this build ' +
|
|
1479
|
+
'does not ship (docs/capability-review/g6-distribution.md)\n');
|
|
1480
|
+
return 2;
|
|
1481
|
+
}
|
|
1482
|
+
if (!MOD_SUBS.includes(sub) || 2 < rest.length) {
|
|
1483
|
+
process.stderr.write(`aontu: mod needs tidy, verify, vendor or manifest\n${MOD_HELP}\n`);
|
|
1484
|
+
return 2;
|
|
1485
|
+
}
|
|
1486
|
+
// `--against` gates a manifest and means nothing to the other two;
|
|
1487
|
+
// accepting it there would say it had been honoured.
|
|
1488
|
+
if (null != against && 'manifest' !== sub) {
|
|
1489
|
+
process.stderr.write('aontu: --against is a manifest option\n');
|
|
1490
|
+
return 2;
|
|
1491
|
+
}
|
|
1492
|
+
const report = 'tidy' === sub ? (0, mod_tool_1.modTidy)(dir, modToolOptions()) :
|
|
1493
|
+
'verify' === sub ? (0, mod_tool_1.modVerify)(dir, modToolOptions()) :
|
|
1494
|
+
'vendor' === sub ? (0, mod_tool_1.modVendor)(dir, modToolOptions()) :
|
|
1495
|
+
(0, mod_tool_1.modManifest)(dir, modToolOptions(), against);
|
|
1496
|
+
process.stdout.write(('json' === format ?
|
|
1497
|
+
(0, aontu_1.exactJSON)({ aontu: { version: version(), verb: 'mod ' + sub }, ...report }, 2) :
|
|
1498
|
+
modText(sub, report)) + '\n');
|
|
1499
|
+
return MOD_EXIT[report.verdict];
|
|
1500
|
+
}
|
|
1501
|
+
const MOD_SUBS = ['tidy', 'verify', 'vendor', 'manifest'];
|
|
1502
|
+
const MOD_EXIT = {
|
|
1503
|
+
ok: 0,
|
|
1504
|
+
missing: 1,
|
|
1505
|
+
// A REFUSED GATE, with `breaking`: a store that no longer means what
|
|
1506
|
+
// the lockfile pins is the integrity check saying no, and a CI job
|
|
1507
|
+
// reading exit codes should not have to learn a third class for it.
|
|
1508
|
+
mismatch: 1,
|
|
1509
|
+
// Likewise a lockfile that does not cover the project: the gate has
|
|
1510
|
+
// nothing to check, which is a refusal and not a pass.
|
|
1511
|
+
unlocked: 1,
|
|
1512
|
+
breaking: 1,
|
|
1513
|
+
undecided: 3,
|
|
1514
|
+
error: 4,
|
|
1515
|
+
};
|
|
1516
|
+
// The tooling's evaluator: the same standalone evaluation the module
|
|
1517
|
+
// resolver verifies with (ts/src/mod.ts), and for the same reason —
|
|
1518
|
+
// only the engine can say what a module MEANS.
|
|
1519
|
+
function modToolOptions() {
|
|
1520
|
+
return {
|
|
1521
|
+
cache: (0, mod_1.modCacheDir)(),
|
|
1522
|
+
eval: (src, path) => {
|
|
1523
|
+
const a0 = new aontu_1.Aontu();
|
|
1524
|
+
const ctx = a0.ctx({ collect: true });
|
|
1525
|
+
const val = a0.unify(src, { path }, ctx);
|
|
1526
|
+
return {
|
|
1527
|
+
gen: val.gen(a0.ctx({ collect: true })),
|
|
1528
|
+
hash: (0, aontu_1.canonHash)(val),
|
|
1529
|
+
canon: val.canon,
|
|
1530
|
+
// The same question `aontu hash` asks before it will answer:
|
|
1531
|
+
// did this document stand up ON ITS OWN? See ModToolEval.
|
|
1532
|
+
ok: 0 === ctx.err.length && true !== val.isNil,
|
|
1533
|
+
};
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
function modText(sub, report) {
|
|
1538
|
+
const lines = ['verdict: ' + report.verdict];
|
|
1539
|
+
if ('manifest' === sub) {
|
|
1540
|
+
if ('' !== report.mod) {
|
|
1541
|
+
lines.push(report.mod + ' ' + report.version);
|
|
1542
|
+
lines.push('config: ' + report.config);
|
|
1543
|
+
}
|
|
1544
|
+
for (const key of Object.keys(report.annotations).sort()) {
|
|
1545
|
+
lines.push(key + ': ' + report.annotations[key]);
|
|
1546
|
+
}
|
|
1547
|
+
for (const file of report.files) {
|
|
1548
|
+
lines.push('layer: ' + file);
|
|
1549
|
+
}
|
|
1550
|
+
for (const f of report.findings) {
|
|
1551
|
+
lines.push(f.path + ': ' + f.message);
|
|
1552
|
+
}
|
|
1553
|
+
// What a manifest lacks is a declaration the module does not make
|
|
1554
|
+
// or an entry file that is not there, and neither is something a
|
|
1555
|
+
// fetch would supply -- so this is not the tail the other two
|
|
1556
|
+
// subcommands share. The name says which kind it is: `mod.version`
|
|
1557
|
+
// is a declaration, `service.aon` is a file.
|
|
1558
|
+
for (const miss of report.missing) {
|
|
1559
|
+
lines.push(miss + ': missing');
|
|
1560
|
+
}
|
|
1561
|
+
return lines.join('\n');
|
|
1562
|
+
}
|
|
1563
|
+
if ('verify' === sub) {
|
|
1564
|
+
for (const mod of report.verified) {
|
|
1565
|
+
lines.push(mod + ': verified');
|
|
1566
|
+
}
|
|
1567
|
+
// BOTH HASHES, because the useful question is which way it moved:
|
|
1568
|
+
// an empty `got` is a module that no longer stands up at all.
|
|
1569
|
+
for (const m of report.mismatched) {
|
|
1570
|
+
lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
|
|
1571
|
+
('' === m.got ? 'nothing (it does not evaluate)' : m.got));
|
|
1572
|
+
}
|
|
1573
|
+
// NOT a fetch: the module may well be sitting in the store. What
|
|
1574
|
+
// is absent is the PIN, and only a tidy writes one.
|
|
1575
|
+
for (const mod of report.unlocked) {
|
|
1576
|
+
lines.push(mod + ': not in the lockfile (run: aontu mod tidy)');
|
|
1577
|
+
}
|
|
1578
|
+
for (const miss of report.missing) {
|
|
1579
|
+
lines.push(miss + ': not fetched (run: aontu mod get)');
|
|
1580
|
+
}
|
|
1581
|
+
return lines.join('\n');
|
|
1582
|
+
}
|
|
1583
|
+
const done = 'tidy' === sub ? report.lock : report.vendored;
|
|
1584
|
+
for (const item of done) {
|
|
1585
|
+
lines.push('tidy' === sub ?
|
|
1586
|
+
item.mod + ' ' + item.v + ' ' + item.canon : '' + item);
|
|
1587
|
+
}
|
|
1588
|
+
// A module that is PRESENT but does not stand up. Named separately
|
|
1589
|
+
// from a missing one because the repair is different: a fetch cannot
|
|
1590
|
+
// help, the module itself has to be fixed (or its own dependencies
|
|
1591
|
+
// vendored beside it). Before the missing tail, as the Go port's
|
|
1592
|
+
// shared renderer orders them.
|
|
1593
|
+
for (const bad of report.unevaluable ?? []) {
|
|
1594
|
+
lines.push(bad + ': does not evaluate on its own; nothing to pin');
|
|
1595
|
+
}
|
|
1596
|
+
for (const miss of report.missing) {
|
|
1597
|
+
lines.push(miss + ': not fetched (run: aontu mod get)');
|
|
1598
|
+
}
|
|
1599
|
+
return lines.join('\n');
|
|
1600
|
+
}
|
|
1601
|
+
function runRelations(argv) {
|
|
1602
|
+
const trusted = takeTrust(argv);
|
|
1603
|
+
if (null == trusted) {
|
|
1604
|
+
return 2;
|
|
1605
|
+
}
|
|
1606
|
+
argv = trusted.argv;
|
|
1607
|
+
const trust = trusted.trust;
|
|
1608
|
+
const files = [];
|
|
1609
|
+
let format = 'text';
|
|
1610
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1611
|
+
const arg = argv[i];
|
|
1612
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1613
|
+
process.stdout.write(HELP);
|
|
1614
|
+
return 0;
|
|
1615
|
+
}
|
|
1616
|
+
if ('--format' === arg) {
|
|
1617
|
+
const f = argv[++i];
|
|
1618
|
+
if ('text' !== f && 'json' !== f) {
|
|
1619
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1620
|
+
return 2;
|
|
1621
|
+
}
|
|
1622
|
+
format = f;
|
|
1623
|
+
}
|
|
1624
|
+
else if (arg.startsWith('-')) {
|
|
1625
|
+
process.stderr.write(`aontu: unknown relations option ${arg} (try --help)\n`);
|
|
1626
|
+
return 2;
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
files.push(arg);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (1 !== files.length) {
|
|
1633
|
+
process.stderr.write(`aontu: relations needs one file\n${RELATIONS_HELP}\n`);
|
|
1634
|
+
return 2;
|
|
1635
|
+
}
|
|
1636
|
+
let src;
|
|
1637
|
+
try {
|
|
1638
|
+
src = (0, node_fs_1.readFileSync)(files[0], 'utf8');
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1642
|
+
return 2;
|
|
1643
|
+
}
|
|
1644
|
+
const report = (0, aontu_1.relationCheck)(src, {
|
|
1645
|
+
path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
|
|
1646
|
+
});
|
|
1647
|
+
const text = 'json' === format
|
|
1648
|
+
? renderRelationsJson(report)
|
|
1649
|
+
: renderRelationsText(report);
|
|
1650
|
+
process.stdout.write(text + '\n');
|
|
1651
|
+
return RELATIONS_EXIT[report.verdict];
|
|
1652
|
+
}
|
|
1653
|
+
function runReaches(argv) {
|
|
1654
|
+
const trusted = takeTrust(argv);
|
|
1655
|
+
if (null == trusted) {
|
|
1656
|
+
return 2;
|
|
1657
|
+
}
|
|
1658
|
+
argv = trusted.argv;
|
|
1659
|
+
const trust = trusted.trust;
|
|
1660
|
+
const rest = [];
|
|
1661
|
+
let format = 'text';
|
|
1662
|
+
let relation = undefined;
|
|
1663
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1664
|
+
const arg = argv[i];
|
|
1665
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1666
|
+
process.stdout.write(HELP);
|
|
1667
|
+
return 0;
|
|
1668
|
+
}
|
|
1669
|
+
if ('--format' === arg) {
|
|
1670
|
+
const f = argv[++i];
|
|
1671
|
+
if ('text' !== f && 'json' !== f) {
|
|
1672
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1673
|
+
return 2;
|
|
1674
|
+
}
|
|
1675
|
+
format = f;
|
|
1676
|
+
}
|
|
1677
|
+
else if ('--relation' === arg) {
|
|
1678
|
+
relation = argv[++i];
|
|
1679
|
+
if (null == relation) {
|
|
1680
|
+
process.stderr.write('aontu: --relation needs a name\n');
|
|
1681
|
+
return 2;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
else if (arg.startsWith('-')) {
|
|
1685
|
+
process.stderr.write(`aontu: unknown reaches option ${arg} (try --help)\n`);
|
|
1686
|
+
return 2;
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
rest.push(arg);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (3 !== rest.length) {
|
|
1693
|
+
process.stderr.write(`aontu: reaches needs two entities and one file\n${REACHES_HELP}\n`);
|
|
1694
|
+
return 2;
|
|
1695
|
+
}
|
|
1696
|
+
let src;
|
|
1697
|
+
try {
|
|
1698
|
+
src = (0, node_fs_1.readFileSync)(rest[2], 'utf8');
|
|
1699
|
+
}
|
|
1700
|
+
catch (err) {
|
|
1701
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1702
|
+
return 2;
|
|
1703
|
+
}
|
|
1704
|
+
const report = (0, reach_1.reachCheck)(src, rest[0], rest[1], {
|
|
1705
|
+
path: rest[2], relation,
|
|
1706
|
+
trust: verbTrust(trust, entryRootOf(rest[2])),
|
|
1707
|
+
});
|
|
1708
|
+
const text = 'json' === format
|
|
1709
|
+
? renderReachesJson(report)
|
|
1710
|
+
: renderReachesText(report, rest[0], rest[1]);
|
|
1711
|
+
process.stdout.write(text + '\n');
|
|
1712
|
+
return REACHES_EXIT[report.verdict];
|
|
1713
|
+
}
|
|
1714
|
+
function renderReachesText(report, from, to) {
|
|
1715
|
+
const head = `verdict: ${report.verdict}`;
|
|
1716
|
+
const errors = report.errors ?? [];
|
|
1717
|
+
if (0 < errors.length) {
|
|
1718
|
+
return [head, ''].concat(errors.map(renderFinding)).join('\n');
|
|
1719
|
+
}
|
|
1720
|
+
// THE PATH IS THE ANSWER, not decoration: "yes" is worth little to an
|
|
1721
|
+
// operator asking what a failure would take out, and the chain is
|
|
1722
|
+
// what they act on.
|
|
1723
|
+
return 'reaches' === report.verdict
|
|
1724
|
+
? [head, '', report.path.join(' -> ')].join('\n')
|
|
1725
|
+
: [head, '', `${from} does not reach ${to}`].join('\n');
|
|
1726
|
+
}
|
|
1727
|
+
function renderReachesJson(report) {
|
|
1728
|
+
return (0, aontu_1.exactJSON)({
|
|
1729
|
+
aontu: { version: version(), verb: 'reaches' },
|
|
1730
|
+
verdict: report.verdict,
|
|
1731
|
+
...(null == report.path ? {} : { path: report.path }),
|
|
1732
|
+
...(null == report.errors ? {} : { errors: report.errors }),
|
|
1733
|
+
}, 2);
|
|
1734
|
+
}
|
|
1735
|
+
function renderRelationsText(report) {
|
|
1736
|
+
const head = `verdict: ${report.verdict}`;
|
|
1737
|
+
// WHY, when the document could not be evaluated at all: rendered as
|
|
1738
|
+
// vet renders a finding, because it IS one (the review's finding F).
|
|
1739
|
+
const errors = report.errors ?? [];
|
|
1740
|
+
if (0 < errors.length) {
|
|
1741
|
+
return [head, ''].concat(errors.map(renderFinding)).join('\n');
|
|
1742
|
+
}
|
|
1743
|
+
if (0 === report.findings.length) {
|
|
1744
|
+
return head;
|
|
1745
|
+
}
|
|
1746
|
+
const lines = report.findings.map((f) => 'relation_cycle' === f.code
|
|
1747
|
+
? `${f.at} ${f.relation}: cycle ${f.detail.join(' -> ')}`
|
|
1748
|
+
: 'relation_target_unmet' === f.code
|
|
1749
|
+
? `${f.at} ${f.relation}: ${f.detail[1]} is not what ` +
|
|
1750
|
+
`${f.relation} targets (${f.detail[2]})`
|
|
1751
|
+
: `${f.at} ${f.relation}: ${f.detail[1]} does not list ` +
|
|
1752
|
+
`${f.detail[0]} under ${f.detail[2]}`);
|
|
1753
|
+
return [head, ''].concat(lines).join('\n');
|
|
1754
|
+
}
|
|
1755
|
+
function renderRelationsJson(report) {
|
|
1756
|
+
return (0, aontu_1.exactJSON)({
|
|
1757
|
+
aontu: { version: version(), verb: 'relations' },
|
|
1758
|
+
verdict: report.verdict,
|
|
1759
|
+
findings: report.findings,
|
|
1760
|
+
...(null == report.errors ? {} : { errors: report.errors }),
|
|
1761
|
+
}, 2);
|
|
1762
|
+
}
|
|
1763
|
+
// ---------------------------------------------------------------------
|
|
1764
|
+
// JSON SCHEMA EXPORT (SUPPORT.md act 2, the review's finding I): the
|
|
1765
|
+
// bridge to every structured-output API, which constrains generation to
|
|
1766
|
+
// JSON Schema and nothing else. Export the model, let the provider
|
|
1767
|
+
// generate under it, then `vet` the result against the model itself --
|
|
1768
|
+
// the hybrid an enterprise actually deploys, and impossible without
|
|
1769
|
+
// this verb.
|
|
1770
|
+
//
|
|
1771
|
+
// THE SCHEMA GOES TO STDOUT AND THE LOSSES TO STDERR, so `aontu
|
|
1772
|
+
// jsonschema x.aon > schema.json` writes a schema and still tells the
|
|
1773
|
+
// reader what it could not carry. `--strict` makes a loss a refusal,
|
|
1774
|
+
// for the CI job that would rather fail than ship a schema weaker than
|
|
1775
|
+
// its model.
|
|
1776
|
+
const JSONSCHEMA_HELP = 'aontu jsonschema [--at <path>] [--strict] <file> (try --help)';
|
|
1777
|
+
function runJsonSchema(argv) {
|
|
1778
|
+
const trusted = takeTrust(argv);
|
|
1779
|
+
if (null == trusted) {
|
|
1780
|
+
return 2;
|
|
1781
|
+
}
|
|
1782
|
+
argv = trusted.argv;
|
|
1783
|
+
const trust = trusted.trust;
|
|
1784
|
+
const files = [];
|
|
1785
|
+
let format = 'text';
|
|
1786
|
+
let at = undefined;
|
|
1787
|
+
let strict = false;
|
|
1788
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1789
|
+
const arg = argv[i];
|
|
1790
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1791
|
+
process.stdout.write(HELP);
|
|
1792
|
+
return 0;
|
|
1793
|
+
}
|
|
1794
|
+
if ('--format' === arg) {
|
|
1795
|
+
const f = argv[++i];
|
|
1796
|
+
if ('text' !== f && 'json' !== f) {
|
|
1797
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1798
|
+
return 2;
|
|
1799
|
+
}
|
|
1800
|
+
format = f;
|
|
1801
|
+
}
|
|
1802
|
+
else if ('--at' === arg) {
|
|
1803
|
+
at = argv[++i];
|
|
1804
|
+
if (null == at) {
|
|
1805
|
+
process.stderr.write('aontu: --at needs a path\n');
|
|
1806
|
+
return 2;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
else if ('--strict' === arg) {
|
|
1810
|
+
strict = true;
|
|
1811
|
+
}
|
|
1812
|
+
else if (arg.startsWith('-')) {
|
|
1813
|
+
process.stderr.write(`aontu: unknown jsonschema option ${arg} (try --help)\n`);
|
|
1814
|
+
return 2;
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
files.push(arg);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (1 !== files.length) {
|
|
1821
|
+
process.stderr.write(`aontu: jsonschema needs one file\n${JSONSCHEMA_HELP}\n`);
|
|
1822
|
+
return 2;
|
|
1823
|
+
}
|
|
1824
|
+
let src;
|
|
1825
|
+
try {
|
|
1826
|
+
src = (0, node_fs_1.readFileSync)(files[0], 'utf8');
|
|
1827
|
+
}
|
|
1828
|
+
catch (err) {
|
|
1829
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1830
|
+
return 2;
|
|
1831
|
+
}
|
|
1832
|
+
const report = (0, jsonschema_1.jsonSchema)(src, {
|
|
1833
|
+
at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
|
|
1834
|
+
});
|
|
1835
|
+
if ('json' === format) {
|
|
1836
|
+
process.stdout.write((0, aontu_1.exactJSON)({
|
|
1837
|
+
aontu: { version: version(), verb: 'jsonschema' },
|
|
1838
|
+
verdict: report.verdict,
|
|
1839
|
+
schema: report.schema,
|
|
1840
|
+
lossy: report.lossy,
|
|
1841
|
+
...(null == report.errors ? {} : { errors: report.errors }),
|
|
1842
|
+
}, 2) + '\n');
|
|
1843
|
+
}
|
|
1844
|
+
else if ('error' === report.verdict) {
|
|
1845
|
+
// Not `?? []`: every `error` return in jsonSchema() sets `errors`,
|
|
1846
|
+
// so the list is the reason for the refusal rather than a maybe,
|
|
1847
|
+
// exactly as Go's `r.Errors` is on this arm.
|
|
1848
|
+
process.stderr.write(report.errors.map(renderFinding).join('\n') + '\n');
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
process.stdout.write((0, aontu_1.exactJSON)(report.schema, 2) + '\n');
|
|
1852
|
+
for (const l of report.lossy) {
|
|
1853
|
+
process.stderr.write(`lossy: ${l.path} ${l.construct}: ${l.reason}\n`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return 'error' === report.verdict ? 4 :
|
|
1857
|
+
strict && 'lossy' === report.verdict ? 1 : 0;
|
|
1858
|
+
}
|
|
1859
|
+
// ---------------------------------------------------------------------
|
|
1860
|
+
// The canon-hash (G6 phase 1): the pin an agent, a lockfile or a
|
|
1861
|
+
// registry stores for "this module, this meaning". The hash covers the
|
|
1862
|
+
// module evaluated STANDALONE -- its own include closure resolved and
|
|
1863
|
+
// unified at its own root, before any consumer context -- which is what
|
|
1864
|
+
// makes the pin transitive: an edit two includes deep changes the
|
|
1865
|
+
// unified root, hence the hash.
|
|
1866
|
+
const HASH_HELP = 'aontu hash <file> (try --help)';
|
|
1867
|
+
function runHash(argv) {
|
|
1868
|
+
const trusted = takeTrust(argv);
|
|
1869
|
+
if (null == trusted) {
|
|
1870
|
+
return 2;
|
|
1871
|
+
}
|
|
1872
|
+
argv = trusted.argv;
|
|
1873
|
+
const trust = trusted.trust;
|
|
1874
|
+
const files = [];
|
|
1875
|
+
let form = false;
|
|
1876
|
+
let format = 'text';
|
|
1877
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1878
|
+
const arg = argv[i];
|
|
1879
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1880
|
+
process.stdout.write(HELP);
|
|
1881
|
+
return 0;
|
|
1882
|
+
}
|
|
1883
|
+
if ('--form' === arg) {
|
|
1884
|
+
form = true;
|
|
1885
|
+
}
|
|
1886
|
+
else if ('--format' === arg) {
|
|
1887
|
+
const f = argv[++i];
|
|
1888
|
+
if ('text' !== f && 'json' !== f) {
|
|
1889
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1890
|
+
return 2;
|
|
1891
|
+
}
|
|
1892
|
+
format = f;
|
|
1893
|
+
}
|
|
1894
|
+
else if (arg.startsWith('-')) {
|
|
1895
|
+
process.stderr.write(`aontu: unknown hash option ${arg} (try --help)\n`);
|
|
1896
|
+
return 2;
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
files.push(arg);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (1 !== files.length) {
|
|
1903
|
+
process.stderr.write(`aontu: hash needs one file\n${HASH_HELP}\n`);
|
|
1904
|
+
return 2;
|
|
1905
|
+
}
|
|
1906
|
+
let src;
|
|
1907
|
+
try {
|
|
1908
|
+
src = (0, node_fs_1.readFileSync)(files[0], 'utf8');
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
1912
|
+
return 2;
|
|
1913
|
+
}
|
|
1914
|
+
// The file's own directory is the include base, as every verb
|
|
1915
|
+
// resolves a named file (vet's aontuForPath rule).
|
|
1916
|
+
const capability = verbTrust(trust, entryRootOf(files[0]));
|
|
1917
|
+
const aontu = new aontu_1.Aontu(null == capability ? undefined : { trust: capability });
|
|
1918
|
+
const ctx = aontu.ctx({ collect: true });
|
|
1919
|
+
const v = aontu.unify(src, { path: files[0] }, ctx);
|
|
1920
|
+
if (0 < ctx.err.length || true === v?.isNil) {
|
|
1921
|
+
// A document that does not stand up on its own has no meaning to
|
|
1922
|
+
// pin, and a hash of a broken evaluation would be a pin that
|
|
1923
|
+
// silently agrees with every other broken evaluation.
|
|
1924
|
+
// WHY it does not stand up, not just that it does not: the same
|
|
1925
|
+
// diagnosis `aontu <file>` prints (the review's finding F).
|
|
1926
|
+
// evalFailure unconditionally, as every other call site does: it
|
|
1927
|
+
// owns the "ctx.err is never empty here" contract, and a guard
|
|
1928
|
+
// that pretends otherwise is a dead arm asserting nothing.
|
|
1929
|
+
process.stderr.write(`aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
|
|
1930
|
+
renderFinding((0, query_1.evalFailure)(ctx)) + '\n');
|
|
1931
|
+
return 4;
|
|
1932
|
+
}
|
|
1933
|
+
const text = 'json' === format
|
|
1934
|
+
? (0, aontu_1.exactJSON)({
|
|
1935
|
+
aontu: { version: version(), verb: 'hash' },
|
|
1936
|
+
hash: (0, aontu_1.canonHash)(v),
|
|
1937
|
+
form: (0, aontu_1.hcanon)(v),
|
|
1938
|
+
}, 2)
|
|
1939
|
+
: (form ? (0, aontu_1.hcanon)(v) : (0, aontu_1.canonHash)(v));
|
|
1940
|
+
process.stdout.write(text + '\n');
|
|
1941
|
+
return 0;
|
|
1942
|
+
}
|
|
1943
|
+
// ---------------------------------------------------------------------
|
|
1944
|
+
// The query surface (G7 phase 1): one node of an evaluated document,
|
|
1945
|
+
// selected by path and rendered. Evaluation is still GLOBAL -- what
|
|
1946
|
+
// `get` buys is the size of the ANSWER, not the cost of producing it --
|
|
1947
|
+
// and the projections are lattice abstractions, each a valid Aontu
|
|
1948
|
+
// document that subsumes the truth it summarises.
|
|
1949
|
+
const GET_HELP = 'aontu get <path> <file> (try --help)';
|
|
1950
|
+
function runGet(argv) {
|
|
1951
|
+
const trusted = takeTrust(argv);
|
|
1952
|
+
if (null == trusted) {
|
|
1953
|
+
return 2;
|
|
1954
|
+
}
|
|
1955
|
+
argv = trusted.argv;
|
|
1956
|
+
const trust = trusted.trust;
|
|
1957
|
+
const rest = [];
|
|
1958
|
+
let view = 'json';
|
|
1959
|
+
let depth;
|
|
1960
|
+
let format = 'text';
|
|
1961
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1962
|
+
const arg = argv[i];
|
|
1963
|
+
if ('-h' === arg || '--help' === arg) {
|
|
1964
|
+
process.stdout.write(HELP);
|
|
1965
|
+
return 0;
|
|
1966
|
+
}
|
|
1967
|
+
if ('-c' === arg || '--canon' === arg) {
|
|
1968
|
+
view = 'canon';
|
|
1969
|
+
}
|
|
1970
|
+
else if ('--keys' === arg) {
|
|
1971
|
+
view = 'keys';
|
|
1972
|
+
}
|
|
1973
|
+
else if ('--types' === arg) {
|
|
1974
|
+
view = 'types';
|
|
1975
|
+
}
|
|
1976
|
+
else if ('--depth' === arg) {
|
|
1977
|
+
const n = Number(argv[++i]);
|
|
1978
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1979
|
+
process.stderr.write('aontu: --depth needs a positive integer\n');
|
|
1980
|
+
return 2;
|
|
1981
|
+
}
|
|
1982
|
+
depth = n;
|
|
1983
|
+
}
|
|
1984
|
+
else if ('--format' === arg) {
|
|
1985
|
+
const f = argv[++i];
|
|
1986
|
+
if ('text' !== f && 'json' !== f) {
|
|
1987
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
1988
|
+
return 2;
|
|
1989
|
+
}
|
|
1990
|
+
format = f;
|
|
1991
|
+
}
|
|
1992
|
+
else if (arg.startsWith('-')) {
|
|
1993
|
+
process.stderr.write(`aontu: unknown get option ${arg} (try --help)\n`);
|
|
1994
|
+
return 2;
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
rest.push(arg);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (2 !== rest.length) {
|
|
2001
|
+
process.stderr.write(`aontu: get needs a path and one file\n${GET_HELP}\n`);
|
|
2002
|
+
return 2;
|
|
2003
|
+
}
|
|
2004
|
+
const [path, file] = rest;
|
|
2005
|
+
// ELIDING BELOW A DEPTH means rendering `top`, which JSON cannot
|
|
2006
|
+
// say. Rather than switch the view silently -- the choice `trim
|
|
2007
|
+
// --check` refused to make -- the combination is a usage error.
|
|
2008
|
+
if (null != depth && 'canon' !== view && 'types' !== view) {
|
|
2009
|
+
process.stderr.write('aontu: --depth needs --canon or --types (JSON cannot say top)\n');
|
|
2010
|
+
return 2;
|
|
2011
|
+
}
|
|
2012
|
+
let src;
|
|
2013
|
+
try {
|
|
2014
|
+
src = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
2015
|
+
}
|
|
2016
|
+
catch (err) {
|
|
2017
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2018
|
+
return 2;
|
|
2019
|
+
}
|
|
2020
|
+
const report = (0, aontu_1.get)(src, path, {
|
|
2021
|
+
view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
|
|
2022
|
+
});
|
|
2023
|
+
if ('json' === format) {
|
|
2024
|
+
process.stdout.write((0, aontu_1.exactJSON)({
|
|
2025
|
+
aontu: { version: version(), verb: 'get' },
|
|
2026
|
+
findings: report.findings,
|
|
2027
|
+
ok: report.ok,
|
|
2028
|
+
out: report.out,
|
|
2029
|
+
}, 2) + '\n');
|
|
2030
|
+
}
|
|
2031
|
+
else if (report.ok) {
|
|
2032
|
+
process.stdout.write(report.out + '\n');
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
|
|
2036
|
+
}
|
|
2037
|
+
if (report.ok) {
|
|
2038
|
+
return 0;
|
|
2039
|
+
}
|
|
2040
|
+
// A path that names nothing is the QUESTION's answer -- exit 1, the
|
|
2041
|
+
// "no" class -- while a document that does not stand up is exit 4,
|
|
2042
|
+
// as it is for every other verb.
|
|
2043
|
+
return 'no_path' === report.findings[0]?.code ? 1 : 4;
|
|
2044
|
+
}
|
|
2045
|
+
// ---------------------------------------------------------------------
|
|
2046
|
+
// Provenance (G7 phase 3): WHY the value at a path holds — the ordered
|
|
2047
|
+
// contributions that met there, each with the site it was written at.
|
|
2048
|
+
// The positive twin of the vet report: errors explain what failed to
|
|
2049
|
+
// unify, this explains what did.
|
|
2050
|
+
const WHY_HELP = 'aontu why <path> <file> (try --help)';
|
|
2051
|
+
function runWhy(argv) {
|
|
2052
|
+
const trusted = takeTrust(argv);
|
|
2053
|
+
if (null == trusted) {
|
|
2054
|
+
return 2;
|
|
2055
|
+
}
|
|
2056
|
+
argv = trusted.argv;
|
|
2057
|
+
const trust = trusted.trust;
|
|
2058
|
+
const rest = [];
|
|
2059
|
+
let format = 'text';
|
|
2060
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2061
|
+
const arg = argv[i];
|
|
2062
|
+
if ('-h' === arg || '--help' === arg) {
|
|
2063
|
+
process.stdout.write(HELP);
|
|
2064
|
+
return 0;
|
|
2065
|
+
}
|
|
2066
|
+
if ('--format' === arg) {
|
|
2067
|
+
const f = argv[++i];
|
|
2068
|
+
if ('text' !== f && 'json' !== f) {
|
|
2069
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
2070
|
+
return 2;
|
|
2071
|
+
}
|
|
2072
|
+
format = f;
|
|
2073
|
+
}
|
|
2074
|
+
else if (arg.startsWith('-')) {
|
|
2075
|
+
process.stderr.write(`aontu: unknown why option ${arg} (try --help)\n`);
|
|
2076
|
+
return 2;
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
rest.push(arg);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
if (2 !== rest.length) {
|
|
2083
|
+
process.stderr.write(`aontu: why needs a path and one file\n${WHY_HELP}\n`);
|
|
2084
|
+
return 2;
|
|
2085
|
+
}
|
|
2086
|
+
const [path, file] = rest;
|
|
2087
|
+
let src;
|
|
2088
|
+
try {
|
|
2089
|
+
src = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
2090
|
+
}
|
|
2091
|
+
catch (err) {
|
|
2092
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2093
|
+
return 2;
|
|
2094
|
+
}
|
|
2095
|
+
const report = (0, aontu_1.why)(src, path, {
|
|
2096
|
+
path: file, trust: verbTrust(trust, entryRootOf(file)),
|
|
2097
|
+
});
|
|
2098
|
+
if ('json' === format) {
|
|
2099
|
+
process.stdout.write((0, aontu_1.exactJSON)({
|
|
2100
|
+
aontu: { version: version(), verb: 'why' },
|
|
2101
|
+
findings: report.findings,
|
|
2102
|
+
ok: report.ok,
|
|
2103
|
+
...(null == report.record ? {} : { record: report.record }),
|
|
2104
|
+
}, 2) + '\n');
|
|
2105
|
+
}
|
|
2106
|
+
else if (report.ok) {
|
|
2107
|
+
process.stdout.write(renderWhyText(report.record) + '\n');
|
|
2108
|
+
}
|
|
2109
|
+
else {
|
|
2110
|
+
process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
|
|
2111
|
+
}
|
|
2112
|
+
if (report.ok) {
|
|
2113
|
+
return 0;
|
|
2114
|
+
}
|
|
2115
|
+
return 'no_path' === report.findings[0]?.code ? 1 : 4;
|
|
2116
|
+
}
|
|
2117
|
+
// One contribution per line, numbered in source order, each with what
|
|
2118
|
+
// was written, where, and how it got here. A siteless contribution
|
|
2119
|
+
// prints no location rather than a `-1:-1` that means nothing —
|
|
2120
|
+
// exported for the direct test, because the site SHAPE allows one
|
|
2121
|
+
// while no document has yet produced one (ADR-002).
|
|
2122
|
+
function renderWhyText(record) {
|
|
2123
|
+
const head = `${record.path} = ${record.value}`;
|
|
2124
|
+
if (0 === record.conjuncts.length) {
|
|
2125
|
+
// A value written once and never met is a fact, not a failure.
|
|
2126
|
+
return head + '\n (no contributions: nothing met at this path)';
|
|
2127
|
+
}
|
|
2128
|
+
return [head].concat(record.conjuncts.map((c, i) => {
|
|
2129
|
+
const where = -1 === c.site.row
|
|
2130
|
+
? ''
|
|
2131
|
+
: ` ${'' === c.site.file ? '' : c.site.file + ':'}` +
|
|
2132
|
+
`${c.site.row}:${c.site.col}`;
|
|
2133
|
+
return ` ${i + 1}. ${c.canon}${where}` +
|
|
2134
|
+
('literal' === c.role ? '' : ` (${c.role})`);
|
|
2135
|
+
})).join('\n');
|
|
2136
|
+
}
|
|
2137
|
+
// ---------------------------------------------------------------------
|
|
2138
|
+
// The overlay patch verb (G7 phase 5): change a document by APPENDING
|
|
2139
|
+
// to an overlay, not by rewriting it. An overlay entry is just another
|
|
2140
|
+
// conjunct and unification is order-independent, so this needs no
|
|
2141
|
+
// rewriter — the format-preserving in-place edit is stage 2, and needs
|
|
2142
|
+
// a comment-preserving CST the parser stack does not have.
|
|
2143
|
+
const SET_HELP = 'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)';
|
|
2144
|
+
function runSet(argv) {
|
|
2145
|
+
const trusted = takeTrust(argv);
|
|
2146
|
+
if (null == trusted) {
|
|
2147
|
+
return 2;
|
|
2148
|
+
}
|
|
2149
|
+
argv = trusted.argv;
|
|
2150
|
+
const trust = trusted.trust;
|
|
2151
|
+
const assignments = [];
|
|
2152
|
+
let entry;
|
|
2153
|
+
let overlayFile;
|
|
2154
|
+
let dryRun = false;
|
|
2155
|
+
let inPlace = false;
|
|
2156
|
+
let format = 'text';
|
|
2157
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2158
|
+
const arg = argv[i];
|
|
2159
|
+
if ('-h' === arg || '--help' === arg) {
|
|
2160
|
+
process.stdout.write(HELP);
|
|
2161
|
+
return 0;
|
|
2162
|
+
}
|
|
2163
|
+
if ('--entry' === arg) {
|
|
2164
|
+
entry = argv[++i];
|
|
2165
|
+
}
|
|
2166
|
+
else if ('--overlay' === arg) {
|
|
2167
|
+
overlayFile = argv[++i];
|
|
2168
|
+
}
|
|
2169
|
+
else if ('--dry-run' === arg) {
|
|
2170
|
+
dryRun = true;
|
|
2171
|
+
}
|
|
2172
|
+
else if ('--in-place' === arg) {
|
|
2173
|
+
inPlace = true;
|
|
2174
|
+
}
|
|
2175
|
+
else if ('--format' === arg) {
|
|
2176
|
+
const f = argv[++i];
|
|
2177
|
+
if ('text' !== f && 'json' !== f) {
|
|
2178
|
+
process.stderr.write('aontu: --format needs text or json\n');
|
|
2179
|
+
return 2;
|
|
2180
|
+
}
|
|
2181
|
+
format = f;
|
|
2182
|
+
}
|
|
2183
|
+
else if (arg.startsWith('-')) {
|
|
2184
|
+
process.stderr.write(`aontu: unknown set option ${arg} (try --help)\n`);
|
|
2185
|
+
return 2;
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
assignments.push(arg);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (0 === assignments.length || null == entry || null == overlayFile) {
|
|
2192
|
+
process.stderr.write(`aontu: set needs assignments, --entry and --overlay\n${SET_HELP}\n`);
|
|
2193
|
+
return 2;
|
|
2194
|
+
}
|
|
2195
|
+
let entrySrc;
|
|
2196
|
+
try {
|
|
2197
|
+
entrySrc = (0, node_fs_1.readFileSync)(entry, 'utf8');
|
|
2198
|
+
}
|
|
2199
|
+
catch (err) {
|
|
2200
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2201
|
+
return 2;
|
|
2202
|
+
}
|
|
2203
|
+
// An ABSENT overlay is the empty overlay, and the file is created by
|
|
2204
|
+
// the write below: "append to the overlay" should not require the
|
|
2205
|
+
// author to have made one first.
|
|
2206
|
+
let overlaySrc = '';
|
|
2207
|
+
try {
|
|
2208
|
+
overlaySrc = (0, node_fs_1.readFileSync)(overlayFile, 'utf8');
|
|
2209
|
+
}
|
|
2210
|
+
catch (err) {
|
|
2211
|
+
if ('ENOENT' !== err?.code) {
|
|
2212
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2213
|
+
return 2;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
const report = (0, aontu_1.patch)(entrySrc, overlaySrc, assignments, {
|
|
2217
|
+
trust: verbTrust(trust, entryRootOf(entry)),
|
|
2218
|
+
entryPath: entry,
|
|
2219
|
+
overlayPath: overlayFile,
|
|
2220
|
+
inPlace,
|
|
2221
|
+
});
|
|
2222
|
+
// WRITTEN ONLY WHEN IT HOLDS. A change that contradicts a pinned
|
|
2223
|
+
// value is a question the author has to answer at the pinning site;
|
|
2224
|
+
// leaving it in the overlay would leave the configuration broken
|
|
2225
|
+
// while the exit code says so somewhere they may not be reading.
|
|
2226
|
+
const wrote = !dryRun &&
|
|
2227
|
+
'invalid' !== report.verdict && 'error' !== report.verdict;
|
|
2228
|
+
if (wrote) {
|
|
2229
|
+
try {
|
|
2230
|
+
(0, node_fs_1.writeFileSync)(overlayFile, report.overlay, 'utf8');
|
|
2231
|
+
}
|
|
2232
|
+
catch (err) {
|
|
2233
|
+
process.stderr.write(`aontu: cannot write ${overlayFile}: ${err.message}\n`);
|
|
2234
|
+
return 2;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if ('json' === format) {
|
|
2238
|
+
process.stdout.write((0, aontu_1.exactJSON)({
|
|
2239
|
+
aontu: { version: version(), verb: 'set' },
|
|
2240
|
+
appended: report.appended,
|
|
2241
|
+
findings: report.findings,
|
|
2242
|
+
overlay: report.overlay,
|
|
2243
|
+
replaced: report.replaced,
|
|
2244
|
+
verdict: report.verdict,
|
|
2245
|
+
written: wrote,
|
|
2246
|
+
}, 2) + '\n');
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
// A replacement is REPORTED as the edit it is, not left for the
|
|
2250
|
+
// reader to infer from a changed file: `where: what -> what`, in
|
|
2251
|
+
// source spelling, because the spelling is what changed.
|
|
2252
|
+
//
|
|
2253
|
+
// PAST TENSE ONLY WHERE IT HAPPENED. A refused write leaves the
|
|
2254
|
+
// file exactly as it was, and one assignment can be replaceable
|
|
2255
|
+
// while another makes the whole run invalid — so `replaced:` there
|
|
2256
|
+
// tells an operator the pin was changed when it was not, and unlike
|
|
2257
|
+
// `--dry-run` there is nothing else on the line to say otherwise.
|
|
2258
|
+
const verb = wrote ? 'replaced' : 'would replace';
|
|
2259
|
+
const edits = report.replaced.map((r) => `${verb}: ${r.file}:${r.row}:${r.col} ${r.from} -> ${r.to}`);
|
|
2260
|
+
const head = [`verdict: ${report.verdict}`].concat(edits).join('\n') +
|
|
2261
|
+
(wrote ? `\nwrote: ${overlayFile}` : dryRun ? '\n(dry run)' : '');
|
|
2262
|
+
// A SUCCESSFUL COMMAND WRITES ITS STATUS TO STDOUT, findings or
|
|
2263
|
+
// not. Routing on `findings.length` was right while every finding
|
|
2264
|
+
// this verb could produce was an ERROR; `--in-place` made a WARNING
|
|
2265
|
+
// possible, and a run that held, wrote the file and exited 0 then
|
|
2266
|
+
// sent its whole report to stderr — leaving stdout empty, so
|
|
2267
|
+
// `$(aontu set ...)` captured nothing and only the JSON form
|
|
2268
|
+
// behaved like a success. The verdict decides the stream; warnings
|
|
2269
|
+
// are diagnostics and go to stderr beside it.
|
|
2270
|
+
const failed = 'invalid' === report.verdict || 'error' === report.verdict;
|
|
2271
|
+
const findingText = report.findings.map(renderFinding);
|
|
2272
|
+
if (failed) {
|
|
2273
|
+
// A FAILED VERDICT ALWAYS CARRIES A FINDING — the conflict, or
|
|
2274
|
+
// the parse error, that made it fail — so the blank separator is
|
|
2275
|
+
// unconditional. Guarding it described a report vet cannot
|
|
2276
|
+
// produce, and the coverage gate said so.
|
|
2277
|
+
process.stderr.write([head, ''].concat(findingText).join('\n') + '\n');
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
process.stdout.write(head + '\n');
|
|
2281
|
+
if (0 < findingText.length) {
|
|
2282
|
+
process.stderr.write(findingText.join('\n') + '\n');
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return VET_EXIT[report.verdict];
|
|
2287
|
+
}
|
|
2288
|
+
// ---------------------------------------------------------------------
|
|
2289
|
+
// The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
|
|
2290
|
+
// derived from the definition, so it cannot drift from the formal
|
|
2291
|
+
// source it points at.
|
|
2292
|
+
const AGENTSMD_HELP = 'aontu agentsmd <file> (try --help)';
|
|
2293
|
+
function runAgentsMd(argv) {
|
|
2294
|
+
const trusted = takeTrust(argv);
|
|
2295
|
+
if (null == trusted) {
|
|
2296
|
+
return 2;
|
|
2297
|
+
}
|
|
2298
|
+
argv = trusted.argv;
|
|
2299
|
+
const trust = trusted.trust;
|
|
2300
|
+
const files = [];
|
|
2301
|
+
let write;
|
|
2302
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2303
|
+
const arg = argv[i];
|
|
2304
|
+
if ('-h' === arg || '--help' === arg) {
|
|
2305
|
+
process.stdout.write(HELP);
|
|
2306
|
+
return 0;
|
|
2307
|
+
}
|
|
2308
|
+
if ('--write' === arg) {
|
|
2309
|
+
write = argv[++i];
|
|
2310
|
+
if (null == write) {
|
|
2311
|
+
process.stderr.write('aontu: --write needs a file\n');
|
|
2312
|
+
return 2;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
else if (arg.startsWith('-')) {
|
|
2316
|
+
process.stderr.write(`aontu: unknown agentsmd option ${arg} (try --help)\n`);
|
|
2317
|
+
return 2;
|
|
2318
|
+
}
|
|
2319
|
+
else {
|
|
2320
|
+
files.push(arg);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (1 !== files.length) {
|
|
2324
|
+
process.stderr.write(`aontu: agentsmd needs one file\n${AGENTSMD_HELP}\n`);
|
|
2325
|
+
return 2;
|
|
2326
|
+
}
|
|
2327
|
+
let src;
|
|
2328
|
+
try {
|
|
2329
|
+
src = (0, node_fs_1.readFileSync)(files[0], 'utf8');
|
|
2330
|
+
}
|
|
2331
|
+
catch (err) {
|
|
2332
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2333
|
+
return 2;
|
|
2334
|
+
}
|
|
2335
|
+
const report = (0, aontu_1.agentsMd)(src, {
|
|
2336
|
+
name: files[0], path: files[0],
|
|
2337
|
+
trust: verbTrust(trust, entryRootOf(files[0])),
|
|
2338
|
+
});
|
|
2339
|
+
if (!report.ok) {
|
|
2340
|
+
process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
|
|
2341
|
+
return 4;
|
|
2342
|
+
}
|
|
2343
|
+
if (null == write) {
|
|
2344
|
+
process.stdout.write(report.stanza);
|
|
2345
|
+
return 0;
|
|
2346
|
+
}
|
|
2347
|
+
// An ABSENT target is an empty one: `--write AGENTS.md` should not
|
|
2348
|
+
// require the author to have made the file first.
|
|
2349
|
+
let existing = '';
|
|
2350
|
+
try {
|
|
2351
|
+
existing = (0, node_fs_1.readFileSync)(write, 'utf8');
|
|
2352
|
+
}
|
|
2353
|
+
catch (err) {
|
|
2354
|
+
if ('ENOENT' !== err?.code) {
|
|
2355
|
+
process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
|
|
2356
|
+
return 2;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
try {
|
|
2360
|
+
(0, node_fs_1.writeFileSync)(write, (0, agentsmd_1.agentsMdSplice)(existing, report.stanza), 'utf8');
|
|
2361
|
+
}
|
|
2362
|
+
catch (err) {
|
|
2363
|
+
process.stderr.write(`aontu: cannot write ${write}: ${err.message}\n`);
|
|
2364
|
+
return 2;
|
|
2365
|
+
}
|
|
2366
|
+
process.stdout.write(`wrote: ${write}\n`);
|
|
2367
|
+
return 0;
|
|
2368
|
+
}
|
|
143
2369
|
// Exit without truncating output.
|
|
144
2370
|
//
|
|
145
2371
|
// process.exit() terminates immediately, discarding anything still
|
|
@@ -157,10 +2383,97 @@ function runRepl(initialMode) {
|
|
|
157
2383
|
function finish(code) {
|
|
158
2384
|
process.exitCode = code;
|
|
159
2385
|
}
|
|
2386
|
+
// Parse a --trust argument value. Returns undefined for an unknown
|
|
2387
|
+
// spelling, so the caller owns the usage error.
|
|
2388
|
+
function parseTrustArg(value) {
|
|
2389
|
+
if ('system' === value) {
|
|
2390
|
+
return { kind: 'system' };
|
|
2391
|
+
}
|
|
2392
|
+
if ('none' === value) {
|
|
2393
|
+
return { kind: 'none' };
|
|
2394
|
+
}
|
|
2395
|
+
if ('root' === value) {
|
|
2396
|
+
return { kind: 'root' };
|
|
2397
|
+
}
|
|
2398
|
+
if (value.startsWith('root:') && 'root:'.length < value.length) {
|
|
2399
|
+
return { kind: 'root', dir: value.slice('root:'.length) };
|
|
2400
|
+
}
|
|
2401
|
+
return undefined;
|
|
2402
|
+
}
|
|
160
2403
|
function main(argv) {
|
|
2404
|
+
// COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
|
|
2405
|
+
// hardcoded their ANSI escapes, so a piped report and a `--jsonl`
|
|
2406
|
+
// answer carried terminal control codes into whatever read them (the
|
|
2407
|
+
// review's finding F). `NO_COLOR` is honoured by the library itself;
|
|
2408
|
+
// only the CLI can see whether its stderr is a terminal, so only the
|
|
2409
|
+
// CLI can make this call. `undefined` means "leave it to NO_COLOR".
|
|
2410
|
+
(0, aontu_1.setColor)(true === process.stderr.isTTY ? undefined : false);
|
|
161
2411
|
let mode = 'json';
|
|
162
|
-
|
|
163
|
-
|
|
2412
|
+
// A LIST, though the bare command evaluates exactly one document.
|
|
2413
|
+
// It used to be one variable and the last argument won, which made a
|
|
2414
|
+
// MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
|
|
2415
|
+
// printed good.json and exited 0, because `vet2` matched no
|
|
2416
|
+
// subcommand, fell through to this loop as a file name, and was
|
|
2417
|
+
// overwritten twice. In a tool loop that reads as a passing
|
|
2418
|
+
// validation. Counting them is what lets the refusal below happen.
|
|
2419
|
+
const files = [];
|
|
2420
|
+
let trust = { kind: 'system-warn' };
|
|
2421
|
+
// The REPL's SESSION protocol (G7 phase 7): one JSON line per
|
|
2422
|
+
// answer, so a harness can drive the session. Named --jsonl rather
|
|
2423
|
+
// than the design's --json, which would read as the `:json` output
|
|
2424
|
+
// mode the REPL already has.
|
|
2425
|
+
let jsonl = false;
|
|
2426
|
+
// Subcommand dispatch, and deliberately only for a FIRST argument:
|
|
2427
|
+
// `aontu vet` is the verb, while `aontu somefile vet` keeps meaning
|
|
2428
|
+
// what it always did. A file named `vet` is still reachable as
|
|
2429
|
+
// `aontu ./vet`.
|
|
2430
|
+
//
|
|
2431
|
+
// Promise.resolve either way: a non-watch run returns its exit class
|
|
2432
|
+
// synchronously (and has already written its report), while `--watch`
|
|
2433
|
+
// resolves only when the watch ends — so one await-shaped line serves
|
|
2434
|
+
// both without a branch to keep covered.
|
|
2435
|
+
if ('vet' === argv[2]) {
|
|
2436
|
+
return void Promise.resolve(runVet(argv.slice(3))).then(finish);
|
|
2437
|
+
}
|
|
2438
|
+
if ('subsume' === argv[2]) {
|
|
2439
|
+
return finish(runSubsume(argv.slice(3)));
|
|
2440
|
+
}
|
|
2441
|
+
if ('breaking' === argv[2]) {
|
|
2442
|
+
return finish(runBreaking(argv.slice(3)));
|
|
2443
|
+
}
|
|
2444
|
+
if ('agentsmd' === argv[2]) {
|
|
2445
|
+
return finish(runAgentsMd(argv.slice(3)));
|
|
2446
|
+
}
|
|
2447
|
+
if ('set' === argv[2]) {
|
|
2448
|
+
return finish(runSet(argv.slice(3)));
|
|
2449
|
+
}
|
|
2450
|
+
if ('why' === argv[2]) {
|
|
2451
|
+
return finish(runWhy(argv.slice(3)));
|
|
2452
|
+
}
|
|
2453
|
+
if ('get' === argv[2]) {
|
|
2454
|
+
return finish(runGet(argv.slice(3)));
|
|
2455
|
+
}
|
|
2456
|
+
if ('hash' === argv[2]) {
|
|
2457
|
+
return finish(runHash(argv.slice(3)));
|
|
2458
|
+
}
|
|
2459
|
+
if ('mod' === argv[2]) {
|
|
2460
|
+
return finish(runMod(argv.slice(3)));
|
|
2461
|
+
}
|
|
2462
|
+
if ('relations' === argv[2]) {
|
|
2463
|
+
return finish(runRelations(argv.slice(3)));
|
|
2464
|
+
}
|
|
2465
|
+
if ('jsonschema' === argv[2]) {
|
|
2466
|
+
return finish(runJsonSchema(argv.slice(3)));
|
|
2467
|
+
}
|
|
2468
|
+
if ('reaches' === argv[2]) {
|
|
2469
|
+
return finish(runReaches(argv.slice(3)));
|
|
2470
|
+
}
|
|
2471
|
+
if ('trim' === argv[2]) {
|
|
2472
|
+
return finish(runTrim(argv.slice(3)));
|
|
2473
|
+
}
|
|
2474
|
+
const args = argv.slice(2);
|
|
2475
|
+
for (let i = 0; i < args.length; i++) {
|
|
2476
|
+
const arg = args[i];
|
|
164
2477
|
if ('-c' === arg || '--canon' === arg) {
|
|
165
2478
|
mode = 'canon';
|
|
166
2479
|
}
|
|
@@ -172,22 +2485,66 @@ function main(argv) {
|
|
|
172
2485
|
process.stdout.write(version() + '\n');
|
|
173
2486
|
return finish(0);
|
|
174
2487
|
}
|
|
2488
|
+
else if ('--trust' === arg) {
|
|
2489
|
+
const parsed = null == args[i + 1] ? undefined : parseTrustArg(args[++i]);
|
|
2490
|
+
if (null == parsed) {
|
|
2491
|
+
process.stderr.write('aontu: --trust needs system, none, or root[:dir]\n');
|
|
2492
|
+
return finish(2);
|
|
2493
|
+
}
|
|
2494
|
+
trust = parsed;
|
|
2495
|
+
}
|
|
2496
|
+
else if ('--jsonl' === arg) {
|
|
2497
|
+
jsonl = true;
|
|
2498
|
+
// A JSONL answer is machine-read by definition, even when the
|
|
2499
|
+
// session happens to be attached to a terminal, so this is a
|
|
2500
|
+
// harder gate than the stderr test above rather than a repeat of
|
|
2501
|
+
// it: escapes inside the answer string are noise the harness has
|
|
2502
|
+
// to strip before it can compare anything.
|
|
2503
|
+
(0, aontu_1.setColor)(false);
|
|
2504
|
+
}
|
|
2505
|
+
else if ('--include-root' === arg) {
|
|
2506
|
+
const dir = args[++i];
|
|
2507
|
+
if (null == dir) {
|
|
2508
|
+
process.stderr.write('aontu: --include-root needs a directory\n');
|
|
2509
|
+
return finish(2);
|
|
2510
|
+
}
|
|
2511
|
+
trust = { kind: 'root', dir };
|
|
2512
|
+
}
|
|
175
2513
|
else if (arg.startsWith('-')) {
|
|
176
2514
|
process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`);
|
|
177
2515
|
return finish(2);
|
|
178
2516
|
}
|
|
179
2517
|
else {
|
|
180
|
-
|
|
2518
|
+
files.push(arg);
|
|
181
2519
|
}
|
|
182
2520
|
}
|
|
2521
|
+
// ONE DOCUMENT. The bare form has always been `aontu [options]
|
|
2522
|
+
// [file]`, singular, and anything past the first was silently
|
|
2523
|
+
// discarded rather than refused -- so every way of getting the verb
|
|
2524
|
+
// wrong (a typo, a verb this port does not have, a verb spelled for
|
|
2525
|
+
// another tool) ended in a plausible answer about the wrong file.
|
|
2526
|
+
// Exit 2, the usage class, and the message names the cause rather
|
|
2527
|
+
// than the symptom: nothing here can tell a mistyped verb from a
|
|
2528
|
+
// second file, but the reader can.
|
|
2529
|
+
if (1 < files.length) {
|
|
2530
|
+
process.stderr.write(`aontu: the bare command evaluates one document, and ${files.length}` +
|
|
2531
|
+
' were given\naontu: a mistyped verb reads as a file name' +
|
|
2532
|
+
' (try --help)\n');
|
|
2533
|
+
return finish(2);
|
|
2534
|
+
}
|
|
2535
|
+
const file = files[0];
|
|
183
2536
|
if (null != file) {
|
|
184
|
-
finish(runFile(file, mode));
|
|
2537
|
+
finish(runFile(file, mode, trust));
|
|
185
2538
|
}
|
|
186
|
-
|
|
187
|
-
|
|
2539
|
+
// `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
|
|
2540
|
+
// a harness over a pipe, so gating it on an interactive terminal
|
|
2541
|
+
// made it reachable only through a pty -- which is to say, not
|
|
2542
|
+
// reachable by the thing it was built for. Mirrors go/cmd/aontu.
|
|
2543
|
+
else if (jsonl || process.stdin.isTTY) {
|
|
2544
|
+
runRepl(mode, jsonl, trust);
|
|
188
2545
|
}
|
|
189
2546
|
else {
|
|
190
|
-
runStdin(mode).then((code) => finish(code));
|
|
2547
|
+
runStdin(mode, trust).then((code) => finish(code));
|
|
191
2548
|
}
|
|
192
|
-
} /* node:coverage ignore next
|
|
2549
|
+
} /* node:coverage ignore next 15 */
|
|
193
2550
|
//# sourceMappingURL=cli.js.map
|