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