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/src/cli.ts CHANGED
@@ -23,11 +23,13 @@ import {
23
23
  exactJSON, vet, subsume, trimCheck, relationCheck,
24
24
  hcanon, canonHash,
25
25
  get, why, patch, agentsMd,
26
+ allow,
26
27
  render,
27
28
  renderProfile,
28
29
  } from './aontu'
30
+ import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
29
31
  import type { RenderCoverage, RenderReport } from './render'
30
- import { desugarTemplate, resugarTemplate, markerFor } from './template'
32
+ import { desugarTemplate, resugarTemplate, templateOutputs, markerFor } from './template'
31
33
  import { outsideRoot } from './mcp'
32
34
  import { sarifReport } from './report-sarif'
33
35
  import { main as lspMain } from './lsp-server'
@@ -39,7 +41,9 @@ import type {
39
41
  } from './mod-tool'
40
42
  import { modCacheDir } from './mod'
41
43
  import { VET_MAX_ERRORS } from './vet'
42
- import type { VetReport, VetFinding, VetVerdict } from './vet'
44
+ import type {
45
+ VetReport, VetFinding, VetVerdict, VetCoverage,
46
+ } from './vet'
43
47
  import type {
44
48
  SubsumeReport, SubsumeVerdict, SubsumeProfile,
45
49
  } from './subsume'
@@ -57,11 +61,21 @@ import type { WhyRecord } from './provenance'
57
61
  import { agentsMdSplice } from './agentsmd'
58
62
  import { format, unifiedDiff } from './format'
59
63
  import { includeOpts } from './utility'
64
+ import { HELPDOC, INITDOC } from './helpdoc'
65
+ import type { HelpTopic } from './helpdoc'
66
+ import { hints, codeClasses, codeClass } from './hints'
67
+ import { cmpCodePoint } from './keyorder'
60
68
  import type { IncludeOptions } from './utility'
61
69
 
62
70
 
63
71
  type Mode = 'json' | 'canon'
64
72
 
73
+ // The REPORT form of the bare command (G11 phase 7), which is a
74
+ // separate axis from Mode: `--canon` chooses what the answer IS,
75
+ // `--format` chooses how the answer is WRAPPED. Every other verb
76
+ // spells the second one this way.
77
+ type EvalFormat = 'text' | 'json'
78
+
65
79
 
66
80
  const HELP = `Usage: aontu [options] [file]
67
81
  aontu vet [options] <schema> <data> [more-data...]
@@ -82,8 +96,12 @@ const HELP = `Usage: aontu [options] [file]
82
96
  aontu get <path> [options] <file>
83
97
  aontu why <path> [options] <file>
84
98
  aontu set <path>=<value>... --entry <file> --overlay <file>
85
- aontu agentsmd [--write <AGENTS.md>] <file>
86
- aontu fmt [-w|-l|--check|-d|--lint] <file>...
99
+ aontu allow --role <role> [--at <path>] <roles-file> <path>...
100
+ aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
101
+ aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
102
+ aontu help [topic] [--format text|json]
103
+ aontu explain <code> | --list [--format text|json]
104
+ aontu init [dir]
87
105
  aontu lsp
88
106
  aontu mcp [--root <dir>]
89
107
 
@@ -91,6 +109,34 @@ Evaluate an aontu source file and print the result as JSON.
91
109
  With no file on an interactive terminal, start a REPL.
92
110
  With no file and piped input, read the source from stdin.
93
111
 
112
+ NEW TO THE LANGUAGE? This page documents the TOOL. The documentation
113
+ of the LANGUAGE travels inside this binary, and this is how to reach it:
114
+
115
+ aontu help List the topics this binary carries
116
+ aontu help tasks Which verb does the job you have
117
+ aontu help language The whole grammar, on one page
118
+ aontu help examples The ladder, from plain JSON upward
119
+ aontu help codes What a refusal means
120
+ aontu help grammar The published ABNF
121
+ aontu explain <code> What one error code a report carries means
122
+ aontu explain --list Every registered code with its class
123
+
124
+ Every one of those answers with no network and no checkout. The
125
+ long-form documentation -- the tutorial, the language and API
126
+ references, the how-to guides -- is in docs/ of the repository, which
127
+ is where to go when the topics above are not enough; the contributor
128
+ and agent guide is AGENTS.md beside it.
129
+
130
+ NOTHING TO EDIT YET? aontu init [dir] writes a working model, an
131
+ instance of it, and the four checks to run -- so the first
132
+ document is an edit of something that already holds, rather than an
133
+ invention. It refuses to overwrite.
134
+
135
+ The one construct to know before writing anything: &: inside a map is
136
+ a TEMPLATE that every key of that map must satisfy. A quoted "*" is a
137
+ key named *, not a wildcard, and a schema written that way constrains
138
+ nothing while still reporting valid.
139
+
94
140
  The vet verb validates data documents against a schema document and
95
141
  reports what does not hold, as text or as a machine-readable object.
96
142
 
@@ -100,7 +146,11 @@ query between a document and its own earlier versions.
100
146
 
101
147
  Options:
102
148
  -c, --canon Print the canonical form instead of generated JSON
103
- -h, --help Show this help and exit
149
+ --format <f> text (default) or json. The json form wraps the
150
+ answer as {aontu, findings, ok, out}, so a failure
151
+ here reads like every other verb's
152
+ -h, --help Show this help and exit (the verbs and their flags);
153
+ aontu help is the LANGUAGE, and lists its own topics
104
154
  --jsonl REPL: answer every command as one JSON line
105
155
  -v, --version Print the version and exit
106
156
  --trust <t> Include capability: system (default), none, or
@@ -131,12 +181,28 @@ Vet options:
131
181
  --closed Refuse keys the anchor does not declare
132
182
  --partial Residue is reported but does not fail the run
133
183
  --max-errors <n> Cap the finding list (default 20)
184
+ --coverage Report what the check EXAMINED: how many data
185
+ leaves a schema declaration constrained, the
186
+ shallowest data paths none did, and the
187
+ declarations no data met
188
+ --strict-coverage --coverage, and exit 1 when the run was VACUOUS --
189
+ when no data leaf was constrained at all. The
190
+ verdict word is unchanged, so nothing that passes
191
+ today starts failing without this flag
192
+ --coverage-at <p> Measure coverage under this path of the data only
134
193
  --format <f> text (default), json or sarif
135
194
  --watch Re-run whenever a watched file changes
136
195
 
196
+ A check that examined NOTHING and a check that passed answer the same
197
+ without --coverage. The usual cause is a schema written with the
198
+ wildcard other tools use: a quoted "*" is a key NAMED *, not a
199
+ template, so it constrains nothing and the run still reports valid.
200
+ The template is &: -- see aontu help language.
201
+
137
202
  Vet exit codes:
138
203
  0 valid data unifies, and is concrete (or --partial)
139
- 1 invalid at least one contradiction
204
+ 1 invalid at least one contradiction, or a vacuous run under
205
+ --strict-coverage
140
206
  2 usage bad option, or a file that cannot be read
141
207
  3 incomplete no contradiction, but the truth is not yet satisfied
142
208
  4 error the schema is unusable on its own
@@ -334,14 +400,61 @@ change contradicts a pinned value -- aontu why locates it, and
334
400
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
335
401
  stand up on its own.
336
402
 
403
+ Allow options:
404
+ --role <role> The role the caller is operating under (required)
405
+ --at <path> Where the roles map lives in the role model
406
+ (default $.roles)
407
+ --format <f> text (default) or json
408
+
409
+ The allow verb asks a role model whether a role may modify every one
410
+ of the given subtrees, and answers before the change is made. The
411
+ role model is an aontu document: one entry per role, each carrying
412
+ allow (the subtrees it may modify) and optionally deny (the ones it
413
+ may not), as path strings starting at $; * in a path matches any one
414
+ key. A path is allowed when an allow entry is at or above it, and
415
+ refused when a deny entry is at, above or below it, whatever the
416
+ order. Every path starts with $, and may be spelled as set's
417
+ assignment, <path>=<value>, whose value must be one value: a value
418
+ carrying a second pair would write a subtree the gate was not asked
419
+ about.
420
+
421
+ Allow exit codes: 0 allowed (every path), 1 refused (at least one
422
+ path, or a role the model does not declare), 2 usage, 4 the role
423
+ model does not stand up on its own.
424
+
337
425
  Agentsmd options:
338
426
  --write <file> Splice the stanza into this file between the
339
427
  aontu:begin and aontu:end markers, appending them
340
428
  when they are absent; the rest is left alone
429
+ --depth <n> How deep the shape line projects (default 2). Two
430
+ levels name the root keys and say top under them; a
431
+ caller that wants the fields asks for them
341
432
 
342
433
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
343
434
  stand up on its own.
344
435
 
436
+ Help options:
437
+ --format <f> text (default) or json, the topic and its text
438
+
439
+ The help verb prints the embedded teaching pack: the language, not the
440
+ tool. With no topic it lists them. Topics are tasks, language,
441
+ examples, codes and grammar; the corpus is generated from docs/skill/
442
+ and grammar/aontu.abnf, so it cannot drift from those sources.
443
+
444
+ Help exit codes: 0 printed, 2 an unknown topic (the topics are listed)
445
+ or a bad option.
446
+
447
+ Explain options:
448
+ --list Every registered error code with its class
449
+ --format <f> text (default) or json
450
+
451
+ The explain verb answers what one error code means, from the same
452
+ table the engine attaches to a finding. Every registered code has an
453
+ entry, so a code read out of a report always resolves.
454
+
455
+ Explain exit codes: 0 explained, 2 an unknown code (near matches are
456
+ named) or a bad option.
457
+
345
458
  Fmt options:
346
459
  -w, --write Rewrite each file in place, when its form would change
347
460
  -l, --list Print the name of each file whose form would change
@@ -351,10 +464,18 @@ Fmt options:
351
464
  --lint Report the style findings, key case and repeated
352
465
  shapes, on standard error, and print nothing else
353
466
  --strict With --lint, and exit 1 when there is a finding
467
+ --marker <t> The file is a generator, and this is its marker
468
+ (default //-, and #- --- /*- by extension)
354
469
 
355
470
  The fmt verb prints one document in the agreed form; with no file it
356
471
  reads standard input. Several files need one of the options above.
357
472
 
473
+ A file whose extension is not .aon is a GENERATOR, as it is for render:
474
+ the aontu its marker lines carry is formatted, the marker stands at the
475
+ left margin with the aontu indented after it, and every line of output
476
+ is held on a line of its own. A file with no marker line in it is
477
+ another language's, and is refused.
478
+
358
479
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
359
480
  a --strict finding, 2 usage, 4 a document does not parse.
360
481
 
@@ -391,13 +512,55 @@ function version(): string {
391
512
  }
392
513
 
393
514
 
515
+ // The terminal colour escapes the parser puts in its message text. A
516
+ // machine-readable report is no place for them, which is the rule
517
+ // findingOf states in ts/src/vet.ts; the twin here rather than an
518
+ // import because go/cmd/aontu carries its own for the same reason (the
519
+ // engine's is not exported to its command).
520
+ const EVAL_ANSI = new RegExp('\u001b\\[[0-9;]*m', 'g')
521
+
522
+
523
+ // THE ENGINE'S DIAGNOSIS AS A FINDING (G11 phase 7). The bare command
524
+ // was the one verb whose failure had no machine-readable form, so the
525
+ // default entry point was the one an agent had to parse with a regular
526
+ // expression.
527
+ //
528
+ // THE HEADLINE ONLY, and no `hint`. Both are parity decisions rather
529
+ // than economies: the frames under the headline are drawn for a person
530
+ // reading a terminal and only the first line is held to byte parity
531
+ // between the ports (the rule findingOf states), and the hint TABLES
532
+ // are deliberately not in parity while the code registry is -- so a
533
+ // hint here would make the two ports answer differently for a code
534
+ // only one of them explains. `aontu explain <code>` is where the hint
535
+ // lives, which is what phase 3 built it for.
536
+ //
537
+ // The CLASS comes from the registry rather than from the nil, because
538
+ // the registry is what both ports hold set-equal
539
+ // (test/spec/errcodes.tsv). Mirrors evalFinding in
540
+ // go/cmd/aontu/main.go.
541
+ function evalFinding(code: string, text: string): VetFinding {
542
+ return {
543
+ class: codeClass(code),
544
+ code,
545
+ message: text.split('\n')[0].replace(EVAL_ANSI, ''),
546
+ path: '$',
547
+ severity: 'error',
548
+ // NO SITE. The bare command's failure is the whole document not
549
+ // standing up, and the two sites a conflict names are in the
550
+ // frames the text form prints; naming one of them here would be a
551
+ // choice the engine has not made.
552
+ sites: [],
553
+ }
554
+ }
555
+
556
+
394
557
  // Evaluate source, returning either the rendered output or the error
395
- // message. Never throws.
558
+ // message, and the failure in the finding shape. Never throws.
396
559
  function evalSource(
397
560
  aontu: Aontu,
398
561
  src: string,
399
562
  mode: Mode,
400
- ): { ok: boolean; text: string } {
563
+ ): { ok: boolean; text: string; findings: VetFinding[] } {
401
564
  try {
402
565
  // exactJSON, not JSON.stringify: a document using the `0d` exact
403
566
  // leaves generates bigints and Decimals, which JSON.stringify cannot
@@ -408,17 +571,54 @@ function evalSource(
408
571
  const text = 'canon' === mode
409
572
  ? aontu.unify(src).canon
410
573
  : exactJSON(aontu.generate(src), 2)
411
- return { ok: true, text }
574
+ return { ok: true, text, findings: [] }
412
575
  }
413
576
  catch (err: any) {
414
577
  const msg = (err instanceof AontuError || true === err?.aontu)
415
578
  ? err.message
416
579
  : String(err?.message ?? err)
417
- return { ok: false, text: msg }
580
+ // WHAT THE ENGINE COLLECTED, when it collected anything: an
581
+ // AontuError carries the NilVals the run failed on, already
582
+ // materialised (handleErrors in ts/src/aontu.ts), and their first
583
+ // is the diagnosis every other verb reports. An error raised
584
+ // outside the engine's own collection -- exactJSON's circular
585
+ // refusal, a foreign object claiming to be one -- carries none,
586
+ // and answers with the text alone rather than an invented code.
587
+ const errs: any[] = 'function' === typeof err?.errs ? err.errs() : []
588
+ const first: any = errs[0]
589
+ return {
590
+ ok: false,
591
+ text: msg,
592
+ findings: null == first ? [] : [evalFinding(first.why, msg)],
593
+ }
418
594
  }
419
595
  }
420
596
 
421
597
 
598
+ // The bare command's answer, in the form the caller asked for. The
599
+ // text form is what it has always printed, on the stream the verdict
600
+ // chooses; `--format json` is the same answer as one object, on
601
+ // stdout, so a harness reads one stream and one shape either way.
602
+ // Mirrors emit in go/cmd/aontu/main.go.
603
+ function emitEval(
604
+ res: { ok: boolean; text: string; findings: VetFinding[] },
605
+ format: EvalFormat,
606
+ ): number {
607
+ if ('json' === format) {
608
+ process.stdout.write(exactJSON({
609
+ aontu: { version: version(), verb: 'eval' },
610
+ findings: res.findings,
611
+ ok: res.ok,
612
+ out: res.ok ? res.text : '',
613
+ }, 2) + '\n')
614
+ }
615
+ else {
616
+ ;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
617
+ }
618
+ return res.ok ? 0 : 1
619
+ }
620
+
621
+
422
622
  // The include capability the main verb runs with (G5, docs/trust.md).
423
623
  // `--trust` and `--include-root` set it explicitly; the default is
424
624
  // 'system' WITH the warning window: every resolution that escapes the
@@ -592,12 +792,35 @@ function entryRootOf(file: string | undefined): string {
592
792
  }
593
793
 
594
794
 
595
- function runFile(file: string, mode: Mode, trust: TrustArg): number {
795
+ function runFile(
796
+ file: string, mode: Mode, format: EvalFormat, trust: TrustArg): number {
596
797
  let src: string
597
798
  try {
598
799
  src = readFileSync(file, 'utf8')
599
800
  }
600
801
  catch (err: any) {
802
+ // A MISTYPED VERB READS AS A FILE NAME, and until G11 phase 2 that
803
+ // was only said when there were TWO of them. The one-argument case
804
+ // is the one an agent actually produces -- `aontu help`, `aontu
805
+ // init`, `aontu ontology` -- and it answered `cannot read help:
806
+ // ...`, which describes the symptom and hides the cause.
807
+ //
808
+ // The test is SHAPE, not existence: a bare word (no separator, no
809
+ // extension) that cannot be read was meant as a verb, while
810
+ // `./help`, `help.aon` and `/tmp/help` were meant as paths and keep
811
+ // the file diagnosis and its exit 1. That is the same escape hatch
812
+ // the subcommand dispatch documents. Mirrors go/cmd/aontu/main.go.
813
+ if (looksLikeVerb(file)) {
814
+ process.stderr.write(
815
+ `aontu: \`${file}\` is not a file, and not a verb this port knows\n`)
816
+ const near = nearestVerb(file, KNOWN_VERBS)
817
+ if ('' !== near) {
818
+ process.stderr.write(`aontu: did you mean \`aontu ${near}\`?\n`)
819
+ }
820
+ process.stderr.write(
821
+ 'aontu: `aontu --help` lists the verbs, `aontu help` the topics\n')
822
+ return 2
823
+ }
601
824
  process.stderr.write(`aontu: cannot read ${file}: ${err.message}\n`)
602
825
  return 1
603
826
  }
@@ -614,13 +837,12 @@ function runFile(file: string, mode: Mode, trust: TrustArg): number {
614
837
  errfs: { existsSync, readFileSync },
615
838
  ...trustOpts(trust, dirname(path)),
616
839
  })
617
- const res = evalSource(aontu, src, mode)
618
- ;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
619
- return res.ok ? 0 : 1
840
+ return emitEval(evalSource(aontu, src, mode), format)
620
841
  }
621
842
 
622
843
 
623
- function runStdin(mode: Mode, trust: TrustArg): Promise<number> {
844
+ function runStdin(
845
+ mode: Mode, format: EvalFormat, trust: TrustArg): Promise<number> {
624
846
  return new Promise((resolve) => {
625
847
  let src = ''
626
848
  process.stdin.setEncoding('utf8')
@@ -628,8 +850,7 @@ function runStdin(mode: Mode, trust: TrustArg): Promise<number> {
628
850
  process.stdin.on('end', () => {
629
851
  const res = evalSource(
630
852
  new Aontu(trustOpts(trust, process.cwd())), src, mode)
631
- ;(res.ok ? process.stdout : process.stderr).write(res.text + '\n')
632
- resolve(res.ok ? 0 : 1)
853
+ resolve(emitEval(res, format))
633
854
  })
634
855
  })
635
856
  }
@@ -862,6 +1083,11 @@ type VetArgs = {
862
1083
  partial?: boolean
863
1084
  maxErrors?: number
864
1085
  watch?: boolean
1086
+ // G11 phase 5. `strictCoverage` implies `coverage`; `coverageAt`
1087
+ // narrows the data side and implies it too.
1088
+ coverage?: boolean
1089
+ strictCoverage?: boolean
1090
+ coverageAt?: string
865
1091
  }
866
1092
 
867
1093
 
@@ -875,6 +1101,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
875
1101
  let partial = false
876
1102
  let maxErrors: number | undefined
877
1103
  let watch = false
1104
+ let coverage = false
1105
+ let strictCoverage = false
1106
+ let coverageAt: string | undefined
878
1107
 
879
1108
  for (let i = 0; i < argv.length; i++) {
880
1109
  const arg = argv[i]
@@ -921,6 +1150,24 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
921
1150
  else if ('--partial' === arg) {
922
1151
  partial = true
923
1152
  }
1153
+ else if ('--coverage' === arg) {
1154
+ coverage = true
1155
+ }
1156
+ else if ('--strict-coverage' === arg) {
1157
+ // IMPLIES THE ACCOUNTING, because a gate cannot fire on what was
1158
+ // never measured. Asking for the strict form and having to
1159
+ // remember `--coverage` beside it is a usage trap with one
1160
+ // correct answer, so the flag takes it.
1161
+ coverage = true
1162
+ strictCoverage = true
1163
+ }
1164
+ else if ('--coverage-at' === arg) {
1165
+ coverageAt = argv[++i]
1166
+ if (null == coverageAt) {
1167
+ return { err: 'aontu: --coverage-at needs a path' }
1168
+ }
1169
+ coverage = true
1170
+ }
924
1171
  else if ('--watch' === arg) {
925
1172
  watch = true
926
1173
  }
@@ -946,6 +1193,9 @@ function parseVetArgs(argv: string[]): { args?: VetArgs; err?: string } {
946
1193
  partial,
947
1194
  maxErrors,
948
1195
  watch,
1196
+ coverage,
1197
+ strictCoverage,
1198
+ coverageAt,
949
1199
  },
950
1200
  }
951
1201
  }
@@ -985,14 +1235,53 @@ function renderVetText(report: VetReport): string {
985
1235
  const head = `verdict: ${report.verdict}` +
986
1236
  (report.truncated ? ' (findings truncated)' : '')
987
1237
 
988
- if (0 === report.findings.length) {
989
- return head
990
- }
1238
+ const body = 0 === report.findings.length ? []
1239
+ : ['', ...report.findings.map(renderFinding)]
1240
+ const cover = null == report.coverage ? []
1241
+ : ['', ...renderVetCoverage(report.coverage)]
991
1242
 
992
- return [head, ''].concat(report.findings.map(renderFinding)).join('\n')
1243
+ return [head, ...body, ...cover].join('\n')
993
1244
  }
994
1245
 
995
1246
 
1247
+ // The coverage block (G11 phase 5). VACUOUS FIRST and in the
1248
+ // imperative, because it is the one line that changes what the reader
1249
+ // should do: a `valid` verdict above it means nothing.
1250
+ function renderVetCoverage(c: VetCoverage): string[] {
1251
+ const out: string[] = []
1252
+ if (c.vacuous) {
1253
+ out.push('coverage: VACUOUS — no data leaf was constrained' +
1254
+ ' by the schema; this run checked nothing')
1255
+ }
1256
+ out.push(`coverage: ${c.checked}/${c.leaves} data leaves checked,` +
1257
+ ` ${c.declared} schema declarations`)
1258
+ // The lists are the SHALLOWEST paths, so each names a subtree rather
1259
+ // than every leaf under it, and both are capped: a report a reader
1260
+ // scrolls past is a report nobody reads.
1261
+ for (const [label, paths] of [
1262
+ ['unchecked', c.unchecked], ['unused', c.unused],
1263
+ ] as [string, string[]][]) {
1264
+ if (0 === paths.length) {
1265
+ continue
1266
+ }
1267
+ const shown = paths.slice(0, COVERAGE_LIST_MAX)
1268
+ for (const p of shown) {
1269
+ out.push(` ${label}: ${p}`)
1270
+ }
1271
+ if (shown.length < paths.length) {
1272
+ out.push(` ${label}: … and ${paths.length - shown.length} more`)
1273
+ }
1274
+ }
1275
+ return out
1276
+ }
1277
+
1278
+
1279
+ // How many coverage paths the TEXT form prints per list. The JSON form
1280
+ // carries every one: a machine reads the whole list, a person reads the
1281
+ // first few and the count.
1282
+ const COVERAGE_LIST_MAX = 10
1283
+
1284
+
996
1285
  // The machine-readable form. `aontu` names the producer, so a report
997
1286
  // read from a file or a pipe says which version and which verb made it
998
1287
  // without the consumer having to know.
@@ -1002,6 +1291,7 @@ function renderVetJson(report: VetReport): string {
1002
1291
  verdict: report.verdict,
1003
1292
  truncated: report.truncated,
1004
1293
  findings: report.findings,
1294
+ ...(null == report.coverage ? {} : { coverage: report.coverage }),
1005
1295
  }, 2)
1006
1296
  }
1007
1297
 
@@ -1049,6 +1339,20 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1049
1339
  let verdict: VetVerdict = 'valid'
1050
1340
  let truncated = false
1051
1341
  const findings: VetFinding[] = []
1342
+ // COVERAGE ACROSS SEVERAL DATA FILES (G11 phase 5). Two data files
1343
+ // are two candidates for one truth, so the schema side is the SAME
1344
+ // for each: `declared` is taken once, and a declaration is unused
1345
+ // only when NO file met it -- the intersection, because a
1346
+ // declaration one file exercised is exercised. The data side adds
1347
+ // up: leaves and checked leaves sum, and `unchecked` is the union.
1348
+ let cov: VetCoverage | undefined
1349
+ // Initialised rather than left undefined: it is filled in the same
1350
+ // block that sets `cov`, so a fallback at the read below would be an
1351
+ // arm nothing can take. The FIRST file replaces it wholesale, which
1352
+ // is what makes the fold an intersection rather than an empty set.
1353
+ let unusedEvery = new Set<string>()
1354
+ let unusedSeen = false
1355
+ const uncheckedAll = new Set<string>()
1052
1356
 
1053
1357
  for (const source of sources) {
1054
1358
  const report = vet(schemaSrc, source.src, {
@@ -1067,6 +1371,8 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1067
1371
  // absolute one would name the same file two ways.
1068
1372
  schemaPath: args.schema,
1069
1373
  dataPath: source.file,
1374
+ coverage: args.coverage,
1375
+ coverageAt: args.coverageAt,
1070
1376
  })
1071
1377
 
1072
1378
  if (VET_RANK[verdict] < VET_RANK[report.verdict]) {
@@ -1075,6 +1381,25 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1075
1381
  truncated = truncated || report.truncated
1076
1382
  findings.push(...report.findings)
1077
1383
 
1384
+ if (null != report.coverage) {
1385
+ const c = report.coverage
1386
+ cov = null == cov ? { ...c } : {
1387
+ checked: cov.checked + c.checked,
1388
+ declared: c.declared,
1389
+ leaves: cov.leaves + c.leaves,
1390
+ unchecked: [],
1391
+ unused: [],
1392
+ vacuous: false,
1393
+ }
1394
+ for (const p of c.unchecked) {
1395
+ uncheckedAll.add(p)
1396
+ }
1397
+ const mine = new Set(c.unused)
1398
+ unusedEvery = unusedSeen
1399
+ ? new Set([...unusedEvery].filter((u) => mine.has(u))) : mine
1400
+ unusedSeen = true
1401
+ }
1402
+
1078
1403
  // A SCHEMA-SIDE FAULT IS THE SAME FAULT FOR EVERY DATA FILE, so it
1079
1404
  // is reported ONCE. `error` means exactly that -- the run could not
1080
1405
  // be set up from the truth's side, never the data's (the exit table
@@ -1099,16 +1424,38 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
1099
1424
  const cap = args.maxErrors ?? VET_MAX_ERRORS
1100
1425
  const kept = cap < findings.length ? findings.slice(0, cap) : findings
1101
1426
 
1427
+ if (null != cov) {
1428
+ cov.unchecked = [...uncheckedAll].sort(cmpCodePoint)
1429
+ cov.unused = [...unusedEvery].sort(cmpCodePoint)
1430
+ cov.vacuous = 0 === cov.checked && 0 < cov.leaves
1431
+ }
1432
+
1102
1433
  const report: VetReport = {
1103
1434
  verdict,
1104
1435
  truncated: truncated || cap < findings.length,
1105
1436
  findings: kept,
1437
+ ...(null == cov ? {} : { coverage: cov }),
1106
1438
  }
1107
1439
  const text = 'json' === args.format ? renderVetJson(report) :
1108
1440
  'sarif' === args.format ? renderVetSarif(report) :
1109
1441
  renderVetText(report)
1110
1442
 
1111
1443
  process.stdout.write(text + '\n')
1444
+
1445
+ // A VACUOUS CHECK IS A FAILED GATE UNDER `--strict-coverage`, and
1446
+ // only under it: the verdict WORD is unchanged, so nothing that
1447
+ // passes today starts failing, and a caller who wants the stronger
1448
+ // gate asks for it. The reason goes to stderr, because stdout is a
1449
+ // report contract -- a JSON consumer reads `coverage.vacuous` and a
1450
+ // person reads this.
1451
+ if (true === args.strictCoverage && true === report.coverage?.vacuous) {
1452
+ process.stderr.write(
1453
+ 'aontu: no data leaf was constrained by the schema:' +
1454
+ ' this run checked nothing\n' +
1455
+ 'aontu: `aontu help language` — a map template is `&:`,' +
1456
+ ' and a quoted "*" is a key named *\n')
1457
+ return 1
1458
+ }
1112
1459
  return VET_EXIT[verdict]
1113
1460
  }
1114
1461
 
@@ -2227,6 +2574,31 @@ function modText(sub: string, report: any): string {
2227
2574
  }
2228
2575
 
2229
2576
 
2577
+ // VACUITY SIGNALS (G11 phase 4,
2578
+ // docs/capability-review/g11-agent-onramp.md).
2579
+ //
2580
+ // The same principle phase 5 applied to `vet`: a verb that did NOTHING
2581
+ // and a verb that did its job answer the same. `aontu view tree` over a
2582
+ // document declaring no relations printed one newline and exited 0;
2583
+ // `aontu render` with no profile printed nothing and exited 0; `aontu
2584
+ // relations` over a document declaring none answered `verdict: pass`.
2585
+ // For a person at a terminal that is a shrug. For an unattended agent
2586
+ // it is a green check mark on an empty box.
2587
+ //
2588
+ // ON STDERR, ALWAYS. stdout is a report contract -- a `--format json`
2589
+ // consumer parses it -- and the exit code is a verdict class that
2590
+ // callers already branch on. Neither changes here: what changes is
2591
+ // that the caller is TOLD. A caller who wants it to be fatal has
2592
+ // `vet --strict-coverage`, and the same argument would give the other
2593
+ // verbs a flag of their own if one is ever asked for.
2594
+ //
2595
+ // The repository already ruled this for one verb, in G8 phase 6 on
2596
+ // `trim`: "doing something else silently is worse than refusing".
2597
+ function vacuous(what: string, why: string): void {
2598
+ process.stderr.write(`aontu: ${what}: ${why}\n`)
2599
+ }
2600
+
2601
+
2230
2602
  function runRelations(argv: string[]): number {
2231
2603
  const trusted = takeTrust(argv)
2232
2604
  if (null == trusted) {
@@ -2275,12 +2647,21 @@ function runRelations(argv: string[]): number {
2275
2647
  }
2276
2648
 
2277
2649
  const report = relationCheck(src, {
2278
- path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2650
+ path: files[0], count: true,
2651
+ ...verbOpts(trust, entryRootOf(files[0])),
2279
2652
  })
2280
2653
  const text = 'json' === format
2281
2654
  ? renderRelationsJson(report)
2282
2655
  : renderRelationsText(report)
2283
2656
  process.stdout.write(text + '\n')
2657
+ // `pass` over NO declarations is the vacuous case, and the engine
2658
+ // knows it exactly: `_reldecls` is empty. The count is asked for
2659
+ // here rather than derived, so the answer costs no second
2660
+ // evaluation.
2661
+ if (0 === report.declared) {
2662
+ vacuous('this document declares no relations',
2663
+ '`pass` means nothing was checked, not that the graph is sound')
2664
+ }
2284
2665
  return RELATIONS_EXIT[report.verdict]
2285
2666
  }
2286
2667
 
@@ -2583,7 +2964,7 @@ function runView(argv: string[]): number {
2583
2964
  }
2584
2965
  }
2585
2966
 
2586
- const report = view(srcs[0], {
2967
+ const viewOpts = {
2587
2968
  ...opts,
2588
2969
  style: viewStyleOf(style, opts.as ?? viewDefaultProfile(kind)),
2589
2970
  kind,
@@ -2591,7 +2972,28 @@ function runView(argv: string[]): number {
2591
2972
  roots,
2592
2973
  ...verbOpts(trust, entryRootOf(files[0])),
2593
2974
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2594
- })
2975
+ }
2976
+ const report = view(srcs[0], viewOpts)
2977
+
2978
+ // AN EMPTY FIGURE IS THE SAME BYTES AS A DRAWN ONE MINUS ITS
2979
+ // CONTENT, and every profile spells "empty" differently: text draws
2980
+ // nothing at all, mermaid still draws its `flowchart LR` header, the
2981
+ // matrix still prints its count line. Rather than teach this one
2982
+ // place each of those spellings -- a list that goes stale the first
2983
+ // time a profile gains a header -- ASK THE SAME KIND TO DRAW AN
2984
+ // EMPTY DOCUMENT and compare. Equal texts mean this document
2985
+ // contributed nothing to the figure, whatever the profile.
2986
+ //
2987
+ // It costs one drawing of `{}`, which is the cheapest document
2988
+ // there is, and only on a run that produced a figure at all.
2989
+ if ('error' !== report.verdict && null != report.text) {
2990
+ const bare = view('{}', viewOpts)
2991
+ if ('error' !== bare.verdict && bare.text === report.text) {
2992
+ vacuous('nothing to draw',
2993
+ 'this figure is what the same view draws for an empty document' +
2994
+ ' — the model declares nothing this kind can show')
2995
+ }
2996
+ }
2595
2997
 
2596
2998
  if ('json' === format) {
2597
2999
  process.stdout.write(renderViewJson(report) + '\n')
@@ -3109,6 +3511,11 @@ function runRender(argv: string[]): number {
3109
3511
  profiles.push(profile)
3110
3512
  }
3111
3513
 
3514
+ // A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
3515
+ // bytes and exit 0. The profile is what maps a model onto a
3516
+ // language, so without one there is nothing for the renderer to
3517
+ // write -- which is a usable answer only if the caller is told.
3518
+ const noProfiles = 0 === profiles.length
3112
3519
  const report = render(src, {
3113
3520
  at, unit, strict, profiles, path: files[0],
3114
3521
  coverage, coverageAt,
@@ -3119,6 +3526,15 @@ function runRender(argv: string[]): number {
3119
3526
  ...verbOpts(trust, entryRootOf(files[0])),
3120
3527
  })
3121
3528
 
3529
+ // Said once, whatever the format: stdout stays the report.
3530
+ if ('error' !== report.verdict && 0 === report.units.length) {
3531
+ vacuous('nothing was rendered',
3532
+ noProfiles
3533
+ ? 'no profile was given, and the document declares none' +
3534
+ ' (see aontu help tasks)'
3535
+ : 'the document produced no units under this profile')
3536
+ }
3537
+
3122
3538
  if ('json' === format) {
3123
3539
  process.stdout.write(exactJSON({
3124
3540
  aontu: { version: version(), verb: 'render' },
@@ -3830,6 +4246,188 @@ function runSet(argv: string[]): number {
3830
4246
  }
3831
4247
 
3832
4248
 
4249
+ // ---------------------------------------------------------------------
4250
+ // The role gate (docs/design/ALLOW.0.md): may the role the caller is
4251
+ // operating under modify these subtrees? Asked before `set`, by an
4252
+ // agent whose skill names its role, and answered from a role model
4253
+ // that is itself an aontu document. The verdict is the exit code, as
4254
+ // it is for every gate here: 0 is yes, 1 is no, 4 is "the model that
4255
+ // was to decide does not stand up", and an agent branches on nothing
4256
+ // else.
4257
+
4258
+ const ALLOW_HELP =
4259
+ 'aontu allow --role <role> <roles-file> <path> [more-paths...] (try --help)'
4260
+
4261
+ const ALLOW_EXIT: Record<AllowVerdict, number> = {
4262
+ allowed: 0,
4263
+ refused: 1,
4264
+ error: 4,
4265
+ }
4266
+
4267
+
4268
+ // One line per asked path: the answer, and the entry that gave it, as
4269
+ // a path into the role model so `aontu why` can locate the rule.
4270
+ function renderAllowDecision(d: AllowDecision, role: string): string {
4271
+ const head = `${d.path}: ${d.allowed ? 'allowed' : 'refused'}`
4272
+ switch (d.reason) {
4273
+ case 'allow':
4274
+ case 'deny':
4275
+ return `${head} by ${d.by} (${d.pattern})`
4276
+ case 'uncovered':
4277
+ return `${head} (no allow entry of ${role} covers it)`
4278
+ default:
4279
+ return `${head} (role ${role} is not declared)`
4280
+ }
4281
+ }
4282
+
4283
+
4284
+ function renderAllowText(report: AllowReport): string {
4285
+ const lines = [`verdict: ${report.verdict}`, `role: ${report.role}`]
4286
+ .concat(report.paths.map((d) => renderAllowDecision(d, report.role)))
4287
+ if (0 === report.findings.length) {
4288
+ return lines.join('\n')
4289
+ }
4290
+ return lines.concat('', report.findings.map(renderFinding)).join('\n')
4291
+ }
4292
+
4293
+
4294
+ // Does the text after `=` parse as exactly one value? Parsed, never
4295
+ // evaluated, with loads denied: the question is the shape of the
4296
+ // argument, and reading a file to answer it would be the write the
4297
+ // gate exists to precede.
4298
+ function oneValue(value: string): boolean {
4299
+ try {
4300
+ const probe: any = new Aontu({ trust: { include: 'none' } })
4301
+ .parse('v: ' + value)
4302
+ return 1 === Object.keys(probe.peg).length
4303
+ }
4304
+ catch {
4305
+ return false
4306
+ }
4307
+ }
4308
+
4309
+
4310
+ function runAllow(argv: string[]): number {
4311
+ const trusted = takeTrust(argv)
4312
+ if (null == trusted) {
4313
+ return 2
4314
+ }
4315
+ argv = trusted.argv
4316
+ const trust = trusted.trust
4317
+ const rest: string[] = []
4318
+ let role: string | undefined
4319
+ let at: string | undefined
4320
+ let format: SubsumeFormat = 'text'
4321
+
4322
+ for (let i = 0; i < argv.length; i++) {
4323
+ const arg = argv[i]
4324
+ if ('-h' === arg || '--help' === arg) {
4325
+ process.stdout.write(HELP)
4326
+ return 0
4327
+ }
4328
+ if ('--role' === arg) {
4329
+ role = argv[++i]
4330
+ if (null == role) {
4331
+ process.stderr.write('aontu: --role needs a role name\n')
4332
+ return 2
4333
+ }
4334
+ }
4335
+ else if ('--at' === arg) {
4336
+ at = argv[++i]
4337
+ if (null == at) {
4338
+ process.stderr.write('aontu: --at needs a path\n')
4339
+ return 2
4340
+ }
4341
+ }
4342
+ else if ('--format' === arg) {
4343
+ const f = argv[++i]
4344
+ if ('text' !== f && 'json' !== f) {
4345
+ process.stderr.write('aontu: --format needs text or json\n')
4346
+ return 2
4347
+ }
4348
+ format = f
4349
+ }
4350
+ else if (arg.startsWith('-')) {
4351
+ process.stderr.write(`aontu: unknown allow option ${arg} (try --help)\n`)
4352
+ return 2
4353
+ }
4354
+ else {
4355
+ rest.push(arg)
4356
+ }
4357
+ }
4358
+
4359
+ if (null == role || rest.length < 2) {
4360
+ process.stderr.write(
4361
+ `aontu: allow needs --role, a role model and at least one path\n` +
4362
+ `${ALLOW_HELP}\n`)
4363
+ return 2
4364
+ }
4365
+ const [file, ...asked] = rest
4366
+
4367
+ // A role is ONE KEY of the roles map. A dotted name would be read as
4368
+ // a path by `why` when it follows the entry the report names, and an
4369
+ // empty one names the map itself.
4370
+ if ('' === role || role.includes('.')) {
4371
+ process.stderr.write('aontu: --role needs one key, without dots\n')
4372
+ return 2
4373
+ }
4374
+
4375
+ // A path may arrive in `set`'s spelling, `$.a.b=1`, so a skill can
4376
+ // hand the gate the very arguments the write will get. The text up
4377
+ // to the first `=` is the path, and it starts with `$`: an empty
4378
+ // argument, or a second file name, would otherwise read as a path
4379
+ // and be answered. The VALUE is checked to be one value. `set`
4380
+ // appends it as source after the flattened path, so a value carrying
4381
+ // a second pair -- `3 secrets: key: "x"` -- writes a sibling of the
4382
+ // overlay root, a subtree the gate was never asked about.
4383
+ const paths: string[] = []
4384
+ for (const arg of asked) {
4385
+ const eq = arg.indexOf('=')
4386
+ const path = eq < 0 ? arg : arg.slice(0, eq)
4387
+ if (!path.startsWith('$')) {
4388
+ process.stderr.write(
4389
+ `aontu: a path starts with $ (got ${JSON.stringify(arg)})\n`)
4390
+ return 2
4391
+ }
4392
+ if (0 <= eq && !oneValue(arg.slice(eq + 1))) {
4393
+ process.stderr.write(
4394
+ `aontu: the value of ${path} is not one value\n`)
4395
+ return 2
4396
+ }
4397
+ paths.push(path)
4398
+ }
4399
+
4400
+ let src: string
4401
+ try {
4402
+ src = readFileSync(file, 'utf8')
4403
+ }
4404
+ catch (err: any) {
4405
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
4406
+ return 2
4407
+ }
4408
+
4409
+ const report = allow(src, role, paths, {
4410
+ at, path: file, ...verbOpts(trust, entryRootOf(file)),
4411
+ })
4412
+
4413
+ const text = 'json' === format ?
4414
+ exactJSON({
4415
+ aontu: { version: version(), verb: 'allow' },
4416
+ findings: report.findings,
4417
+ paths: report.paths,
4418
+ role: report.role,
4419
+ verdict: report.verdict,
4420
+ }, 2) :
4421
+ renderAllowText(report)
4422
+
4423
+ // The report IS the answer, refused or not, so it goes to stdout as
4424
+ // vet's does; the exit code carries the verdict for a caller that
4425
+ // reads nothing else.
4426
+ process.stdout.write(text + '\n')
4427
+ return ALLOW_EXIT[report.verdict]
4428
+ }
4429
+
4430
+
3833
4431
  // ---------------------------------------------------------------------
3834
4432
  // The generated AGENTS.md stanza (G7 phase 6): the prose entrypoint,
3835
4433
  // derived from the definition, so it cannot drift from the formal
@@ -3846,6 +4444,10 @@ function runAgentsMd(argv: string[]): number {
3846
4444
  const trust = trusted.trust
3847
4445
  const files: string[] = []
3848
4446
  let write: string | undefined
4447
+ // The SHAPE's depth (G11 phase 7). Default 2, unchanged: the stanza
4448
+ // is spliced into a file people read, and a deeper shape is a
4449
+ // question the caller asks rather than one it is handed.
4450
+ let depth = 2
3849
4451
 
3850
4452
  for (let i = 0; i < argv.length; i++) {
3851
4453
  const arg = argv[i]
@@ -3860,6 +4462,14 @@ function runAgentsMd(argv: string[]): number {
3860
4462
  return 2
3861
4463
  }
3862
4464
  }
4465
+ else if ('--depth' === arg) {
4466
+ const n = Number(argv[++i])
4467
+ if (!Number.isInteger(n) || n < 1) {
4468
+ process.stderr.write('aontu: --depth needs a positive integer\n')
4469
+ return 2
4470
+ }
4471
+ depth = n
4472
+ }
3863
4473
  else if (arg.startsWith('-')) {
3864
4474
  process.stderr.write(
3865
4475
  `aontu: unknown agentsmd option ${arg} (try --help)\n`)
@@ -3886,7 +4496,7 @@ function runAgentsMd(argv: string[]): number {
3886
4496
  }
3887
4497
 
3888
4498
  const report = agentsMd(src, {
3889
- name: files[0], path: files[0],
4499
+ depth, name: files[0], path: files[0],
3890
4500
  ...verbOpts(trust, entryRootOf(files[0])),
3891
4501
  })
3892
4502
  if (!report.ok) {
@@ -3945,7 +4555,8 @@ function runAgentsMd(argv: string[]): number {
3945
4555
  // the form itself is the library's (ts/src/format.ts), and the two
3946
4556
  // ports agree on it row by row in test/spec/fmt.tsv.
3947
4557
 
3948
- const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] <file>... (try --help)'
4558
+ const FMT_HELP =
4559
+ 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>... (try --help)'
3949
4560
 
3950
4561
  type FmtFlags = {
3951
4562
  write: boolean, list: boolean, check: boolean, diff: boolean, lint: boolean, strict: boolean,
@@ -3953,11 +4564,13 @@ type FmtFlags = {
3953
4564
 
3954
4565
  function runFmt(argv: string[]): number | Promise<number> {
3955
4566
  const files: string[] = []
4567
+ let marker: string | undefined = undefined
3956
4568
  const flags: FmtFlags = {
3957
4569
  write: false, list: false, check: false, diff: false, lint: false, strict: false,
3958
4570
  }
3959
4571
 
3960
- for (const arg of argv) {
4572
+ for (let i = 0; i < argv.length; i++) {
4573
+ const arg = argv[i]
3961
4574
  if ('-h' === arg || '--help' === arg) {
3962
4575
  process.stdout.write(HELP)
3963
4576
  return 0
@@ -3981,6 +4594,16 @@ function runFmt(argv: string[]): number | Promise<number> {
3981
4594
  flags.lint = true
3982
4595
  flags.strict = true
3983
4596
  }
4597
+ else if ('--marker' === arg) {
4598
+ // THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
4599
+ // extension: `render` and `template` take the same option for
4600
+ // the same reason, a language the table has never seen.
4601
+ marker = argv[++i]
4602
+ if (null == marker) {
4603
+ process.stderr.write('aontu: --marker needs a token\n')
4604
+ return 2
4605
+ }
4606
+ }
3984
4607
  else if (arg.startsWith('-')) {
3985
4608
  process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
3986
4609
  return 2
@@ -4002,7 +4625,7 @@ function runFmt(argv: string[]): number | Promise<number> {
4002
4625
  let src = ''
4003
4626
  process.stdin.setEncoding('utf8')
4004
4627
  process.stdin.on('data', (d) => (src += d))
4005
- process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)))
4628
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags, marker)))
4006
4629
  })
4007
4630
  }
4008
4631
 
@@ -4018,23 +4641,6 @@ function runFmt(argv: string[]): number | Promise<number> {
4018
4641
 
4019
4642
  let worst = 0
4020
4643
  for (const file of files) {
4021
- // FMT FORMATS AONTU SOURCE, AND THE EXTENSION SAYS WHAT A FILE IS
4022
- // (ADR-012's rule, and the one `render` reads a template by).
4023
- // A TEMPLATE FILE IS NOT AONTU (docs/design/TEMPLATE.0.md; P8):
4024
- // its marker lines are fragments of a document and its other lines
4025
- // are the target's, so there is nothing here to format that would
4026
- // not also rewrite the output. Refused rather than attempted, and
4027
- // refused BY NAME rather than by a parse failure, because a `#-`
4028
- // template parses: `#` opens a comment, so every marker line
4029
- // vanishes and what is left is read as a document that was never
4030
- // written. The verb answered `0` over one, having understood none
4031
- // of it.
4032
- if (!/[.](aon|aontu)$/.test(file)) {
4033
- process.stderr.write(
4034
- `aontu: ${file} is not aontu source (.aon, .aontu); a generator ` +
4035
- 'written in the target\'s own syntax is aontu template\'s\n')
4036
- return 2
4037
- }
4038
4644
  let src: string
4039
4645
  try {
4040
4646
  src = readFileSync(file, 'utf8')
@@ -4043,11 +4649,46 @@ function runFmt(argv: string[]): number | Promise<number> {
4043
4649
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
4044
4650
  return 2
4045
4651
  }
4046
- worst = Math.max(worst, fmtOne(file, src, flags))
4652
+ const mark = fmtMarker(file, src, marker)
4653
+ if (false === mark) {
4654
+ process.stderr.write(
4655
+ `aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
4656
+ `${markerFor(file)} marker line, so there is no aontu in it to ` +
4657
+ 'format; --marker names the marker for a language the table does ' +
4658
+ 'not know\n')
4659
+ return 2
4660
+ }
4661
+ worst = Math.max(worst, fmtOne(file, src, flags, mark))
4047
4662
  }
4048
4663
  return worst
4049
4664
  }
4050
4665
 
4666
+ // WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
4667
+ // `render` reads an entry by): `.aon` and `.aontu` are aontu source,
4668
+ // and anything else is a GENERATOR written in the target's own syntax
4669
+ // (docs/design/TEMPLATE.0.md), whose marker lines carry the document
4670
+ // this formats and whose other lines are output. `undefined` is aontu,
4671
+ // a string is the generator's marker, and `false` is neither.
4672
+ //
4673
+ // A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
4674
+ // FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
4675
+ // include is another language's file, and reading one as a generator
4676
+ // would answer it back unchanged having understood none of it. The
4677
+ // marker is the evidence that a file was written to carry aontu at
4678
+ // all. `--marker` says so outright, and then the file is a generator
4679
+ // whatever it is called.
4680
+ function fmtMarker(
4681
+ file: string, src: string, marker: string | undefined): string | undefined | false {
4682
+ if (undefined !== marker) {
4683
+ return marker
4684
+ }
4685
+ if (/[.](aon|aontu)$/.test(file)) {
4686
+ return undefined
4687
+ }
4688
+ const mark = markerFor(file)
4689
+ return templateOutputs(src, mark).some((out) => !out) ? mark : false
4690
+ }
4691
+
4051
4692
  // An option that says what to do with a file, in place of printing
4052
4693
  // it: what to do when its form would change, or the lint.
4053
4694
  function fmtQuiet(flags: FmtFlags): boolean {
@@ -4059,8 +4700,9 @@ function fmtQuiet(flags: FmtFlags): boolean {
4059
4700
  // document that does not format, with the finding that says why. The
4060
4701
  // style findings go to standard error, one line each, in the shape
4061
4702
  // every linter prints: `file:line:col: rule: message`.
4062
- function fmtOne(name: string, src: string, flags: FmtFlags): number {
4063
- const report = format(src, { path: name, lint: flags.lint })
4703
+ function fmtOne(
4704
+ name: string, src: string, flags: FmtFlags, marker?: string): number {
4705
+ const report = format(src, { path: name, lint: flags.lint, template: marker })
4064
4706
  if ('error' === report.verdict) {
4065
4707
  process.stderr.write(`aontu: ${name} was not formatted\n` +
4066
4708
  report.errors.map(renderFinding).join('\n') + '\n')
@@ -4162,6 +4804,410 @@ function parseTrustArg(value: string): TrustArg | undefined {
4162
4804
  }
4163
4805
 
4164
4806
 
4807
+ // THE TEACHING PACK, SERVED FROM THE COMMAND (G11 phase 1,
4808
+ // docs/capability-review/g11-agent-onramp.md; mirrors
4809
+ // go/cmd/aontu/help.go).
4810
+ //
4811
+ // HELP documents the TOOLCHAIN and says nothing about the LANGUAGE:
4812
+ // `&`, the map template and the one construct an ontology cannot be
4813
+ // written without, occurs zero times in it, while `template` occurs
4814
+ // fourteen times and names an unrelated verb every time. docs/skill/
4815
+ // was already the right content and already gated; the gap was
4816
+ // DELIVERY, since it reached an installation as
4817
+ // node_modules/aontu/skill/ where nothing looks. ts/src/helpdoc.ts is
4818
+ // generated from those sources by ts/scripts/helpdoc.cjs and asserted
4819
+ // byte-identical with them by ts/test/helpdoc.test.ts.
4820
+
4821
+ const HELP_VERB_HELP = 'aontu help [topic] (try `aontu help` for the topics)'
4822
+ const EXPLAIN_HELP = 'aontu explain <code> (try `aontu explain --list`)'
4823
+
4824
+
4825
+ function helpIndexText(index: HelpTopic[]): string {
4826
+ const width = index.reduce((w, t) => Math.max(w, t.topic.length), 0)
4827
+ return 'aontu help <topic> — the language, offline.\n\n' +
4828
+ index.map((t) =>
4829
+ ' ' + t.topic.padEnd(width) + ' ' + t.summary).join('\n') +
4830
+ '\n\n' +
4831
+ '`aontu --help` documents the verbs, their flags and their exit\n' +
4832
+ 'codes. `aontu explain <code>` explains one error code.\n' +
4833
+ 'Start at `aontu help tasks` if you know the job but not the verb.'
4834
+ }
4835
+
4836
+
4837
+ function runHelp(argv: string[]): number {
4838
+ let format: SubsumeFormat = 'text'
4839
+ const topics: string[] = []
4840
+
4841
+ for (let i = 0; i < argv.length; i++) {
4842
+ const arg = argv[i]
4843
+ if ('-h' === arg || '--help' === arg) {
4844
+ process.stdout.write(HELP)
4845
+ return 0
4846
+ }
4847
+ else if ('--format' === arg) {
4848
+ const f = argv[++i]
4849
+ if ('text' !== f && 'json' !== f) {
4850
+ process.stderr.write('aontu: --format needs text or json\n')
4851
+ return 2
4852
+ }
4853
+ format = f
4854
+ }
4855
+ else if (arg.startsWith('-')) {
4856
+ process.stderr.write(`aontu: unknown help option ${arg} (try --help)\n`)
4857
+ return 2
4858
+ }
4859
+ else {
4860
+ topics.push(arg)
4861
+ }
4862
+ }
4863
+
4864
+ if (1 < topics.length) {
4865
+ process.stderr.write(`aontu: help takes one topic\n${HELP_VERB_HELP}\n`)
4866
+ return 2
4867
+ }
4868
+
4869
+ if (0 === topics.length) {
4870
+ process.stdout.write(('json' === format
4871
+ ? exactJSON({
4872
+ aontu: { version: version(), verb: 'help' },
4873
+ topics: HELPDOC.map(
4874
+ (t) => ({ topic: t.topic, summary: t.summary, source: t.source })),
4875
+ }, 2)
4876
+ : helpIndexText(HELPDOC)) + '\n')
4877
+ return 0
4878
+ }
4879
+
4880
+ const found = HELPDOC.find((t) => topics[0] === t.topic)
4881
+ if (null != found) {
4882
+ if ('json' === format) {
4883
+ process.stdout.write(exactJSON({
4884
+ aontu: { version: version(), verb: 'help' },
4885
+ topic: found.topic,
4886
+ summary: found.summary,
4887
+ source: found.source,
4888
+ text: found.text,
4889
+ }, 2) + '\n')
4890
+ return 0
4891
+ }
4892
+ process.stdout.write(found.text)
4893
+ return 0
4894
+ }
4895
+
4896
+ // AN UNKNOWN TOPIC IS A USAGE ERROR AND NAMES THE ALTERNATIVES,
4897
+ // because the caller who typed it has no other way to find out what
4898
+ // exists -- that is the whole condition this verb was added for.
4899
+ process.stderr.write(
4900
+ `aontu: no help topic \`${topics[0]}\`\n` +
4901
+ `aontu: topics are ${HELPDOC.map((t) => t.topic).join(', ')}\n`)
4902
+ return 2
4903
+ }
4904
+
4905
+
4906
+ // `aontu explain <code>` (G11 phase 3; mirrors
4907
+ // go/cmd/aontu/explain.go).
4908
+ //
4909
+ // THE REGISTRY IS THE LIST, NOT THE HINT TABLE. test/spec/errcodes.tsv
4910
+ // registers 157 codes and the spec suite asserts set equality between
4911
+ // the file and codeClasses IN BOTH PORTS, so listing from codeClasses
4912
+ // is listing the shared contract. The hint tables are smaller and are
4913
+ // NOT in parity -- 130 entries here against 131 in Go, the extra being
4914
+ // decimal_syntax, which this port never raises -- so listing from them
4915
+ // would make `aontu explain --list` differ between ports over a
4916
+ // difference that is not about what either port can report.
4917
+ //
4918
+ // A REGISTERED CODE WITH NO HINT ANSWERS WITH ITS CLASS AND SAYS SO.
4919
+ // Twenty-seven registered codes carry no explanation text here; before
4920
+ // this verb their absence was invisible, because a hint is only ever
4921
+ // seen beside the error that raises it.
4922
+
4923
+ // The dynamic prefixes a generated code extends (`func:upper`,
4924
+ // `op[+]`). Mirrors CODE_PREFIXES in ts/src/hints.ts, which is not
4925
+ // exported; a code that extends one is registered through its prefix
4926
+ // and carries that prefix's hint.
4927
+ const EXPLAIN_PREFIXES = ['func:', 'op:', 'op[', 'var[', 'ref[']
4928
+
4929
+
4930
+ function explainCode(
4931
+ code: string): { cls: string, hint: string, registered: boolean } {
4932
+ const cls = codeClass(code)
4933
+ let hint = hints[code] ?? ''
4934
+ let registered = null != codeClasses[code]
4935
+ if (!registered) {
4936
+ for (const prefix of EXPLAIN_PREFIXES) {
4937
+ if (code.startsWith(prefix)) {
4938
+ // No guard on `hint` here: every hint key is also a registry
4939
+ // key (the spec suite asserts codeClasses set-equal with
4940
+ // test/spec/errcodes.tsv, and hints is a subset of it), so a
4941
+ // code that reaches this loop is unregistered and therefore
4942
+ // has no hint of its own.
4943
+ registered = true
4944
+ hint = hints[prefix] ?? ''
4945
+ break
4946
+ }
4947
+ }
4948
+ }
4949
+ return { cls, hint, registered }
4950
+ }
4951
+
4952
+
4953
+ // Every code in the shared registry, sorted by code point so both
4954
+ // ports list them in the same order.
4955
+ function explainCodes(): string[] {
4956
+ return Object.keys(codeClasses).sort(cmpCodePoint)
4957
+ }
4958
+
4959
+
4960
+ function explainListText(format: SubsumeFormat): string {
4961
+ const codes = explainCodes()
4962
+ if ('json' === format) {
4963
+ return exactJSON({
4964
+ aontu: { version: version(), verb: 'explain' },
4965
+ codes: codes.map((code) => ({
4966
+ code,
4967
+ class: codeClass(code),
4968
+ // Whether this port carries explanation text for the code. The
4969
+ // registry is in parity; the hint tables are not, so a consumer
4970
+ // that wants only explained codes can filter rather than guess.
4971
+ explained: '' !== explainCode(code).hint,
4972
+ })),
4973
+ }, 2)
4974
+ }
4975
+ const width = codes.reduce((w, c) => Math.max(w, c.length), 0)
4976
+ return codes.map((c) =>
4977
+ c.padEnd(width) + ' ' + codeClass(c) +
4978
+ ('' === explainCode(c).hint ? ' (no text)' : '')).join('\n')
4979
+ }
4980
+
4981
+
4982
+ function runExplain(argv: string[]): number {
4983
+ let format: SubsumeFormat = 'text'
4984
+ let list = false
4985
+ const codes: string[] = []
4986
+
4987
+ for (let i = 0; i < argv.length; i++) {
4988
+ const arg = argv[i]
4989
+ if ('-h' === arg || '--help' === arg) {
4990
+ process.stdout.write(HELP)
4991
+ return 0
4992
+ }
4993
+ else if ('--list' === arg) {
4994
+ list = true
4995
+ }
4996
+ else if ('--format' === arg) {
4997
+ const f = argv[++i]
4998
+ if ('text' !== f && 'json' !== f) {
4999
+ process.stderr.write('aontu: --format needs text or json\n')
5000
+ return 2
5001
+ }
5002
+ format = f
5003
+ }
5004
+ else if (arg.startsWith('-')) {
5005
+ process.stderr.write(
5006
+ `aontu: unknown explain option ${arg} (try --help)\n`)
5007
+ return 2
5008
+ }
5009
+ else {
5010
+ codes.push(arg)
5011
+ }
5012
+ }
5013
+
5014
+ if (list) {
5015
+ if (0 < codes.length) {
5016
+ process.stderr.write(`aontu: --list takes no code\n${EXPLAIN_HELP}\n`)
5017
+ return 2
5018
+ }
5019
+ process.stdout.write(explainListText(format) + '\n')
5020
+ return 0
5021
+ }
5022
+
5023
+ if (1 !== codes.length) {
5024
+ process.stderr.write(`aontu: explain needs one code\n${EXPLAIN_HELP}\n`)
5025
+ return 2
5026
+ }
5027
+
5028
+ const code = codes[0]
5029
+ const { cls, hint, registered } = explainCode(code)
5030
+ if (!registered) {
5031
+ // AN UNKNOWN CODE IS A USAGE ERROR AND NAMES NEAR MATCHES. A
5032
+ // caller reading a code out of a report has almost certainly typed
5033
+ // it correctly, so the likely cause is a code from another tool or
5034
+ // a truncated one, and the near matches say which.
5035
+ process.stderr.write(`aontu: no such error code \`${code}\`\n`)
5036
+ const near = nearestVerb(code, explainCodes())
5037
+ if ('' !== near) {
5038
+ process.stderr.write(`aontu: did you mean \`${near}\`?\n`)
5039
+ }
5040
+ process.stderr.write(
5041
+ 'aontu: `aontu explain --list` lists every registered code\n')
5042
+ return 2
5043
+ }
5044
+
5045
+ if ('json' === format) {
5046
+ process.stdout.write(exactJSON({
5047
+ aontu: { version: version(), verb: 'explain' },
5048
+ code,
5049
+ class: cls,
5050
+ hint,
5051
+ }, 2) + '\n')
5052
+ return 0
5053
+ }
5054
+ // A REGISTERED CODE WITH NO HINT SAYS SO rather than printing an
5055
+ // empty block, which would read as an explanation that happened to
5056
+ // be blank.
5057
+ const body = '' === hint
5058
+ ? '(no explanation text is registered for this code)'
5059
+ : hint
5060
+ process.stdout.write(`code: ${code}\nclass: ${cls}\n\n${body}\n`)
5061
+ return 0
5062
+ }
5063
+
5064
+
5065
+ // `aontu init` (G11 phase 6,
5066
+ // docs/capability-review/g11-agent-onramp.md).
5067
+ //
5068
+ // NOT SCAFFOLDING CONVENIENCE. The agent's most expensive failure is
5069
+ // writing a FIRST document at all: the measurement that opened G11
5070
+ // found one reaching for the wildcard its neighbours use and getting
5071
+ // `verdict: valid` over data that violates it. A known-good starting
5072
+ // document turns generation into editing, which is the operation a
5073
+ // model is reliably good at.
5074
+ //
5075
+ // The trio is real, runnable and tested where it lives
5076
+ // (docs/skill/init/, run by ts/test/helpdoc.test.ts), and staged into
5077
+ // both ports by the same generator that stages the teaching pack, so
5078
+ // the two write the same bytes.
5079
+
5080
+ const INIT_HELP = 'aontu init [dir] (try --help)'
5081
+
5082
+
5083
+ function runInit(argv: string[]): number {
5084
+ const dirs: string[] = []
5085
+
5086
+ for (const arg of argv) {
5087
+ if ('-h' === arg || '--help' === arg) {
5088
+ process.stdout.write(HELP)
5089
+ return 0
5090
+ }
5091
+ if (arg.startsWith('-')) {
5092
+ process.stderr.write(`aontu: unknown init option ${arg} (try --help)\n`)
5093
+ return 2
5094
+ }
5095
+ dirs.push(arg)
5096
+ }
5097
+
5098
+ if (1 < dirs.length) {
5099
+ process.stderr.write(`aontu: init takes one directory\n${INIT_HELP}\n`)
5100
+ return 2
5101
+ }
5102
+ const dir = dirs[0] ?? '.'
5103
+
5104
+ // REFUSES TO OVERWRITE, and checks every member BEFORE writing any of
5105
+ // them: a scaffold that wrote two files and then refused the third
5106
+ // would leave a directory in a state neither the caller nor a re-run
5107
+ // can reason about.
5108
+ const standing = INITDOC.filter((f) => existsSync(join(dir, f.name)))
5109
+ if (0 < standing.length) {
5110
+ process.stderr.write(
5111
+ `aontu: ${dir} already holds ${standing.map((f) => f.name).join(', ')}\n` +
5112
+ 'aontu: init never overwrites; move them aside or name an' +
5113
+ ' empty directory\n')
5114
+ return 2
5115
+ }
5116
+
5117
+ try {
5118
+ mkdirSync(dir, { recursive: true })
5119
+ for (const f of INITDOC) {
5120
+ writeFileSync(join(dir, f.name), f.text, { mode: f.mode })
5121
+ }
5122
+ }
5123
+ catch (err: any) {
5124
+ process.stderr.write(`aontu: cannot write in ${dir}: ${err.message}\n`)
5125
+ return 2
5126
+ }
5127
+
5128
+ process.stdout.write(
5129
+ INITDOC.map((f) => join(dir, f.name)).join('\n') + '\n' +
5130
+ '\nA model, an instance of it, and the four questions to ask.\n' +
5131
+ 'Run the checks: sh ' + join(dir, 'check.sh') + '\n' +
5132
+ 'Learn the language: aontu help language\n')
5133
+ return 0
5134
+ }
5135
+
5136
+
5137
+ // EVERY VERB THIS PORT DISPATCHES, for the nearest-verb suggestion
5138
+ // G11 phase 2 prints. A separate list from the if-chain in main()
5139
+ // because the chain's arms have three different shapes and cannot be
5140
+ // a table; ts/test/cli-help.test.ts keeps the two from drifting by
5141
+ // running each name and requiring it not to fall through to the bare
5142
+ // command.
5143
+ const KNOWN_VERBS = [
5144
+ 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
5145
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
5146
+ 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
5147
+ 'view', 'why',
5148
+ ]
5149
+
5150
+
5151
+ // looksLikeVerb reports whether an unreadable argument was meant as a
5152
+ // verb rather than as a path. A bare word has no separator and no
5153
+ // extension; `./help`, `help.aon`, `/tmp/help` and `sub/dir` are paths
5154
+ // and keep the file diagnosis. Mirrors go/cmd/aontu/main.go.
5155
+ function looksLikeVerb(arg: string): boolean {
5156
+ return '' !== arg &&
5157
+ !/[/\\.]/.test(arg) &&
5158
+ !arg.startsWith('-')
5159
+ }
5160
+
5161
+
5162
+ // NEAREST-VERB SUGGESTION (G11 phase 2). Restricted
5163
+ // Damerau-Levenshtein with a cap that grows with the word and stops at
5164
+ // three: one edit is a convincing suggestion on any length, three is
5165
+ // the most that can be believed on a long one, and an UNCAPPED
5166
+ // nearest match on a three-letter typo names something unrelated with
5167
+ // confidence. Mirrors go/cmd/aontu/help.go, including the sort, so
5168
+ // the two ports suggest the same verb on a tie.
5169
+ function nearestVerb(word: string, verbs: string[]): string {
5170
+ let best = ''
5171
+ let bestDist = Infinity
5172
+ const limit = Math.min(3, 1 + Math.floor(word.length / 4))
5173
+ for (const v of [...verbs].sort(cmpCodePoint)) {
5174
+ const d = editDistance(word.toLowerCase(), v)
5175
+ if (d < bestDist) {
5176
+ best = v
5177
+ bestDist = d
5178
+ }
5179
+ }
5180
+ return bestDist > limit ? '' : best
5181
+ }
5182
+
5183
+
5184
+ // Levenshtein with a transposition, iterative over two rows. Mirrors
5185
+ // editDistance in go/cmd/aontu/help.go exactly.
5186
+ function editDistance(a: string, b: string): number {
5187
+ const ar = [...a]
5188
+ const br = [...b]
5189
+ let prev2 = new Array(br.length + 1).fill(0)
5190
+ let prev = new Array(br.length + 1).fill(0).map((_, j) => j)
5191
+ let cur = new Array(br.length + 1).fill(0)
5192
+ for (let i = 1; i <= ar.length; i++) {
5193
+ cur[0] = i
5194
+ for (let j = 1; j <= br.length; j++) {
5195
+ const cost = ar[i - 1] === br[j - 1] ? 0 : 1
5196
+ let m = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
5197
+ if (1 < i && 1 < j &&
5198
+ ar[i - 1] === br[j - 2] && ar[i - 2] === br[j - 1] &&
5199
+ prev2[j - 2] + 1 < m) {
5200
+ m = prev2[j - 2] + 1
5201
+ }
5202
+ cur[j] = m
5203
+ }
5204
+ prev2 = [...prev]
5205
+ prev = [...cur]
5206
+ }
5207
+ return prev[br.length]
5208
+ }
5209
+
5210
+
4165
5211
  function main(argv: string[], servers: Servers = SERVERS): void {
4166
5212
  // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
4167
5213
  // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
@@ -4172,6 +5218,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4172
5218
  setColor(true === process.stderr.isTTY ? undefined : false)
4173
5219
 
4174
5220
  let mode: Mode = 'json'
5221
+ // THE REPORT FORM (G11 phase 7), default text: every existing caller
5222
+ // reads exactly what it always read, and a caller that asks for json
5223
+ // gets the failure in the finding shape every other verb reports.
5224
+ let format: EvalFormat = 'text'
4175
5225
  // A LIST, though the bare command evaluates exactly one document.
4176
5226
  // It used to be one variable and the last argument won, which made a
4177
5227
  // MISTYPED VERB a silent success: `aontu vet2 schema.aon good.json`
@@ -4224,6 +5274,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4224
5274
  return finish(runSet(argv.slice(3)))
4225
5275
  }
4226
5276
 
5277
+ if ('allow' === argv[2]) {
5278
+ return finish(runAllow(argv.slice(3)))
5279
+ }
5280
+
4227
5281
  if ('why' === argv[2]) {
4228
5282
  return finish(runWhy(argv.slice(3)))
4229
5283
  }
@@ -4236,6 +5290,20 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4236
5290
  return finish(runHash(argv.slice(3)))
4237
5291
  }
4238
5292
 
5293
+ // G11 phases 1 and 3. Dispatched with the rest, so `aontu ./help`
5294
+ // still reads a file named help exactly as `aontu ./vet` does.
5295
+ if ('help' === argv[2]) {
5296
+ return finish(runHelp(argv.slice(3)))
5297
+ }
5298
+
5299
+ if ('explain' === argv[2]) {
5300
+ return finish(runExplain(argv.slice(3)))
5301
+ }
5302
+
5303
+ if ('init' === argv[2]) {
5304
+ return finish(runInit(argv.slice(3)))
5305
+ }
5306
+
4239
5307
  if ('mod' === argv[2]) {
4240
5308
  return finish(runMod(argv.slice(3)))
4241
5309
  }
@@ -4291,6 +5359,14 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4291
5359
  }
4292
5360
  trust = parsed
4293
5361
  }
5362
+ else if ('--format' === arg) {
5363
+ const f = args[++i]
5364
+ if ('text' !== f && 'json' !== f) {
5365
+ process.stderr.write('aontu: --format needs text or json\n')
5366
+ return finish(2)
5367
+ }
5368
+ format = f
5369
+ }
4294
5370
  else if ('--jsonl' === arg) {
4295
5371
  jsonl = true
4296
5372
  // A JSONL answer is machine-read by definition, even when the
@@ -4350,7 +5426,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4350
5426
 
4351
5427
  const file = files[0]
4352
5428
  if (null != file) {
4353
- finish(runFile(file, mode, trust))
5429
+ finish(runFile(file, mode, format, trust))
4354
5430
  }
4355
5431
  // `--jsonl` overrides the TTY gate: the mode exists to be DRIVEN by
4356
5432
  // a harness over a pipe, so gating it on an interactive terminal
@@ -4360,9 +5436,9 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4360
5436
  runRepl(mode, jsonl, trust)
4361
5437
  }
4362
5438
  else {
4363
- runStdin(mode, trust).then((code) => finish(code))
5439
+ runStdin(mode, format, trust).then((code) => finish(code))
4364
5440
  }
4365
- } /* node:coverage ignore next 18 */
5441
+ } /* node:coverage ignore next 20 */
4366
5442
 
4367
5443
 
4368
5444
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -4377,7 +5453,9 @@ export {
4377
5453
  runRender,
4378
5454
  runTemplate,
4379
5455
  runMod,
4380
- runHash, runGet,
4381
- runWhy, renderWhyText, runSet, runAgentsMd, runFmt,
5456
+ runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
5457
+ looksLikeVerb,
5458
+ KNOWN_VERBS,
5459
+ runWhy, renderWhyText, runSet, runAllow, runAgentsMd, runFmt,
4382
5460
  watchChange, watchSignature, vetWaiter, deprecatedAt,
4383
5461
  }