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.
Files changed (273) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +145 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +16 -0
  13. package/dist/ctx.js +44 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +48 -8
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +223 -5
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +698 -35
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -46
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +3 -1
  83. package/dist/unify.js +287 -18
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +48 -1
  103. package/dist/val/ConstraintVal.js +1501 -110
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.d.ts +1 -0
  120. package/dist/val/ExpectVal.js +41 -4
  121. package/dist/val/ExpectVal.js.map +1 -1
  122. package/dist/val/FeatureVal.js +1 -1
  123. package/dist/val/FeatureVal.js.map +1 -1
  124. package/dist/val/FilterFuncVal.d.ts +15 -0
  125. package/dist/val/FilterFuncVal.js +91 -0
  126. package/dist/val/FilterFuncVal.js.map +1 -0
  127. package/dist/val/FuncBaseVal.d.ts +6 -1
  128. package/dist/val/FuncBaseVal.js +178 -2
  129. package/dist/val/FuncBaseVal.js.map +1 -1
  130. package/dist/val/HideFuncVal.js.map +1 -1
  131. package/dist/val/IdFuncVal.d.ts +13 -0
  132. package/dist/val/IdFuncVal.js +54 -0
  133. package/dist/val/IdFuncVal.js.map +1 -0
  134. package/dist/val/JunctionVal.js +7 -1
  135. package/dist/val/JunctionVal.js.map +1 -1
  136. package/dist/val/KeyFuncVal.d.ts +1 -1
  137. package/dist/val/KeyFuncVal.js +38 -30
  138. package/dist/val/KeyFuncVal.js.map +1 -1
  139. package/dist/val/ListVal.js +117 -17
  140. package/dist/val/ListVal.js.map +1 -1
  141. package/dist/val/LowerFuncVal.js.map +1 -1
  142. package/dist/val/MapVal.js +102 -8
  143. package/dist/val/MapVal.js.map +1 -1
  144. package/dist/val/MatchFuncVal.d.ts +15 -0
  145. package/dist/val/MatchFuncVal.js +107 -0
  146. package/dist/val/MatchFuncVal.js.map +1 -0
  147. package/dist/val/MoveFuncVal.js.map +1 -1
  148. package/dist/val/NilVal.js +24 -0
  149. package/dist/val/NilVal.js.map +1 -1
  150. package/dist/val/OpBaseVal.d.ts +1 -1
  151. package/dist/val/OpBaseVal.js +24 -2
  152. package/dist/val/OpBaseVal.js.map +1 -1
  153. package/dist/val/OpenFuncVal.js +4 -1
  154. package/dist/val/OpenFuncVal.js.map +1 -1
  155. package/dist/val/PackFuncVal.d.ts +15 -0
  156. package/dist/val/PackFuncVal.js +108 -0
  157. package/dist/val/PackFuncVal.js.map +1 -0
  158. package/dist/val/PathFuncVal.js.map +1 -1
  159. package/dist/val/PlaceVal.d.ts +13 -0
  160. package/dist/val/PlaceVal.js +131 -0
  161. package/dist/val/PlaceVal.js.map +1 -0
  162. package/dist/val/PlusOpVal.js +11 -2
  163. package/dist/val/PlusOpVal.js.map +1 -1
  164. package/dist/val/PrefFuncVal.js.map +1 -1
  165. package/dist/val/PrefVal.d.ts +2 -2
  166. package/dist/val/PrefVal.js +78 -23
  167. package/dist/val/PrefVal.js.map +1 -1
  168. package/dist/val/RefVal.d.ts +1 -1
  169. package/dist/val/RefVal.js +158 -30
  170. package/dist/val/RefVal.js.map +1 -1
  171. package/dist/val/ReferFuncVal.d.ts +36 -0
  172. package/dist/val/ReferFuncVal.js +303 -0
  173. package/dist/val/ReferFuncVal.js.map +1 -0
  174. package/dist/val/ScalarKindVal.d.ts +1 -2
  175. package/dist/val/ScalarKindVal.js +0 -11
  176. package/dist/val/ScalarKindVal.js.map +1 -1
  177. package/dist/val/TopVal.js.map +1 -1
  178. package/dist/val/TypeFuncVal.js.map +1 -1
  179. package/dist/val/UpperFuncVal.js.map +1 -1
  180. package/dist/val/Val.d.ts +9 -2
  181. package/dist/val/Val.js +150 -4
  182. package/dist/val/Val.js.map +1 -1
  183. package/dist/val/VarVal.js.map +1 -1
  184. package/dist/val/arith.d.ts +6 -0
  185. package/dist/val/arith.js +170 -0
  186. package/dist/val/arith.js.map +1 -0
  187. package/dist/vet.d.ts +45 -0
  188. package/dist/vet.js +776 -0
  189. package/dist/vet.js.map +1 -0
  190. package/dist/walk.d.ts +2 -0
  191. package/dist/walk.js +91 -0
  192. package/dist/walk.js.map +1 -0
  193. package/grammar/aontu.gbnf +130 -0
  194. package/grammar/aontu.lark +113 -0
  195. package/package.json +30 -15
  196. package/skill/SKILL.md +37 -0
  197. package/skill/error-codes.md +62 -0
  198. package/skill/examples.md +99 -0
  199. package/skill/grammar-card.md +57 -0
  200. package/src/agentsmd.ts +135 -0
  201. package/src/aontu.ts +192 -4
  202. package/src/cli.ts +2858 -71
  203. package/src/ctx.ts +81 -0
  204. package/src/diff.ts +196 -0
  205. package/src/err.ts +52 -8
  206. package/src/graph.ts +135 -0
  207. package/src/hcanon.ts +169 -0
  208. package/src/hints.ts +271 -5
  209. package/src/jsonschema.ts +511 -0
  210. package/src/lang.ts +779 -37
  211. package/src/lsp.ts +281 -47
  212. package/src/mcp-server.ts +187 -0
  213. package/src/mcp.ts +993 -0
  214. package/src/mod-tool.ts +679 -0
  215. package/src/mod.ts +344 -0
  216. package/src/patch.ts +624 -0
  217. package/src/provenance.ts +430 -0
  218. package/src/query.ts +379 -0
  219. package/src/reach.ts +184 -0
  220. package/src/relation.ts +395 -0
  221. package/src/report-sarif.ts +137 -0
  222. package/src/site.ts +36 -1
  223. package/src/std.ts +73 -0
  224. package/src/subsume.ts +690 -0
  225. package/src/trim.ts +195 -0
  226. package/src/tsconfig.json +10 -4
  227. package/src/type.ts +51 -2
  228. package/src/unify.ts +311 -16
  229. package/src/utility.ts +139 -1
  230. package/src/val/AggFuncVal.ts +319 -0
  231. package/src/val/ArithFuncVal.ts +108 -0
  232. package/src/val/BagVal.ts +101 -4
  233. package/src/val/CloseFuncVal.ts +9 -1
  234. package/src/val/ConjunctVal.ts +20 -0
  235. package/src/val/ConstraintVal.ts +1699 -116
  236. package/src/val/CopyFuncVal.ts +7 -1
  237. package/src/val/Decimal.ts +15 -0
  238. package/src/val/DeprecateFuncVal.ts +84 -0
  239. package/src/val/DisjunctVal.ts +139 -28
  240. package/src/val/EachFuncVal.ts +133 -0
  241. package/src/val/ExpectVal.ts +43 -6
  242. package/src/val/FeatureVal.ts +1 -1
  243. package/src/val/FilterFuncVal.ts +154 -0
  244. package/src/val/FuncBaseVal.ts +200 -3
  245. package/src/val/HideFuncVal.ts +0 -2
  246. package/src/val/IdFuncVal.ts +91 -0
  247. package/src/val/JunctionVal.ts +7 -1
  248. package/src/val/KeyFuncVal.ts +39 -35
  249. package/src/val/ListVal.ts +125 -18
  250. package/src/val/LowerFuncVal.ts +0 -1
  251. package/src/val/MapVal.ts +110 -8
  252. package/src/val/MatchFuncVal.ts +176 -0
  253. package/src/val/MoveFuncVal.ts +0 -2
  254. package/src/val/NilVal.ts +25 -0
  255. package/src/val/OpBaseVal.ts +26 -3
  256. package/src/val/OpenFuncVal.ts +4 -2
  257. package/src/val/PackFuncVal.ts +175 -0
  258. package/src/val/PathFuncVal.ts +0 -1
  259. package/src/val/PlaceVal.ts +193 -0
  260. package/src/val/PlusOpVal.ts +11 -2
  261. package/src/val/PrefFuncVal.ts +0 -1
  262. package/src/val/PrefVal.ts +79 -36
  263. package/src/val/RefVal.ts +163 -30
  264. package/src/val/ReferFuncVal.ts +387 -0
  265. package/src/val/ScalarKindVal.ts +0 -13
  266. package/src/val/TopVal.ts +0 -1
  267. package/src/val/TypeFuncVal.ts +0 -2
  268. package/src/val/UpperFuncVal.ts +0 -1
  269. package/src/val/Val.ts +213 -3
  270. package/src/val/VarVal.ts +0 -1
  271. package/src/val/arith.ts +316 -0
  272. package/src/vet.ts +992 -0
  273. 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
- function runFile(file, mode) {
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 aontu = new aontu_1.Aontu({ path: (0, node_path_1.resolve)(file) });
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
- function runRepl(initialMode) {
94
- let mode = initialMode;
95
- const aontu = new aontu_1.Aontu();
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
- process.stdout.write(`Aontu v${version()} REPL — :help for commands, :quit to exit\n`);
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 s = line.trim();
105
- if ('' === s) {
106
- rl.prompt();
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 (s.startsWith(':')) {
110
- switch (s) {
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
- process.stdout.write('\n');
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
- let file;
163
- for (const arg of argv.slice(2)) {
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
- file = arg;
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
- else if (process.stdin.isTTY) {
187
- runRepl(mode);
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 8 */
2549
+ } /* node:coverage ignore next 15 */
193
2550
  //# sourceMappingURL=cli.js.map