aontu 0.60.0 → 0.62.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 (113) hide show
  1. package/README.md +3 -3
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +7 -1
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/allow.d.ts +23 -0
  6. package/dist/allow.js +230 -0
  7. package/dist/allow.js.map +1 -0
  8. package/dist/aontu.d.ts +4 -2
  9. package/dist/aontu.js +4 -2
  10. package/dist/aontu.js.map +1 -1
  11. package/dist/cli.d.ts +10 -1
  12. package/dist/cli.js +1000 -47
  13. package/dist/cli.js.map +1 -1
  14. package/dist/format.d.ts +1 -0
  15. package/dist/format.js +154 -12
  16. package/dist/format.js.map +1 -1
  17. package/dist/helpdoc.d.ts +16 -0
  18. package/dist/helpdoc.js +59 -0
  19. package/dist/helpdoc.js.map +1 -0
  20. package/dist/hints.js +15 -2
  21. package/dist/hints.js.map +1 -1
  22. package/dist/lang.js +93 -40
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lower.d.ts +3 -0
  25. package/dist/lower.js +3 -0
  26. package/dist/lower.js.map +1 -1
  27. package/dist/lsp.d.ts +1 -1
  28. package/dist/lsp.js +1 -1
  29. package/dist/lsp.js.map +1 -1
  30. package/dist/relation.d.ts +2 -0
  31. package/dist/relation.js +7 -1
  32. package/dist/relation.js.map +1 -1
  33. package/dist/render.js +26 -21
  34. package/dist/render.js.map +1 -1
  35. package/dist/sigdecl.js +1 -1
  36. package/dist/sigdecl.js.map +1 -1
  37. package/dist/std.js +112 -69
  38. package/dist/std.js.map +1 -1
  39. package/dist/template.d.ts +2 -1
  40. package/dist/template.js +51 -15
  41. package/dist/template.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/dist/val/AggFuncVal.js +2 -2
  44. package/dist/val/AggFuncVal.js.map +1 -1
  45. package/dist/val/CmpFuncVal.d.ts +20 -0
  46. package/dist/val/CmpFuncVal.js +245 -0
  47. package/dist/val/CmpFuncVal.js.map +1 -0
  48. package/dist/val/EachFuncVal.d.ts +1 -2
  49. package/dist/val/EachFuncVal.js +15 -29
  50. package/dist/val/EachFuncVal.js.map +1 -1
  51. package/dist/val/FormFuncVal.js.map +1 -1
  52. package/dist/val/LowerFuncVal.js +17 -1
  53. package/dist/val/LowerFuncVal.js.map +1 -1
  54. package/dist/val/NamerFuncVal.d.ts +12 -0
  55. package/dist/val/NamerFuncVal.js +176 -0
  56. package/dist/val/NamerFuncVal.js.map +1 -0
  57. package/dist/val/NilVal.js +29 -3
  58. package/dist/val/NilVal.js.map +1 -1
  59. package/dist/val/NomFuncVal.d.ts +12 -0
  60. package/dist/val/NomFuncVal.js +187 -0
  61. package/dist/val/NomFuncVal.js.map +1 -0
  62. package/dist/val/PackFuncVal.js +3 -3
  63. package/dist/val/PackFuncVal.js.map +1 -1
  64. package/dist/val/RefVal.js +1 -1
  65. package/dist/val/TranslateFuncVal.d.ts +12 -0
  66. package/dist/val/TranslateFuncVal.js +101 -0
  67. package/dist/val/TranslateFuncVal.js.map +1 -0
  68. package/dist/val/UpperFuncVal.js +17 -1
  69. package/dist/val/UpperFuncVal.js.map +1 -1
  70. package/dist/val/caserange.d.ts +3 -0
  71. package/dist/val/caserange.js +110 -0
  72. package/dist/val/caserange.js.map +1 -0
  73. package/dist/vet.d.ts +12 -0
  74. package/dist/vet.js +209 -1
  75. package/dist/vet.js.map +1 -1
  76. package/grammar/aontu.abnf +1 -1
  77. package/grammar/aontu.gbnf +1 -1
  78. package/grammar/aontu.lark +1 -1
  79. package/grammar/aontu.tmLanguage.json +1 -1
  80. package/package.json +1 -1
  81. package/skill/SKILL.md +8 -0
  82. package/skill/init/check.sh +28 -0
  83. package/skill/init/data.aon +12 -0
  84. package/skill/init/model.aon +19 -0
  85. package/skill/tasks.md +151 -0
  86. package/src/agentsmd.ts +13 -2
  87. package/src/allow.ts +316 -0
  88. package/src/aontu.ts +10 -1
  89. package/src/cli.ts +1131 -53
  90. package/src/format.ts +191 -14
  91. package/src/helpdoc.ts +77 -0
  92. package/src/hints.ts +16 -2
  93. package/src/lang.ts +98 -42
  94. package/src/lower.ts +3 -3
  95. package/src/lsp.ts +1 -1
  96. package/src/relation.ts +21 -1
  97. package/src/render.ts +26 -21
  98. package/src/sigdecl.ts +1 -1
  99. package/src/std.ts +112 -69
  100. package/src/template.ts +55 -15
  101. package/src/val/AggFuncVal.ts +2 -2
  102. package/src/val/CmpFuncVal.ts +411 -0
  103. package/src/val/EachFuncVal.ts +49 -50
  104. package/src/val/LowerFuncVal.ts +18 -1
  105. package/src/val/NilVal.ts +29 -3
  106. package/src/val/NomFuncVal.ts +287 -0
  107. package/src/val/PackFuncVal.ts +3 -3
  108. package/src/val/RefVal.ts +1 -1
  109. package/src/val/TranslateFuncVal.ts +182 -0
  110. package/src/val/UpperFuncVal.ts +18 -1
  111. package/src/val/caserange.ts +115 -0
  112. package/src/vet.ts +287 -1
  113. 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,9 +18,15 @@ 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;
@@ -55,6 +61,9 @@ const view_1 = require("./view");
55
61
  const agentsmd_1 = require("./agentsmd");
56
62
  const format_1 = require("./format");
57
63
  const utility_1 = require("./utility");
64
+ const helpdoc_1 = require("./helpdoc");
65
+ const hints_1 = require("./hints");
66
+ const keyorder_1 = require("./keyorder");
58
67
  const HELP = `Usage: aontu [options] [file]
59
68
  aontu vet [options] <schema> <data> [more-data...]
60
69
  aontu subsume [options] <general> <specific>
@@ -74,8 +83,12 @@ const HELP = `Usage: aontu [options] [file]
74
83
  aontu get <path> [options] <file>
75
84
  aontu why <path> [options] <file>
76
85
  aontu set <path>=<value>... --entry <file> --overlay <file>
77
- aontu agentsmd [--write <AGENTS.md>] <file>
78
- aontu fmt [-w|-l|--check|-d|--lint] <file>...
86
+ aontu allow --role <role> [--at <path>] <roles-file> <path>...
87
+ aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
88
+ aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
89
+ aontu help [topic] [--format text|json]
90
+ aontu explain <code> | --list [--format text|json]
91
+ aontu init [dir]
79
92
  aontu lsp
80
93
  aontu mcp [--root <dir>]
81
94
 
@@ -83,6 +96,34 @@ Evaluate an aontu source file and print the result as JSON.
83
96
  With no file on an interactive terminal, start a REPL.
84
97
  With no file and piped input, read the source from stdin.
85
98
 
99
+ NEW TO THE LANGUAGE? This page documents the TOOL. The documentation
100
+ of the LANGUAGE travels inside this binary, and this is how to reach it:
101
+
102
+ aontu help List the topics this binary carries
103
+ aontu help tasks Which verb does the job you have
104
+ aontu help language The whole grammar, on one page
105
+ aontu help examples The ladder, from plain JSON upward
106
+ aontu help codes What a refusal means
107
+ aontu help grammar The published ABNF
108
+ aontu explain <code> What one error code a report carries means
109
+ aontu explain --list Every registered code with its class
110
+
111
+ Every one of those answers with no network and no checkout. The
112
+ long-form documentation -- the tutorial, the language and API
113
+ references, the how-to guides -- is in docs/ of the repository, which
114
+ is where to go when the topics above are not enough; the contributor
115
+ and agent guide is AGENTS.md beside it.
116
+
117
+ NOTHING TO EDIT YET? aontu init [dir] writes a working model, an
118
+ instance of it, and the four checks to run -- so the first
119
+ document is an edit of something that already holds, rather than an
120
+ invention. It refuses to overwrite.
121
+
122
+ The one construct to know before writing anything: &: inside a map is
123
+ a TEMPLATE that every key of that map must satisfy. A quoted "*" is a
124
+ key named *, not a wildcard, and a schema written that way constrains
125
+ nothing while still reporting valid.
126
+
86
127
  The vet verb validates data documents against a schema document and
87
128
  reports what does not hold, as text or as a machine-readable object.
88
129
 
@@ -92,7 +133,11 @@ query between a document and its own earlier versions.
92
133
 
93
134
  Options:
94
135
  -c, --canon Print the canonical form instead of generated JSON
95
- -h, --help Show this help and exit
136
+ --format <f> text (default) or json. The json form wraps the
137
+ answer as {aontu, findings, ok, out}, so a failure
138
+ here reads like every other verb's
139
+ -h, --help Show this help and exit (the verbs and their flags);
140
+ aontu help is the LANGUAGE, and lists its own topics
96
141
  --jsonl REPL: answer every command as one JSON line
97
142
  -v, --version Print the version and exit
98
143
  --trust <t> Include capability: system (default), none, or
@@ -123,12 +168,28 @@ Vet options:
123
168
  --closed Refuse keys the anchor does not declare
124
169
  --partial Residue is reported but does not fail the run
125
170
  --max-errors <n> Cap the finding list (default 20)
171
+ --coverage Report what the check EXAMINED: how many data
172
+ leaves a schema declaration constrained, the
173
+ shallowest data paths none did, and the
174
+ declarations no data met
175
+ --strict-coverage --coverage, and exit 1 when the run was VACUOUS --
176
+ when no data leaf was constrained at all. The
177
+ verdict word is unchanged, so nothing that passes
178
+ today starts failing without this flag
179
+ --coverage-at <p> Measure coverage under this path of the data only
126
180
  --format <f> text (default), json or sarif
127
181
  --watch Re-run whenever a watched file changes
128
182
 
183
+ A check that examined NOTHING and a check that passed answer the same
184
+ without --coverage. The usual cause is a schema written with the
185
+ wildcard other tools use: a quoted "*" is a key NAMED *, not a
186
+ template, so it constrains nothing and the run still reports valid.
187
+ The template is &: -- see aontu help language.
188
+
129
189
  Vet exit codes:
130
190
  0 valid data unifies, and is concrete (or --partial)
131
- 1 invalid at least one contradiction
191
+ 1 invalid at least one contradiction, or a vacuous run under
192
+ --strict-coverage
132
193
  2 usage bad option, or a file that cannot be read
133
194
  3 incomplete no contradiction, but the truth is not yet satisfied
134
195
  4 error the schema is unusable on its own
@@ -326,14 +387,61 @@ change contradicts a pinned value -- aontu why locates it, and
326
387
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
327
388
  stand up on its own.
328
389
 
390
+ Allow options:
391
+ --role <role> The role the caller is operating under (required)
392
+ --at <path> Where the roles map lives in the role model
393
+ (default $.roles)
394
+ --format <f> text (default) or json
395
+
396
+ The allow verb asks a role model whether a role may modify every one
397
+ of the given subtrees, and answers before the change is made. The
398
+ role model is an aontu document: one entry per role, each carrying
399
+ allow (the subtrees it may modify) and optionally deny (the ones it
400
+ may not), as path strings starting at $; * in a path matches any one
401
+ key. A path is allowed when an allow entry is at or above it, and
402
+ refused when a deny entry is at, above or below it, whatever the
403
+ order. Every path starts with $, and may be spelled as set's
404
+ assignment, <path>=<value>, whose value must be one value: a value
405
+ carrying a second pair would write a subtree the gate was not asked
406
+ about.
407
+
408
+ Allow exit codes: 0 allowed (every path), 1 refused (at least one
409
+ path, or a role the model does not declare), 2 usage, 4 the role
410
+ model does not stand up on its own.
411
+
329
412
  Agentsmd options:
330
413
  --write <file> Splice the stanza into this file between the
331
414
  aontu:begin and aontu:end markers, appending them
332
415
  when they are absent; the rest is left alone
416
+ --depth <n> How deep the shape line projects (default 2). Two
417
+ levels name the root keys and say top under them; a
418
+ caller that wants the fields asks for them
333
419
 
334
420
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
335
421
  stand up on its own.
336
422
 
423
+ Help options:
424
+ --format <f> text (default) or json, the topic and its text
425
+
426
+ The help verb prints the embedded teaching pack: the language, not the
427
+ tool. With no topic it lists them. Topics are tasks, language,
428
+ examples, codes and grammar; the corpus is generated from docs/skill/
429
+ and grammar/aontu.abnf, so it cannot drift from those sources.
430
+
431
+ Help exit codes: 0 printed, 2 an unknown topic (the topics are listed)
432
+ or a bad option.
433
+
434
+ Explain options:
435
+ --list Every registered error code with its class
436
+ --format <f> text (default) or json
437
+
438
+ The explain verb answers what one error code means, from the same
439
+ table the engine attaches to a finding. Every registered code has an
440
+ entry, so a code read out of a report always resolves.
441
+
442
+ Explain exit codes: 0 explained, 2 an unknown code (near matches are
443
+ named) or a bad option.
444
+
337
445
  Fmt options:
338
446
  -w, --write Rewrite each file in place, when its form would change
339
447
  -l, --list Print the name of each file whose form would change
@@ -343,10 +451,18 @@ Fmt options:
343
451
  --lint Report the style findings, key case and repeated
344
452
  shapes, on standard error, and print nothing else
345
453
  --strict With --lint, and exit 1 when there is a finding
454
+ --marker <t> The file is a generator, and this is its marker
455
+ (default //-, and #- --- /*- by extension)
346
456
 
347
457
  The fmt verb prints one document in the agreed form; with no file it
348
458
  reads standard input. Several files need one of the options above.
349
459
 
460
+ A file whose extension is not .aon is a GENERATOR, as it is for render:
461
+ the aontu its marker lines carry is formatted, the marker stands at the
462
+ left margin with the aontu indented after it, and every line of output
463
+ is held on a line of its own. A file with no marker line in it is
464
+ another language's, and is refused.
465
+
350
466
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
351
467
  a --strict finding, 2 usage, 4 a document does not parse.
352
468
 
@@ -379,8 +495,46 @@ function version() {
379
495
  return '0.0.0';
380
496
  }
381
497
  }
498
+ // The terminal colour escapes the parser puts in its message text. A
499
+ // machine-readable report is no place for them, which is the rule
500
+ // findingOf states in ts/src/vet.ts; the twin here rather than an
501
+ // import because go/cmd/aontu carries its own for the same reason (the
502
+ // engine's is not exported to its command).
503
+ const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g');
504
+ // THE ENGINE'S DIAGNOSIS AS A FINDING (G11 phase 7). The bare command
505
+ // was the one verb whose failure had no machine-readable form, so the
506
+ // default entry point was the one an agent had to parse with a regular
507
+ // expression.
508
+ //
509
+ // THE HEADLINE ONLY, and no `hint`. Both are parity decisions rather
510
+ // than economies: the frames under the headline are drawn for a person
511
+ // reading a terminal and only the first line is held to byte parity
512
+ // between the ports (the rule findingOf states), and the hint TABLES
513
+ // are deliberately not in parity while the code registry is -- so a
514
+ // hint here would make the two ports answer differently for a code
515
+ // only one of them explains. `aontu explain <code>` is where the hint
516
+ // lives, which is what phase 3 built it for.
517
+ //
518
+ // The CLASS comes from the registry rather than from the nil, because
519
+ // the registry is what both ports hold set-equal
520
+ // (test/spec/errcodes.tsv). Mirrors evalFinding in
521
+ // go/cmd/aontu/main.go.
522
+ function evalFinding(code, text) {
523
+ return {
524
+ class: (0, hints_1.codeClass)(code),
525
+ code,
526
+ message: text.split('\n')[0].replace(EVAL_ANSI, ''),
527
+ path: '$',
528
+ severity: 'error',
529
+ // NO SITE. The bare command's failure is the whole document not
530
+ // standing up, and the two sites a conflict names are in the
531
+ // frames the text form prints; naming one of them here would be a
532
+ // choice the engine has not made.
533
+ sites: [],
534
+ };
535
+ }
382
536
  // Evaluate source, returning either the rendered output or the error
383
- // message. Never throws.
537
+ // message, and the failure in the finding shape. Never throws.
384
538
  function evalSource(aontu, src, mode) {
385
539
  try {
386
540
  // exactJSON, not JSON.stringify: a document using the `0d` exact
@@ -392,15 +546,48 @@ function evalSource(aontu, src, mode) {
392
546
  const text = 'canon' === mode
393
547
  ? aontu.unify(src).canon
394
548
  : (0, aontu_1.exactJSON)(aontu.generate(src), 2);
395
- return { ok: true, text };
549
+ return { ok: true, text, findings: [] };
396
550
  }
397
551
  catch (err) {
398
552
  const msg = (err instanceof aontu_1.AontuError || true === err?.aontu)
399
553
  ? err.message
400
554
  : String(err?.message ?? err);
401
- return { ok: false, text: msg };
555
+ // WHAT THE ENGINE COLLECTED, when it collected anything: an
556
+ // AontuError carries the NilVals the run failed on, already
557
+ // materialised (handleErrors in ts/src/aontu.ts), and their first
558
+ // is the diagnosis every other verb reports. An error raised
559
+ // outside the engine's own collection -- exactJSON's circular
560
+ // refusal, a foreign object claiming to be one -- carries none,
561
+ // and answers with the text alone rather than an invented code.
562
+ const errs = 'function' === typeof err?.errs ? err.errs() : [];
563
+ const first = errs[0];
564
+ return {
565
+ ok: false,
566
+ text: msg,
567
+ findings: null == first ? [] : [evalFinding(first.why, msg)],
568
+ };
402
569
  }
403
570
  }
571
+ // The bare command's answer, in the form the caller asked for. The
572
+ // text form is what it has always printed, on the stream the verdict
573
+ // chooses; `--format json` is the same answer as one object, on
574
+ // stdout, so a harness reads one stream and one shape either way.
575
+ // Mirrors emit in go/cmd/aontu/main.go.
576
+ function emitEval(res, format) {
577
+ if ('json' === format) {
578
+ process.stdout.write((0, aontu_1.exactJSON)({
579
+ aontu: { version: version(), verb: 'eval' },
580
+ findings: res.findings,
581
+ ok: res.ok,
582
+ out: res.ok ? res.text : '',
583
+ }, 2) + '\n');
584
+ }
585
+ else {
586
+ ;
587
+ (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
588
+ }
589
+ return res.ok ? 0 : 1;
590
+ }
404
591
  // The one-line warning of the staged default flip. Once per (kind,
405
592
  // path): a fixpoint re-resolves nothing (includes load at parse), but
406
593
  // several includes may escape and each deserves exactly one line.
@@ -534,12 +721,32 @@ function verbOpts(trust, entryRoot) {
534
721
  function entryRootOf(file) {
535
722
  return null == file ? process.cwd() : (0, node_path_1.dirname)((0, node_path_1.resolve)(file));
536
723
  }
537
- function runFile(file, mode, trust) {
724
+ function runFile(file, mode, format, trust) {
538
725
  let src;
539
726
  try {
540
727
  src = (0, node_fs_1.readFileSync)(file, 'utf8');
541
728
  }
542
729
  catch (err) {
730
+ // A MISTYPED VERB READS AS A FILE NAME, and until G11 phase 2 that
731
+ // was only said when there were TWO of them. The one-argument case
732
+ // is the one an agent actually produces -- `aontu help`, `aontu
733
+ // init`, `aontu ontology` -- and it answered `cannot read help:
734
+ // ...`, which describes the symptom and hides the cause.
735
+ //
736
+ // The test is SHAPE, not existence: a bare word (no separator, no
737
+ // extension) that cannot be read was meant as a verb, while
738
+ // `./help`, `help.aon` and `/tmp/help` were meant as paths and keep
739
+ // the file diagnosis and its exit 1. That is the same escape hatch
740
+ // the subcommand dispatch documents. Mirrors go/cmd/aontu/main.go.
741
+ if (looksLikeVerb(file)) {
742
+ process.stderr.write(`aontu: \`${file}\` is not a file, and not a verb this port knows\n`);
743
+ const near = nearestVerb(file, KNOWN_VERBS);
744
+ if ('' !== near) {
745
+ process.stderr.write(`aontu: did you mean \`aontu ${near}\`?\n`);
746
+ }
747
+ process.stderr.write('aontu: `aontu --help` lists the verbs, `aontu help` the topics\n');
748
+ return 2;
749
+ }
543
750
  process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`);
544
751
  return 1;
545
752
  }
@@ -555,19 +762,16 @@ function runFile(file, mode, trust) {
555
762
  errfs: { existsSync: node_fs_1.existsSync, readFileSync: node_fs_1.readFileSync },
556
763
  ...trustOpts(trust, (0, node_path_1.dirname)(path)),
557
764
  });
558
- const res = evalSource(aontu, src, mode);
559
- (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
560
- return res.ok ? 0 : 1;
765
+ return emitEval(evalSource(aontu, src, mode), format);
561
766
  }
562
- function runStdin(mode, trust) {
767
+ function runStdin(mode, format, trust) {
563
768
  return new Promise((resolve) => {
564
769
  let src = '';
565
770
  process.stdin.setEncoding('utf8');
566
771
  process.stdin.on('data', (d) => (src += d));
567
772
  process.stdin.on('end', () => {
568
773
  const res = evalSource(new aontu_1.Aontu(trustOpts(trust, process.cwd())), src, mode);
569
- (res.ok ? process.stdout : process.stderr).write(res.text + '\n');
570
- resolve(res.ok ? 0 : 1);
774
+ resolve(emitEval(res, format));
571
775
  });
572
776
  });
573
777
  }
@@ -731,6 +935,9 @@ function parseVetArgs(argv) {
731
935
  let partial = false;
732
936
  let maxErrors;
733
937
  let watch = false;
938
+ let coverage = false;
939
+ let strictCoverage = false;
940
+ let coverageAt;
734
941
  for (let i = 0; i < argv.length; i++) {
735
942
  const arg = argv[i];
736
943
  // `-h`/`--help` before anything else, INCLUDING the file count:
@@ -774,6 +981,24 @@ function parseVetArgs(argv) {
774
981
  else if ('--partial' === arg) {
775
982
  partial = true;
776
983
  }
984
+ else if ('--coverage' === arg) {
985
+ coverage = true;
986
+ }
987
+ else if ('--strict-coverage' === arg) {
988
+ // IMPLIES THE ACCOUNTING, because a gate cannot fire on what was
989
+ // never measured. Asking for the strict form and having to
990
+ // remember `--coverage` beside it is a usage trap with one
991
+ // correct answer, so the flag takes it.
992
+ coverage = true;
993
+ strictCoverage = true;
994
+ }
995
+ else if ('--coverage-at' === arg) {
996
+ coverageAt = argv[++i];
997
+ if (null == coverageAt) {
998
+ return { err: 'aontu: --coverage-at needs a path' };
999
+ }
1000
+ coverage = true;
1001
+ }
777
1002
  else if ('--watch' === arg) {
778
1003
  watch = true;
779
1004
  }
@@ -797,6 +1022,9 @@ function parseVetArgs(argv) {
797
1022
  partial,
798
1023
  maxErrors,
799
1024
  watch,
1025
+ coverage,
1026
+ strictCoverage,
1027
+ coverageAt,
800
1028
  },
801
1029
  };
802
1030
  }
@@ -829,11 +1057,46 @@ function renderFinding(f) {
829
1057
  function renderVetText(report) {
830
1058
  const head = `verdict: ${report.verdict}` +
831
1059
  (report.truncated ? ' (findings truncated)' : '');
832
- if (0 === report.findings.length) {
833
- return head;
1060
+ const body = 0 === report.findings.length ? []
1061
+ : ['', ...report.findings.map(renderFinding)];
1062
+ const cover = null == report.coverage ? []
1063
+ : ['', ...renderVetCoverage(report.coverage)];
1064
+ return [head, ...body, ...cover].join('\n');
1065
+ }
1066
+ // The coverage block (G11 phase 5). VACUOUS FIRST and in the
1067
+ // imperative, because it is the one line that changes what the reader
1068
+ // should do: a `valid` verdict above it means nothing.
1069
+ function renderVetCoverage(c) {
1070
+ const out = [];
1071
+ if (c.vacuous) {
1072
+ out.push('coverage: VACUOUS — no data leaf was constrained' +
1073
+ ' by the schema; this run checked nothing');
834
1074
  }
835
- return [head, ''].concat(report.findings.map(renderFinding)).join('\n');
1075
+ out.push(`coverage: ${c.checked}/${c.leaves} data leaves checked,` +
1076
+ ` ${c.declared} schema declarations`);
1077
+ // The lists are the SHALLOWEST paths, so each names a subtree rather
1078
+ // than every leaf under it, and both are capped: a report a reader
1079
+ // scrolls past is a report nobody reads.
1080
+ for (const [label, paths] of [
1081
+ ['unchecked', c.unchecked], ['unused', c.unused],
1082
+ ]) {
1083
+ if (0 === paths.length) {
1084
+ continue;
1085
+ }
1086
+ const shown = paths.slice(0, COVERAGE_LIST_MAX);
1087
+ for (const p of shown) {
1088
+ out.push(` ${label}: ${p}`);
1089
+ }
1090
+ if (shown.length < paths.length) {
1091
+ out.push(` ${label}: … and ${paths.length - shown.length} more`);
1092
+ }
1093
+ }
1094
+ return out;
836
1095
  }
1096
+ // How many coverage paths the TEXT form prints per list. The JSON form
1097
+ // carries every one: a machine reads the whole list, a person reads the
1098
+ // first few and the count.
1099
+ const COVERAGE_LIST_MAX = 10;
837
1100
  // The machine-readable form. `aontu` names the producer, so a report
838
1101
  // read from a file or a pipe says which version and which verb made it
839
1102
  // without the consumer having to know.
@@ -843,6 +1106,7 @@ function renderVetJson(report) {
843
1106
  verdict: report.verdict,
844
1107
  truncated: report.truncated,
845
1108
  findings: report.findings,
1109
+ ...(null == report.coverage ? {} : { coverage: report.coverage }),
846
1110
  }, 2);
847
1111
  }
848
1112
  // The machine-interchange form (G2 phase 5): SARIF 2.1.0, rendered by
@@ -883,6 +1147,20 @@ function vetOnce(args, trust) {
883
1147
  let verdict = 'valid';
884
1148
  let truncated = false;
885
1149
  const findings = [];
1150
+ // COVERAGE ACROSS SEVERAL DATA FILES (G11 phase 5). Two data files
1151
+ // are two candidates for one truth, so the schema side is the SAME
1152
+ // for each: `declared` is taken once, and a declaration is unused
1153
+ // only when NO file met it -- the intersection, because a
1154
+ // declaration one file exercised is exercised. The data side adds
1155
+ // up: leaves and checked leaves sum, and `unchecked` is the union.
1156
+ let cov;
1157
+ // Initialised rather than left undefined: it is filled in the same
1158
+ // block that sets `cov`, so a fallback at the read below would be an
1159
+ // arm nothing can take. The FIRST file replaces it wholesale, which
1160
+ // is what makes the fold an intersection rather than an empty set.
1161
+ let unusedEvery = new Set();
1162
+ let unusedSeen = false;
1163
+ const uncheckedAll = new Set();
886
1164
  for (const source of sources) {
887
1165
  const report = (0, aontu_1.vet)(schemaSrc, source.src, {
888
1166
  ...verbOpts(trust, entryRootOf(args.schema)),
@@ -900,12 +1178,32 @@ function vetOnce(args, trust) {
900
1178
  // absolute one would name the same file two ways.
901
1179
  schemaPath: args.schema,
902
1180
  dataPath: source.file,
1181
+ coverage: args.coverage,
1182
+ coverageAt: args.coverageAt,
903
1183
  });
904
1184
  if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
905
1185
  verdict = report.verdict;
906
1186
  }
907
1187
  truncated = truncated || report.truncated;
908
1188
  findings.push(...report.findings);
1189
+ if (null != report.coverage) {
1190
+ const c = report.coverage;
1191
+ cov = null == cov ? { ...c } : {
1192
+ checked: cov.checked + c.checked,
1193
+ declared: c.declared,
1194
+ leaves: cov.leaves + c.leaves,
1195
+ unchecked: [],
1196
+ unused: [],
1197
+ vacuous: false,
1198
+ };
1199
+ for (const p of c.unchecked) {
1200
+ uncheckedAll.add(p);
1201
+ }
1202
+ const mine = new Set(c.unused);
1203
+ unusedEvery = unusedSeen
1204
+ ? new Set([...unusedEvery].filter((u) => mine.has(u))) : mine;
1205
+ unusedSeen = true;
1206
+ }
909
1207
  // A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
910
1208
  // is reported ONCE. `error` means exactly that -- the run could not
911
1209
  // be set up from the truth's side, never the data's (the exit table
@@ -928,15 +1226,34 @@ function vetOnce(args, trust) {
928
1226
  // gets here; this is the second, honest cut.
929
1227
  const cap = args.maxErrors ?? vet_1.VET_MAX_ERRORS;
930
1228
  const kept = cap < findings.length ? findings.slice(0, cap) : findings;
1229
+ if (null != cov) {
1230
+ cov.unchecked = [...uncheckedAll].sort(keyorder_1.cmpCodePoint);
1231
+ cov.unused = [...unusedEvery].sort(keyorder_1.cmpCodePoint);
1232
+ cov.vacuous = 0 === cov.checked && 0 < cov.leaves;
1233
+ }
931
1234
  const report = {
932
1235
  verdict,
933
1236
  truncated: truncated || cap < findings.length,
934
1237
  findings: kept,
1238
+ ...(null == cov ? {} : { coverage: cov }),
935
1239
  };
936
1240
  const text = 'json' === args.format ? renderVetJson(report) :
937
1241
  'sarif' === args.format ? renderVetSarif(report) :
938
1242
  renderVetText(report);
939
1243
  process.stdout.write(text + '\n');
1244
+ // A VACUOUS CHECK IS A FAILED GATE UNDER `--strict-coverage`, and
1245
+ // only under it: the verdict WORD is unchanged, so nothing that
1246
+ // passes today starts failing, and a caller who wants the stronger
1247
+ // gate asks for it. The reason goes to stderr, because stdout is a
1248
+ // report contract -- a JSON consumer reads `coverage.vacuous` and a
1249
+ // person reads this.
1250
+ if (true === args.strictCoverage && true === report.coverage?.vacuous) {
1251
+ process.stderr.write('aontu: no data leaf was constrained by the schema:' +
1252
+ ' this run checked nothing\n' +
1253
+ 'aontu: `aontu help language` — a map template is `&:`,' +
1254
+ ' and a quoted "*" is a key named *\n');
1255
+ return 1;
1256
+ }
940
1257
  return VET_EXIT[verdict];
941
1258
  }
942
1259
  // How often `--watch` polls for a change. Polling by mtime+size rather
@@ -1869,6 +2186,29 @@ function modText(sub, report) {
1869
2186
  }
1870
2187
  return lines.join('\n');
1871
2188
  }
2189
+ // VACUITY SIGNALS (G11 phase 4,
2190
+ // docs/capability-review/g11-agent-onramp.md).
2191
+ //
2192
+ // The same principle phase 5 applied to `vet`: a verb that did NOTHING
2193
+ // and a verb that did its job answer the same. `aontu view tree` over a
2194
+ // document declaring no relations printed one newline and exited 0;
2195
+ // `aontu render` with no profile printed nothing and exited 0; `aontu
2196
+ // relations` over a document declaring none answered `verdict: pass`.
2197
+ // For a person at a terminal that is a shrug. For an unattended agent
2198
+ // it is a green check mark on an empty box.
2199
+ //
2200
+ // ON STDERR, ALWAYS. stdout is a report contract -- a `--format json`
2201
+ // consumer parses it -- and the exit code is a verdict class that
2202
+ // callers already branch on. Neither changes here: what changes is
2203
+ // that the caller is TOLD. A caller who wants it to be fatal has
2204
+ // `vet --strict-coverage`, and the same argument would give the other
2205
+ // verbs a flag of their own if one is ever asked for.
2206
+ //
2207
+ // The repository already ruled this for one verb, in G8 phase 6 on
2208
+ // `trim`: "doing something else silently is worse than refusing".
2209
+ function vacuous(what, why) {
2210
+ process.stderr.write(`aontu: ${what}: ${why}\n`);
2211
+ }
1872
2212
  function runRelations(argv) {
1873
2213
  const trusted = takeTrust(argv);
1874
2214
  if (null == trusted) {
@@ -1913,12 +2253,20 @@ function runRelations(argv) {
1913
2253
  return 2;
1914
2254
  }
1915
2255
  const report = (0, aontu_1.relationCheck)(src, {
1916
- path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2256
+ path: files[0], count: true,
2257
+ ...verbOpts(trust, entryRootOf(files[0])),
1917
2258
  });
1918
2259
  const text = 'json' === format
1919
2260
  ? renderRelationsJson(report)
1920
2261
  : renderRelationsText(report);
1921
2262
  process.stdout.write(text + '\n');
2263
+ // `pass` over NO declarations is the vacuous case, and the engine
2264
+ // knows it exactly: `_reldecls` is empty. The count is asked for
2265
+ // here rather than derived, so the answer costs no second
2266
+ // evaluation.
2267
+ if (0 === report.declared) {
2268
+ vacuous('this document declares no relations', '`pass` means nothing was checked, not that the graph is sound');
2269
+ }
1922
2270
  return RELATIONS_EXIT[report.verdict];
1923
2271
  }
1924
2272
  function runReaches(argv) {
@@ -2195,7 +2543,7 @@ function runView(argv) {
2195
2543
  return 2;
2196
2544
  }
2197
2545
  }
2198
- const report = (0, view_1.view)(srcs[0], {
2546
+ const viewOpts = {
2199
2547
  ...opts,
2200
2548
  style: viewStyleOf(style, opts.as ?? (0, view_1.viewDefaultProfile)(kind)),
2201
2549
  kind,
@@ -2203,7 +2551,26 @@ function runView(argv) {
2203
2551
  roots,
2204
2552
  ...verbOpts(trust, entryRootOf(files[0])),
2205
2553
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2206
- });
2554
+ };
2555
+ const report = (0, view_1.view)(srcs[0], viewOpts);
2556
+ // AN EMPTY FIGURE IS THE SAME BYTES AS A DRAWN ONE MINUS ITS
2557
+ // CONTENT, and every profile spells "empty" differently: text draws
2558
+ // nothing at all, mermaid still draws its `flowchart LR` header, the
2559
+ // matrix still prints its count line. Rather than teach this one
2560
+ // place each of those spellings -- a list that goes stale the first
2561
+ // time a profile gains a header -- ASK THE SAME KIND TO DRAW AN
2562
+ // EMPTY DOCUMENT and compare. Equal texts mean this document
2563
+ // contributed nothing to the figure, whatever the profile.
2564
+ //
2565
+ // It costs one drawing of `{}`, which is the cheapest document
2566
+ // there is, and only on a run that produced a figure at all.
2567
+ if ('error' !== report.verdict && null != report.text) {
2568
+ const bare = (0, view_1.view)('{}', viewOpts);
2569
+ if ('error' !== bare.verdict && bare.text === report.text) {
2570
+ vacuous('nothing to draw', 'this figure is what the same view draws for an empty document' +
2571
+ ' — the model declares nothing this kind can show');
2572
+ }
2573
+ }
2207
2574
  if ('json' === format) {
2208
2575
  process.stdout.write(renderViewJson(report) + '\n');
2209
2576
  }
@@ -2669,6 +3036,11 @@ function runRender(argv) {
2669
3036
  langs.set(profile.lang, pf);
2670
3037
  profiles.push(profile);
2671
3038
  }
3039
+ // A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
3040
+ // bytes and exit 0. The profile is what maps a model onto a
3041
+ // language, so without one there is nothing for the renderer to
3042
+ // write -- which is a usable answer only if the caller is told.
3043
+ const noProfiles = 0 === profiles.length;
2672
3044
  const report = (0, aontu_1.render)(src, {
2673
3045
  at, unit, strict, profiles, path: files[0],
2674
3046
  coverage, coverageAt,
@@ -2678,6 +3050,13 @@ function runRender(argv) {
2678
3050
  trace: 'json' === format,
2679
3051
  ...verbOpts(trust, entryRootOf(files[0])),
2680
3052
  });
3053
+ // Said once, whatever the format: stdout stays the report.
3054
+ if ('error' !== report.verdict && 0 === report.units.length) {
3055
+ vacuous('nothing was rendered', noProfiles
3056
+ ? 'no profile was given, and the document declares none' +
3057
+ ' (see aontu help tasks)'
3058
+ : 'the document produced no units under this profile');
3059
+ }
2681
3060
  if ('json' === format) {
2682
3061
  process.stdout.write((0, aontu_1.exactJSON)({
2683
3062
  aontu: { version: version(), verb: 'render' },
@@ -3320,6 +3699,164 @@ function runSet(argv) {
3320
3699
  return VET_EXIT[report.verdict];
3321
3700
  }
3322
3701
  // ---------------------------------------------------------------------
3702
+ // The role gate (docs/design/ALLOW.0.md): may the role the caller is
3703
+ // operating under modify these subtrees? Asked before `set`, by an
3704
+ // agent whose skill names its role, and answered from a role model
3705
+ // that is itself an aontu document. The verdict is the exit code, as
3706
+ // it is for every gate here: 0 is yes, 1 is no, 4 is "the model that
3707
+ // was to decide does not stand up", and an agent branches on nothing
3708
+ // else.
3709
+ const ALLOW_HELP = 'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)';
3710
+ const ALLOW_EXIT = {
3711
+ allowed: 0,
3712
+ refused: 1,
3713
+ error: 4,
3714
+ };
3715
+ // One line per asked path: the answer, and the entry that gave it, as
3716
+ // a path into the role model so `aontu why` can locate the rule.
3717
+ function renderAllowDecision(d, role) {
3718
+ const head = `${d.path}: ${d.allowed ? 'allowed' : 'refused'}`;
3719
+ switch (d.reason) {
3720
+ case 'allow':
3721
+ case 'deny':
3722
+ return `${head} by ${d.by} (${d.pattern})`;
3723
+ case 'uncovered':
3724
+ return `${head} (no allow entry of ${role} covers it)`;
3725
+ default:
3726
+ return `${head} (role ${role} is not declared)`;
3727
+ }
3728
+ }
3729
+ function renderAllowText(report) {
3730
+ const lines = [`verdict: ${report.verdict}`, `role: ${report.role}`]
3731
+ .concat(report.paths.map((d) => renderAllowDecision(d, report.role)));
3732
+ if (0 === report.findings.length) {
3733
+ return lines.join('\n');
3734
+ }
3735
+ return lines.concat('', report.findings.map(renderFinding)).join('\n');
3736
+ }
3737
+ // Does the text after `=` parse as exactly one value? Parsed, never
3738
+ // evaluated, with loads denied: the question is the shape of the
3739
+ // argument, and reading a file to answer it would be the write the
3740
+ // gate exists to precede.
3741
+ function oneValue(value) {
3742
+ try {
3743
+ const probe = new aontu_1.Aontu({ trust: { include: 'none' } })
3744
+ .parse('v: ' + value);
3745
+ return 1 === Object.keys(probe.peg).length;
3746
+ }
3747
+ catch {
3748
+ return false;
3749
+ }
3750
+ }
3751
+ function runAllow(argv) {
3752
+ const trusted = takeTrust(argv);
3753
+ if (null == trusted) {
3754
+ return 2;
3755
+ }
3756
+ argv = trusted.argv;
3757
+ const trust = trusted.trust;
3758
+ const rest = [];
3759
+ let role;
3760
+ let at;
3761
+ let format = 'text';
3762
+ for (let i = 0; i < argv.length; i++) {
3763
+ const arg = argv[i];
3764
+ if ('-h' === arg || '--help' === arg) {
3765
+ process.stdout.write(HELP);
3766
+ return 0;
3767
+ }
3768
+ if ('--role' === arg) {
3769
+ role = argv[++i];
3770
+ if (null == role) {
3771
+ process.stderr.write('aontu: --role needs a role name\n');
3772
+ return 2;
3773
+ }
3774
+ }
3775
+ else if ('--at' === arg) {
3776
+ at = argv[++i];
3777
+ if (null == at) {
3778
+ process.stderr.write('aontu: --at needs a path\n');
3779
+ return 2;
3780
+ }
3781
+ }
3782
+ else if ('--format' === arg) {
3783
+ const f = argv[++i];
3784
+ if ('text' !== f && 'json' !== f) {
3785
+ process.stderr.write('aontu: --format needs text or json\n');
3786
+ return 2;
3787
+ }
3788
+ format = f;
3789
+ }
3790
+ else if (arg.startsWith('-')) {
3791
+ process.stderr.write(`aontu: unknown allow option ${arg} (try --help)\n`);
3792
+ return 2;
3793
+ }
3794
+ else {
3795
+ rest.push(arg);
3796
+ }
3797
+ }
3798
+ if (null == role || rest.length < 2) {
3799
+ process.stderr.write(`aontu: allow needs --role, a role model and at least one path\n` +
3800
+ `${ALLOW_HELP}\n`);
3801
+ return 2;
3802
+ }
3803
+ const [file, ...asked] = rest;
3804
+ // A role is ONE KEY of the roles map. A dotted name would be read as
3805
+ // a path by `why` when it follows the entry the report names, and an
3806
+ // empty one names the map itself.
3807
+ if ('' === role || role.includes('.')) {
3808
+ process.stderr.write('aontu: --role needs one key, without dots\n');
3809
+ return 2;
3810
+ }
3811
+ // A path may arrive in `set`'s spelling, `$.a.b=1`, so a skill can
3812
+ // hand the gate the very arguments the write will get. The text up
3813
+ // to the first `=` is the path, and it starts with `$`: an empty
3814
+ // argument, or a second file name, would otherwise read as a path
3815
+ // and be answered. The VALUE is checked to be one value. `set`
3816
+ // appends it as source after the flattened path, so a value carrying
3817
+ // a second pair -- `3 secrets: key: "x"` -- writes a sibling of the
3818
+ // overlay root, a subtree the gate was never asked about.
3819
+ const paths = [];
3820
+ for (const arg of asked) {
3821
+ const eq = arg.indexOf('=');
3822
+ const path = eq < 0 ? arg : arg.slice(0, eq);
3823
+ if (!path.startsWith('$')) {
3824
+ process.stderr.write(`aontu: a path starts with $ (got ${JSON.stringify(arg)})\n`);
3825
+ return 2;
3826
+ }
3827
+ if (0 <= eq && !oneValue(arg.slice(eq + 1))) {
3828
+ process.stderr.write(`aontu: the value of ${path} is not one value\n`);
3829
+ return 2;
3830
+ }
3831
+ paths.push(path);
3832
+ }
3833
+ let src;
3834
+ try {
3835
+ src = (0, node_fs_1.readFileSync)(file, 'utf8');
3836
+ }
3837
+ catch (err) {
3838
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
3839
+ return 2;
3840
+ }
3841
+ const report = (0, aontu_1.allow)(src, role, paths, {
3842
+ at, path: file, ...verbOpts(trust, entryRootOf(file)),
3843
+ });
3844
+ const text = 'json' === format ?
3845
+ (0, aontu_1.exactJSON)({
3846
+ aontu: { version: version(), verb: 'allow' },
3847
+ findings: report.findings,
3848
+ paths: report.paths,
3849
+ role: report.role,
3850
+ verdict: report.verdict,
3851
+ }, 2) :
3852
+ renderAllowText(report);
3853
+ // The report IS the answer, refused or not, so it goes to stdout as
3854
+ // vet's does; the exit code carries the verdict for a caller that
3855
+ // reads nothing else.
3856
+ process.stdout.write(text + '\n');
3857
+ return ALLOW_EXIT[report.verdict];
3858
+ }
3859
+ // ---------------------------------------------------------------------
3323
3860
  // The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
3324
3861
  // derived from the definition, so it cannot drift from the formal
3325
3862
  // source it points at.
@@ -3333,6 +3870,10 @@ function runAgentsMd(argv) {
3333
3870
  const trust = trusted.trust;
3334
3871
  const files = [];
3335
3872
  let write;
3873
+ // The SHAPE's depth (G11 phase 7). Default 2, unchanged: the stanza
3874
+ // is spliced into a file people read, and a deeper shape is a
3875
+ // question the caller asks rather than one it is handed.
3876
+ let depth = 2;
3336
3877
  for (let i = 0; i < argv.length; i++) {
3337
3878
  const arg = argv[i];
3338
3879
  if ('-h' === arg || '--help' === arg) {
@@ -3346,6 +3887,14 @@ function runAgentsMd(argv) {
3346
3887
  return 2;
3347
3888
  }
3348
3889
  }
3890
+ else if ('--depth' === arg) {
3891
+ const n = Number(argv[++i]);
3892
+ if (!Number.isInteger(n) || n < 1) {
3893
+ process.stderr.write('aontu: --depth needs a positive integer\n');
3894
+ return 2;
3895
+ }
3896
+ depth = n;
3897
+ }
3349
3898
  else if (arg.startsWith('-')) {
3350
3899
  process.stderr.write(`aontu: unknown agentsmd option ${arg} (try --help)\n`);
3351
3900
  return 2;
@@ -3367,7 +3916,7 @@ function runAgentsMd(argv) {
3367
3916
  return 2;
3368
3917
  }
3369
3918
  const report = (0, aontu_1.agentsMd)(src, {
3370
- name: files[0], path: files[0],
3919
+ depth, name: files[0], path: files[0],
3371
3920
  ...verbOpts(trust, entryRootOf(files[0])),
3372
3921
  });
3373
3922
  if (!report.ok) {
@@ -3419,13 +3968,15 @@ function runAgentsMd(argv) {
3419
3968
  // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
3420
3969
  // the form itself is the library's (ts/src/format.ts), and the two
3421
3970
  // ports agree on it row by row in test/spec/fmt.tsv.
3422
- const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] <file>... (try --help)';
3971
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>... (try --help)';
3423
3972
  function runFmt(argv) {
3424
3973
  const files = [];
3974
+ let marker = undefined;
3425
3975
  const flags = {
3426
3976
  write: false, list: false, check: false, diff: false, lint: false, strict: false,
3427
3977
  };
3428
- for (const arg of argv) {
3978
+ for (let i = 0; i < argv.length; i++) {
3979
+ const arg = argv[i];
3429
3980
  if ('-h' === arg || '--help' === arg) {
3430
3981
  process.stdout.write(HELP);
3431
3982
  return 0;
@@ -3449,6 +4000,16 @@ function runFmt(argv) {
3449
4000
  flags.lint = true;
3450
4001
  flags.strict = true;
3451
4002
  }
4003
+ else if ('--marker' === arg) {
4004
+ // THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
4005
+ // extension: `render` and `template` take the same option for
4006
+ // the same reason, a language the table has never seen.
4007
+ marker = argv[++i];
4008
+ if (null == marker) {
4009
+ process.stderr.write('aontu: --marker needs a token\n');
4010
+ return 2;
4011
+ }
4012
+ }
3452
4013
  else if (arg.startsWith('-')) {
3453
4014
  process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`);
3454
4015
  return 2;
@@ -3469,7 +4030,7 @@ function runFmt(argv) {
3469
4030
  let src = '';
3470
4031
  process.stdin.setEncoding('utf8');
3471
4032
  process.stdin.on('data', (d) => (src += d));
3472
- process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)));
4033
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags, marker)));
3473
4034
  });
3474
4035
  }
3475
4036
  // Several files onto standard output would be one stream nobody can
@@ -3482,22 +4043,6 @@ function runFmt(argv) {
3482
4043
  }
3483
4044
  let worst = 0;
3484
4045
  for (const file of files) {
3485
- // FMT FORMATS AONTU SOURCE, AND THE EXTENSION SAYS WHAT A FILE IS
3486
- // (ADR-012's rule, and the one `render` reads a template by).
3487
- // A TEMPLATE FILE IS NOT AONTU (docs/design/TEMPLATE.0.md; P8):
3488
- // its marker lines are fragments of a document and its other lines
3489
- // are the target's, so there is nothing here to format that would
3490
- // not also rewrite the output. Refused rather than attempted, and
3491
- // refused BY NAME rather than by a parse failure, because a `#-`
3492
- // template parses: `#` opens a comment, so every marker line
3493
- // vanishes and what is left is read as a document that was never
3494
- // written. The verb answered `0` over one, having understood none
3495
- // of it.
3496
- if (!/[.](aon|aontu)$/.test(file)) {
3497
- process.stderr.write(`aontu: ${file} is not aontu source (.aon, .aontu); a generator ` +
3498
- 'written in the target\'s own syntax is aontu template\'s\n');
3499
- return 2;
3500
- }
3501
4046
  let src;
3502
4047
  try {
3503
4048
  src = (0, node_fs_1.readFileSync)(file, 'utf8');
@@ -3506,10 +4051,42 @@ function runFmt(argv) {
3506
4051
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
3507
4052
  return 2;
3508
4053
  }
3509
- worst = Math.max(worst, fmtOne(file, src, flags));
4054
+ const mark = fmtMarker(file, src, marker);
4055
+ if (false === mark) {
4056
+ process.stderr.write(`aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
4057
+ `${(0, template_1.markerFor)(file)} marker line, so there is no aontu in it to ` +
4058
+ 'format; --marker names the marker for a language the table does ' +
4059
+ 'not know\n');
4060
+ return 2;
4061
+ }
4062
+ worst = Math.max(worst, fmtOne(file, src, flags, mark));
3510
4063
  }
3511
4064
  return worst;
3512
4065
  }
4066
+ // WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
4067
+ // `render` reads an entry by): `.aon` and `.aontu` are aontu source,
4068
+ // and anything else is a GENERATOR written in the target's own syntax
4069
+ // (docs/design/TEMPLATE.0.md), whose marker lines carry the document
4070
+ // this formats and whose other lines are output. `undefined` is aontu,
4071
+ // a string is the generator's marker, and `false` is neither.
4072
+ //
4073
+ // A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
4074
+ // FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
4075
+ // include is another language's file, and reading one as a generator
4076
+ // would answer it back unchanged having understood none of it. The
4077
+ // marker is the evidence that a file was written to carry aontu at
4078
+ // all. `--marker` says so outright, and then the file is a generator
4079
+ // whatever it is called.
4080
+ function fmtMarker(file, src, marker) {
4081
+ if (undefined !== marker) {
4082
+ return marker;
4083
+ }
4084
+ if (/[.](aon|aontu)$/.test(file)) {
4085
+ return undefined;
4086
+ }
4087
+ const mark = (0, template_1.markerFor)(file);
4088
+ return (0, template_1.templateOutputs)(src, mark).some((out) => !out) ? mark : false;
4089
+ }
3513
4090
  // An option that says what to do with a file, in place of printing
3514
4091
  // it: what to do when its form would change, or the lint.
3515
4092
  function fmtQuiet(flags) {
@@ -3520,8 +4097,8 @@ function fmtQuiet(flags) {
3520
4097
  // document that does not format, with the finding that says why. The
3521
4098
  // style findings go to standard error, one line each, in the shape
3522
4099
  // every linter prints: `file:line:col: rule: message`.
3523
- function fmtOne(name, src, flags) {
3524
- const report = (0, format_1.format)(src, { path: name, lint: flags.lint });
4100
+ function fmtOne(name, src, flags, marker) {
4101
+ const report = (0, format_1.format)(src, { path: name, lint: flags.lint, template: marker });
3525
4102
  if ('error' === report.verdict) {
3526
4103
  process.stderr.write(`aontu: ${name} was not formatted\n` +
3527
4104
  report.errors.map(renderFinding).join('\n') + '\n');
@@ -3599,6 +4176,356 @@ function parseTrustArg(value) {
3599
4176
  }
3600
4177
  return undefined;
3601
4178
  }
4179
+ // THE TEACHING PACK, SERVED FROM THE COMMAND (G11 phase 1,
4180
+ // docs/capability-review/g11-agent-onramp.md; mirrors
4181
+ // go/cmd/aontu/help.go).
4182
+ //
4183
+ // HELP documents the TOOLCHAIN and says nothing about the LANGUAGE:
4184
+ // `&`, the map template and the one construct an ontology cannot be
4185
+ // written without, occurs zero times in it, while `template` occurs
4186
+ // fourteen times and names an unrelated verb every time. docs/skill/
4187
+ // was already the right content and already gated; the gap was
4188
+ // DELIVERY, since it reached an installation as
4189
+ // node_modules/aontu/skill/ where nothing looks. ts/src/helpdoc.ts is
4190
+ // generated from those sources by ts/scripts/helpdoc.cjs and asserted
4191
+ // byte-identical with them by ts/test/helpdoc.test.ts.
4192
+ const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)';
4193
+ const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)';
4194
+ function helpIndexText(index) {
4195
+ const width = index.reduce((w, t) => Math.max(w, t.topic.length), 0);
4196
+ return 'aontu help <topic> — the language, offline.\n\n' +
4197
+ index.map((t) => ' ' + t.topic.padEnd(width) + ' ' + t.summary).join('\n') +
4198
+ '\n\n' +
4199
+ '`aontu --help` documents the verbs, their flags and their exit\n' +
4200
+ 'codes. `aontu explain <code>` explains one error code.\n' +
4201
+ 'Start at `aontu help tasks` if you know the job but not the verb.';
4202
+ }
4203
+ function runHelp(argv) {
4204
+ let format = 'text';
4205
+ const topics = [];
4206
+ for (let i = 0; i < argv.length; i++) {
4207
+ const arg = argv[i];
4208
+ if ('-h' === arg || '--help' === arg) {
4209
+ process.stdout.write(HELP);
4210
+ return 0;
4211
+ }
4212
+ else if ('--format' === arg) {
4213
+ const f = argv[++i];
4214
+ if ('text' !== f && 'json' !== f) {
4215
+ process.stderr.write('aontu: --format needs text or json\n');
4216
+ return 2;
4217
+ }
4218
+ format = f;
4219
+ }
4220
+ else if (arg.startsWith('-')) {
4221
+ process.stderr.write(`aontu: unknown help option ${arg} (try --help)\n`);
4222
+ return 2;
4223
+ }
4224
+ else {
4225
+ topics.push(arg);
4226
+ }
4227
+ }
4228
+ if (1 < topics.length) {
4229
+ process.stderr.write(`aontu: help takes one topic\n${HELP_VERB_HELP}\n`);
4230
+ return 2;
4231
+ }
4232
+ if (0 === topics.length) {
4233
+ process.stdout.write(('json' === format
4234
+ ? (0, aontu_1.exactJSON)({
4235
+ aontu: { version: version(), verb: 'help' },
4236
+ topics: helpdoc_1.HELPDOC.map((t) => ({ topic: t.topic, summary: t.summary, source: t.source })),
4237
+ }, 2)
4238
+ : helpIndexText(helpdoc_1.HELPDOC)) + '\n');
4239
+ return 0;
4240
+ }
4241
+ const found = helpdoc_1.HELPDOC.find((t) => topics[0] === t.topic);
4242
+ if (null != found) {
4243
+ if ('json' === format) {
4244
+ process.stdout.write((0, aontu_1.exactJSON)({
4245
+ aontu: { version: version(), verb: 'help' },
4246
+ topic: found.topic,
4247
+ summary: found.summary,
4248
+ source: found.source,
4249
+ text: found.text,
4250
+ }, 2) + '\n');
4251
+ return 0;
4252
+ }
4253
+ process.stdout.write(found.text);
4254
+ return 0;
4255
+ }
4256
+ // AN UNKNOWN TOPIC IS A USAGE ERROR AND NAMES THE ALTERNATIVES,
4257
+ // because the caller who typed it has no other way to find out what
4258
+ // exists -- that is the whole condition this verb was added for.
4259
+ process.stderr.write(`aontu: no help topic \`${topics[0]}\`\n` +
4260
+ `aontu: topics are ${helpdoc_1.HELPDOC.map((t) => t.topic).join(', ')}\n`);
4261
+ return 2;
4262
+ }
4263
+ // `aontu explain <code>` (G11 phase 3; mirrors
4264
+ // go/cmd/aontu/explain.go).
4265
+ //
4266
+ // THE REGISTRY IS THE LIST, NOT THE HINT TABLE. test/spec/errcodes.tsv
4267
+ // registers 157 codes and the spec suite asserts set equality between
4268
+ // the file and codeClasses IN BOTH PORTS, so listing from codeClasses
4269
+ // is listing the shared contract. The hint tables are smaller and are
4270
+ // NOT in parity -- 130 entries here against 131 in Go, the extra being
4271
+ // decimal_syntax, which this port never raises -- so listing from them
4272
+ // would make `aontu explain --list` differ between ports over a
4273
+ // difference that is not about what either port can report.
4274
+ //
4275
+ // A REGISTERED CODE WITH NO HINT ANSWERS WITH ITS CLASS AND SAYS SO.
4276
+ // Twenty-seven registered codes carry no explanation text here; before
4277
+ // this verb their absence was invisible, because a hint is only ever
4278
+ // seen beside the error that raises it.
4279
+ // The dynamic prefixes a generated code extends (`func:upper`,
4280
+ // `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
4281
+ // exported; a code that extends one is registered through its prefix
4282
+ // and carries that prefix's hint.
4283
+ const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref['];
4284
+ function explainCode(code) {
4285
+ const cls = (0, hints_1.codeClass)(code);
4286
+ let hint = hints_1.hints[code] ?? '';
4287
+ let registered = null != hints_1.codeClasses[code];
4288
+ if (!registered) {
4289
+ for (const prefix of EXPLAIN_PREFIXES) {
4290
+ if (code.startsWith(prefix)) {
4291
+ // No guard on `hint` here: every hint key is also a registry
4292
+ // key (the spec suite asserts codeClasses set-equal with
4293
+ // test/spec/errcodes.tsv, and hints is a subset of it), so a
4294
+ // code that reaches this loop is unregistered and therefore
4295
+ // has no hint of its own.
4296
+ registered = true;
4297
+ hint = hints_1.hints[prefix] ?? '';
4298
+ break;
4299
+ }
4300
+ }
4301
+ }
4302
+ return { cls, hint, registered };
4303
+ }
4304
+ // Every code in the shared registry, sorted by code point so both
4305
+ // ports list them in the same order.
4306
+ function explainCodes() {
4307
+ return Object.keys(hints_1.codeClasses).sort(keyorder_1.cmpCodePoint);
4308
+ }
4309
+ function explainListText(format) {
4310
+ const codes = explainCodes();
4311
+ if ('json' === format) {
4312
+ return (0, aontu_1.exactJSON)({
4313
+ aontu: { version: version(), verb: 'explain' },
4314
+ codes: codes.map((code) => ({
4315
+ code,
4316
+ class: (0, hints_1.codeClass)(code),
4317
+ // Whether this port carries explanation text for the code. The
4318
+ // registry is in parity; the hint tables are not, so a consumer
4319
+ // that wants only explained codes can filter rather than guess.
4320
+ explained: '' !== explainCode(code).hint,
4321
+ })),
4322
+ }, 2);
4323
+ }
4324
+ const width = codes.reduce((w, c) => Math.max(w, c.length), 0);
4325
+ return codes.map((c) => c.padEnd(width) + ' ' + (0, hints_1.codeClass)(c) +
4326
+ ('' === explainCode(c).hint ? ' (no text)' : '')).join('\n');
4327
+ }
4328
+ function runExplain(argv) {
4329
+ let format = 'text';
4330
+ let list = false;
4331
+ const codes = [];
4332
+ for (let i = 0; i < argv.length; i++) {
4333
+ const arg = argv[i];
4334
+ if ('-h' === arg || '--help' === arg) {
4335
+ process.stdout.write(HELP);
4336
+ return 0;
4337
+ }
4338
+ else if ('--list' === arg) {
4339
+ list = true;
4340
+ }
4341
+ else if ('--format' === arg) {
4342
+ const f = argv[++i];
4343
+ if ('text' !== f && 'json' !== f) {
4344
+ process.stderr.write('aontu: --format needs text or json\n');
4345
+ return 2;
4346
+ }
4347
+ format = f;
4348
+ }
4349
+ else if (arg.startsWith('-')) {
4350
+ process.stderr.write(`aontu: unknown explain option ${arg} (try --help)\n`);
4351
+ return 2;
4352
+ }
4353
+ else {
4354
+ codes.push(arg);
4355
+ }
4356
+ }
4357
+ if (list) {
4358
+ if (0 < codes.length) {
4359
+ process.stderr.write(`aontu: --list takes no code\n${EXPLAIN_HELP}\n`);
4360
+ return 2;
4361
+ }
4362
+ process.stdout.write(explainListText(format) + '\n');
4363
+ return 0;
4364
+ }
4365
+ if (1 !== codes.length) {
4366
+ process.stderr.write(`aontu: explain needs one code\n${EXPLAIN_HELP}\n`);
4367
+ return 2;
4368
+ }
4369
+ const code = codes[0];
4370
+ const { cls, hint, registered } = explainCode(code);
4371
+ if (!registered) {
4372
+ // AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
4373
+ // caller reading a code out of a report has almost certainly typed
4374
+ // it correctly, so the likely cause is a code from another tool or
4375
+ // a truncated one, and the near matches say which.
4376
+ process.stderr.write(`aontu: no such error code \`${code}\`\n`);
4377
+ const near = nearestVerb(code, explainCodes());
4378
+ if ('' !== near) {
4379
+ process.stderr.write(`aontu: did you mean \`${near}\`?\n`);
4380
+ }
4381
+ process.stderr.write('aontu: `aontu explain --list` lists every registered code\n');
4382
+ return 2;
4383
+ }
4384
+ if ('json' === format) {
4385
+ process.stdout.write((0, aontu_1.exactJSON)({
4386
+ aontu: { version: version(), verb: 'explain' },
4387
+ code,
4388
+ class: cls,
4389
+ hint,
4390
+ }, 2) + '\n');
4391
+ return 0;
4392
+ }
4393
+ // A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
4394
+ // empty block, which would read as an explanation that happened to
4395
+ // be blank.
4396
+ const body = '' === hint
4397
+ ? '(no explanation text is registered for this code)'
4398
+ : hint;
4399
+ process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`);
4400
+ return 0;
4401
+ }
4402
+ // `aontu init` (G11 phase 6,
4403
+ // docs/capability-review/g11-agent-onramp.md).
4404
+ //
4405
+ // NOT SCAFFOLDING CONVENIENCE. The agent's most expensive failure is
4406
+ // writing a FIRST document at all: the measurement that opened G11
4407
+ // found one reaching for the wildcard its neighbours use and getting
4408
+ // `verdict: valid` over data that violates it. A known-good starting
4409
+ // document turns generation into editing, which is the operation a
4410
+ // model is reliably good at.
4411
+ //
4412
+ // The trio is real, runnable and tested where it lives
4413
+ // (docs/skill/init/, run by ts/test/helpdoc.test.ts), and staged into
4414
+ // both ports by the same generator that stages the teaching pack, so
4415
+ // the two write the same bytes.
4416
+ const INIT_HELP = 'aontu init [dir] (try --help)';
4417
+ function runInit(argv) {
4418
+ const dirs = [];
4419
+ for (const arg of argv) {
4420
+ if ('-h' === arg || '--help' === arg) {
4421
+ process.stdout.write(HELP);
4422
+ return 0;
4423
+ }
4424
+ if (arg.startsWith('-')) {
4425
+ process.stderr.write(`aontu: unknown init option ${arg} (try --help)\n`);
4426
+ return 2;
4427
+ }
4428
+ dirs.push(arg);
4429
+ }
4430
+ if (1 < dirs.length) {
4431
+ process.stderr.write(`aontu: init takes one directory\n${INIT_HELP}\n`);
4432
+ return 2;
4433
+ }
4434
+ const dir = dirs[0] ?? '.';
4435
+ // REFUSES TO OVERWRITE, and checks every member BEFORE writing any of
4436
+ // them: a scaffold that wrote two files and then refused the third
4437
+ // would leave a directory in a state neither the caller nor a re-run
4438
+ // can reason about.
4439
+ const standing = helpdoc_1.INITDOC.filter((f) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, f.name)));
4440
+ if (0 < standing.length) {
4441
+ process.stderr.write(`aontu: ${dir} already holds ${standing.map((f) => f.name).join(', ')}\n` +
4442
+ 'aontu: init never overwrites; move them aside or name an' +
4443
+ ' empty directory\n');
4444
+ return 2;
4445
+ }
4446
+ try {
4447
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
4448
+ for (const f of helpdoc_1.INITDOC) {
4449
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, f.name), f.text, { mode: f.mode });
4450
+ }
4451
+ }
4452
+ catch (err) {
4453
+ process.stderr.write(`aontu: cannot write in ${dir}: ${err.message}\n`);
4454
+ return 2;
4455
+ }
4456
+ process.stdout.write(helpdoc_1.INITDOC.map((f) => (0, node_path_1.join)(dir, f.name)).join('\n') + '\n' +
4457
+ '\nA model, an instance of it, and the four questions to ask.\n' +
4458
+ 'Run the checks: sh ' + (0, node_path_1.join)(dir, 'check.sh') + '\n' +
4459
+ 'Learn the language: aontu help language\n');
4460
+ return 0;
4461
+ }
4462
+ // EVERY VERB THIS PORT DISPATCHES, for the nearest-verb suggestion
4463
+ // G11 phase 2 prints. A separate list from the if-chain in main()
4464
+ // because the chain's arms have three different shapes and cannot be
4465
+ // a table; ts/test/cli-help.test.ts keeps the two from drifting by
4466
+ // running each name and requiring it not to fall through to the bare
4467
+ // command.
4468
+ const KNOWN_VERBS = [
4469
+ 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4470
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
4471
+ 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
4472
+ 'view', 'why',
4473
+ ];
4474
+ exports.KNOWN_VERBS = KNOWN_VERBS;
4475
+ // looksLikeVerb reports whether an unreadable argument was meant as a
4476
+ // verb rather than as a path. A bare word has no separator and no
4477
+ // extension; `./help`, `help.aon`, `/tmp/help` and `sub/dir` are paths
4478
+ // and keep the file diagnosis. Mirrors go/cmd/aontu/main.go.
4479
+ function looksLikeVerb(arg) {
4480
+ return '' !== arg &&
4481
+ !/[/\\.]/.test(arg) &&
4482
+ !arg.startsWith('-');
4483
+ }
4484
+ // NEAREST-VERB SUGGESTION (G11 phase 2). Restricted
4485
+ // Damerau-Levenshtein with a cap that grows with the word and stops at
4486
+ // three: one edit is a convincing suggestion on any length, three is
4487
+ // the most that can be believed on a long one, and an UNCAPPED
4488
+ // nearest match on a three-letter typo names something unrelated with
4489
+ // confidence. Mirrors go/cmd/aontu/help.go, including the sort, so
4490
+ // the two ports suggest the same verb on a tie.
4491
+ function nearestVerb(word, verbs) {
4492
+ let best = '';
4493
+ let bestDist = Infinity;
4494
+ const limit = Math.min(3, 1 + Math.floor(word.length / 4));
4495
+ for (const v of [...verbs].sort(keyorder_1.cmpCodePoint)) {
4496
+ const d = editDistance(word.toLowerCase(), v);
4497
+ if (d < bestDist) {
4498
+ best = v;
4499
+ bestDist = d;
4500
+ }
4501
+ }
4502
+ return bestDist > limit ? '' : best;
4503
+ }
4504
+ // Levenshtein with a transposition, iterative over two rows. Mirrors
4505
+ // editDistance in go/cmd/aontu/help.go exactly.
4506
+ function editDistance(a, b) {
4507
+ const ar = [...a];
4508
+ const br = [...b];
4509
+ let prev2 = new Array(br.length + 1).fill(0);
4510
+ let prev = new Array(br.length + 1).fill(0).map((_, j) => j);
4511
+ let cur = new Array(br.length + 1).fill(0);
4512
+ for (let i = 1; i <= ar.length; i++) {
4513
+ cur[0] = i;
4514
+ for (let j = 1; j <= br.length; j++) {
4515
+ const cost = ar[i - 1] === br[j - 1] ? 0 : 1;
4516
+ let m = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
4517
+ if (1 < i && 1 < j &&
4518
+ ar[i - 1] === br[j - 2] && ar[i - 2] === br[j - 1] &&
4519
+ prev2[j - 2] + 1 < m) {
4520
+ m = prev2[j - 2] + 1;
4521
+ }
4522
+ cur[j] = m;
4523
+ }
4524
+ prev2 = [...prev];
4525
+ prev = [...cur];
4526
+ }
4527
+ return prev[br.length];
4528
+ }
3602
4529
  function main(argv, servers = SERVERS) {
3603
4530
  // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
3604
4531
  // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
@@ -3608,6 +4535,10 @@ function main(argv, servers = SERVERS) {
3608
4535
  // CLI can make this call. `undefined` means "leave it to NO_COLOR".
3609
4536
  (0, aontu_1.setColor)(true === process.stderr.isTTY ? undefined : false);
3610
4537
  let mode = 'json';
4538
+ // THE REPORT FORM (G11 phase 7), default text: every existing caller
4539
+ // reads exactly what it always read, and a caller that asks for json
4540
+ // gets the failure in the finding shape every other verb reports.
4541
+ let format = 'text';
3611
4542
  // A LIST, though the bare command evaluates exactly one document.
3612
4543
  // It used to be one variable and the last argument won, which made a
3613
4544
  // MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
@@ -3657,6 +4588,9 @@ function main(argv, servers = SERVERS) {
3657
4588
  if ('set' === argv[2]) {
3658
4589
  return finish(runSet(argv.slice(3)));
3659
4590
  }
4591
+ if ('allow' === argv[2]) {
4592
+ return finish(runAllow(argv.slice(3)));
4593
+ }
3660
4594
  if ('why' === argv[2]) {
3661
4595
  return finish(runWhy(argv.slice(3)));
3662
4596
  }
@@ -3666,6 +4600,17 @@ function main(argv, servers = SERVERS) {
3666
4600
  if ('hash' === argv[2]) {
3667
4601
  return finish(runHash(argv.slice(3)));
3668
4602
  }
4603
+ // G11 phases 1 and 3. Dispatched with the rest, so `aontu ./help`
4604
+ // still reads a file named help exactly as `aontu ./vet` does.
4605
+ if ('help' === argv[2]) {
4606
+ return finish(runHelp(argv.slice(3)));
4607
+ }
4608
+ if ('explain' === argv[2]) {
4609
+ return finish(runExplain(argv.slice(3)));
4610
+ }
4611
+ if ('init' === argv[2]) {
4612
+ return finish(runInit(argv.slice(3)));
4613
+ }
3669
4614
  if ('mod' === argv[2]) {
3670
4615
  return finish(runMod(argv.slice(3)));
3671
4616
  }
@@ -3712,6 +4657,14 @@ function main(argv, servers = SERVERS) {
3712
4657
  }
3713
4658
  trust = parsed;
3714
4659
  }
4660
+ else if ('--format' === arg) {
4661
+ const f = args[++i];
4662
+ if ('text' !== f && 'json' !== f) {
4663
+ process.stderr.write('aontu: --format needs text or json\n');
4664
+ return finish(2);
4665
+ }
4666
+ format = f;
4667
+ }
3715
4668
  else if ('--jsonl' === arg) {
3716
4669
  jsonl = true;
3717
4670
  // A JSONL answer is machine-read by definition, even when the
@@ -3766,7 +4719,7 @@ function main(argv, servers = SERVERS) {
3766
4719
  trust = { ...trust, textExt };
3767
4720
  const file = files[0];
3768
4721
  if (null != file) {
3769
- finish(runFile(file, mode, trust));
4722
+ finish(runFile(file, mode, format, trust));
3770
4723
  }
3771
4724
  // `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
3772
4725
  // a harness over a pipe, so gating it on an interactive terminal
@@ -3776,7 +4729,7 @@ function main(argv, servers = SERVERS) {
3776
4729
  runRepl(mode, jsonl, trust);
3777
4730
  }
3778
4731
  else {
3779
- runStdin(mode, trust).then((code) => finish(code));
4732
+ runStdin(mode, format, trust).then((code) => finish(code));
3780
4733
  }
3781
- } /* node:coverage ignore next 18 */
4734
+ } /* node:coverage ignore next 20 */
3782
4735
  //# sourceMappingURL=cli.js.map