aontu 0.54.0 → 0.56.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 (84) hide show
  1. package/README.md +3 -3
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +10 -3
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/aontu.d.ts +3 -2
  6. package/dist/aontu.js +5 -2
  7. package/dist/aontu.js.map +1 -1
  8. package/dist/cli.d.ts +5 -2
  9. package/dist/cli.js +310 -50
  10. package/dist/cli.js.map +1 -1
  11. package/dist/diff.d.ts +1 -0
  12. package/dist/diff.js +3 -2
  13. package/dist/diff.js.map +1 -1
  14. package/dist/format.d.ts +17 -0
  15. package/dist/format.js +1000 -0
  16. package/dist/format.js.map +1 -0
  17. package/dist/hints.js +8 -0
  18. package/dist/hints.js.map +1 -1
  19. package/dist/jsonschema.d.ts +1 -0
  20. package/dist/jsonschema.js +3 -2
  21. package/dist/jsonschema.js.map +1 -1
  22. package/dist/lang.js +64 -5
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lsp.d.ts +2 -1
  25. package/dist/lsp.js +1 -1
  26. package/dist/lsp.js.map +1 -1
  27. package/dist/mcp.js +25 -3
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/patch.d.ts +1 -0
  30. package/dist/patch.js +1 -0
  31. package/dist/patch.js.map +1 -1
  32. package/dist/query.d.ts +1 -0
  33. package/dist/query.js +4 -3
  34. package/dist/query.js.map +1 -1
  35. package/dist/reach.d.ts +1 -0
  36. package/dist/reach.js +3 -2
  37. package/dist/reach.js.map +1 -1
  38. package/dist/relation.d.ts +1 -0
  39. package/dist/relation.js +3 -2
  40. package/dist/relation.js.map +1 -1
  41. package/dist/std.js +5 -1
  42. package/dist/std.js.map +1 -1
  43. package/dist/subsume.d.ts +1 -0
  44. package/dist/subsume.js +3 -2
  45. package/dist/subsume.js.map +1 -1
  46. package/dist/trim.d.ts +1 -0
  47. package/dist/trim.js +3 -2
  48. package/dist/trim.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/dist/type.d.ts +1 -0
  51. package/dist/type.js.map +1 -1
  52. package/dist/utility.d.ts +8 -2
  53. package/dist/utility.js +9 -1
  54. package/dist/utility.js.map +1 -1
  55. package/dist/vet.d.ts +2 -0
  56. package/dist/vet.js +6 -1
  57. package/dist/vet.js.map +1 -1
  58. package/dist/view.d.ts +7 -1
  59. package/dist/view.js +655 -64
  60. package/dist/view.js.map +1 -1
  61. package/grammar/aontu.abnf +155 -0
  62. package/package.json +2 -1
  63. package/skill/grammar-card.md +5 -2
  64. package/src/agentsmd.ts +15 -3
  65. package/src/aontu.ts +7 -1
  66. package/src/cli.ts +350 -56
  67. package/src/diff.ts +7 -2
  68. package/src/format.ts +1146 -0
  69. package/src/hints.ts +9 -0
  70. package/src/jsonschema.ts +7 -1
  71. package/src/lang.ts +69 -5
  72. package/src/lsp.ts +5 -3
  73. package/src/mcp.ts +26 -3
  74. package/src/patch.ts +7 -0
  75. package/src/query.ts +8 -4
  76. package/src/reach.ts +7 -2
  77. package/src/relation.ts +7 -2
  78. package/src/std.ts +5 -1
  79. package/src/subsume.ts +7 -2
  80. package/src/trim.ts +7 -2
  81. package/src/type.ts +8 -0
  82. package/src/utility.ts +27 -2
  83. package/src/vet.ts +14 -4
  84. package/src/view.ts +817 -65
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ exports.runWhy = runWhy;
20
20
  exports.renderWhyText = renderWhyText;
21
21
  exports.runSet = runSet;
22
22
  exports.runAgentsMd = runAgentsMd;
23
+ exports.runFmt = runFmt;
23
24
  exports.watchChange = watchChange;
24
25
  exports.watchSignature = watchSignature;
25
26
  exports.deprecatedAt = deprecatedAt;
@@ -46,6 +47,8 @@ const vet_1 = require("./vet");
46
47
  const reach_1 = require("./reach");
47
48
  const view_1 = require("./view");
48
49
  const agentsmd_1 = require("./agentsmd");
50
+ const format_1 = require("./format");
51
+ const utility_1 = require("./utility");
49
52
  const HELP = `Usage: aontu [options] [file]
50
53
  aontu vet [options] <schema> <data> [more-data...]
51
54
  aontu subsume [options] <general> <specific>
@@ -62,6 +65,7 @@ const HELP = `Usage: aontu [options] [file]
62
65
  aontu why <path> [options] <file>
63
66
  aontu set <path>=<value>... --entry <file> --overlay <file>
64
67
  aontu agentsmd [--write <AGENTS.md>] <file>
68
+ aontu fmt [-w|-l|--check|-d] <file>...
65
69
 
66
70
  Evaluate an Aontu source file and print the result as JSON.
67
71
  With no file on an interactive terminal, start a REPL.
@@ -84,6 +88,10 @@ Options:
84
88
  Every verb takes it too, and a bare root means the
85
89
  document's own directory
86
90
  --include-root <dir> Shorthand for --trust root:<dir>
91
+ --text-ext <e> Read these extensions as text too, comma-separated
92
+ and without dots (md,sql). .txt needs no flag; a
93
+ named format keeps its meaning, and .js stays
94
+ refused. Every verb takes it
87
95
 
88
96
  Mod options:
89
97
  --format <f> text (default) or json
@@ -175,21 +183,22 @@ Why options:
175
183
  Why exit codes mirror get's: 0 explained, 1 the path names nothing,
176
184
  2 usage, 4 the document does not stand up on its own.
177
185
 
178
- View kinds: tree, matrix, graph, layer, sets, layers, ladder, poset
179
- (the poset takes several files). The figure goes to stdout, the loss
186
+ View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
187
+ ladder, poset (the poset takes several files). The figure goes to stdout, the loss
180
188
  report to stderr. With --views it draws every figure a document
181
189
  declares as data, from one evaluation: each declaration names its own
182
190
  kind and out file, nothing is written unless every figure rendered,
183
191
  and --check gates the committed set.
184
192
 
185
193
  View options:
186
- --as <profile> text | mermaid | dot | er | svg, per kind: tree,
187
- matrix, sets and layers draw text (default) or
188
- svg; graph draws mermaid (default), dot or er;
189
- layer draws text (default), mermaid or svg; ladder
190
- and poset draw mermaid (default) or dot
194
+ --as <profile> text | mermaid | dot | er | svg, per kind: doc,
195
+ lattice, tree, matrix, sets and layers draw text
196
+ (default) or svg; graph draws mermaid (default),
197
+ dot or er; layer draws text (default), mermaid or
198
+ svg; ladder and poset draw mermaid (default) or dot
191
199
  --at <path> Restrict the figure to nodes under this path; the
192
- path the ladder draws; where the poset compares
200
+ subtree doc draws; the subtree the lattice counts;
201
+ the path the ladder draws; where the poset compares
193
202
  --views <path> Draw every figure the document declares at this
194
203
  path, one evaluation, all or nothing; each
195
204
  declaration names its own kind and out file
@@ -198,7 +207,20 @@ View options:
198
207
  nothing is written
199
208
  --strict Exit 1 when the loss report holds anything beyond
200
209
  edges_deduped, inverse_suppressed and crossings
210
+ --depth <n> doc: how many levels of key to draw (default 3)
201
211
  --max-rows <n> Refuse a figure above this many rows (default 60)
212
+ --style <s> auto (default), none, ansi or css. A figure's
213
+ marks carry their meaning -- a direct cell, a
214
+ closure cell, an upward edge -- and each profile
215
+ has one way to show it: SGR escapes for text, CSS
216
+ classes for svg. auto picks that mechanism where
217
+ the destination can carry it: escapes only on a
218
+ terminal (NO_COLOR is honoured), and an svg keeps
219
+ the stylesheet that makes it standalone. none
220
+ drops both; on svg the classes stay and only the
221
+ stylesheet goes, for a host page that has already
222
+ bound --av-ink and its kin. Escapes are never
223
+ written to a file
202
224
  --format <f> text (default) or json, the whole report
203
225
  --relation <n> tree, matrix, layer: draw over this relation only;
204
226
  graph: keep this predicate (repeatable)
@@ -255,6 +277,19 @@ Agentsmd options:
255
277
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
256
278
  stand up on its own.
257
279
 
280
+ Fmt options:
281
+ -w, --write Rewrite each file in place, when its form would change
282
+ -l, --list Print the name of each file whose form would change
283
+ --check Like --list, and exit 1 when any would: the CI gate
284
+ -d, --diff Print a unified diff for each file whose form would
285
+ change
286
+
287
+ The fmt verb prints one document in the agreed form; with no file it
288
+ reads standard input. Several files need one of the options above.
289
+
290
+ Fmt exit codes: 0 formatted or clean, 1 a --check file would change,
291
+ 2 usage, 4 a document does not parse.
292
+
258
293
  REPL commands:
259
294
  :help Show REPL help
260
295
  :load <file> Evaluate a document and hold it for the commands below
@@ -319,15 +354,16 @@ function makeTrustWarn() {
319
354
  // entryRoot (the entry file's directory, or the working directory for
320
355
  // stdin/REPL).
321
356
  function trustOpts(trust, entryRoot) {
357
+ const text = 0 === trust.textExt.length ? {} : { textExt: trust.textExt };
322
358
  switch (trust.kind) {
323
359
  case 'none':
324
- return { trust: { include: 'none' } };
360
+ return { ...text, trust: { include: 'none' } };
325
361
  case 'root':
326
- return { trust: { include: { root: trust.dir ?? entryRoot } } };
362
+ return { ...text, trust: { include: { root: trust.dir ?? entryRoot } } };
327
363
  case 'system':
328
- return {};
364
+ return { ...text };
329
365
  default: // system-warn: today's default plus the warning window
330
- return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
366
+ return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
331
367
  }
332
368
  }
333
369
  // EVERY VERB honours the include capability, not just the bare
@@ -342,7 +378,8 @@ function trustOpts(trust, entryRoot) {
342
378
  // already printed: the caller answers the usage class.
343
379
  function takeTrust(argv) {
344
380
  const rest = [];
345
- let trust = { kind: 'system-warn' };
381
+ let trust = { kind: 'system-warn', textExt: [] };
382
+ let textExt = [];
346
383
  for (let i = 0; i < argv.length; i++) {
347
384
  const arg = argv[i];
348
385
  if ('--trust' === arg) {
@@ -359,18 +396,42 @@ function takeTrust(argv) {
359
396
  process.stderr.write('aontu: --include-root needs a directory\n');
360
397
  return undefined;
361
398
  }
362
- trust = { kind: 'root', dir };
399
+ trust = { kind: 'root', dir, textExt };
400
+ }
401
+ else if ('--text-ext' === arg) {
402
+ const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i]);
403
+ if (null == list) {
404
+ process.stderr.write('aontu: --text-ext needs extensions, without dots' +
405
+ ' (--text-ext md,sql)\n');
406
+ return undefined;
407
+ }
408
+ textExt = [...textExt, ...list];
363
409
  }
364
410
  else {
365
411
  rest.push(arg);
366
412
  }
367
413
  }
368
- return { argv: rest, trust };
414
+ return { argv: rest, trust: { ...trust, textExt } };
415
+ }
416
+ // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
417
+ // reader who has just written `@"notes.txt"` reaches for one. An empty
418
+ // element, or anything that is not an extension, is a usage error
419
+ // rather than a silently ignored word: a flag that quietly does
420
+ // nothing is how a document ends up refused with no reason visible.
421
+ function parseTextExt(arg) {
422
+ const out = [];
423
+ for (const raw of arg.split(',')) {
424
+ const ext = raw.trim().replace(/^\./, '').toLowerCase();
425
+ if ('' === ext || !/^[a-z0-9]+$/.test(ext)) {
426
+ return undefined;
427
+ }
428
+ out.push(ext);
429
+ }
430
+ return out;
369
431
  }
370
432
  // The evaluator options a REPL session's capability means.
371
433
  function replTrust(state, entryRoot) {
372
- const capability = verbTrust(state.trust ?? { kind: 'system-warn' }, entryRoot);
373
- return null == capability ? {} : { trust: capability };
434
+ return verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRoot);
374
435
  }
375
436
  // The capability a verb's engine runs under. `system` and the staged
376
437
  // warning default both mean today's behaviour (no option); the warning
@@ -386,6 +447,18 @@ function verbTrust(trust, entryRoot) {
386
447
  return undefined;
387
448
  }
388
449
  }
450
+ // THE INCLUDE OPTIONS A VERB RUNS UNDER, spread into its engine call:
451
+ // the capability above, and the extensions `--text-ext` widened. Both
452
+ // are absent when unset rather than present-and-undefined, so a verb's
453
+ // options bag is byte-identical to what it was before either flag
454
+ // existed and no engine sees a key it has to ignore.
455
+ function verbOpts(trust, entryRoot) {
456
+ const include = verbTrust(trust, entryRoot);
457
+ return {
458
+ ...(undefined === include ? {} : { trust: include }),
459
+ ...(0 === trust.textExt.length ? {} : { textExt: trust.textExt }),
460
+ };
461
+ }
389
462
  // The directory a bare `--trust root` confines to for a verb: the
390
463
  // primary document's own, matching the bare command's entry root.
391
464
  function entryRootOf(file) {
@@ -491,7 +564,7 @@ function replCommand(state, line, read) {
491
564
  if (':why' === cmd) {
492
565
  const report = (0, aontu_1.why)(src, path, {
493
566
  path: state.name,
494
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
567
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
495
568
  });
496
569
  return report.ok
497
570
  ? answer(renderWhyText(report.record))
@@ -501,7 +574,7 @@ function replCommand(state, line, read) {
501
574
  ? 'keys' : 'canon' === state.mode ? 'canon' : 'json';
502
575
  const report = (0, aontu_1.get)(src, path, {
503
576
  view, path: state.name,
504
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
577
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
505
578
  });
506
579
  return report.ok
507
580
  ? answer(report.out)
@@ -732,7 +805,7 @@ function vetOnce(args, trust) {
732
805
  const findings = [];
733
806
  for (const source of sources) {
734
807
  const report = (0, aontu_1.vet)(schemaSrc, source.src, {
735
- trust: verbTrust(trust, entryRootOf(args.schema)),
808
+ ...verbOpts(trust, entryRootOf(args.schema)),
736
809
  at: args.at,
737
810
  closed: args.closed,
738
811
  partial: args.partial,
@@ -971,7 +1044,7 @@ function runSubsume(argv) {
971
1044
  return 2;
972
1045
  }
973
1046
  const report = (0, aontu_1.subsume)(generalSrc, specificSrc, {
974
- trust: verbTrust(trust, entryRootOf(args.general)),
1047
+ ...verbOpts(trust, entryRootOf(args.general)),
975
1048
  profile: args.profile,
976
1049
  at: args.at,
977
1050
  generalUrl: args.general,
@@ -1151,15 +1224,17 @@ function oldVersion(spec, file) {
1151
1224
  // The document's own compatibility declaration: `$.aontu_policy.compat`,
1152
1225
  // a disjunction whose default is the declared mode. Undefined when the
1153
1226
  // key is absent or does not spell a mode.
1154
- function policyCompat(newSrc, path, trust) {
1227
+ function policyCompat(newSrc, path, include) {
1155
1228
  const aontu = new aontu_1.Aontu();
1156
1229
  const ctx = aontu.ctx({ collect: true });
1157
1230
  // The declaration is read by EVALUATING the document, so this leg
1158
- // runs the include resolver too and has to run it under the verb's
1159
- // capability -- a `breaking --trust none` that read its own mode
1160
- // through an unconfined resolver would confine the comparison and
1161
- // not the question (use-cases/REVIEW.md finding G).
1162
- const v = aontu.unify(newSrc, { path, ...(null == trust ? {} : { trust }) }, ctx);
1231
+ // runs the include resolver too and has to run it under BOTH of the
1232
+ // verb's include options -- a `breaking --trust none` that read its
1233
+ // own mode through an unconfined resolver would confine the
1234
+ // comparison and not the question (use-cases/REVIEW.md finding G),
1235
+ // and one that took the capability alone read no mode at all when
1236
+ // the declaration arrived through a `--text-ext` include.
1237
+ const v = aontu.unify(newSrc, { path, ...(0, utility_1.includeOpts)(include) }, ctx);
1163
1238
  if (0 < ctx.err.length || true === v?.isNil) {
1164
1239
  return undefined;
1165
1240
  }
@@ -1252,7 +1327,7 @@ function runBreaking(argv) {
1252
1327
  // neither means backward, the index's framing (v1-valid documents
1253
1328
  // stay valid).
1254
1329
  const mode = args.mode ??
1255
- policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
1330
+ policyCompat(newSrc, args.file, verbOpts(trust, entryRootOf(args.file))) ??
1256
1331
  'backward';
1257
1332
  if ('none' === mode) {
1258
1333
  // The document declares no compatibility promise: nothing to check.
@@ -1295,7 +1370,7 @@ function runBreaking(argv) {
1295
1370
  const oldPath = old.path;
1296
1371
  for (const check of checks) {
1297
1372
  const report = (0, aontu_1.subsume)(check.general[0], check.specific[0], {
1298
- trust: verbTrust(trust, entryRootOf(args.file)),
1373
+ ...verbOpts(trust, entryRootOf(args.file)),
1299
1374
  at: args.at,
1300
1375
  generalUrl: check.general[1],
1301
1376
  specificUrl: check.specific[1],
@@ -1425,7 +1500,7 @@ function runTrim(argv) {
1425
1500
  return 2;
1426
1501
  }
1427
1502
  const report = (0, aontu_1.trimCheck)(src, {
1428
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1503
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1429
1504
  });
1430
1505
  const text = 'json' === format
1431
1506
  ? renderTrimJson(report)
@@ -1476,9 +1551,40 @@ const REACHES_EXIT = {
1476
1551
  error: 4,
1477
1552
  };
1478
1553
  const VIEW_HELP = 'aontu view <kind> [options] <file>... (try --help)';
1479
- const VIEW_KINDS = ['tree', 'matrix', 'graph', 'layer', 'sets', 'layers', 'ladder', 'poset'];
1554
+ const VIEW_KINDS = ['doc', 'lattice', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers',
1555
+ 'ladder', 'poset'];
1480
1556
  const VIEW_PROFILES = ['text', 'mermaid', 'dot', 'er', 'svg'];
1481
1557
  const VIEW_EDGES = ['upward', 'all', 'none'];
1558
+ // The styles the CLI accepts (VIEWS.0.md, "7. Styling"). `auto` is
1559
+ // here and NOT in ViewStyle: resolving it means knowing whether stdout
1560
+ // is a terminal, which is the CLI's to know and the library's never --
1561
+ // the same division err.ts already draws for the error frames.
1562
+ const VIEW_STYLES = ['auto', 'none', 'ansi', 'css'];
1563
+ // `--style auto` resolved, which only the CLI can do. The mechanism is
1564
+ // the PROFILE's and the library knows it -- an SVG carries its
1565
+ // stylesheet unless told not to, which is what makes a figure stand
1566
+ // alone. What the library cannot know is whether the DESTINATION is a
1567
+ // terminal, so that is the only thing decided here: escapes on the
1568
+ // text profile when stdout is a terminal and NO_COLOR is unset, the
1569
+ // same two conditions the error frames use. `undefined` leaves the
1570
+ // profile's own default in place.
1571
+ function viewStyleOf(asked, as) {
1572
+ if (undefined !== asked && 'auto' !== asked) {
1573
+ return asked;
1574
+ }
1575
+ // STDOUT'S OWN TERMINAL-NESS, and NO_COLOR read here rather than
1576
+ // through colorActive(). The figure goes to STDOUT and the error
1577
+ // frames go to STDERR, and they are not the same destination: main()
1578
+ // has already called setColor for stderr, so asking colorActive()
1579
+ // would answer the wrong question twice --- no escapes for
1580
+ // `aontu view tree m.aon 2>/dev/null` at a terminal, and escapes
1581
+ // into the pipe for `aontu view tree m.aon | less`. The NO_COLOR
1582
+ // rule is the one no-color.org states and err.ts implements:
1583
+ // set, to anything but empty, means no colour.
1584
+ const no = process.env.NO_COLOR;
1585
+ return 'text' === as && true === process.stdout.isTTY
1586
+ && (null == no || '' === no) ? 'ansi' : undefined;
1587
+ }
1482
1588
  // The figure was drawn (0, `lossy` included: the loss report says
1483
1589
  // what it could not draw, and --strict is the gate on that), or the
1484
1590
  // document could not be drawn (4). An EMPTY figure is a drawing, not
@@ -1493,7 +1599,7 @@ const VIEW_EXIT = {
1493
1599
  const VIEW_USAGE_CODES = [
1494
1600
  'view_kind_unknown', 'view_profile_unknown', 'view_rows_exceeded',
1495
1601
  'view_at_required', 'view_sets_required', 'view_group_required',
1496
- 'view_document_shape',
1602
+ 'view_document_shape', 'view_style_profile', 'view_style_unknown',
1497
1603
  ];
1498
1604
  const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)';
1499
1605
  // The module tooling (G6 phase 3, ts/src/mod-tool.ts). All LOCAL:
@@ -1552,7 +1658,8 @@ function runMod(argv) {
1552
1658
  const dir = rest[1] ?? '.';
1553
1659
  if ('get' === sub || 'publish' === sub) {
1554
1660
  process.stderr.write('aontu: mod ' + sub + ' needs a registry client, which this build ' +
1555
- 'does not ship (docs/capability-review/g6-distribution.md)\n');
1661
+ 'does not ship; vendor the module by hand and run ' +
1662
+ "'aontu mod tidy'\n");
1556
1663
  return 2;
1557
1664
  }
1558
1665
  if (!MOD_SUBS.includes(sub) || 2 < rest.length) {
@@ -1718,7 +1825,7 @@ function runRelations(argv) {
1718
1825
  return 2;
1719
1826
  }
1720
1827
  const report = (0, aontu_1.relationCheck)(src, {
1721
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1828
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1722
1829
  });
1723
1830
  const text = 'json' === format
1724
1831
  ? renderRelationsJson(report)
@@ -1779,7 +1886,7 @@ function runReaches(argv) {
1779
1886
  }
1780
1887
  const report = (0, reach_1.reachCheck)(src, rest[0], rest[1], {
1781
1888
  path: rest[2], relation,
1782
- trust: verbTrust(trust, entryRootOf(rest[2])),
1889
+ ...verbOpts(trust, entryRootOf(rest[2])),
1783
1890
  });
1784
1891
  const text = 'json' === format
1785
1892
  ? renderReachesJson(report)
@@ -1826,6 +1933,9 @@ function runView(argv) {
1826
1933
  const relations = [];
1827
1934
  const roots = [];
1828
1935
  const opts = {};
1936
+ // The style ASKED FOR, which may be `auto` -- a word ViewStyle does
1937
+ // not have, because resolving it is the CLI's job.
1938
+ let style = undefined;
1829
1939
  // A flag that takes a value, read into `opts` by name.
1830
1940
  const valued = {
1831
1941
  '--as': 'as', '--at': 'at', '--order': 'order', '--group-by': 'groupBy',
@@ -1836,6 +1946,7 @@ function runView(argv) {
1836
1946
  const counted = {
1837
1947
  '--max-rows': 'maxRows', '--max-cols': 'maxCols',
1838
1948
  '--min-degree': 'minDegree', '--min-size': 'minSize',
1949
+ '--depth': 'depth',
1839
1950
  };
1840
1951
  for (let i = 0; i < argv.length; i++) {
1841
1952
  const arg = argv[i];
@@ -1874,6 +1985,13 @@ function runView(argv) {
1874
1985
  return 2;
1875
1986
  }
1876
1987
  }
1988
+ else if ('--style' === arg) {
1989
+ style = argv[++i];
1990
+ if (null == style || !VIEW_STYLES.includes(style)) {
1991
+ process.stderr.write(`aontu: --style needs one of ${VIEW_STYLES.join(', ')}\n`);
1992
+ return 2;
1993
+ }
1994
+ }
1877
1995
  else if ('--check' === arg) {
1878
1996
  check = true;
1879
1997
  }
@@ -1915,9 +2033,24 @@ function runView(argv) {
1915
2033
  rest.push(arg);
1916
2034
  }
1917
2035
  }
2036
+ // ESCAPES NEVER GO INTO A FILE. A pinned golden holding terminal
2037
+ // control codes is not a golden anybody can read, and a byte
2038
+ // comparison against one would fail on the reader's terminal
2039
+ // settings. `auto` resolves to `none` there on its own; asking for
2040
+ // `ansi` explicitly is a usage error rather than a silent downgrade,
2041
+ // so a script that wanted colour is told where it went.
2042
+ if ('ansi' === style && (undefined !== out || undefined !== opts.views)) {
2043
+ process.stderr.write('aontu: --style ansi writes to a terminal, not to a file\n');
2044
+ return 2;
2045
+ }
1918
2046
  // THE VIEW DOCUMENT draws every figure a document declares, so it
1919
2047
  // names no kind: the declarations do, one each.
1920
2048
  if (undefined !== opts.views) {
2049
+ // A declaration names its own profile, so the style is left to
2050
+ // each figure's own default; `--style none` still reaches every
2051
+ // one of them, which is how a host page that binds the CSS
2052
+ // variables asks for eight figures without eight stylesheets.
2053
+ opts.style = viewStyleOf(style, undefined);
1921
2054
  return runViewSet(rest, opts, trust, { format, check, strict, out });
1922
2055
  }
1923
2056
  if (2 > rest.length) {
@@ -1976,10 +2109,11 @@ function runView(argv) {
1976
2109
  }
1977
2110
  const report = (0, view_1.view)(srcs[0], {
1978
2111
  ...opts,
2112
+ style: viewStyleOf(style, opts.as ?? (0, view_1.viewDefaultProfile)(kind)),
1979
2113
  kind,
1980
2114
  path: files[0],
1981
2115
  roots,
1982
- trust: verbTrust(trust, entryRootOf(files[0])),
2116
+ ...verbOpts(trust, entryRootOf(files[0])),
1983
2117
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
1984
2118
  });
1985
2119
  if ('json' === format) {
@@ -2049,7 +2183,7 @@ function runViewSet(rest, opts, trust, how) {
2049
2183
  return 2;
2050
2184
  }
2051
2185
  const report = (0, view_1.viewSet)(src, {
2052
- ...opts, path: file, trust: verbTrust(trust, entryRootOf(file)),
2186
+ ...opts, path: file, ...verbOpts(trust, entryRootOf(file)),
2053
2187
  });
2054
2188
  if ('json' === how.format) {
2055
2189
  process.stdout.write(renderViewSetJson(report) + '\n');
@@ -2246,7 +2380,7 @@ function runJsonSchema(argv) {
2246
2380
  return 2;
2247
2381
  }
2248
2382
  const report = (0, jsonschema_1.jsonSchema)(src, {
2249
- at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2383
+ at, path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2250
2384
  });
2251
2385
  if ('json' === format) {
2252
2386
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2329,8 +2463,7 @@ function runHash(argv) {
2329
2463
  }
2330
2464
  // The file's own directory is the include base, as every verb
2331
2465
  // resolves a named file (vet's aontuForPath rule).
2332
- const capability = verbTrust(trust, entryRootOf(files[0]));
2333
- const aontu = new aontu_1.Aontu(null == capability ? undefined : { trust: capability });
2466
+ const aontu = new aontu_1.Aontu(verbOpts(trust, entryRootOf(files[0])));
2334
2467
  const ctx = aontu.ctx({ collect: true });
2335
2468
  const v = aontu.unify(src, { path: files[0] }, ctx);
2336
2469
  if (0 < ctx.err.length || true === v?.isNil) {
@@ -2434,7 +2567,7 @@ function runGet(argv) {
2434
2567
  return 2;
2435
2568
  }
2436
2569
  const report = (0, aontu_1.get)(src, path, {
2437
- view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
2570
+ view, depth, path: file, ...verbOpts(trust, entryRootOf(file)),
2438
2571
  });
2439
2572
  if ('json' === format) {
2440
2573
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2509,7 +2642,7 @@ function runWhy(argv) {
2509
2642
  return 2;
2510
2643
  }
2511
2644
  const report = (0, aontu_1.why)(src, path, {
2512
- path: file, trust: verbTrust(trust, entryRootOf(file)),
2645
+ path: file, ...verbOpts(trust, entryRootOf(file)),
2513
2646
  });
2514
2647
  if ('json' === format) {
2515
2648
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2630,7 +2763,7 @@ function runSet(argv) {
2630
2763
  }
2631
2764
  }
2632
2765
  const report = (0, aontu_1.patch)(entrySrc, overlaySrc, assignments, {
2633
- trust: verbTrust(trust, entryRootOf(entry)),
2766
+ ...verbOpts(trust, entryRootOf(entry)),
2634
2767
  entryPath: entry,
2635
2768
  overlayPath: overlayFile,
2636
2769
  inPlace,
@@ -2750,7 +2883,7 @@ function runAgentsMd(argv) {
2750
2883
  }
2751
2884
  const report = (0, aontu_1.agentsMd)(src, {
2752
2885
  name: files[0], path: files[0],
2753
- trust: verbTrust(trust, entryRootOf(files[0])),
2886
+ ...verbOpts(trust, entryRootOf(files[0])),
2754
2887
  });
2755
2888
  if (!report.ok) {
2756
2889
  process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
@@ -2796,6 +2929,116 @@ function runAgentsMd(argv) {
2796
2929
  // on the parity-probe discipline in AGENTS.md, which derives expected
2797
2930
  // spec values by piping BOTH CLIs and comparing. A truncated pipe there
2798
2931
  // reads as a port divergence.
2932
+ // ---------------------------------------------------------------------
2933
+ // The source formatter (docs/design/FMT.0.md): one agreed form, in the
2934
+ // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
2935
+ // the form itself is the library's (ts/src/format.ts), and the two
2936
+ // ports agree on it row by row in test/spec/fmt.tsv.
2937
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d] <file>... (try --help)';
2938
+ function runFmt(argv) {
2939
+ const files = [];
2940
+ const flags = { write: false, list: false, check: false, diff: false };
2941
+ for (const arg of argv) {
2942
+ if ('-h' === arg || '--help' === arg) {
2943
+ process.stdout.write(HELP);
2944
+ return 0;
2945
+ }
2946
+ if ('-w' === arg || '--write' === arg) {
2947
+ flags.write = true;
2948
+ }
2949
+ else if ('-l' === arg || '--list' === arg) {
2950
+ flags.list = true;
2951
+ }
2952
+ else if ('--check' === arg) {
2953
+ flags.check = true;
2954
+ }
2955
+ else if ('-d' === arg || '--diff' === arg) {
2956
+ flags.diff = true;
2957
+ }
2958
+ else if (arg.startsWith('-')) {
2959
+ process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`);
2960
+ return 2;
2961
+ }
2962
+ else {
2963
+ files.push(arg);
2964
+ }
2965
+ }
2966
+ if (0 === files.length) {
2967
+ // Standard input: formatted onto standard output, or listed,
2968
+ // checked and diffed under the name <stdin>. It cannot be written
2969
+ // back.
2970
+ if (flags.write) {
2971
+ process.stderr.write(`aontu: --write needs a file\n${FMT_HELP}\n`);
2972
+ return 2;
2973
+ }
2974
+ return new Promise((resolve) => {
2975
+ let src = '';
2976
+ process.stdin.setEncoding('utf8');
2977
+ process.stdin.on('data', (d) => (src += d));
2978
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)));
2979
+ });
2980
+ }
2981
+ // Several files onto standard output would be one stream nobody can
2982
+ // split again (the note's X-6): the verb refuses unless an option
2983
+ // says what to do with each.
2984
+ if (1 < files.length && !fmtQuiet(flags)) {
2985
+ process.stderr.write(`aontu: fmt prints one file; with ${files.length}, say --write, ` +
2986
+ `--list, --check or --diff\n${FMT_HELP}\n`);
2987
+ return 2;
2988
+ }
2989
+ let worst = 0;
2990
+ for (const file of files) {
2991
+ let src;
2992
+ try {
2993
+ src = (0, node_fs_1.readFileSync)(file, 'utf8');
2994
+ }
2995
+ catch (err) {
2996
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
2997
+ return 2;
2998
+ }
2999
+ worst = Math.max(worst, fmtOne(file, src, flags));
3000
+ }
3001
+ return worst;
3002
+ }
3003
+ // An option that says what to do with a file that would change, in
3004
+ // place of printing it.
3005
+ function fmtQuiet(flags) {
3006
+ return flags.write || flags.list || flags.check || flags.diff;
3007
+ }
3008
+ // One document: 0 printed, clean or done; 1 a --check that would
3009
+ // change; 2 a file that cannot be written; 4 a document that does not
3010
+ // format, with the finding that says why.
3011
+ function fmtOne(name, src, flags) {
3012
+ const report = (0, format_1.format)(src, { path: name });
3013
+ if ('error' === report.verdict) {
3014
+ process.stderr.write(`aontu: ${name} was not formatted\n` +
3015
+ report.errors.map(renderFinding).join('\n') + '\n');
3016
+ return 4;
3017
+ }
3018
+ if (!fmtQuiet(flags)) {
3019
+ process.stdout.write(report.text);
3020
+ return 0;
3021
+ }
3022
+ if (!report.changed) {
3023
+ return 0;
3024
+ }
3025
+ if (flags.list || flags.check) {
3026
+ process.stdout.write(name + '\n');
3027
+ }
3028
+ if (flags.diff) {
3029
+ process.stdout.write((0, format_1.unifiedDiff)(name, src, report.text));
3030
+ }
3031
+ if (flags.write) {
3032
+ try {
3033
+ (0, node_fs_1.writeFileSync)(name, report.text);
3034
+ }
3035
+ catch (err) {
3036
+ process.stderr.write(`aontu: cannot write ${name}: ${err.message}\n`);
3037
+ return 2;
3038
+ }
3039
+ }
3040
+ return flags.check ? 1 : 0;
3041
+ }
2799
3042
  function finish(code) {
2800
3043
  process.exitCode = code;
2801
3044
  }
@@ -2803,16 +3046,16 @@ function finish(code) {
2803
3046
  // spelling, so the caller owns the usage error.
2804
3047
  function parseTrustArg(value) {
2805
3048
  if ('system' === value) {
2806
- return { kind: 'system' };
3049
+ return { kind: 'system', textExt: [] };
2807
3050
  }
2808
3051
  if ('none' === value) {
2809
- return { kind: 'none' };
3052
+ return { kind: 'none', textExt: [] };
2810
3053
  }
2811
3054
  if ('root' === value) {
2812
- return { kind: 'root' };
3055
+ return { kind: 'root', textExt: [] };
2813
3056
  }
2814
3057
  if (value.startsWith('root:') && 'root:'.length < value.length) {
2815
- return { kind: 'root', dir: value.slice('root:'.length) };
3058
+ return { kind: 'root', dir: value.slice('root:'.length), textExt: [] };
2816
3059
  }
2817
3060
  return undefined;
2818
3061
  }
@@ -2833,7 +3076,8 @@ function main(argv) {
2833
3076
  // overwritten twice. In a tool loop that reads as a passing
2834
3077
  // validation. Counting them is what lets the refusal below happen.
2835
3078
  const files = [];
2836
- let trust = { kind: 'system-warn' };
3079
+ let trust = { kind: 'system-warn', textExt: [] };
3080
+ let textExt = [];
2837
3081
  // The REPL's SESSION protocol (G7 phase 7): one JSON line per
2838
3082
  // answer, so a harness can drive the session. Named --jsonl rather
2839
3083
  // than the design's --json, which would read as the `:json` output
@@ -2860,6 +3104,9 @@ function main(argv) {
2860
3104
  if ('agentsmd' === argv[2]) {
2861
3105
  return finish(runAgentsMd(argv.slice(3)));
2862
3106
  }
3107
+ if ('fmt' === argv[2]) {
3108
+ return void Promise.resolve(runFmt(argv.slice(3))).then(finish);
3109
+ }
2863
3110
  if ('set' === argv[2]) {
2864
3111
  return finish(runSet(argv.slice(3)));
2865
3112
  }
@@ -2927,7 +3174,16 @@ function main(argv) {
2927
3174
  process.stderr.write('aontu: --include-root needs a directory\n');
2928
3175
  return finish(2);
2929
3176
  }
2930
- trust = { kind: 'root', dir };
3177
+ trust = { kind: 'root', dir, textExt };
3178
+ }
3179
+ else if ('--text-ext' === arg) {
3180
+ const list = null == args[i + 1] ? undefined : parseTextExt(args[++i]);
3181
+ if (null == list) {
3182
+ process.stderr.write('aontu: --text-ext needs extensions, without dots' +
3183
+ ' (--text-ext md,sql)\n');
3184
+ return finish(2);
3185
+ }
3186
+ textExt = [...textExt, ...list];
2931
3187
  }
2932
3188
  else if (arg.startsWith('-')) {
2933
3189
  process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`);
@@ -2951,6 +3207,10 @@ function main(argv) {
2951
3207
  ' (try --help)\n');
2952
3208
  return finish(2);
2953
3209
  }
3210
+ // The extensions ride with the capability from here on, so the three
3211
+ // entry shapes below (file, REPL, stdin) each get them by threading
3212
+ // the one value they already thread.
3213
+ trust = { ...trust, textExt };
2954
3214
  const file = files[0];
2955
3215
  if (null != file) {
2956
3216
  finish(runFile(file, mode, trust));