aontu 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +4 -4
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +7 -28
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/alias.js.map +1 -1
  6. package/dist/allow.d.ts +23 -0
  7. package/dist/allow.js +138 -0
  8. package/dist/allow.js.map +1 -0
  9. package/dist/aontu.d.ts +4 -2
  10. package/dist/aontu.js +4 -80
  11. package/dist/aontu.js.map +1 -1
  12. package/dist/aontumodel.d.ts +4 -0
  13. package/dist/aontumodel.js +33 -0
  14. package/dist/aontumodel.js.map +1 -0
  15. package/dist/cli.d.ts +10 -1
  16. package/dist/cli.js +877 -479
  17. package/dist/cli.js.map +1 -1
  18. package/dist/ctx.js +0 -48
  19. package/dist/ctx.js.map +1 -1
  20. package/dist/diff.js +0 -32
  21. package/dist/diff.js.map +1 -1
  22. package/dist/err.js +0 -40
  23. package/dist/err.js.map +1 -1
  24. package/dist/escape.js +0 -45
  25. package/dist/escape.js.map +1 -1
  26. package/dist/exactjson.d.ts +0 -35
  27. package/dist/exactjson.js +0 -131
  28. package/dist/exactjson.js.map +1 -1
  29. package/dist/format.js +55 -189
  30. package/dist/format.js.map +1 -1
  31. package/dist/grammar.d.ts +9 -0
  32. package/dist/grammar.js +54 -0
  33. package/dist/grammar.js.map +1 -0
  34. package/dist/graph.js +0 -26
  35. package/dist/graph.js.map +1 -1
  36. package/dist/hcanon.js +0 -82
  37. package/dist/hcanon.js.map +1 -1
  38. package/dist/helpdoc.d.ts +16 -0
  39. package/dist/helpdoc.js +59 -0
  40. package/dist/helpdoc.js.map +1 -0
  41. package/dist/hints.d.ts +0 -6
  42. package/dist/hints.js +59 -47
  43. package/dist/hints.js.map +1 -1
  44. package/dist/jsonschema.js +0 -114
  45. package/dist/jsonschema.js.map +1 -1
  46. package/dist/keyorder.d.ts +0 -7
  47. package/dist/keyorder.js +0 -41
  48. package/dist/keyorder.js.map +1 -1
  49. package/dist/lang.js +32 -877
  50. package/dist/lang.js.map +1 -1
  51. package/dist/lower.d.ts +3 -0
  52. package/dist/lower.js +14 -61
  53. package/dist/lower.js.map +1 -1
  54. package/dist/lsp-server.js +0 -16
  55. package/dist/lsp-server.js.map +1 -1
  56. package/dist/lsp.d.ts +1 -1
  57. package/dist/lsp.js +12 -159
  58. package/dist/lsp.js.map +1 -1
  59. package/dist/mcp-server.js +0 -26
  60. package/dist/mcp-server.js.map +1 -1
  61. package/dist/mcp.js +0 -113
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/mod-tool.js +0 -130
  64. package/dist/mod-tool.js.map +1 -1
  65. package/dist/mod.js +0 -162
  66. package/dist/mod.js.map +1 -1
  67. package/dist/patch.js +0 -217
  68. package/dist/patch.js.map +1 -1
  69. package/dist/provenance.js +0 -140
  70. package/dist/provenance.js.map +1 -1
  71. package/dist/query.js +0 -75
  72. package/dist/query.js.map +1 -1
  73. package/dist/reach.js +0 -43
  74. package/dist/reach.js.map +1 -1
  75. package/dist/relation.d.ts +2 -0
  76. package/dist/relation.js +3 -58
  77. package/dist/relation.js.map +1 -1
  78. package/dist/render.js +33 -143
  79. package/dist/render.js.map +1 -1
  80. package/dist/report-sarif.d.ts +0 -11
  81. package/dist/report-sarif.js +0 -28
  82. package/dist/report-sarif.js.map +1 -1
  83. package/dist/sig.js +0 -35
  84. package/dist/sig.js.map +1 -1
  85. package/dist/sigdecl.js +1 -1
  86. package/dist/sigdecl.js.map +1 -1
  87. package/dist/siggate.js +0 -4
  88. package/dist/siggate.js.map +1 -1
  89. package/dist/site.js +3 -29
  90. package/dist/site.js.map +1 -1
  91. package/dist/subsume.d.ts +0 -10
  92. package/dist/subsume.js +0 -137
  93. package/dist/subsume.js.map +1 -1
  94. package/dist/template.d.ts +2 -1
  95. package/dist/template.js +58 -138
  96. package/dist/template.js.map +1 -1
  97. package/dist/trim.js +0 -41
  98. package/dist/trim.js.map +1 -1
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/dist/type.js.map +1 -1
  101. package/dist/unify.js +12 -242
  102. package/dist/unify.js.map +1 -1
  103. package/dist/utility.js +0 -22
  104. package/dist/utility.js.map +1 -1
  105. package/dist/val/AbnfFuncVal.d.ts +18 -0
  106. package/dist/val/AbnfFuncVal.js +132 -0
  107. package/dist/val/AbnfFuncVal.js.map +1 -0
  108. package/dist/val/AbsentVal.d.ts +11 -0
  109. package/dist/val/AbsentVal.js +30 -0
  110. package/dist/val/AbsentVal.js.map +1 -0
  111. package/dist/val/AggFuncVal.d.ts +10 -1
  112. package/dist/val/AggFuncVal.js +104 -116
  113. package/dist/val/AggFuncVal.js.map +1 -1
  114. package/dist/val/ArithFuncVal.js +0 -12
  115. package/dist/val/ArithFuncVal.js.map +1 -1
  116. package/dist/val/BagVal.js +1 -78
  117. package/dist/val/BagVal.js.map +1 -1
  118. package/dist/val/BigDecimalVal.js +0 -16
  119. package/dist/val/BigDecimalVal.js.map +1 -1
  120. package/dist/val/BigIntegerVal.js +0 -16
  121. package/dist/val/BigIntegerVal.js.map +1 -1
  122. package/dist/val/CloseFuncVal.js +0 -9
  123. package/dist/val/CloseFuncVal.js.map +1 -1
  124. package/dist/val/CmpFuncVal.d.ts +20 -0
  125. package/dist/val/CmpFuncVal.js +188 -0
  126. package/dist/val/CmpFuncVal.js.map +1 -0
  127. package/dist/val/ConjunctVal.js +0 -29
  128. package/dist/val/ConjunctVal.js.map +1 -1
  129. package/dist/val/ConstraintVal.js +0 -500
  130. package/dist/val/ConstraintVal.js.map +1 -1
  131. package/dist/val/ContainerKindVal.js +0 -2
  132. package/dist/val/ContainerKindVal.js.map +1 -1
  133. package/dist/val/CopyFuncVal.js +0 -3
  134. package/dist/val/CopyFuncVal.js.map +1 -1
  135. package/dist/val/Decimal.js +0 -179
  136. package/dist/val/Decimal.js.map +1 -1
  137. package/dist/val/DeprecateFuncVal.js.map +1 -1
  138. package/dist/val/DisjunctVal.js +0 -152
  139. package/dist/val/DisjunctVal.js.map +1 -1
  140. package/dist/val/EachFuncVal.d.ts +1 -2
  141. package/dist/val/EachFuncVal.js +12 -29
  142. package/dist/val/EachFuncVal.js.map +1 -1
  143. package/dist/val/EmitFuncVal.d.ts +1 -1
  144. package/dist/val/EmitFuncVal.js +6 -119
  145. package/dist/val/EmitFuncVal.js.map +1 -1
  146. package/dist/val/ExpectVal.js +0 -62
  147. package/dist/val/ExpectVal.js.map +1 -1
  148. package/dist/val/FilterFuncVal.js +0 -25
  149. package/dist/val/FilterFuncVal.js.map +1 -1
  150. package/dist/val/FuncBaseVal.d.ts +1 -0
  151. package/dist/val/FuncBaseVal.js +7 -127
  152. package/dist/val/FuncBaseVal.js.map +1 -1
  153. package/dist/val/GraphAtomVal.js +0 -15
  154. package/dist/val/GraphAtomVal.js.map +1 -1
  155. package/dist/val/HideFuncVal.js +0 -13
  156. package/dist/val/HideFuncVal.js.map +1 -1
  157. package/dist/val/IntegerVal.js +0 -61
  158. package/dist/val/IntegerVal.js.map +1 -1
  159. package/dist/val/JunctionVal.js +0 -20
  160. package/dist/val/JunctionVal.js.map +1 -1
  161. package/dist/val/KeyFuncVal.js +0 -46
  162. package/dist/val/KeyFuncVal.js.map +1 -1
  163. package/dist/val/ListVal.js +0 -57
  164. package/dist/val/ListVal.js.map +1 -1
  165. package/dist/val/LowerFuncVal.js +11 -6
  166. package/dist/val/LowerFuncVal.js.map +1 -1
  167. package/dist/val/MapVal.js +0 -151
  168. package/dist/val/MapVal.js.map +1 -1
  169. package/dist/val/MatchFuncVal.js +0 -27
  170. package/dist/val/MatchFuncVal.js.map +1 -1
  171. package/dist/val/{FormFuncVal.d.ts → MaybeFuncVal.d.ts} +5 -5
  172. package/dist/val/MaybeFuncVal.js +50 -0
  173. package/dist/val/MaybeFuncVal.js.map +1 -0
  174. package/dist/val/MoveFuncVal.js +0 -18
  175. package/dist/val/MoveFuncVal.js.map +1 -1
  176. package/dist/val/NilVal.js +2 -36
  177. package/dist/val/NilVal.js.map +1 -1
  178. package/dist/val/NomFuncVal.d.ts +12 -0
  179. package/dist/val/NomFuncVal.js +153 -0
  180. package/dist/val/NomFuncVal.js.map +1 -0
  181. package/dist/val/NumberVal.js +0 -15
  182. package/dist/val/NumberVal.js.map +1 -1
  183. package/dist/val/OpBaseVal.d.ts +1 -0
  184. package/dist/val/OpBaseVal.js +3 -15
  185. package/dist/val/OpBaseVal.js.map +1 -1
  186. package/dist/val/PackFuncVal.js +0 -34
  187. package/dist/val/PackFuncVal.js.map +1 -1
  188. package/dist/val/PathFuncVal.js +0 -6
  189. package/dist/val/PathFuncVal.js.map +1 -1
  190. package/dist/val/PathVal.js +0 -41
  191. package/dist/val/PathVal.js.map +1 -1
  192. package/dist/val/PlaceVal.js +0 -25
  193. package/dist/val/PlaceVal.js.map +1 -1
  194. package/dist/val/PlusOpVal.d.ts +1 -7
  195. package/dist/val/PlusOpVal.js +13 -74
  196. package/dist/val/PlusOpVal.js.map +1 -1
  197. package/dist/val/PrefFuncVal.js +0 -1
  198. package/dist/val/PrefFuncVal.js.map +1 -1
  199. package/dist/val/PrefVal.js +0 -167
  200. package/dist/val/PrefVal.js.map +1 -1
  201. package/dist/val/RecurseVal.js +0 -55
  202. package/dist/val/RecurseVal.js.map +1 -1
  203. package/dist/val/RefVal.js +0 -282
  204. package/dist/val/RefVal.js.map +1 -1
  205. package/dist/val/ReferFuncVal.js +0 -232
  206. package/dist/val/ReferFuncVal.js.map +1 -1
  207. package/dist/val/ScalarKindVal.js +0 -49
  208. package/dist/val/ScalarKindVal.js.map +1 -1
  209. package/dist/val/ScalarVal.js +0 -11
  210. package/dist/val/ScalarVal.js.map +1 -1
  211. package/dist/val/StrFuncVal.js +0 -18
  212. package/dist/val/StrFuncVal.js.map +1 -1
  213. package/dist/val/SuperFuncVal.js +0 -32
  214. package/dist/val/SuperFuncVal.js.map +1 -1
  215. package/dist/val/TopVal.js +0 -1
  216. package/dist/val/TopVal.js.map +1 -1
  217. package/dist/val/TranslateFuncVal.d.ts +12 -0
  218. package/dist/val/TranslateFuncVal.js +99 -0
  219. package/dist/val/TranslateFuncVal.js.map +1 -0
  220. package/dist/val/UpperFuncVal.js +11 -6
  221. package/dist/val/UpperFuncVal.js.map +1 -1
  222. package/dist/val/Val.d.ts +1 -0
  223. package/dist/val/Val.js +2 -133
  224. package/dist/val/Val.js.map +1 -1
  225. package/dist/val/VarVal.js +0 -12
  226. package/dist/val/VarVal.js.map +1 -1
  227. package/dist/val/arith.js +0 -37
  228. package/dist/val/arith.js.map +1 -1
  229. package/dist/val/caserange.d.ts +3 -0
  230. package/dist/val/caserange.js +49 -0
  231. package/dist/val/caserange.js.map +1 -0
  232. package/dist/val/members.js +0 -6
  233. package/dist/val/members.js.map +1 -1
  234. package/dist/val/numcmp.js +0 -11
  235. package/dist/val/numcmp.js.map +1 -1
  236. package/dist/val/numkind.js +0 -145
  237. package/dist/val/numkind.js.map +1 -1
  238. package/dist/val/valutil.js +0 -16
  239. package/dist/val/valutil.js.map +1 -1
  240. package/dist/vet.d.ts +12 -0
  241. package/dist/vet.js +159 -412
  242. package/dist/vet.js.map +1 -1
  243. package/dist/view.js +0 -414
  244. package/dist/view.js.map +1 -1
  245. package/dist/walk.js +0 -41
  246. package/dist/walk.js.map +1 -1
  247. package/grammar/aontu.abnf +9 -7
  248. package/grammar/aontu.gbnf +5 -5
  249. package/grammar/aontu.lark +5 -5
  250. package/grammar/aontu.tmLanguage.json +1 -1
  251. package/package.json +4 -2
  252. package/skill/SKILL.md +8 -0
  253. package/skill/init/check.sh +28 -0
  254. package/skill/init/data.aon +12 -0
  255. package/skill/init/model.aon +19 -0
  256. package/skill/tasks.md +151 -0
  257. package/src/agentsmd.ts +8 -32
  258. package/src/alias.ts +0 -39
  259. package/src/allow.ts +221 -0
  260. package/src/aontu.ts +10 -108
  261. package/src/aontumodel.ts +32 -0
  262. package/src/cli.ts +1009 -540
  263. package/src/ctx.ts +0 -103
  264. package/src/diff.ts +0 -40
  265. package/src/err.ts +0 -40
  266. package/src/escape.ts +0 -46
  267. package/src/exactjson.ts +0 -131
  268. package/src/format.ts +63 -234
  269. package/src/grammar.ts +72 -0
  270. package/src/graph.ts +0 -61
  271. package/src/hcanon.ts +0 -82
  272. package/src/helpdoc.ts +77 -0
  273. package/src/hints.ts +72 -49
  274. package/src/jsonschema.ts +0 -123
  275. package/src/keyorder.ts +0 -42
  276. package/src/lang.ts +39 -895
  277. package/src/lower.ts +15 -65
  278. package/src/lsp-server.ts +0 -16
  279. package/src/lsp.ts +12 -180
  280. package/src/mcp-server.ts +0 -31
  281. package/src/mcp.ts +0 -130
  282. package/src/mod-tool.ts +0 -158
  283. package/src/mod.ts +0 -178
  284. package/src/patch.ts +0 -232
  285. package/src/provenance.ts +0 -183
  286. package/src/query.ts +0 -84
  287. package/src/reach.ts +0 -53
  288. package/src/relation.ts +7 -71
  289. package/src/render.ts +33 -180
  290. package/src/report-sarif.ts +0 -48
  291. package/src/sig.ts +0 -35
  292. package/src/sigdecl.ts +1 -1
  293. package/src/siggate.ts +0 -30
  294. package/src/site.ts +3 -29
  295. package/src/subsume.ts +1 -161
  296. package/src/template.ts +69 -140
  297. package/src/trim.ts +0 -53
  298. package/src/type.ts +2 -45
  299. package/src/unify.ts +13 -251
  300. package/src/utility.ts +0 -31
  301. package/src/val/AbnfFuncVal.ts +181 -0
  302. package/src/val/AbsentVal.ts +54 -0
  303. package/src/val/AggFuncVal.ts +152 -188
  304. package/src/val/ArithFuncVal.ts +0 -20
  305. package/src/val/BagVal.ts +1 -78
  306. package/src/val/BigDecimalVal.ts +0 -16
  307. package/src/val/BigIntegerVal.ts +0 -16
  308. package/src/val/CloseFuncVal.ts +0 -9
  309. package/src/val/CmpFuncVal.ts +249 -0
  310. package/src/val/ConjunctVal.ts +0 -33
  311. package/src/val/ConstraintVal.ts +2 -537
  312. package/src/val/ContainerKindVal.ts +0 -18
  313. package/src/val/CopyFuncVal.ts +0 -5
  314. package/src/val/Decimal.ts +1 -185
  315. package/src/val/DeprecateFuncVal.ts +0 -10
  316. package/src/val/DisjunctVal.ts +0 -157
  317. package/src/val/EachFuncVal.ts +12 -53
  318. package/src/val/EmitFuncVal.ts +8 -208
  319. package/src/val/ExpectVal.ts +0 -62
  320. package/src/val/FilterFuncVal.ts +0 -55
  321. package/src/val/FuncBaseVal.ts +9 -130
  322. package/src/val/GraphAtomVal.ts +0 -42
  323. package/src/val/HideFuncVal.ts +0 -15
  324. package/src/val/IntegerVal.ts +0 -61
  325. package/src/val/JunctionVal.ts +0 -20
  326. package/src/val/KeyFuncVal.ts +0 -48
  327. package/src/val/ListVal.ts +0 -59
  328. package/src/val/LowerFuncVal.ts +12 -7
  329. package/src/val/MapVal.ts +0 -151
  330. package/src/val/MatchFuncVal.ts +0 -59
  331. package/src/val/MaybeFuncVal.ts +86 -0
  332. package/src/val/MoveFuncVal.ts +0 -20
  333. package/src/val/NilVal.ts +2 -36
  334. package/src/val/NomFuncVal.ts +200 -0
  335. package/src/val/NumberVal.ts +0 -16
  336. package/src/val/OpBaseVal.ts +4 -17
  337. package/src/val/PackFuncVal.ts +0 -63
  338. package/src/val/PathFuncVal.ts +0 -32
  339. package/src/val/PathVal.ts +0 -66
  340. package/src/val/PlaceVal.ts +0 -45
  341. package/src/val/PlusOpVal.ts +18 -75
  342. package/src/val/PrefFuncVal.ts +0 -1
  343. package/src/val/PrefVal.ts +0 -179
  344. package/src/val/RecurseVal.ts +0 -81
  345. package/src/val/RefVal.ts +1 -285
  346. package/src/val/ReferFuncVal.ts +0 -255
  347. package/src/val/ScalarKindVal.ts +0 -50
  348. package/src/val/ScalarVal.ts +0 -12
  349. package/src/val/StrFuncVal.ts +0 -44
  350. package/src/val/SuperFuncVal.ts +0 -42
  351. package/src/val/TopVal.ts +0 -1
  352. package/src/val/TranslateFuncVal.ts +132 -0
  353. package/src/val/UpperFuncVal.ts +12 -7
  354. package/src/val/Val.ts +3 -192
  355. package/src/val/VarVal.ts +0 -15
  356. package/src/val/arith.ts +0 -92
  357. package/src/val/caserange.ts +53 -0
  358. package/src/val/members.ts +0 -23
  359. package/src/val/numcmp.ts +1 -27
  360. package/src/val/numkind.ts +0 -149
  361. package/src/val/valutil.ts +0 -16
  362. package/src/vet.ts +209 -504
  363. package/src/view.ts +0 -507
  364. package/src/walk.ts +0 -41
  365. package/dist/std.d.ts +0 -3
  366. package/dist/std.js +0 -637
  367. package/dist/std.js.map +0 -1
  368. package/dist/val/FormFuncVal.js +0 -55
  369. package/dist/val/FormFuncVal.js.map +0 -1
  370. package/src/std.ts +0 -648
  371. package/src/val/FormFuncVal.ts +0 -119
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
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;
4
+ exports.vetWaiter = exports.KNOWN_VERBS = void 0;
5
5
  exports.replCommand = replCommand;
6
6
  exports.evalSource = evalSource;
7
7
  exports.main = main;
@@ -18,21 +18,20 @@ exports.runTemplate = runTemplate;
18
18
  exports.runMod = runMod;
19
19
  exports.runHash = runHash;
20
20
  exports.runGet = runGet;
21
+ exports.runHelp = runHelp;
22
+ exports.runExplain = runExplain;
23
+ exports.runInit = runInit;
24
+ exports.nearestVerb = nearestVerb;
25
+ exports.looksLikeVerb = looksLikeVerb;
21
26
  exports.runWhy = runWhy;
22
27
  exports.renderWhyText = renderWhyText;
23
28
  exports.runSet = runSet;
29
+ exports.runAllow = runAllow;
24
30
  exports.runAgentsMd = runAgentsMd;
25
31
  exports.runFmt = runFmt;
26
32
  exports.watchChange = watchChange;
27
33
  exports.watchSignature = watchSignature;
28
34
  exports.deprecatedAt = deprecatedAt;
29
- // Command-line interface for Aontu.
30
- //
31
- // aontu [options] [file]
32
- //
33
- // With a file argument, the file is evaluated and the result printed.
34
- // With no file on an interactive terminal, a REPL is started. With no
35
- // file and piped input, the source is read from stdin. See HELP below.
36
35
  // Named imports, not `import * as`: the namespace form makes tsc emit the
37
36
  const query_1 = require("./query");
38
37
  // __importStar downlevel helper, whose branches no supported Node takes.
@@ -55,6 +54,9 @@ const view_1 = require("./view");
55
54
  const agentsmd_1 = require("./agentsmd");
56
55
  const format_1 = require("./format");
57
56
  const utility_1 = require("./utility");
57
+ const helpdoc_1 = require("./helpdoc");
58
+ const hints_1 = require("./hints");
59
+ const keyorder_1 = require("./keyorder");
58
60
  const HELP = `Usage: aontu [options] [file]
59
61
  aontu vet [options] <schema> <data> [more-data...]
60
62
  aontu subsume [options] <general> <specific>
@@ -68,14 +70,20 @@ const HELP = `Usage: aontu [options] [file]
68
70
  aontu render [--at <path>] [--profile <file>]... [--unit <path>]
69
71
  [--stdout | --out <dir> | --check <dir> | --coverage]
70
72
  [--coverage-at <path>] [--strict] <file>
71
- aontu template [--resugar] [--check] [--marker <token>] <file>
73
+ aontu template [--resugar] [--check] [--marker <token>]
74
+ [--profile <file>] <file>
72
75
  aontu hash [options] <file>
73
76
  aontu mod tidy|verify|vendor|manifest [options] [dir]
74
77
  aontu get <path> [options] <file>
75
78
  aontu why <path> [options] <file>
76
79
  aontu set <path>=<value>... --entry <file> --overlay <file>
77
- aontu agentsmd [--write <AGENTS.md>] <file>
78
- aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
80
+ aontu allow --role <role> [--at <path>] <roles-file> <path>...
81
+ aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
82
+ aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
83
+ [--profile <file>] <file>...
84
+ aontu help [topic] [--format text|json]
85
+ aontu explain <code> | --list [--format text|json]
86
+ aontu init [dir]
79
87
  aontu lsp
80
88
  aontu mcp [--root <dir>]
81
89
 
@@ -83,6 +91,34 @@ Evaluate an aontu source file and print the result as JSON.
83
91
  With no file on an interactive terminal, start a REPL.
84
92
  With no file and piped input, read the source from stdin.
85
93
 
94
+ NEW TO THE LANGUAGE? This page documents the TOOL. The documentation
95
+ of the LANGUAGE travels inside this binary, and this is how to reach it:
96
+
97
+ aontu help List the topics this binary carries
98
+ aontu help tasks Which verb does the job you have
99
+ aontu help language The whole grammar, on one page
100
+ aontu help examples The ladder, from plain JSON upward
101
+ aontu help codes What a refusal means
102
+ aontu help grammar The published ABNF
103
+ aontu explain <code> What one error code a report carries means
104
+ aontu explain --list Every registered code with its class
105
+
106
+ Every one of those answers with no network and no checkout. The
107
+ long-form documentation -- the tutorial, the language and API
108
+ references, the how-to guides -- is in docs/ of the repository, which
109
+ is where to go when the topics above are not enough; the contributor
110
+ and agent guide is AGENTS.md beside it.
111
+
112
+ NOTHING TO EDIT YET? aontu init [dir] writes a working model, an
113
+ instance of it, and the four checks to run -- so the first
114
+ document is an edit of something that already holds, rather than an
115
+ invention. It refuses to overwrite.
116
+
117
+ The one construct to know before writing anything: &: inside a map is
118
+ a TEMPLATE that every key of that map must satisfy. A quoted "*" is a
119
+ key named *, not a wildcard, and a schema written that way constrains
120
+ nothing while still reporting valid.
121
+
86
122
  The vet verb validates data documents against a schema document and
87
123
  reports what does not hold, as text or as a machine-readable object.
88
124
 
@@ -92,7 +128,11 @@ query between a document and its own earlier versions.
92
128
 
93
129
  Options:
94
130
  -c, --canon Print the canonical form instead of generated JSON
95
- -h, --help Show this help and exit
131
+ --format <f> text (default) or json. The json form wraps the
132
+ answer as {aontu, findings, ok, out}, so a failure
133
+ here reads like every other verb's
134
+ -h, --help Show this help and exit (the verbs and their flags);
135
+ aontu help is the LANGUAGE, and lists its own topics
96
136
  --jsonl REPL: answer every command as one JSON line
97
137
  -v, --version Print the version and exit
98
138
  --trust <t> Include capability: system (default), none, or
@@ -123,12 +163,28 @@ Vet options:
123
163
  --closed Refuse keys the anchor does not declare
124
164
  --partial Residue is reported but does not fail the run
125
165
  --max-errors <n> Cap the finding list (default 20)
166
+ --coverage Report what the check EXAMINED: how many data
167
+ leaves a schema declaration constrained, the
168
+ shallowest data paths none did, and the
169
+ declarations no data met
170
+ --strict-coverage --coverage, and exit 1 when the run was VACUOUS --
171
+ when no data leaf was constrained at all. The
172
+ verdict word is unchanged, so nothing that passes
173
+ today starts failing without this flag
174
+ --coverage-at <p> Measure coverage under this path of the data only
126
175
  --format <f> text (default), json or sarif
127
176
  --watch Re-run whenever a watched file changes
128
177
 
178
+ A check that examined NOTHING and a check that passed answer the same
179
+ without --coverage. The usual cause is a schema written with the
180
+ wildcard other tools use: a quoted "*" is a key NAMED *, not a
181
+ template, so it constrains nothing and the run still reports valid.
182
+ The template is &: -- see aontu help language.
183
+
129
184
  Vet exit codes:
130
185
  0 valid data unifies, and is concrete (or --partial)
131
- 1 invalid at least one contradiction
186
+ 1 invalid at least one contradiction, or a vacuous run under
187
+ --strict-coverage
132
188
  2 usage bad option, or a file that cannot be read
133
189
  3 incomplete no contradiction, but the truth is not yet satisfied
134
190
  4 error the schema is unusable on its own
@@ -264,8 +320,9 @@ up on its own, or a relation, root or path that names nothing.
264
320
  Render options:
265
321
  --at <path> Render the value at this path ($.a.b); the root by
266
322
  default
267
- --profile <file> A profile document, profile: {lang, ...}, vetted
268
- against aontu:profile; repeatable, one per language
323
+ --profile <file> A profile document, aontu: render: Lang: {lang,
324
+ ...}, vetted against aontu:render; repeatable,
325
+ one per language
269
326
  --unit <path> Render only the unit with this path
270
327
  --stdout One unit's bytes and nothing else (with --unit when
271
328
  the instance has several)
@@ -289,8 +346,8 @@ does not stand up or the instance is not aontu:code.
289
346
  A render entry file whose extension is not .aon is a TEMPLATE: a
290
347
  generator in the target's own syntax, whose marker lines carry aontu
291
348
  and whose other lines are output. It is desugared before it is
292
- evaluated, and --marker names the marker for a language the table does
293
- not know.
349
+ evaluated, and a language the table does not know names its marker with
350
+ --marker, or declares it once in a profile file that --profile reads.
294
351
 
295
352
  Template options:
296
353
  --resugar The file is the canonical aontu; print the template
@@ -298,7 +355,9 @@ Template options:
298
355
  --check Desugar and resugar, and exit 1 if the file is not
299
356
  what the round trip answers
300
357
  --marker <t> The marker, when the extension does not name it
301
- (default //-, and #- --- /*- by extension)
358
+ (default //-, and #- --- /*- <!--- by extension)
359
+ --profile <f> A profile file, whose template.ext names the
360
+ extensions it marks and template.marker the marker
302
361
 
303
362
  The template verb prints the canonical aontu form of a generator
304
363
  written in the target's own syntax: a marked line is aontu source, and
@@ -326,14 +385,61 @@ change contradicts a pinned value -- aontu why locates it, and
326
385
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
327
386
  stand up on its own.
328
387
 
388
+ Allow options:
389
+ --role <role> The role the caller is operating under (required)
390
+ --at <path> Where the roles map lives in the role model
391
+ (default $.roles)
392
+ --format <f> text (default) or json
393
+
394
+ The allow verb asks a role model whether a role may modify every one
395
+ of the given subtrees, and answers before the change is made. The
396
+ role model is an aontu document: one entry per role, each carrying
397
+ allow (the subtrees it may modify) and optionally deny (the ones it
398
+ may not), as path strings starting at $; * in a path matches any one
399
+ key. A path is allowed when an allow entry is at or above it, and
400
+ refused when a deny entry is at, above or below it, whatever the
401
+ order. Every path starts with $, and may be spelled as set's
402
+ assignment, <path>=<value>, whose value must be one value: a value
403
+ carrying a second pair would write a subtree the gate was not asked
404
+ about.
405
+
406
+ Allow exit codes: 0 allowed (every path), 1 refused (at least one
407
+ path, or a role the model does not declare), 2 usage, 4 the role
408
+ model does not stand up on its own.
409
+
329
410
  Agentsmd options:
330
411
  --write <file> Splice the stanza into this file between the
331
412
  aontu:begin and aontu:end markers, appending them
332
413
  when they are absent; the rest is left alone
414
+ --depth <n> How deep the shape line projects (default 2). Two
415
+ levels name the root keys and say top under them; a
416
+ caller that wants the fields asks for them
333
417
 
334
418
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
335
419
  stand up on its own.
336
420
 
421
+ Help options:
422
+ --format <f> text (default) or json, the topic and its text
423
+
424
+ The help verb prints the embedded teaching pack: the language, not the
425
+ tool. With no topic it lists them. Topics are tasks, language,
426
+ examples, codes and grammar; the corpus is generated from docs/skill/
427
+ and grammar/aontu.abnf, so it cannot drift from those sources.
428
+
429
+ Help exit codes: 0 printed, 2 an unknown topic (the topics are listed)
430
+ or a bad option.
431
+
432
+ Explain options:
433
+ --list Every registered error code with its class
434
+ --format <f> text (default) or json
435
+
436
+ The explain verb answers what one error code means, from the same
437
+ table the engine attaches to a finding. Every registered code has an
438
+ entry, so a code read out of a report always resolves.
439
+
440
+ Explain exit codes: 0 explained, 2 an unknown code (near matches are
441
+ named) or a bad option.
442
+
337
443
  Fmt options:
338
444
  -w, --write Rewrite each file in place, when its form would change
339
445
  -l, --list Print the name of each file whose form would change
@@ -344,7 +450,9 @@ Fmt options:
344
450
  shapes, on standard error, and print nothing else
345
451
  --strict With --lint, and exit 1 when there is a finding
346
452
  --marker <t> The file is a generator, and this is its marker
347
- (default //-, and #- --- /*- by extension)
453
+ (default //-, and #- --- /*- <!--- by extension)
454
+ --profile <f> A profile file, whose template.ext names the
455
+ extensions it marks and template.marker the marker
348
456
 
349
457
  The fmt verb prints one document in the agreed form; with no file it
350
458
  reads standard input. Several files need one of the options above.
@@ -387,27 +495,58 @@ function version() {
387
495
  return '0.0.0';
388
496
  }
389
497
  }
498
+ const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g');
499
+ function evalFinding(code, text) {
500
+ return {
501
+ class: (0, hints_1.codeClass)(code),
502
+ code,
503
+ message: text.split('\n')[0].replace(EVAL_ANSI, ''),
504
+ path: '$',
505
+ severity: 'error',
506
+ sites: [],
507
+ };
508
+ }
390
509
  // Evaluate source, returning either the rendered output or the error
391
- // message. Never throws.
510
+ // message, and the failure in the finding shape. Never throws.
392
511
  function evalSource(aontu, src, mode) {
393
512
  try {
394
- // exactJSON, not JSON.stringify: a document using the `0d` exact
395
- // leaves generates bigints and Decimals, which JSON.stringify cannot
396
- // write (D9). The CLI prints INDENTED JSON and the shared suite's
397
- // `gens` mode prints COMPACT JSON, but both go through this one
398
- // emitter -- an indent argument rather than a second implementation,
399
- // so the two cannot drift from each other or from the Go port.
400
513
  const text = 'canon' === mode
401
514
  ? aontu.unify(src).canon
402
515
  : (0, aontu_1.exactJSON)(aontu.generate(src), 2);
403
- return { ok: true, text };
516
+ return { ok: true, text, findings: [] };
404
517
  }
405
518
  catch (err) {
406
519
  const msg = (err instanceof aontu_1.AontuError || true === err?.aontu)
407
520
  ? err.message
408
521
  : String(err?.message ?? err);
409
- return { ok: false, text: msg };
522
+ const errs = 'function' === typeof err?.errs ? err.errs() : [];
523
+ const first = errs[0];
524
+ return {
525
+ ok: false,
526
+ text: msg,
527
+ findings: null == first ? [] : [evalFinding(first.why, msg)],
528
+ };
529
+ }
530
+ }
531
+ // The bare command's answer, in the form the caller asked for. The
532
+ // text form is what it has always printed, on the stream the verdict
533
+ // chooses; `--format json` is the same answer as one object, on
534
+ // stdout, so a harness reads one stream and one shape either way.
535
+ // Mirrors emit in go/cmd/aontu/main.go.
536
+ function emitEval(res, format) {
537
+ if ('json' === format) {
538
+ process.stdout.write((0, aontu_1.exactJSON)({
539
+ aontu: { version: version(), verb: 'eval' },
540
+ findings: res.findings,
541
+ ok: res.ok,
542
+ out: res.ok ? res.text : '',
543
+ }, 2) + '\n');
544
+ }
545
+ else {
546
+ ;
547
+ (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
410
548
  }
549
+ return res.ok ? 0 : 1;
411
550
  }
412
551
  // The one-line warning of the staged default flip. Once per (kind,
413
552
  // path): a fixpoint re-resolves nothing (includes load at parse), but
@@ -444,16 +583,6 @@ function trustOpts(trust, entryRoot) {
444
583
  return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
445
584
  }
446
585
  }
447
- // EVERY VERB honours the include capability, not just the bare
448
- // command. G5 wired `--trust`/`--include-root` to `aontu <file>` alone,
449
- // so `aontu vet schema.aon data.json` -- the surface an agent actually
450
- // scripts -- ran the full system resolver with no flag to confine it
451
- // and no warning (use-cases/REVIEW.md finding G). The flags are
452
- // stripped here, before each verb parses its own tail, so a verb only
453
- // has to pass the profile on to its engine.
454
- //
455
- // Returns undefined when the spelling is wrong, with the message
456
- // already printed: the caller answers the usage class.
457
586
  function takeTrust(argv) {
458
587
  const rest = [];
459
588
  let trust = { kind: 'system-warn', textExt: [] };
@@ -542,40 +671,40 @@ function verbOpts(trust, entryRoot) {
542
671
  function entryRootOf(file) {
543
672
  return null == file ? process.cwd() : (0, node_path_1.dirname)((0, node_path_1.resolve)(file));
544
673
  }
545
- function runFile(file, mode, trust) {
674
+ function runFile(file, mode, format, trust) {
546
675
  let src;
547
676
  try {
548
677
  src = (0, node_fs_1.readFileSync)(file, 'utf8');
549
678
  }
550
679
  catch (err) {
680
+ if (looksLikeVerb(file)) {
681
+ process.stderr.write(`aontu: \`${file}\` is not a file, and not a verb this port knows\n`);
682
+ const near = nearestVerb(file, KNOWN_VERBS);
683
+ if ('' !== near) {
684
+ process.stderr.write(`aontu: did you mean \`aontu ${near}\`?\n`);
685
+ }
686
+ process.stderr.write('aontu: `aontu --help` lists the verbs, `aontu help` the topics\n');
687
+ return 2;
688
+ }
551
689
  process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`);
552
690
  return 1;
553
691
  }
554
692
  const path = (0, node_path_1.resolve)(file);
555
- // `fs` IS WHAT MAKES A FRAME EXCERPT THE FILE IT NAMES. Without it,
556
- // err.ts's resolveSrc falls back to the ENTRY text, so a frame whose
557
- // arrow says `lib/types.aon:2:6` printed the entry's line 2 under it
558
- // -- a real file name over another file's line, which
559
- // docs/reference-api.md forbids in the same words it uses to require
560
- // the name.
561
693
  const aontu = new aontu_1.Aontu({
562
694
  path,
563
695
  errfs: { existsSync: node_fs_1.existsSync, readFileSync: node_fs_1.readFileSync },
564
696
  ...trustOpts(trust, (0, node_path_1.dirname)(path)),
565
697
  });
566
- const res = evalSource(aontu, src, mode);
567
- (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
568
- return res.ok ? 0 : 1;
698
+ return emitEval(evalSource(aontu, src, mode), format);
569
699
  }
570
- function runStdin(mode, trust) {
700
+ function runStdin(mode, format, trust) {
571
701
  return new Promise((resolve) => {
572
702
  let src = '';
573
703
  process.stdin.setEncoding('utf8');
574
704
  process.stdin.on('data', (d) => (src += d));
575
705
  process.stdin.on('end', () => {
576
706
  const res = evalSource(new aontu_1.Aontu(trustOpts(trust, process.cwd())), src, mode);
577
- (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
578
- resolve(res.ok ? 0 : 1);
707
+ resolve(emitEval(res, format));
579
708
  });
580
709
  });
581
710
  }
@@ -696,14 +825,6 @@ function runRepl(initialMode, jsonl, trust) {
696
825
  rl.prompt();
697
826
  });
698
827
  rl.on('close', () => {
699
- // The closing newline is for a HUMAN, so it is written only for
700
- // one: it moves the terminal off the prompt line that `rl` left
701
- // hanging. In `--jsonl` there is no prompt, every answer already
702
- // ends in its own newline, and this one appended a bare empty line
703
- // to the stream -- a record that is not JSON, at the end of a
704
- // protocol whose whole contract is one JSON object per line. A
705
- // harness parsing every line it receives failed on it, after the
706
- // commands had all succeeded. Mirrors go/cmd/aontu/repl.go.
707
828
  if (!jsonl) {
708
829
  process.stdout.write('\n');
709
830
  }
@@ -713,15 +834,6 @@ function runRepl(initialMode, jsonl, trust) {
713
834
  process.exitCode = 0;
714
835
  });
715
836
  }
716
- // THE VET VERB (G2 phase 3).
717
- //
718
- // Exit codes are VERDICT CLASSES, not a pass/fail bit: an agent loop
719
- // branches on "the data contradicts the truth" (1) differently from
720
- // "the data has not supplied everything the truth requires" (3), and
721
- // differently again from "the schema itself is broken" (4), which is
722
- // never the data's fault. 2 stays what it already was for this CLI --
723
- // the caller got the invocation wrong -- which is why an unreadable
724
- // file is a 2 rather than a 4.
725
837
  const VET_EXIT = {
726
838
  valid: 0,
727
839
  invalid: 1,
@@ -739,6 +851,9 @@ function parseVetArgs(argv) {
739
851
  let partial = false;
740
852
  let maxErrors;
741
853
  let watch = false;
854
+ let coverage = false;
855
+ let strictCoverage = false;
856
+ let coverageAt;
742
857
  for (let i = 0; i < argv.length; i++) {
743
858
  const arg = argv[i];
744
859
  // `-h`/`--help` before anything else, INCLUDING the file count:
@@ -762,14 +877,6 @@ function parseVetArgs(argv) {
762
877
  format = f;
763
878
  }
764
879
  else if ('--max-errors' === arg) {
765
- // ONE GRAMMAR, spelled the same way in both ports: decimal
766
- // digits, one to nine of them, at least 1. `Number()` alone
767
- // accepted `1.0`, `1e2`, `0x10` and ` 3`, which Go's parser
768
- // refuses -- so the same documented invocation meant different
769
- // things in the two shipped commands. The nine-digit ceiling is
770
- // where the ports would part company again: beyond it Go's
771
- // integer conversion saturates, and a cap nobody can reach is
772
- // not worth a divergence.
773
880
  const raw = argv[++i];
774
881
  if (!/^[0-9]{1,9}$/.test(raw ?? '') || 1 > Number(raw)) {
775
882
  return { err: 'aontu: --max-errors needs a positive whole number' };
@@ -782,6 +889,24 @@ function parseVetArgs(argv) {
782
889
  else if ('--partial' === arg) {
783
890
  partial = true;
784
891
  }
892
+ else if ('--coverage' === arg) {
893
+ coverage = true;
894
+ }
895
+ else if ('--strict-coverage' === arg) {
896
+ // IMPLIES THE ACCOUNTING, because a gate cannot fire on what was
897
+ // never measured. Asking for the strict form and having to
898
+ // remember `--coverage` beside it is a usage trap with one
899
+ // correct answer, so the flag takes it.
900
+ coverage = true;
901
+ strictCoverage = true;
902
+ }
903
+ else if ('--coverage-at' === arg) {
904
+ coverageAt = argv[++i];
905
+ if (null == coverageAt) {
906
+ return { err: 'aontu: --coverage-at needs a path' };
907
+ }
908
+ coverage = true;
909
+ }
785
910
  else if ('--watch' === arg) {
786
911
  watch = true;
787
912
  }
@@ -805,6 +930,9 @@ function parseVetArgs(argv) {
805
930
  partial,
806
931
  maxErrors,
807
932
  watch,
933
+ coverage,
934
+ strictCoverage,
935
+ coverageAt,
808
936
  },
809
937
  };
810
938
  }
@@ -826,10 +954,6 @@ function renderFinding(f) {
826
954
  out.push(` actual: ${f.actual}`);
827
955
  }
828
956
  for (const s of f.sites) {
829
- // Every site carries the canon of the value it stands for: that is
830
- // what makes the two sides of a conflict readable side by side. A
831
- // site's file is always a string -- empty when the value belongs to
832
- // neither document -- so there is nothing to coalesce here.
833
957
  out.push(` ${s.role}: ${s.file}:${s.row}:${s.col} (${s.value})`);
834
958
  }
835
959
  return out.join('\n');
@@ -837,11 +961,46 @@ function renderFinding(f) {
837
961
  function renderVetText(report) {
838
962
  const head = `verdict: ${report.verdict}` +
839
963
  (report.truncated ? ' (findings truncated)' : '');
840
- if (0 === report.findings.length) {
841
- return head;
964
+ const body = 0 === report.findings.length ? []
965
+ : ['', ...report.findings.map(renderFinding)];
966
+ const cover = null == report.coverage ? []
967
+ : ['', ...renderVetCoverage(report.coverage)];
968
+ return [head, ...body, ...cover].join('\n');
969
+ }
970
+ // The coverage block (G11 phase 5). VACUOUS FIRST and in the
971
+ // imperative, because it is the one line that changes what the reader
972
+ // should do: a `valid` verdict above it means nothing.
973
+ function renderVetCoverage(c) {
974
+ const out = [];
975
+ if (c.vacuous) {
976
+ out.push('coverage: VACUOUS — no data leaf was constrained' +
977
+ ' by the schema; this run checked nothing');
842
978
  }
843
- return [head, ''].concat(report.findings.map(renderFinding)).join('\n');
979
+ out.push(`coverage: ${c.checked}/${c.leaves} data leaves checked,` +
980
+ ` ${c.declared} schema declarations`);
981
+ // The lists are the SHALLOWEST paths, so each names a subtree rather
982
+ // than every leaf under it, and both are capped: a report a reader
983
+ // scrolls past is a report nobody reads.
984
+ for (const [label, paths] of [
985
+ ['unchecked', c.unchecked], ['unused', c.unused],
986
+ ]) {
987
+ if (0 === paths.length) {
988
+ continue;
989
+ }
990
+ const shown = paths.slice(0, COVERAGE_LIST_MAX);
991
+ for (const p of shown) {
992
+ out.push(` ${label}: ${p}`);
993
+ }
994
+ if (shown.length < paths.length) {
995
+ out.push(` ${label}: … and ${paths.length - shown.length} more`);
996
+ }
997
+ }
998
+ return out;
844
999
  }
1000
+ // How many coverage paths the TEXT form prints per list. The JSON form
1001
+ // carries every one: a machine reads the whole list, a person reads the
1002
+ // first few and the count.
1003
+ const COVERAGE_LIST_MAX = 10;
845
1004
  // The machine-readable form. `aontu` names the producer, so a report
846
1005
  // read from a file or a pipe says which version and which verb made it
847
1006
  // without the consumer having to know.
@@ -851,6 +1010,7 @@ function renderVetJson(report) {
851
1010
  verdict: report.verdict,
852
1011
  truncated: report.truncated,
853
1012
  findings: report.findings,
1013
+ ...(null == report.coverage ? {} : { coverage: report.coverage }),
854
1014
  }, 2);
855
1015
  }
856
1016
  // The machine-interchange form (G2 phase 5): SARIF 2.1.0, rendered by
@@ -885,12 +1045,17 @@ function vetOnce(args, trust) {
885
1045
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
886
1046
  return 2;
887
1047
  }
888
- // Each data file is vetted on its own, because a parsed tree is
889
- // single-use (docs/reference-api.md) -- and because two data files
890
- // are two candidates for the same truth, not one merged candidate.
891
1048
  let verdict = 'valid';
892
1049
  let truncated = false;
893
1050
  const findings = [];
1051
+ let cov;
1052
+ // Initialised rather than left undefined: it is filled in the same
1053
+ // block that sets `cov`, so a fallback at the read below would be an
1054
+ // arm nothing can take. The FIRST file replaces it wholesale, which
1055
+ // is what makes the fold an intersection rather than an empty set.
1056
+ let unusedEvery = new Set();
1057
+ let unusedSeen = false;
1058
+ const uncheckedAll = new Set();
894
1059
  for (const source of sources) {
895
1060
  const report = (0, aontu_1.vet)(schemaSrc, source.src, {
896
1061
  ...verbOpts(trust, entryRootOf(args.schema)),
@@ -900,51 +1065,62 @@ function vetOnce(args, trust) {
900
1065
  maxErrors: args.maxErrors,
901
1066
  schemaUrl: args.schema,
902
1067
  dataUrl: source.file,
903
- // The paths as well as the labels: a relative `@"file"` load
904
- // inside either document resolves from ITS OWN directory, the
905
- // way `aontu <file>` already resolves one (runFile above). The
906
- // path is passed AS TYPED, not resolved: it doubles as the
907
- // label above, and a report that mixed the typed path with an
908
- // absolute one would name the same file two ways.
909
1068
  schemaPath: args.schema,
910
1069
  dataPath: source.file,
1070
+ coverage: args.coverage,
1071
+ coverageAt: args.coverageAt,
911
1072
  });
912
1073
  if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
913
1074
  verdict = report.verdict;
914
1075
  }
915
1076
  truncated = truncated || report.truncated;
916
1077
  findings.push(...report.findings);
917
- // A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
918
- // is reported ONCE. `error` means exactly that -- the run could not
919
- // be set up from the truth's side, never the data's (the exit table
920
- // in docs/reference-api.md) -- so the report the first file
921
- // produced is the report every later file would produce, character
922
- // for character. Concatenating them repeated one broken schema N
923
- // times and, past the cap, marked the report `truncated` over a
924
- // single underlying fault. It only became visible once the `error`
925
- // verdict started carrying findings at all: while the list was
926
- // empty there was nothing to duplicate.
1078
+ if (null != report.coverage) {
1079
+ const c = report.coverage;
1080
+ cov = null == cov ? { ...c } : {
1081
+ checked: cov.checked + c.checked,
1082
+ declared: c.declared,
1083
+ leaves: cov.leaves + c.leaves,
1084
+ unchecked: [],
1085
+ unused: [],
1086
+ vacuous: false,
1087
+ };
1088
+ for (const p of c.unchecked) {
1089
+ uncheckedAll.add(p);
1090
+ }
1091
+ const mine = new Set(c.unused);
1092
+ unusedEvery = unusedSeen
1093
+ ? new Set([...unusedEvery].filter((u) => mine.has(u))) : mine;
1094
+ unusedSeen = true;
1095
+ }
927
1096
  if ('error' === report.verdict) {
928
1097
  break;
929
1098
  }
930
1099
  }
931
- // The cap is on the REPORT, not on each file. Capping every file's
932
- // list and then concatenating them let `--max-errors 1` emit one
933
- // finding PER FILE -- and leave `truncated` false while doing it,
934
- // because no single file had been cut. The engine still caps each
935
- // run, so a pathological file cannot flood the aggregate before it
936
- // gets here; this is the second, honest cut.
937
1100
  const cap = args.maxErrors ?? vet_1.VET_MAX_ERRORS;
938
1101
  const kept = cap < findings.length ? findings.slice(0, cap) : findings;
1102
+ if (null != cov) {
1103
+ cov.unchecked = [...uncheckedAll].sort(keyorder_1.cmpCodePoint);
1104
+ cov.unused = [...unusedEvery].sort(keyorder_1.cmpCodePoint);
1105
+ cov.vacuous = 0 === cov.checked && 0 < cov.leaves;
1106
+ }
939
1107
  const report = {
940
1108
  verdict,
941
1109
  truncated: truncated || cap < findings.length,
942
1110
  findings: kept,
1111
+ ...(null == cov ? {} : { coverage: cov }),
943
1112
  };
944
1113
  const text = 'json' === args.format ? renderVetJson(report) :
945
1114
  'sarif' === args.format ? renderVetSarif(report) :
946
1115
  renderVetText(report);
947
1116
  process.stdout.write(text + '\n');
1117
+ if (true === args.strictCoverage && true === report.coverage?.vacuous) {
1118
+ process.stderr.write('aontu: no data leaf was constrained by the schema:' +
1119
+ ' this run checked nothing\n' +
1120
+ 'aontu: `aontu help language` — a map template is `&:`,' +
1121
+ ' and a quoted "*" is a key named *\n');
1122
+ return 1;
1123
+ }
948
1124
  return VET_EXIT[verdict];
949
1125
  }
950
1126
  // How often `--watch` polls for a change. Polling by mtime+size rather
@@ -964,19 +1140,6 @@ function watchSignature(files) {
964
1140
  function sleep(ms) {
965
1141
  return new Promise((done) => setTimeout(done, ms));
966
1142
  }
967
- // Resolve true when any watched file's signature moves off `before`.
968
- // This is the real waiter: it never resolves false, so a real watch
969
- // runs until the process is interrupted; tests inject their own waiter
970
- // to bound the loop, and pass a short pollMs when they drive this one
971
- // directly. The interval is a required argument (the command passes
972
- // WATCH_POLL_MS) so there is no defaulting branch a test could never
973
- // take.
974
- //
975
- // The BASELINE is an argument, not a snapshot taken here: the loop
976
- // records it BEFORE each vet run, so a save landing between the run's
977
- // reads and the wait still compares as a change. A waiter that
978
- // snapshotted on entry would adopt that unvetted save as its baseline
979
- // and wait indefinitely on a stale report.
980
1143
  async function watchChange(files, before, pollMs) {
981
1144
  for (;;) {
982
1145
  await sleep(pollMs);
@@ -1005,9 +1168,6 @@ async function watchVet(args, wait, trust) {
1005
1168
  }
1006
1169
  return code;
1007
1170
  }
1008
- // The vet verb. Non-watch runs are synchronous and return the exit
1009
- // class directly; `--watch` returns a promise that resolves only when
1010
- // the waiter says stop (never, for the real one).
1011
1171
  function runVet(argv, wait) {
1012
1172
  const trusted = takeTrust(argv);
1013
1173
  if (null == trusted) {
@@ -1218,30 +1378,7 @@ function parseBreakingArgs(argv) {
1218
1378
  },
1219
1379
  };
1220
1380
  }
1221
- // A source file the include resolver can actually load. `git#<rev>`
1222
- // materialises these and nothing else: an include names an Aontu
1223
- // document (`.aon`/`.aontu`, the two extensions `@"foo"` tries) or a
1224
- // JSON one, so the rest of a revision's tree cannot be part of any
1225
- // include closure and copying it would be pure cost.
1226
1381
  const INCLUDABLE = /\.(aon|aontu|jsonic|json)$/;
1227
- // Resolve one --against spelling to an old version.
1228
- //
1229
- // A `git#<rev>` spelling is the old version of the WHOLE TREE, not of
1230
- // the entry file alone. It used to be `git show <rev>:./<file>`, whose
1231
- // text was then evaluated with `generalPath`/`specificPath` pointing at
1232
- // the WORKING file -- so every `@"..."` include in the old document
1233
- // resolved against the working tree, and the "old" side was old entry
1234
- // text meeting new includes. A breaking change inside an included file
1235
- // therefore compared against itself and answered `compatible`: the
1236
- // documented CI gate silently un-gated every non-entry file of the
1237
- // multi-file layout real models use (use-cases/BUGS.md §26). The old
1238
- // tree's includable sources are copied into a temporary directory and
1239
- // the old document is evaluated from THERE.
1240
- //
1241
- // Sources outside the revision -- package includes under node_modules,
1242
- // the bundled `std/system` -- still resolve as they do today: they are
1243
- // not in the tree, and their versions travel with the lockfile rather
1244
- // than with this comparison.
1245
1382
  function oldVersion(spec, file) {
1246
1383
  if (!spec.startsWith('git#')) {
1247
1384
  try {
@@ -1269,23 +1406,9 @@ function oldVersion(spec, file) {
1269
1406
  // that only some failures take.
1270
1407
  const temp = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'aontu-against-'));
1271
1408
  try {
1272
- // THE REPO-RELATIVE PATH COMES FROM GIT, not from path arithmetic.
1273
- // Relativising `rev-parse --show-toplevel` against `resolve(file)`
1274
- // puts two DIFFERENT COORDINATE SYSTEMS on either side of the
1275
- // subtraction: git prints the real path, while the caller's is
1276
- // whatever they typed. On macOS a temp file under /var is
1277
- // /private/var to git, and on Windows a TMP short name
1278
- // (RUNNER~1) is the long form to git -- so the subtraction gave a
1279
- // `../..` climb, the entry was "not in that revision", and the
1280
- // documented CI spelling failed on both platforms while passing on
1281
- // Linux (this PR's own CI). `--show-prefix` is the same question
1282
- // asked in git's coordinates: the repo-relative directory of the
1283
- // cwd, already slash-separated and already normalised.
1284
1409
  const prefix = git(['rev-parse', '--show-prefix'], dir).trim();
1285
1410
  const entryRel = prefix + (0, node_path_1.basename)(file);
1286
1411
  const top = git(['rev-parse', '--show-toplevel'], dir).trim();
1287
- // `-z` so a path with a newline or a quote cannot be mistaken for
1288
- // two paths (git otherwise quotes such names).
1289
1412
  const listed = git(['ls-tree', '-r', '-z', '--name-only', rev], top)
1290
1413
  .split('\0').filter((p) => '' !== p);
1291
1414
  if (!listed.includes(entryRel)) {
@@ -1315,13 +1438,6 @@ function oldVersion(spec, file) {
1315
1438
  function policyCompat(newSrc, path, include) {
1316
1439
  const aontu = new aontu_1.Aontu();
1317
1440
  const ctx = aontu.ctx({ collect: true });
1318
- // The declaration is read by EVALUATING the document, so this leg
1319
- // runs the include resolver too and has to run it under BOTH of the
1320
- // verb's include options -- a `breaking --trust none` that read its
1321
- // own mode through an unconfined resolver would confine the
1322
- // comparison and not the question (use-cases/REVIEW.md finding G),
1323
- // and one that took the capability alone read no mode at all when
1324
- // the declaration arrived through a `--text-ext` include.
1325
1441
  const v = aontu.unify(newSrc, { path, ...(0, utility_1.includeOpts)(include) }, ctx);
1326
1442
  if (0 < ctx.err.length || true === v?.isNil) {
1327
1443
  return undefined;
@@ -1340,11 +1456,6 @@ function policyCompat(newSrc, path, include) {
1340
1456
  return 'backward' === m || 'forward' === m || 'full' === m || 'none' === m
1341
1457
  ? m : undefined;
1342
1458
  }
1343
- // Is the evaluated old version's value at the finding path deprecated?
1344
- // The --allow-deprecated-removal downgrade (G3 phase 4): removing (or
1345
- // otherwise changing) a value the old version already deprecated warns
1346
- // instead of breaking. The Go port exports the same reader as
1347
- // aontu.DeprecatedAt.
1348
1459
  function deprecatedAt(oldSrc, path, filePath) {
1349
1460
  const aontu = new aontu_1.Aontu();
1350
1461
  const ctx = aontu.ctx({ collect: true });
@@ -1411,9 +1522,6 @@ function runBreaking(argv) {
1411
1522
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
1412
1523
  return 2;
1413
1524
  }
1414
- // The declared mode: --mode overrides the document's own policy;
1415
- // neither means backward, the index's framing (v1-valid documents
1416
- // stay valid).
1417
1525
  const mode = args.mode ??
1418
1526
  policyCompat(newSrc, args.file, verbOpts(trust, entryRootOf(args.file))) ??
1419
1527
  'backward';
@@ -1446,8 +1554,6 @@ function runBreaking(argv) {
1446
1554
  if (null != old.temp) {
1447
1555
  temps.push(old.temp);
1448
1556
  }
1449
- // backward: the NEW document is the general side — every old
1450
- // instance must still be admitted. forward: the old one is.
1451
1557
  const checks = [];
1452
1558
  if ('backward' === mode || 'full' === mode) {
1453
1559
  checks.push({ general: [newSrc, args.file], specific: [oldSrc, spec] });
@@ -1462,17 +1568,9 @@ function runBreaking(argv) {
1462
1568
  at: args.at,
1463
1569
  generalUrl: check.general[1],
1464
1570
  specificUrl: check.specific[1],
1465
- // The old side's relative loads resolve from ITS own tree --
1466
- // the materialised revision for a git spelling, the named
1467
- // file's directory otherwise -- so an included file's change
1468
- // is part of the comparison rather than invisible to it.
1469
1571
  generalPath: check.general[1] === spec ? oldPath : args.file,
1470
1572
  specificPath: check.specific[1] === spec ? oldPath : args.file,
1471
1573
  });
1472
- // The deprecated-removal downgrade: a finding about a value the
1473
- // OLD version already deprecated becomes a warning, and warnings
1474
- // do not move the verdict. Deprecate-then-remove is the
1475
- // supported rename path (the design's own sequencing).
1476
1574
  let verdict = report.verdict;
1477
1575
  if (args.allowDeprecatedRemoval) {
1478
1576
  let liveFindings = 0;
@@ -1523,12 +1621,6 @@ function renderBreakingJson(report, mode) {
1523
1621
  findings: report.findings,
1524
1622
  }, 2);
1525
1623
  }
1526
- // ---------------------------------------------------------------------
1527
- // The trim reporter (G3 phase 6): report redundant entries as paths.
1528
- // Report-only — REWRITING needs G7's format-preserving patch surface —
1529
- // which is why --check is REQUIRED rather than defaulted: `aontu trim
1530
- // f.aon` reads as "trim this file", and doing something else silently
1531
- // is worse than saying so.
1532
1624
  const TRIM_HELP = 'aontu trim --check <file> (try --help)';
1533
1625
  const TRIM_EXIT = {
1534
1626
  clean: 0,
@@ -1598,8 +1690,6 @@ function runTrim(argv) {
1598
1690
  }
1599
1691
  function renderTrimText(report) {
1600
1692
  const head = `verdict: ${report.verdict}`;
1601
- // WHY, when the document could not be evaluated at all: rendered as
1602
- // vet renders a finding, because it IS one (the review's finding F).
1603
1693
  const errors = report.errors ?? [];
1604
1694
  if (0 < errors.length) {
1605
1695
  return [head, ''].concat(errors.map(renderFinding)).join('\n');
@@ -1648,27 +1738,10 @@ const VIEW_EDGES = ['upward', 'all', 'none'];
1648
1738
  // is a terminal, which is the CLI's to know and the library's never --
1649
1739
  // the same division err.ts already draws for the error frames.
1650
1740
  const VIEW_STYLES = ['auto', 'none', 'ansi', 'css'];
1651
- // `--style auto` resolved, which only the CLI can do. The mechanism is
1652
- // the PROFILE's and the library knows it -- an SVG carries its
1653
- // stylesheet unless told not to, which is what makes a figure stand
1654
- // alone. What the library cannot know is whether the DESTINATION is a
1655
- // terminal, so that is the only thing decided here: escapes on the
1656
- // text profile when stdout is a terminal and NO_COLOR is unset, the
1657
- // same two conditions the error frames use. `undefined` leaves the
1658
- // profile's own default in place.
1659
1741
  function viewStyleOf(asked, as) {
1660
1742
  if (undefined !== asked && 'auto' !== asked) {
1661
1743
  return asked;
1662
1744
  }
1663
- // STDOUT'S OWN TERMINAL-NESS, and NO_COLOR read here rather than
1664
- // through colorActive(). The figure goes to STDOUT and the error
1665
- // frames go to STDERR, and they are not the same destination: main()
1666
- // has already called setColor for stderr, so asking colorActive()
1667
- // would answer the wrong question twice --- no escapes for
1668
- // `aontu view tree m.aon 2>/dev/null` at a terminal, and escapes
1669
- // into the pipe for `aontu view tree m.aon | less`. The NO_COLOR
1670
- // rule is the one no-color.org states and err.ts implements:
1671
- // set, to anything but empty, means no colour.
1672
1745
  const no = process.env.NO_COLOR;
1673
1746
  return 'text' === as && true === process.stdout.isTTY
1674
1747
  && (null == no || '' === no) ? 'ansi' : undefined;
@@ -1690,24 +1763,6 @@ const VIEW_USAGE_CODES = [
1690
1763
  'view_document_shape', 'view_style_profile', 'view_style_unknown',
1691
1764
  ];
1692
1765
  const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)';
1693
- // The module tooling (G6 phase 3, ts/src/mod-tool.ts). All LOCAL:
1694
- // `tidy` resolves the closure from what is in the stores and rewrites
1695
- // the lockfile, `verify` asks whether the stores still mean what the
1696
- // lockfile pins and changes nothing, `vendor` materialises the locked
1697
- // closure into the project, `manifest` prints what a publish would
1698
- // push.
1699
- //
1700
- // TIDY AND VERIFY ARE DIFFERENT QUESTIONS, and that is why both exist.
1701
- // Tidy recomputes and rewrites by design -- a pin is what a module
1702
- // means NOW -- so it makes the lockfile agree with whatever the store
1703
- // holds, tampering included. Verify is the gate: a CI job runs it
1704
- // BEFORE tidy, or instead of it.
1705
- //
1706
- // `get` and `publish` are the NETWORK half of the design and are not in
1707
- // this build. They are named here rather than left to fall out as an
1708
- // unknown subcommand, because a reader of the design will type them and
1709
- // deserves to be told which half is missing rather than that the word
1710
- // is wrong.
1711
1766
  function runMod(argv) {
1712
1767
  const rest = [];
1713
1768
  let format = 'text';
@@ -1754,10 +1809,6 @@ function runMod(argv) {
1754
1809
  process.stderr.write(`aontu: mod needs tidy, verify, vendor or manifest\n${MOD_HELP}\n`);
1755
1810
  return 2;
1756
1811
  }
1757
- // THE OLD LAYOUT IS NAMED, NOT READ. The lockfile and the vendored
1758
- // closure moved under aontu_meta/; a project that still carries them
1759
- // at its root would otherwise look untouched by any of these verbs,
1760
- // which is the one silence worth breaking.
1761
1812
  if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'aon_vendor')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'mod-lock.aon'))) {
1762
1813
  process.stderr.write('aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
1763
1814
  'move them, or run aontu mod tidy and aontu mod vendor\n');
@@ -1781,9 +1832,6 @@ const MOD_SUBS = ['tidy', 'verify', 'vendor', 'manifest'];
1781
1832
  const MOD_EXIT = {
1782
1833
  ok: 0,
1783
1834
  missing: 1,
1784
- // A REFUSED GATE, with `breaking`: a store that no longer means what
1785
- // the lockfile pins is the integrity check saying no, and a CI job
1786
- // reading exit codes should not have to learn a third class for it.
1787
1835
  mismatch: 1,
1788
1836
  // Likewise a lockfile that does not cover the project: the gate has
1789
1837
  // nothing to check, which is a refusal and not a pass.
@@ -1829,11 +1877,6 @@ function modText(sub, report) {
1829
1877
  for (const f of report.findings) {
1830
1878
  lines.push(f.path + ': ' + f.message);
1831
1879
  }
1832
- // What a manifest lacks is a declaration the module does not make
1833
- // or an entry file that is not there, and neither is something a
1834
- // fetch would supply -- so this is not the tail the other two
1835
- // subcommands share. The name says which kind it is: `mod.version`
1836
- // is a declaration, `service.aon` is a file.
1837
1880
  for (const miss of report.missing) {
1838
1881
  lines.push(miss + ': missing');
1839
1882
  }
@@ -1843,8 +1886,6 @@ function modText(sub, report) {
1843
1886
  for (const mod of report.verified) {
1844
1887
  lines.push(mod + ': verified');
1845
1888
  }
1846
- // BOTH HASHES, because the useful question is which way it moved:
1847
- // an empty `got` is a module that no longer stands up at all.
1848
1889
  for (const m of report.mismatched) {
1849
1890
  lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
1850
1891
  ('' === m.got ? 'nothing (it does not evaluate)' : m.got));
@@ -1877,6 +1918,9 @@ function modText(sub, report) {
1877
1918
  }
1878
1919
  return lines.join('\n');
1879
1920
  }
1921
+ function vacuous(what, why) {
1922
+ process.stderr.write(`aontu: ${what}: ${why}\n`);
1923
+ }
1880
1924
  function runRelations(argv) {
1881
1925
  const trusted = takeTrust(argv);
1882
1926
  if (null == trusted) {
@@ -1921,12 +1965,20 @@ function runRelations(argv) {
1921
1965
  return 2;
1922
1966
  }
1923
1967
  const report = (0, aontu_1.relationCheck)(src, {
1924
- path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1968
+ path: files[0], count: true,
1969
+ ...verbOpts(trust, entryRootOf(files[0])),
1925
1970
  });
1926
1971
  const text = 'json' === format
1927
1972
  ? renderRelationsJson(report)
1928
1973
  : renderRelationsText(report);
1929
1974
  process.stdout.write(text + '\n');
1975
+ // `pass` over NO declarations is the vacuous case, and the engine
1976
+ // knows it exactly: `_reldecls` is empty. The count is asked for
1977
+ // here rather than derived, so the answer costs no second
1978
+ // evaluation.
1979
+ if (0 === report.declared) {
1980
+ vacuous('this document declares no relations', '`pass` means nothing was checked, not that the graph is sound');
1981
+ }
1930
1982
  return RELATIONS_EXIT[report.verdict];
1931
1983
  }
1932
1984
  function runReaches(argv) {
@@ -2129,12 +2181,6 @@ function runView(argv) {
2129
2181
  rest.push(arg);
2130
2182
  }
2131
2183
  }
2132
- // ESCAPES NEVER GO INTO A FILE. A pinned golden holding terminal
2133
- // control codes is not a golden anybody can read, and a byte
2134
- // comparison against one would fail on the reader's terminal
2135
- // settings. `auto` resolves to `none` there on its own; asking for
2136
- // `ansi` explicitly is a usage error rather than a silent downgrade,
2137
- // so a script that wanted colour is told where it went.
2138
2184
  if ('ansi' === style && (undefined !== out || undefined !== opts.views)) {
2139
2185
  process.stderr.write('aontu: --style ansi writes to a terminal, not to a file\n');
2140
2186
  return 2;
@@ -2142,10 +2188,6 @@ function runView(argv) {
2142
2188
  // THE VIEW DOCUMENT draws every figure a document declares, so it
2143
2189
  // names no kind: the declarations do, one each.
2144
2190
  if (undefined !== opts.views) {
2145
- // A declaration names its own profile, so the style is left to
2146
- // each figure's own default; `--style none` still reaches every
2147
- // one of them, which is how a host page that binds the CSS
2148
- // variables asks for eight figures without eight stylesheets.
2149
2191
  opts.style = viewStyleOf(style, undefined);
2150
2192
  return runViewSet(rest, opts, trust, { format, check, strict, out });
2151
2193
  }
@@ -2203,7 +2245,7 @@ function runView(argv) {
2203
2245
  return 2;
2204
2246
  }
2205
2247
  }
2206
- const report = (0, view_1.view)(srcs[0], {
2248
+ const viewOpts = {
2207
2249
  ...opts,
2208
2250
  style: viewStyleOf(style, opts.as ?? (0, view_1.viewDefaultProfile)(kind)),
2209
2251
  kind,
@@ -2211,7 +2253,15 @@ function runView(argv) {
2211
2253
  roots,
2212
2254
  ...verbOpts(trust, entryRootOf(files[0])),
2213
2255
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2214
- });
2256
+ };
2257
+ const report = (0, view_1.view)(srcs[0], viewOpts);
2258
+ if ('error' !== report.verdict && null != report.text) {
2259
+ const bare = (0, view_1.view)('{}', viewOpts);
2260
+ if ('error' !== bare.verdict && bare.text === report.text) {
2261
+ vacuous('nothing to draw', 'this figure is what the same view draws for an empty document' +
2262
+ ' — the model declares nothing this kind can show');
2263
+ }
2264
+ }
2215
2265
  if ('json' === format) {
2216
2266
  process.stdout.write(renderViewJson(report) + '\n');
2217
2267
  }
@@ -2253,13 +2303,6 @@ function runView(argv) {
2253
2303
  }
2254
2304
  return strict && 'lossy' === report.verdict ? 1 : VIEW_EXIT[report.verdict];
2255
2305
  }
2256
- // `aontu view --views <path> <file>`: every figure the document
2257
- // declares, from one evaluation, all or nothing.
2258
- //
2259
- // A declared `out` is resolved against the DOCUMENT's own directory,
2260
- // not the caller's: a view document is committed beside the figures it
2261
- // gates, and a gate that only passes from one working directory is not
2262
- // a gate.
2263
2306
  function runViewSet(rest, opts, trust, how) {
2264
2307
  if (1 !== rest.length) {
2265
2308
  process.stderr.write('aontu: view --views takes one file\n');
@@ -2383,8 +2426,6 @@ function renderViewJson(report) {
2383
2426
  }
2384
2427
  function renderRelationsText(report) {
2385
2428
  const head = `verdict: ${report.verdict}`;
2386
- // WHY, when the document could not be evaluated at all: rendered as
2387
- // vet renders a finding, because it IS one (the review's finding F).
2388
2429
  const errors = report.errors ?? [];
2389
2430
  if (0 < errors.length) {
2390
2431
  return [head, ''].concat(errors.map(renderFinding)).join('\n');
@@ -2406,19 +2447,6 @@ function renderRelationsJson(report) {
2406
2447
  ...(null == report.errors ? {} : { errors: report.errors }),
2407
2448
  }, 2);
2408
2449
  }
2409
- // ---------------------------------------------------------------------
2410
- // JSON SCHEMA EXPORT (SUPPORT.md act 2, the review's finding I): the
2411
- // bridge to every structured-output API, which constrains generation to
2412
- // JSON Schema and nothing else. Export the model, let the provider
2413
- // generate under it, then `vet` the result against the model itself --
2414
- // the hybrid an enterprise actually deploys, and impossible without
2415
- // this verb.
2416
- //
2417
- // THE SCHEMA GOES TO STDOUT AND THE LOSSES TO STDERR, so `aontu
2418
- // jsonschema x.aon > schema.json` writes a schema and still tells the
2419
- // reader what it could not carry. `--strict` makes a loss a refusal,
2420
- // for the CI job that would rather fail than ship a schema weaker than
2421
- // its model.
2422
2450
  const JSONSCHEMA_HELP = 'aontu jsonschema [--at <path>] [--strict] <file> (try --help)';
2423
2451
  function runJsonSchema(argv) {
2424
2452
  const trusted = takeTrust(argv);
@@ -2502,14 +2530,6 @@ function runJsonSchema(argv) {
2502
2530
  return 'error' === report.verdict ? 4 :
2503
2531
  strict && 'lossy' === report.verdict ? 1 : 0;
2504
2532
  }
2505
- // ---------------------------------------------------------------------
2506
- // THE RENDER VERB (docs/design/RENDER.0.md D8): evaluate a document,
2507
- // vet the value at --at against aontu:code, fold code.units into bytes,
2508
- // and put them where the flag says -- one unit on stdout, every unit
2509
- // below --out (all or nothing), or compared against --check. Exit codes
2510
- // mirror jsonschema's: 0 ok; 1 lossy under --strict or drift under
2511
- // --check; 2 usage or I/O, a refused unit path included; 4 the
2512
- // document does not stand up or the instance is not aontu:code.
2513
2533
  const RENDER_HELP = 'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
2514
2534
  '[--stdout | --out <dir> | --check <dir> | --coverage] ' +
2515
2535
  '[--coverage-at <path>] [--strict] [--marker <token>] <file> (try --help)';
@@ -2638,45 +2658,20 @@ function runRender(argv) {
2638
2658
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
2639
2659
  return 2;
2640
2660
  }
2641
- // THE ENTRY MAY BE A TEMPLATE (TEMPLATE.0.md; P8), and its EXTENSION
2642
- // decides, as an include's extension decides what the include is
2643
- // (ADR-012): a generator is a file in the target's own syntax, so it
2644
- // carries the target's extension and never `.aon`. Desugared here
2645
- // rather than anywhere deeper, because a template is an entry
2646
- // spelling and not a value: an include is still aontu.
2647
- if (!files[0].endsWith('.aon')) {
2648
- src = (0, template_1.desugarTemplate)(src, marker ?? (0, template_1.markerFor)(files[0]));
2649
- }
2650
- // THE PROFILES (D5): each --profile file is a document whose root is
2651
- // `profile: {lang, ...}`, evaluated under the verb's trust and vetted
2652
- // against aontu:profile as a settled value before the fold reads it
2653
- // (renderProfile, which also fills the defaults). Two files claiming
2654
- // one lang is a usage error: the fold could not choose.
2655
- const profiles = [];
2656
- const langs = new Map();
2657
- for (const pf of profileFiles) {
2658
- let text;
2659
- try {
2660
- text = (0, node_fs_1.readFileSync)(pf, 'utf8');
2661
- }
2662
- catch (err) {
2663
- process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
2664
- return 2;
2665
- }
2666
- const loaded = (0, aontu_1.renderProfile)(text, { path: (0, node_path_1.resolve)(pf), ...verbOpts(trust, entryRootOf(pf)) });
2667
- if (undefined !== loaded.errors) {
2668
- process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n');
2669
- return 4;
2670
- }
2671
- const profile = loaded.profile;
2672
- const prev = langs.get(profile.lang);
2673
- if (undefined !== prev) {
2674
- process.stderr.write(`aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`);
2675
- return 2;
2676
- }
2677
- langs.set(profile.lang, pf);
2678
- profiles.push(profile);
2661
+ const loadedProfiles = loadProfiles(profileFiles, trust);
2662
+ if ('number' === typeof loadedProfiles) {
2663
+ return loadedProfiles;
2679
2664
  }
2665
+ const profiles = loadedProfiles;
2666
+ if (!files[0].endsWith('.aon')) {
2667
+ src = (0, template_1.desugarTemplate)(src, marker ??
2668
+ (0, template_1.markerFromProfiles)(profiles, files[0]) ?? (0, template_1.markerFor)(files[0]));
2669
+ }
2670
+ // A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
2671
+ // bytes and exit 0. The profile is what maps a model onto a
2672
+ // language, so without one there is nothing for the renderer to
2673
+ // write -- which is a usable answer only if the caller is told.
2674
+ const noProfiles = 0 === profiles.length;
2680
2675
  const report = (0, aontu_1.render)(src, {
2681
2676
  at, unit, strict, profiles, path: files[0],
2682
2677
  coverage, coverageAt,
@@ -2686,6 +2681,13 @@ function runRender(argv) {
2686
2681
  trace: 'json' === format,
2687
2682
  ...verbOpts(trust, entryRootOf(files[0])),
2688
2683
  });
2684
+ // Said once, whatever the format: stdout stays the report.
2685
+ if ('error' !== report.verdict && 0 === report.units.length) {
2686
+ vacuous('nothing was rendered', noProfiles
2687
+ ? 'no profile was given, and the document declares none' +
2688
+ ' (see aontu help tasks)'
2689
+ : 'the document produced no units under this profile');
2690
+ }
2689
2691
  if ('json' === format) {
2690
2692
  process.stdout.write((0, aontu_1.exactJSON)({
2691
2693
  aontu: { version: version(), verb: 'render' },
@@ -2803,18 +2805,19 @@ function renderExit(report, drift) {
2803
2805
  }
2804
2806
  return 0 < drift ? 1 : 0;
2805
2807
  }
2806
- // ---------------------------------------------------------------------
2807
- // THE TEMPLATE SURFACE (docs/design/TEMPLATE.0.md; RENDER.0.md P8): the
2808
- // two transforms and the round trip between them. `render` reads a
2809
- // template directly, by its extension; this verb is for seeing the
2810
- // canonical form, for writing one by hand and sugaring it, and for the
2811
- // check that keeps a committed template and its meaning in agreement.
2812
2808
  const TEMPLATE_HELP = 'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)';
2813
2809
  function runTemplate(argv) {
2810
+ const trusted = takeTrust(argv);
2811
+ if (null == trusted) {
2812
+ return 2;
2813
+ }
2814
+ argv = trusted.argv;
2815
+ const trust = trusted.trust;
2814
2816
  const files = [];
2815
2817
  let resugar = false;
2816
2818
  let check = false;
2817
2819
  let marker = undefined;
2820
+ const profileFiles = [];
2818
2821
  for (let i = 0; i < argv.length; i++) {
2819
2822
  const arg = argv[i];
2820
2823
  if ('-h' === arg || '--help' === arg) {
@@ -2834,6 +2837,14 @@ function runTemplate(argv) {
2834
2837
  return 2;
2835
2838
  }
2836
2839
  }
2840
+ else if ('--profile' === arg) {
2841
+ const pf = argv[++i];
2842
+ if (null == pf) {
2843
+ process.stderr.write('aontu: --profile needs a file\n');
2844
+ return 2;
2845
+ }
2846
+ profileFiles.push(pf);
2847
+ }
2837
2848
  else if (arg.startsWith('-')) {
2838
2849
  process.stderr.write(`aontu: unknown template option ${arg} (try --help)\n`);
2839
2850
  return 2;
@@ -2846,10 +2857,6 @@ function runTemplate(argv) {
2846
2857
  process.stderr.write(`aontu: template needs one file\n${TEMPLATE_HELP}\n`);
2847
2858
  return 2;
2848
2859
  }
2849
- // THE TWO ARE DIRECTIONS, NOT MODES THAT COMPOSE: `--check` reads a
2850
- // template and asks whether the round trip answers it back, and
2851
- // `--resugar` reads the canonical form instead. A run cannot be both
2852
- // at once, because the file is one thing or the other.
2853
2860
  if (resugar && check) {
2854
2861
  process.stderr.write('aontu: template takes one of --resugar or --check\n');
2855
2862
  return 2;
@@ -2862,18 +2869,13 @@ function runTemplate(argv) {
2862
2869
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
2863
2870
  return 2;
2864
2871
  }
2865
- const mark = marker ?? (0, template_1.markerFor)(files[0]);
2872
+ const declared = loadProfiles(profileFiles, trust);
2873
+ if ('number' === typeof declared) {
2874
+ return declared;
2875
+ }
2876
+ const mark = marker ?? (0, template_1.markerFromProfiles)(declared, files[0]) ??
2877
+ (0, template_1.markerFor)(files[0]);
2866
2878
  if (check) {
2867
- // THE ROUND TRIP IS THE CHECK (D6): the file held to the spelling
2868
- // the two transforms answer. What that names is a marker line the
2869
- // transform would not have written -- one without its space, or one
2870
- // whose aontu is indented after the marker rather than before it,
2871
- // since the marker keeps its own indentation. It does NOT name a
2872
- // changed body line: a template's whitespace is output, so a
2873
- // trimmed trailing space is still a valid template and it is
2874
- // `render --check` against the committed files that catches it.
2875
- // The first line that differs is the report, since a whole diff of
2876
- // a generator is the file again.
2877
2879
  const back = (0, template_1.resugarTemplate)((0, template_1.desugarTemplate)(src, mark), mark);
2878
2880
  if (back === src) {
2879
2881
  return 0;
@@ -2884,12 +2886,6 @@ function runTemplate(argv) {
2884
2886
  while (n < want.length && n < have.length && want[n] === have[n]) {
2885
2887
  n++;
2886
2888
  }
2887
- // THE TWO ARE THE SAME LENGTH, always: each transform maps one
2888
- // line to one line and applies the same trailing-newline rule, so
2889
- // `back` has as many lines as `src`. The loop above therefore stops
2890
- // at a real difference rather than by running out of either -- an
2891
- // equal prefix all the way to the end IS `back === src`, which
2892
- // returned above. So both indexes are in range here.
2893
2889
  process.stderr.write(`aontu: ${files[0]}:${n + 1} is not what the round trip answers\n` +
2894
2890
  ` have: ${JSON.stringify(have[n])}\n` +
2895
2891
  ` want: ${JSON.stringify(want[n])}\n`);
@@ -2899,13 +2895,38 @@ function runTemplate(argv) {
2899
2895
  (0, template_1.resugarTemplate)(src, mark) : (0, template_1.desugarTemplate)(src, mark));
2900
2896
  return 0;
2901
2897
  }
2902
- // ---------------------------------------------------------------------
2903
- // The canon-hash (G6 phase 1): the pin an agent, a lockfile or a
2904
- // registry stores for "this module, this meaning". The hash covers the
2905
- // module evaluated STANDALONE -- its own include closure resolved and
2906
- // unified at its own root, before any consumer context -- which is what
2907
- // makes the pin transitive: an edit two includes deep changes the
2908
- // unified root, hence the hash.
2898
+ // The profiles named by --profile, vetted, or the exit code that says
2899
+ // why not. A profile is a language declared as data: `render` matches
2900
+ // one to a unit by `lang`, and `template` and `fmt` match one to a file
2901
+ // by the extensions its `template.ext` names.
2902
+ function loadProfiles(profileFiles, trust) {
2903
+ const profiles = [];
2904
+ const langs = new Map();
2905
+ for (const pf of profileFiles) {
2906
+ let text;
2907
+ try {
2908
+ text = (0, node_fs_1.readFileSync)(pf, 'utf8');
2909
+ }
2910
+ catch (err) {
2911
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
2912
+ return 2;
2913
+ }
2914
+ const loaded = (0, aontu_1.renderProfile)(text, { path: (0, node_path_1.resolve)(pf), ...verbOpts(trust, entryRootOf(pf)) });
2915
+ if (undefined !== loaded.errors) {
2916
+ process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n');
2917
+ return 4;
2918
+ }
2919
+ const profile = loaded.profile;
2920
+ const prev = langs.get(profile.lang);
2921
+ if (undefined !== prev) {
2922
+ process.stderr.write(`aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`);
2923
+ return 2;
2924
+ }
2925
+ langs.set(profile.lang, pf);
2926
+ profiles.push(profile);
2927
+ }
2928
+ return profiles;
2929
+ }
2909
2930
  const HASH_HELP = 'aontu hash <file> (try --help)';
2910
2931
  function runHash(argv) {
2911
2932
  const trusted = takeTrust(argv);
@@ -2960,14 +2981,6 @@ function runHash(argv) {
2960
2981
  const ctx = aontu.ctx({ collect: true });
2961
2982
  const v = aontu.unify(src, { path: files[0] }, ctx);
2962
2983
  if (0 < ctx.err.length || true === v?.isNil) {
2963
- // A document that does not stand up on its own has no meaning to
2964
- // pin, and a hash of a broken evaluation would be a pin that
2965
- // silently agrees with every other broken evaluation.
2966
- // WHY it does not stand up, not just that it does not: the same
2967
- // diagnosis `aontu <file>` prints (the review's finding F).
2968
- // evalFailure unconditionally, as every other call site does: it
2969
- // owns the "ctx.err is never empty here" contract, and a guard
2970
- // that pretends otherwise is a dead arm asserting nothing.
2971
2984
  process.stderr.write(`aontu: ${files[0]} does not evaluate on its own; nothing to hash\n` +
2972
2985
  renderFinding((0, query_1.evalFailure)(ctx)) + '\n');
2973
2986
  return 4;
@@ -2982,12 +2995,6 @@ function runHash(argv) {
2982
2995
  process.stdout.write(text + '\n');
2983
2996
  return 0;
2984
2997
  }
2985
- // ---------------------------------------------------------------------
2986
- // The query surface (G7 phase 1): one node of an evaluated document,
2987
- // selected by path and rendered. Evaluation is still GLOBAL -- what
2988
- // `get` buys is the size of the ANSWER, not the cost of producing it --
2989
- // and the projections are lattice abstractions, each a valid Aontu
2990
- // document that subsumes the truth it summarises.
2991
2998
  const GET_HELP = 'aontu get <path> <file> (try --help)';
2992
2999
  function runGet(argv) {
2993
3000
  const trusted = takeTrust(argv);
@@ -3156,11 +3163,6 @@ function runWhy(argv) {
3156
3163
  }
3157
3164
  return 'no_path' === report.findings[0]?.code ? 1 : 4;
3158
3165
  }
3159
- // One contribution per line, numbered in source order, each with what
3160
- // was written, where, and how it got here. A siteless contribution
3161
- // prints no location rather than a `-1:-1` that means nothing —
3162
- // exported for the direct test, because the site SHAPE allows one
3163
- // while no document has yet produced one (ADR-002).
3164
3166
  function renderWhyText(record) {
3165
3167
  const head = `${record.path} = ${record.value}`;
3166
3168
  if (0 === record.conjuncts.length) {
@@ -3176,12 +3178,6 @@ function renderWhyText(record) {
3176
3178
  ('literal' === c.role ? '' : ` (${c.role})`);
3177
3179
  })).join('\n');
3178
3180
  }
3179
- // ---------------------------------------------------------------------
3180
- // The overlay patch verb (G7 phase 5): change a document by APPENDING
3181
- // to an overlay, not by rewriting it. An overlay entry is just another
3182
- // conjunct and unification is order-independent, so this needs no
3183
- // rewriter — the format-preserving in-place edit is stage 2, and needs
3184
- // a comment-preserving CST the parser stack does not have.
3185
3181
  const SET_HELP = 'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)';
3186
3182
  function runSet(argv) {
3187
3183
  const trusted = takeTrust(argv);
@@ -3288,27 +3284,10 @@ function runSet(argv) {
3288
3284
  }, 2) + '\n');
3289
3285
  }
3290
3286
  else {
3291
- // A replacement is REPORTED as the edit it is, not left for the
3292
- // reader to infer from a changed file: `where: what -> what`, in
3293
- // source spelling, because the spelling is what changed.
3294
- //
3295
- // PAST TENSE ONLY WHERE IT HAPPENED. A refused write leaves the
3296
- // file exactly as it was, and one assignment can be replaceable
3297
- // while another makes the whole run invalid — so `replaced:` there
3298
- // tells an operator the pin was changed when it was not, and unlike
3299
- // `--dry-run` there is nothing else on the line to say otherwise.
3300
3287
  const verb = wrote ? 'replaced' : 'would replace';
3301
3288
  const edits = report.replaced.map((r) => `${verb}: ${r.file}:${r.row}:${r.col} ${r.from} -> ${r.to}`);
3302
3289
  const head = [`verdict: ${report.verdict}`].concat(edits).join('\n') +
3303
3290
  (wrote ? `\nwrote: ${overlayFile}` : dryRun ? '\n(dry run)' : '');
3304
- // A SUCCESSFUL COMMAND WRITES ITS STATUS TO STDOUT, findings or
3305
- // not. Routing on `findings.length` was right while every finding
3306
- // this verb could produce was an ERROR; `--in-place` made a WARNING
3307
- // possible, and a run that held, wrote the file and exited 0 then
3308
- // sent its whole report to stderr — leaving stdout empty, so
3309
- // `$(aontu set ...)` captured nothing and only the JSON form
3310
- // behaved like a success. The verdict decides the stream; warnings
3311
- // are diagnostics and go to stderr beside it.
3312
3291
  const failed = 'invalid' === report.verdict || 'error' === report.verdict;
3313
3292
  const findingText = report.findings.map(renderFinding);
3314
3293
  if (failed) {
@@ -3327,6 +3306,148 @@ function runSet(argv) {
3327
3306
  }
3328
3307
  return VET_EXIT[report.verdict];
3329
3308
  }
3309
+ const ALLOW_HELP = 'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)';
3310
+ const ALLOW_EXIT = {
3311
+ allowed: 0,
3312
+ refused: 1,
3313
+ error: 4,
3314
+ };
3315
+ // One line per asked path: the answer, and the entry that gave it, as
3316
+ // a path into the role model so `aontu why` can locate the rule.
3317
+ function renderAllowDecision(d, role) {
3318
+ const head = `${d.path}: ${d.allowed ? 'allowed' : 'refused'}`;
3319
+ switch (d.reason) {
3320
+ case 'allow':
3321
+ case 'deny':
3322
+ return `${head} by ${d.by} (${d.pattern})`;
3323
+ case 'uncovered':
3324
+ return `${head} (no allow entry of ${role} covers it)`;
3325
+ default:
3326
+ return `${head} (role ${role} is not declared)`;
3327
+ }
3328
+ }
3329
+ function renderAllowText(report) {
3330
+ const lines = [`verdict: ${report.verdict}`, `role: ${report.role}`]
3331
+ .concat(report.paths.map((d) => renderAllowDecision(d, report.role)));
3332
+ if (0 === report.findings.length) {
3333
+ return lines.join('\n');
3334
+ }
3335
+ return lines.concat('', report.findings.map(renderFinding)).join('\n');
3336
+ }
3337
+ // Does the text after `=` parse as exactly one value? Parsed, never
3338
+ // evaluated, with loads denied: the question is the shape of the
3339
+ // argument, and reading a file to answer it would be the write the
3340
+ // gate exists to precede.
3341
+ function oneValue(value) {
3342
+ try {
3343
+ const probe = new aontu_1.Aontu({ trust: { include: 'none' } })
3344
+ .parse('v: ' + value);
3345
+ return 1 === Object.keys(probe.peg).length;
3346
+ }
3347
+ catch {
3348
+ return false;
3349
+ }
3350
+ }
3351
+ function runAllow(argv) {
3352
+ const trusted = takeTrust(argv);
3353
+ if (null == trusted) {
3354
+ return 2;
3355
+ }
3356
+ argv = trusted.argv;
3357
+ const trust = trusted.trust;
3358
+ const rest = [];
3359
+ let role;
3360
+ let at;
3361
+ let format = 'text';
3362
+ for (let i = 0; i < argv.length; i++) {
3363
+ const arg = argv[i];
3364
+ if ('-h' === arg || '--help' === arg) {
3365
+ process.stdout.write(HELP);
3366
+ return 0;
3367
+ }
3368
+ if ('--role' === arg) {
3369
+ role = argv[++i];
3370
+ if (null == role) {
3371
+ process.stderr.write('aontu: --role needs a role name\n');
3372
+ return 2;
3373
+ }
3374
+ }
3375
+ else if ('--at' === arg) {
3376
+ at = argv[++i];
3377
+ if (null == at) {
3378
+ process.stderr.write('aontu: --at needs a path\n');
3379
+ return 2;
3380
+ }
3381
+ }
3382
+ else if ('--format' === arg) {
3383
+ const f = argv[++i];
3384
+ if ('text' !== f && 'json' !== f) {
3385
+ process.stderr.write('aontu: --format needs text or json\n');
3386
+ return 2;
3387
+ }
3388
+ format = f;
3389
+ }
3390
+ else if (arg.startsWith('-')) {
3391
+ process.stderr.write(`aontu: unknown allow option ${arg} (try --help)\n`);
3392
+ return 2;
3393
+ }
3394
+ else {
3395
+ rest.push(arg);
3396
+ }
3397
+ }
3398
+ if (null == role || rest.length < 2) {
3399
+ process.stderr.write(`aontu: allow needs --role, a role model and at least one path\n` +
3400
+ `${ALLOW_HELP}\n`);
3401
+ return 2;
3402
+ }
3403
+ const [file, ...asked] = rest;
3404
+ // A role is ONE KEY of the roles map. A dotted name would be read as
3405
+ // a path by `why` when it follows the entry the report names, and an
3406
+ // empty one names the map itself.
3407
+ if ('' === role || role.includes('.')) {
3408
+ process.stderr.write('aontu: --role needs one key, without dots\n');
3409
+ return 2;
3410
+ }
3411
+ const paths = [];
3412
+ for (const arg of asked) {
3413
+ const eq = arg.indexOf('=');
3414
+ const path = eq < 0 ? arg : arg.slice(0, eq);
3415
+ if (!path.startsWith('$')) {
3416
+ process.stderr.write(`aontu: a path starts with $ (got ${JSON.stringify(arg)})\n`);
3417
+ return 2;
3418
+ }
3419
+ if (0 <= eq && !oneValue(arg.slice(eq + 1))) {
3420
+ process.stderr.write(`aontu: the value of ${path} is not one value\n`);
3421
+ return 2;
3422
+ }
3423
+ paths.push(path);
3424
+ }
3425
+ let src;
3426
+ try {
3427
+ src = (0, node_fs_1.readFileSync)(file, 'utf8');
3428
+ }
3429
+ catch (err) {
3430
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
3431
+ return 2;
3432
+ }
3433
+ const report = (0, aontu_1.allow)(src, role, paths, {
3434
+ at, path: file, ...verbOpts(trust, entryRootOf(file)),
3435
+ });
3436
+ const text = 'json' === format ?
3437
+ (0, aontu_1.exactJSON)({
3438
+ aontu: { version: version(), verb: 'allow' },
3439
+ findings: report.findings,
3440
+ paths: report.paths,
3441
+ role: report.role,
3442
+ verdict: report.verdict,
3443
+ }, 2) :
3444
+ renderAllowText(report);
3445
+ // The report IS the answer, refused or not, so it goes to stdout as
3446
+ // vet's does; the exit code carries the verdict for a caller that
3447
+ // reads nothing else.
3448
+ process.stdout.write(text + '\n');
3449
+ return ALLOW_EXIT[report.verdict];
3450
+ }
3330
3451
  // ---------------------------------------------------------------------
3331
3452
  // The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
3332
3453
  // derived from the definition, so it cannot drift from the formal
@@ -3341,6 +3462,10 @@ function runAgentsMd(argv) {
3341
3462
  const trust = trusted.trust;
3342
3463
  const files = [];
3343
3464
  let write;
3465
+ // The SHAPE's depth (G11 phase 7). Default 2, unchanged: the stanza
3466
+ // is spliced into a file people read, and a deeper shape is a
3467
+ // question the caller asks rather than one it is handed.
3468
+ let depth = 2;
3344
3469
  for (let i = 0; i < argv.length; i++) {
3345
3470
  const arg = argv[i];
3346
3471
  if ('-h' === arg || '--help' === arg) {
@@ -3354,6 +3479,14 @@ function runAgentsMd(argv) {
3354
3479
  return 2;
3355
3480
  }
3356
3481
  }
3482
+ else if ('--depth' === arg) {
3483
+ const n = Number(argv[++i]);
3484
+ if (!Number.isInteger(n) || n < 1) {
3485
+ process.stderr.write('aontu: --depth needs a positive integer\n');
3486
+ return 2;
3487
+ }
3488
+ depth = n;
3489
+ }
3357
3490
  else if (arg.startsWith('-')) {
3358
3491
  process.stderr.write(`aontu: unknown agentsmd option ${arg} (try --help)\n`);
3359
3492
  return 2;
@@ -3375,7 +3508,7 @@ function runAgentsMd(argv) {
3375
3508
  return 2;
3376
3509
  }
3377
3510
  const report = (0, aontu_1.agentsMd)(src, {
3378
- name: files[0], path: files[0],
3511
+ depth, name: files[0], path: files[0],
3379
3512
  ...verbOpts(trust, entryRootOf(files[0])),
3380
3513
  });
3381
3514
  if (!report.ok) {
@@ -3408,28 +3541,17 @@ function runAgentsMd(argv) {
3408
3541
  process.stdout.write(`wrote: ${write}\n`);
3409
3542
  return 0;
3410
3543
  }
3411
- // Exit without truncating output.
3412
- //
3413
- // process.exit() terminates immediately, discarding anything still
3414
- // queued on stdout. A write to a PIPE is asynchronous once it exceeds
3415
- // the pipe buffer, so `write(big); exit(0)` silently truncated output at
3416
- // 65536 bytes — while a write to a TTY or a file, being synchronous,
3417
- // looked fine. Setting exitCode instead lets the process end naturally,
3418
- // after the queue drains.
3419
- //
3420
- // This predates the exact leaves but they make it trivially reachable
3421
- // (one long biginteger canon exceeds the buffer), and it lands squarely
3422
- // on the parity-probe discipline in AGENTS.md, which derives expected
3423
- // spec values by piping BOTH CLIs and comparing. A truncated pipe there
3424
- // reads as a port divergence.
3425
- // ---------------------------------------------------------------------
3426
- // The source formatter (docs/design/FMT.0.md): one agreed form, in the
3427
- // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
3428
- // the form itself is the library's (ts/src/format.ts), and the two
3429
- // ports agree on it row by row in test/spec/fmt.tsv.
3430
- const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>... (try --help)';
3544
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] ' +
3545
+ '[--profile <file>] <file>... (try --help)';
3431
3546
  function runFmt(argv) {
3547
+ const trusted = takeTrust(argv);
3548
+ if (null == trusted) {
3549
+ return 2;
3550
+ }
3551
+ argv = trusted.argv;
3552
+ const trust = trusted.trust;
3432
3553
  const files = [];
3554
+ const profileFiles = [];
3433
3555
  let marker = undefined;
3434
3556
  const flags = {
3435
3557
  write: false, list: false, check: false, diff: false, lint: false, strict: false,
@@ -3469,6 +3591,14 @@ function runFmt(argv) {
3469
3591
  return 2;
3470
3592
  }
3471
3593
  }
3594
+ else if ('--profile' === arg) {
3595
+ const pf = argv[++i];
3596
+ if (null == pf) {
3597
+ process.stderr.write('aontu: --profile needs a file\n');
3598
+ return 2;
3599
+ }
3600
+ profileFiles.push(pf);
3601
+ }
3472
3602
  else if (arg.startsWith('-')) {
3473
3603
  process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`);
3474
3604
  return 2;
@@ -3492,6 +3622,10 @@ function runFmt(argv) {
3492
3622
  process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags, marker)));
3493
3623
  });
3494
3624
  }
3625
+ const declared = loadProfiles(profileFiles, trust);
3626
+ if ('number' === typeof declared) {
3627
+ return declared;
3628
+ }
3495
3629
  // Several files onto standard output would be one stream nobody can
3496
3630
  // split again (the note's X-6): the verb refuses unless an option
3497
3631
  // says what to do with each.
@@ -3510,32 +3644,18 @@ function runFmt(argv) {
3510
3644
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
3511
3645
  return 2;
3512
3646
  }
3513
- const mark = fmtMarker(file, src, marker);
3647
+ const mark = fmtMarker(file, src, marker ?? (0, template_1.markerFromProfiles)(declared, file));
3514
3648
  if (false === mark) {
3515
3649
  process.stderr.write(`aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
3516
3650
  `${(0, template_1.markerFor)(file)} marker line, so there is no aontu in it to ` +
3517
- 'format; --marker names the marker for a language the table does ' +
3518
- 'not know\n');
3651
+ 'format; --marker names the marker for a language the table ' +
3652
+ 'does not know, and --profile reads one that declares it\n');
3519
3653
  return 2;
3520
3654
  }
3521
3655
  worst = Math.max(worst, fmtOne(file, src, flags, mark));
3522
3656
  }
3523
3657
  return worst;
3524
3658
  }
3525
- // WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
3526
- // `render` reads an entry by): `.aon` and `.aontu` are aontu source,
3527
- // and anything else is a GENERATOR written in the target's own syntax
3528
- // (docs/design/TEMPLATE.0.md), whose marker lines carry the document
3529
- // this formats and whose other lines are output. `undefined` is aontu,
3530
- // a string is the generator's marker, and `false` is neither.
3531
- //
3532
- // A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
3533
- // FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
3534
- // include is another language's file, and reading one as a generator
3535
- // would answer it back unchanged having understood none of it. The
3536
- // marker is the evidence that a file was written to carry aontu at
3537
- // all. `--marker` says so outright, and then the file is a generator
3538
- // whatever it is called.
3539
3659
  function fmtMarker(file, src, marker) {
3540
3660
  if (undefined !== marker) {
3541
3661
  return marker;
@@ -3591,11 +3711,8 @@ function fmtOne(name, src, flags, marker) {
3591
3711
  }
3592
3712
  return flags.check ? 1 : strict;
3593
3713
  }
3594
- // The real pair takes the process's own stdin and stdout, which no
3595
- // in-process test can lend it; the executable-entry tests in
3596
- // cli.test.ts run each through a child process instead, so these two
3597
- // lines are excluded from the in-process count, as the stdio wiring
3598
- // of lsp-server.ts is.
3714
+ // Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
3715
+ // drives each server through a child process instead.
3599
3716
  /* node:coverage ignore next 4 */
3600
3717
  const SERVERS = {
3601
3718
  lsp: () => void (0, lsp_server_1.main)(),
@@ -3635,22 +3752,301 @@ function parseTrustArg(value) {
3635
3752
  }
3636
3753
  return undefined;
3637
3754
  }
3755
+ const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)';
3756
+ const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)';
3757
+ function helpIndexText(index) {
3758
+ const width = index.reduce((w, t) => Math.max(w, t.topic.length), 0);
3759
+ return 'aontu help <topic> — the language, offline.\n\n' +
3760
+ index.map((t) => ' ' + t.topic.padEnd(width) + ' ' + t.summary).join('\n') +
3761
+ '\n\n' +
3762
+ '`aontu --help` documents the verbs, their flags and their exit\n' +
3763
+ 'codes. `aontu explain <code>` explains one error code.\n' +
3764
+ 'Start at `aontu help tasks` if you know the job but not the verb.';
3765
+ }
3766
+ function runHelp(argv) {
3767
+ let format = 'text';
3768
+ const topics = [];
3769
+ for (let i = 0; i < argv.length; i++) {
3770
+ const arg = argv[i];
3771
+ if ('-h' === arg || '--help' === arg) {
3772
+ process.stdout.write(HELP);
3773
+ return 0;
3774
+ }
3775
+ else if ('--format' === arg) {
3776
+ const f = argv[++i];
3777
+ if ('text' !== f && 'json' !== f) {
3778
+ process.stderr.write('aontu: --format needs text or json\n');
3779
+ return 2;
3780
+ }
3781
+ format = f;
3782
+ }
3783
+ else if (arg.startsWith('-')) {
3784
+ process.stderr.write(`aontu: unknown help option ${arg} (try --help)\n`);
3785
+ return 2;
3786
+ }
3787
+ else {
3788
+ topics.push(arg);
3789
+ }
3790
+ }
3791
+ if (1 < topics.length) {
3792
+ process.stderr.write(`aontu: help takes one topic\n${HELP_VERB_HELP}\n`);
3793
+ return 2;
3794
+ }
3795
+ if (0 === topics.length) {
3796
+ process.stdout.write(('json' === format
3797
+ ? (0, aontu_1.exactJSON)({
3798
+ aontu: { version: version(), verb: 'help' },
3799
+ topics: helpdoc_1.HELPDOC.map((t) => ({ topic: t.topic, summary: t.summary, source: t.source })),
3800
+ }, 2)
3801
+ : helpIndexText(helpdoc_1.HELPDOC)) + '\n');
3802
+ return 0;
3803
+ }
3804
+ const found = helpdoc_1.HELPDOC.find((t) => topics[0] === t.topic);
3805
+ if (null != found) {
3806
+ if ('json' === format) {
3807
+ process.stdout.write((0, aontu_1.exactJSON)({
3808
+ aontu: { version: version(), verb: 'help' },
3809
+ topic: found.topic,
3810
+ summary: found.summary,
3811
+ source: found.source,
3812
+ text: found.text,
3813
+ }, 2) + '\n');
3814
+ return 0;
3815
+ }
3816
+ process.stdout.write(found.text);
3817
+ return 0;
3818
+ }
3819
+ // AN UNKNOWN TOPIC IS A USAGE ERROR AND NAMES THE ALTERNATIVES,
3820
+ // because the caller who typed it has no other way to find out what
3821
+ // exists -- that is the whole condition this verb was added for.
3822
+ process.stderr.write(`aontu: no help topic \`${topics[0]}\`\n` +
3823
+ `aontu: topics are ${helpdoc_1.HELPDOC.map((t) => t.topic).join(', ')}\n`);
3824
+ return 2;
3825
+ }
3826
+ // The dynamic prefixes a generated code extends (`func:upper`,
3827
+ // `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
3828
+ // exported; a code that extends one is registered through its prefix
3829
+ // and carries that prefix's hint.
3830
+ const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref['];
3831
+ function explainCode(code) {
3832
+ const cls = (0, hints_1.codeClass)(code);
3833
+ let hint = hints_1.hints[code] ?? '';
3834
+ let registered = null != hints_1.codeClasses[code];
3835
+ if (!registered) {
3836
+ for (const prefix of EXPLAIN_PREFIXES) {
3837
+ if (code.startsWith(prefix)) {
3838
+ // No guard on `hint` here: every hint key is also a registry
3839
+ // key (the spec suite asserts codeClasses set-equal with
3840
+ // test/spec/errcodes.tsv, and hints is a subset of it), so a
3841
+ // code that reaches this loop is unregistered and therefore
3842
+ // has no hint of its own.
3843
+ registered = true;
3844
+ hint = hints_1.hints[prefix] ?? '';
3845
+ break;
3846
+ }
3847
+ }
3848
+ }
3849
+ return { cls, hint, registered };
3850
+ }
3851
+ // Every code in the shared registry, sorted by code point so both
3852
+ // ports list them in the same order.
3853
+ function explainCodes() {
3854
+ return Object.keys(hints_1.codeClasses).sort(keyorder_1.cmpCodePoint);
3855
+ }
3856
+ function explainListText(format) {
3857
+ const codes = explainCodes();
3858
+ if ('json' === format) {
3859
+ return (0, aontu_1.exactJSON)({
3860
+ aontu: { version: version(), verb: 'explain' },
3861
+ codes: codes.map((code) => ({
3862
+ code,
3863
+ class: (0, hints_1.codeClass)(code),
3864
+ // Whether this port carries explanation text for the code. The
3865
+ // registry is in parity; the hint tables are not, so a consumer
3866
+ // that wants only explained codes can filter rather than guess.
3867
+ explained: '' !== explainCode(code).hint,
3868
+ })),
3869
+ }, 2);
3870
+ }
3871
+ const width = codes.reduce((w, c) => Math.max(w, c.length), 0);
3872
+ return codes.map((c) => c.padEnd(width) + ' ' + (0, hints_1.codeClass)(c) +
3873
+ ('' === explainCode(c).hint ? ' (no text)' : '')).join('\n');
3874
+ }
3875
+ function runExplain(argv) {
3876
+ let format = 'text';
3877
+ let list = false;
3878
+ const codes = [];
3879
+ for (let i = 0; i < argv.length; i++) {
3880
+ const arg = argv[i];
3881
+ if ('-h' === arg || '--help' === arg) {
3882
+ process.stdout.write(HELP);
3883
+ return 0;
3884
+ }
3885
+ else if ('--list' === arg) {
3886
+ list = true;
3887
+ }
3888
+ else if ('--format' === arg) {
3889
+ const f = argv[++i];
3890
+ if ('text' !== f && 'json' !== f) {
3891
+ process.stderr.write('aontu: --format needs text or json\n');
3892
+ return 2;
3893
+ }
3894
+ format = f;
3895
+ }
3896
+ else if (arg.startsWith('-')) {
3897
+ process.stderr.write(`aontu: unknown explain option ${arg} (try --help)\n`);
3898
+ return 2;
3899
+ }
3900
+ else {
3901
+ codes.push(arg);
3902
+ }
3903
+ }
3904
+ if (list) {
3905
+ if (0 < codes.length) {
3906
+ process.stderr.write(`aontu: --list takes no code\n${EXPLAIN_HELP}\n`);
3907
+ return 2;
3908
+ }
3909
+ process.stdout.write(explainListText(format) + '\n');
3910
+ return 0;
3911
+ }
3912
+ if (1 !== codes.length) {
3913
+ process.stderr.write(`aontu: explain needs one code\n${EXPLAIN_HELP}\n`);
3914
+ return 2;
3915
+ }
3916
+ const code = codes[0];
3917
+ const { cls, hint, registered } = explainCode(code);
3918
+ if (!registered) {
3919
+ // AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
3920
+ // caller reading a code out of a report has almost certainly typed
3921
+ // it correctly, so the likely cause is a code from another tool or
3922
+ // a truncated one, and the near matches say which.
3923
+ process.stderr.write(`aontu: no such error code \`${code}\`\n`);
3924
+ const near = nearestVerb(code, explainCodes());
3925
+ if ('' !== near) {
3926
+ process.stderr.write(`aontu: did you mean \`${near}\`?\n`);
3927
+ }
3928
+ process.stderr.write('aontu: `aontu explain --list` lists every registered code\n');
3929
+ return 2;
3930
+ }
3931
+ if ('json' === format) {
3932
+ process.stdout.write((0, aontu_1.exactJSON)({
3933
+ aontu: { version: version(), verb: 'explain' },
3934
+ code,
3935
+ class: cls,
3936
+ hint,
3937
+ }, 2) + '\n');
3938
+ return 0;
3939
+ }
3940
+ // A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
3941
+ // empty block, which would read as an explanation that happened to
3942
+ // be blank.
3943
+ const body = '' === hint
3944
+ ? '(no explanation text is registered for this code)'
3945
+ : hint;
3946
+ process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`);
3947
+ return 0;
3948
+ }
3949
+ const INIT_HELP = 'aontu init [dir] (try --help)';
3950
+ function runInit(argv) {
3951
+ const dirs = [];
3952
+ for (const arg of argv) {
3953
+ if ('-h' === arg || '--help' === arg) {
3954
+ process.stdout.write(HELP);
3955
+ return 0;
3956
+ }
3957
+ if (arg.startsWith('-')) {
3958
+ process.stderr.write(`aontu: unknown init option ${arg} (try --help)\n`);
3959
+ return 2;
3960
+ }
3961
+ dirs.push(arg);
3962
+ }
3963
+ if (1 < dirs.length) {
3964
+ process.stderr.write(`aontu: init takes one directory\n${INIT_HELP}\n`);
3965
+ return 2;
3966
+ }
3967
+ const dir = dirs[0] ?? '.';
3968
+ const standing = helpdoc_1.INITDOC.filter((f) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, f.name)));
3969
+ if (0 < standing.length) {
3970
+ process.stderr.write(`aontu: ${dir} already holds ${standing.map((f) => f.name).join(', ')}\n` +
3971
+ 'aontu: init never overwrites; move them aside or name an' +
3972
+ ' empty directory\n');
3973
+ return 2;
3974
+ }
3975
+ try {
3976
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
3977
+ for (const f of helpdoc_1.INITDOC) {
3978
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, f.name), f.text, { mode: f.mode });
3979
+ }
3980
+ }
3981
+ catch (err) {
3982
+ process.stderr.write(`aontu: cannot write in ${dir}: ${err.message}\n`);
3983
+ return 2;
3984
+ }
3985
+ process.stdout.write(helpdoc_1.INITDOC.map((f) => (0, node_path_1.join)(dir, f.name)).join('\n') + '\n' +
3986
+ '\nA model, an instance of it, and the four questions to ask.\n' +
3987
+ 'Run the checks: sh ' + (0, node_path_1.join)(dir, 'check.sh') + '\n' +
3988
+ 'Learn the language: aontu help language\n');
3989
+ return 0;
3990
+ }
3991
+ const KNOWN_VERBS = [
3992
+ 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
3993
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
3994
+ 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
3995
+ 'view', 'why',
3996
+ ];
3997
+ exports.KNOWN_VERBS = KNOWN_VERBS;
3998
+ // looksLikeVerb reports whether an unreadable argument was meant as a
3999
+ // verb rather than as a path. A bare word has no separator and no
4000
+ // extension; `./help`, `help.aon`, `/tmp/help` and `sub/dir` are paths
4001
+ // and keep the file diagnosis. Mirrors go/cmd/aontu/main.go.
4002
+ function looksLikeVerb(arg) {
4003
+ return '' !== arg &&
4004
+ !/[/\\.]/.test(arg) &&
4005
+ !arg.startsWith('-');
4006
+ }
4007
+ function nearestVerb(word, verbs) {
4008
+ let best = '';
4009
+ let bestDist = Infinity;
4010
+ const limit = Math.min(3, 1 + Math.floor(word.length / 4));
4011
+ for (const v of [...verbs].sort(keyorder_1.cmpCodePoint)) {
4012
+ const d = editDistance(word.toLowerCase(), v);
4013
+ if (d < bestDist) {
4014
+ best = v;
4015
+ bestDist = d;
4016
+ }
4017
+ }
4018
+ return bestDist > limit ? '' : best;
4019
+ }
4020
+ function editDistance(a, b) {
4021
+ const ar = [...a];
4022
+ const br = [...b];
4023
+ let prev2 = new Array(br.length + 1).fill(0);
4024
+ let prev = new Array(br.length + 1).fill(0).map((_, j) => j);
4025
+ let cur = new Array(br.length + 1).fill(0);
4026
+ for (let i = 1; i <= ar.length; i++) {
4027
+ cur[0] = i;
4028
+ for (let j = 1; j <= br.length; j++) {
4029
+ const cost = ar[i - 1] === br[j - 1] ? 0 : 1;
4030
+ let m = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
4031
+ if (1 < i && 1 < j &&
4032
+ ar[i - 1] === br[j - 2] && ar[i - 2] === br[j - 1] &&
4033
+ prev2[j - 2] + 1 < m) {
4034
+ m = prev2[j - 2] + 1;
4035
+ }
4036
+ cur[j] = m;
4037
+ }
4038
+ prev2 = [...prev];
4039
+ prev = [...cur];
4040
+ }
4041
+ return prev[br.length];
4042
+ }
3638
4043
  function main(argv, servers = SERVERS) {
3639
- // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
3640
- // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
3641
- // answer carried terminal control codes into whatever read them (the
3642
- // review's finding F). `NO_COLOR` is honoured by the library itself;
3643
- // only the CLI can see whether its stderr is a terminal, so only the
3644
- // CLI can make this call. `undefined` means "leave it to NO_COLOR".
3645
4044
  (0, aontu_1.setColor)(true === process.stderr.isTTY ? undefined : false);
3646
4045
  let mode = 'json';
3647
- // A LIST, though the bare command evaluates exactly one document.
3648
- // It used to be one variable and the last argument won, which made a
3649
- // MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
3650
- // printed good.json and exited 0, because `vet2` matched no
3651
- // subcommand, fell through to this loop as a file name, and was
3652
- // overwritten twice. In a tool loop that reads as a passing
3653
- // validation. Counting them is what lets the refusal below happen.
4046
+ // THE REPORT FORM (G11 phase 7), default text: every existing caller
4047
+ // reads exactly what it always read, and a caller that asks for json
4048
+ // gets the failure in the finding shape every other verb reports.
4049
+ let format = 'text';
3654
4050
  const files = [];
3655
4051
  let trust = { kind: 'system-warn', textExt: [] };
3656
4052
  let textExt = [];
@@ -3659,15 +4055,6 @@ function main(argv, servers = SERVERS) {
3659
4055
  // than the design's --json, which would read as the `:json` output
3660
4056
  // mode the REPL already has.
3661
4057
  let jsonl = false;
3662
- // Subcommand dispatch, and deliberately only for a FIRST argument:
3663
- // `aontu vet` is the verb, while `aontu somefile vet` keeps meaning
3664
- // what it always did. A file named `vet` is still reachable as
3665
- // `aontu ./vet`.
3666
- //
3667
- // Promise.resolve either way: a non-watch run returns its exit class
3668
- // synchronously (and has already written its report), while `--watch`
3669
- // resolves only when the watch ends — so one await-shaped line serves
3670
- // both without a branch to keep covered.
3671
4058
  if ('vet' === argv[2]) {
3672
4059
  return void Promise.resolve(runVet(argv.slice(3))).then(finish);
3673
4060
  }
@@ -3693,6 +4080,9 @@ function main(argv, servers = SERVERS) {
3693
4080
  if ('set' === argv[2]) {
3694
4081
  return finish(runSet(argv.slice(3)));
3695
4082
  }
4083
+ if ('allow' === argv[2]) {
4084
+ return finish(runAllow(argv.slice(3)));
4085
+ }
3696
4086
  if ('why' === argv[2]) {
3697
4087
  return finish(runWhy(argv.slice(3)));
3698
4088
  }
@@ -3702,6 +4092,17 @@ function main(argv, servers = SERVERS) {
3702
4092
  if ('hash' === argv[2]) {
3703
4093
  return finish(runHash(argv.slice(3)));
3704
4094
  }
4095
+ // G11 phases 1 and 3. Dispatched with the rest, so `aontu ./help`
4096
+ // still reads a file named help exactly as `aontu ./vet` does.
4097
+ if ('help' === argv[2]) {
4098
+ return finish(runHelp(argv.slice(3)));
4099
+ }
4100
+ if ('explain' === argv[2]) {
4101
+ return finish(runExplain(argv.slice(3)));
4102
+ }
4103
+ if ('init' === argv[2]) {
4104
+ return finish(runInit(argv.slice(3)));
4105
+ }
3705
4106
  if ('mod' === argv[2]) {
3706
4107
  return finish(runMod(argv.slice(3)));
3707
4108
  }
@@ -3748,6 +4149,14 @@ function main(argv, servers = SERVERS) {
3748
4149
  }
3749
4150
  trust = parsed;
3750
4151
  }
4152
+ else if ('--format' === arg) {
4153
+ const f = args[++i];
4154
+ if ('text' !== f && 'json' !== f) {
4155
+ process.stderr.write('aontu: --format needs text or json\n');
4156
+ return finish(2);
4157
+ }
4158
+ format = f;
4159
+ }
3751
4160
  else if ('--jsonl' === arg) {
3752
4161
  jsonl = true;
3753
4162
  // A JSONL answer is machine-read by definition, even when the
@@ -3782,27 +4191,16 @@ function main(argv, servers = SERVERS) {
3782
4191
  files.push(arg);
3783
4192
  }
3784
4193
  }
3785
- // ONE DOCUMENT. The bare form has always been `aontu [options]
3786
- // [file]`, singular, and anything past the first was silently
3787
- // discarded rather than refused -- so every way of getting the verb
3788
- // wrong (a typo, a verb this port does not have, a verb spelled for
3789
- // another tool) ended in a plausible answer about the wrong file.
3790
- // Exit 2, the usage class, and the message names the cause rather
3791
- // than the symptom: nothing here can tell a mistyped verb from a
3792
- // second file, but the reader can.
3793
4194
  if (1 < files.length) {
3794
4195
  process.stderr.write(`aontu: the bare command evaluates one document, and ${files.length}` +
3795
4196
  ' were given\naontu: a mistyped verb reads as a file name' +
3796
4197
  ' (try --help)\n');
3797
4198
  return finish(2);
3798
4199
  }
3799
- // The extensions ride with the capability from here on, so the three
3800
- // entry shapes below (file, REPL, stdin) each get them by threading
3801
- // the one value they already thread.
3802
4200
  trust = { ...trust, textExt };
3803
4201
  const file = files[0];
3804
4202
  if (null != file) {
3805
- finish(runFile(file, mode, trust));
4203
+ finish(runFile(file, mode, format, trust));
3806
4204
  }
3807
4205
  // `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
3808
4206
  // a harness over a pipe, so gating it on an interactive terminal
@@ -3812,7 +4210,7 @@ function main(argv, servers = SERVERS) {
3812
4210
  runRepl(mode, jsonl, trust);
3813
4211
  }
3814
4212
  else {
3815
- runStdin(mode, trust).then((code) => finish(code));
4213
+ runStdin(mode, format, trust).then((code) => finish(code));
3816
4214
  }
3817
- } /* node:coverage ignore next 18 */
4215
+ } /* node:coverage ignore next 20 */
3818
4216
  //# sourceMappingURL=cli.js.map