aontu 0.55.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 +236 -49
  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 +4 -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 +6 -4
  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 +2 -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 +1 -0
  56. package/dist/vet.js +1 -1
  57. package/dist/vet.js.map +1 -1
  58. package/dist/view.d.ts +2 -1
  59. package/dist/view.js +379 -8
  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 +267 -53
  67. package/src/diff.ts +7 -2
  68. package/src/format.ts +1146 -0
  69. package/src/hints.ts +5 -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 +6 -4
  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 +2 -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 +9 -3
  84. package/src/view.ts +442 -9
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,8 +183,8 @@ 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,
@@ -184,13 +192,13 @@ and --check gates the committed set.
184
192
 
185
193
  View options:
186
194
  --as <profile> text | mermaid | dot | er | svg, per kind: doc,
187
- tree, matrix, sets and layers draw text (default)
188
- or 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
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
- subtree doc draws; the path the ladder draws;
193
- where the poset compares
200
+ subtree doc draws; the subtree the lattice counts;
201
+ the path the ladder draws; where the poset compares
194
202
  --views <path> Draw every figure the document declares at this
195
203
  path, one evaluation, all or nothing; each
196
204
  declaration names its own kind and out file
@@ -269,6 +277,19 @@ Agentsmd options:
269
277
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
270
278
  stand up on its own.
271
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
+
272
293
  REPL commands:
273
294
  :help Show REPL help
274
295
  :load <file> Evaluate a document and hold it for the commands below
@@ -333,15 +354,16 @@ function makeTrustWarn() {
333
354
  // entryRoot (the entry file's directory, or the working directory for
334
355
  // stdin/REPL).
335
356
  function trustOpts(trust, entryRoot) {
357
+ const text = 0 === trust.textExt.length ? {} : { textExt: trust.textExt };
336
358
  switch (trust.kind) {
337
359
  case 'none':
338
- return { trust: { include: 'none' } };
360
+ return { ...text, trust: { include: 'none' } };
339
361
  case 'root':
340
- return { trust: { include: { root: trust.dir ?? entryRoot } } };
362
+ return { ...text, trust: { include: { root: trust.dir ?? entryRoot } } };
341
363
  case 'system':
342
- return {};
364
+ return { ...text };
343
365
  default: // system-warn: today's default plus the warning window
344
- return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
366
+ return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
345
367
  }
346
368
  }
347
369
  // EVERY VERB honours the include capability, not just the bare
@@ -356,7 +378,8 @@ function trustOpts(trust, entryRoot) {
356
378
  // already printed: the caller answers the usage class.
357
379
  function takeTrust(argv) {
358
380
  const rest = [];
359
- let trust = { kind: 'system-warn' };
381
+ let trust = { kind: 'system-warn', textExt: [] };
382
+ let textExt = [];
360
383
  for (let i = 0; i < argv.length; i++) {
361
384
  const arg = argv[i];
362
385
  if ('--trust' === arg) {
@@ -373,18 +396,42 @@ function takeTrust(argv) {
373
396
  process.stderr.write('aontu: --include-root needs a directory\n');
374
397
  return undefined;
375
398
  }
376
- 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];
377
409
  }
378
410
  else {
379
411
  rest.push(arg);
380
412
  }
381
413
  }
382
- 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;
383
431
  }
384
432
  // The evaluator options a REPL session's capability means.
385
433
  function replTrust(state, entryRoot) {
386
- const capability = verbTrust(state.trust ?? { kind: 'system-warn' }, entryRoot);
387
- return null == capability ? {} : { trust: capability };
434
+ return verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRoot);
388
435
  }
389
436
  // The capability a verb's engine runs under. `system` and the staged
390
437
  // warning default both mean today's behaviour (no option); the warning
@@ -400,6 +447,18 @@ function verbTrust(trust, entryRoot) {
400
447
  return undefined;
401
448
  }
402
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
+ }
403
462
  // The directory a bare `--trust root` confines to for a verb: the
404
463
  // primary document's own, matching the bare command's entry root.
405
464
  function entryRootOf(file) {
@@ -505,7 +564,7 @@ function replCommand(state, line, read) {
505
564
  if (':why' === cmd) {
506
565
  const report = (0, aontu_1.why)(src, path, {
507
566
  path: state.name,
508
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
567
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
509
568
  });
510
569
  return report.ok
511
570
  ? answer(renderWhyText(report.record))
@@ -515,7 +574,7 @@ function replCommand(state, line, read) {
515
574
  ? 'keys' : 'canon' === state.mode ? 'canon' : 'json';
516
575
  const report = (0, aontu_1.get)(src, path, {
517
576
  view, path: state.name,
518
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
577
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
519
578
  });
520
579
  return report.ok
521
580
  ? answer(report.out)
@@ -746,7 +805,7 @@ function vetOnce(args, trust) {
746
805
  const findings = [];
747
806
  for (const source of sources) {
748
807
  const report = (0, aontu_1.vet)(schemaSrc, source.src, {
749
- trust: verbTrust(trust, entryRootOf(args.schema)),
808
+ ...verbOpts(trust, entryRootOf(args.schema)),
750
809
  at: args.at,
751
810
  closed: args.closed,
752
811
  partial: args.partial,
@@ -985,7 +1044,7 @@ function runSubsume(argv) {
985
1044
  return 2;
986
1045
  }
987
1046
  const report = (0, aontu_1.subsume)(generalSrc, specificSrc, {
988
- trust: verbTrust(trust, entryRootOf(args.general)),
1047
+ ...verbOpts(trust, entryRootOf(args.general)),
989
1048
  profile: args.profile,
990
1049
  at: args.at,
991
1050
  generalUrl: args.general,
@@ -1165,15 +1224,17 @@ function oldVersion(spec, file) {
1165
1224
  // The document's own compatibility declaration: `$.aontu_policy.compat`,
1166
1225
  // a disjunction whose default is the declared mode. Undefined when the
1167
1226
  // key is absent or does not spell a mode.
1168
- function policyCompat(newSrc, path, trust) {
1227
+ function policyCompat(newSrc, path, include) {
1169
1228
  const aontu = new aontu_1.Aontu();
1170
1229
  const ctx = aontu.ctx({ collect: true });
1171
1230
  // The declaration is read by EVALUATING the document, so this leg
1172
- // runs the include resolver too and has to run it under the verb's
1173
- // capability -- a `breaking --trust none` that read its own mode
1174
- // through an unconfined resolver would confine the comparison and
1175
- // not the question (use-cases/REVIEW.md finding G).
1176
- 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);
1177
1238
  if (0 < ctx.err.length || true === v?.isNil) {
1178
1239
  return undefined;
1179
1240
  }
@@ -1266,7 +1327,7 @@ function runBreaking(argv) {
1266
1327
  // neither means backward, the index's framing (v1-valid documents
1267
1328
  // stay valid).
1268
1329
  const mode = args.mode ??
1269
- policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
1330
+ policyCompat(newSrc, args.file, verbOpts(trust, entryRootOf(args.file))) ??
1270
1331
  'backward';
1271
1332
  if ('none' === mode) {
1272
1333
  // The document declares no compatibility promise: nothing to check.
@@ -1309,7 +1370,7 @@ function runBreaking(argv) {
1309
1370
  const oldPath = old.path;
1310
1371
  for (const check of checks) {
1311
1372
  const report = (0, aontu_1.subsume)(check.general[0], check.specific[0], {
1312
- trust: verbTrust(trust, entryRootOf(args.file)),
1373
+ ...verbOpts(trust, entryRootOf(args.file)),
1313
1374
  at: args.at,
1314
1375
  generalUrl: check.general[1],
1315
1376
  specificUrl: check.specific[1],
@@ -1439,7 +1500,7 @@ function runTrim(argv) {
1439
1500
  return 2;
1440
1501
  }
1441
1502
  const report = (0, aontu_1.trimCheck)(src, {
1442
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1503
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1443
1504
  });
1444
1505
  const text = 'json' === format
1445
1506
  ? renderTrimJson(report)
@@ -1490,8 +1551,8 @@ const REACHES_EXIT = {
1490
1551
  error: 4,
1491
1552
  };
1492
1553
  const VIEW_HELP = 'aontu view <kind> [options] <file>... (try --help)';
1493
- const VIEW_KINDS = ['doc', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers', 'ladder',
1494
- 'poset'];
1554
+ const VIEW_KINDS = ['doc', 'lattice', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers',
1555
+ 'ladder', 'poset'];
1495
1556
  const VIEW_PROFILES = ['text', 'mermaid', 'dot', 'er', 'svg'];
1496
1557
  const VIEW_EDGES = ['upward', 'all', 'none'];
1497
1558
  // The styles the CLI accepts (VIEWS.0.md, "7. Styling"). `auto` is
@@ -1764,7 +1825,7 @@ function runRelations(argv) {
1764
1825
  return 2;
1765
1826
  }
1766
1827
  const report = (0, aontu_1.relationCheck)(src, {
1767
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1828
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1768
1829
  });
1769
1830
  const text = 'json' === format
1770
1831
  ? renderRelationsJson(report)
@@ -1825,7 +1886,7 @@ function runReaches(argv) {
1825
1886
  }
1826
1887
  const report = (0, reach_1.reachCheck)(src, rest[0], rest[1], {
1827
1888
  path: rest[2], relation,
1828
- trust: verbTrust(trust, entryRootOf(rest[2])),
1889
+ ...verbOpts(trust, entryRootOf(rest[2])),
1829
1890
  });
1830
1891
  const text = 'json' === format
1831
1892
  ? renderReachesJson(report)
@@ -2052,7 +2113,7 @@ function runView(argv) {
2052
2113
  kind,
2053
2114
  path: files[0],
2054
2115
  roots,
2055
- trust: verbTrust(trust, entryRootOf(files[0])),
2116
+ ...verbOpts(trust, entryRootOf(files[0])),
2056
2117
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2057
2118
  });
2058
2119
  if ('json' === format) {
@@ -2122,7 +2183,7 @@ function runViewSet(rest, opts, trust, how) {
2122
2183
  return 2;
2123
2184
  }
2124
2185
  const report = (0, view_1.viewSet)(src, {
2125
- ...opts, path: file, trust: verbTrust(trust, entryRootOf(file)),
2186
+ ...opts, path: file, ...verbOpts(trust, entryRootOf(file)),
2126
2187
  });
2127
2188
  if ('json' === how.format) {
2128
2189
  process.stdout.write(renderViewSetJson(report) + '\n');
@@ -2319,7 +2380,7 @@ function runJsonSchema(argv) {
2319
2380
  return 2;
2320
2381
  }
2321
2382
  const report = (0, jsonschema_1.jsonSchema)(src, {
2322
- at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2383
+ at, path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2323
2384
  });
2324
2385
  if ('json' === format) {
2325
2386
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2402,8 +2463,7 @@ function runHash(argv) {
2402
2463
  }
2403
2464
  // The file's own directory is the include base, as every verb
2404
2465
  // resolves a named file (vet's aontuForPath rule).
2405
- const capability = verbTrust(trust, entryRootOf(files[0]));
2406
- const aontu = new aontu_1.Aontu(null == capability ? undefined : { trust: capability });
2466
+ const aontu = new aontu_1.Aontu(verbOpts(trust, entryRootOf(files[0])));
2407
2467
  const ctx = aontu.ctx({ collect: true });
2408
2468
  const v = aontu.unify(src, { path: files[0] }, ctx);
2409
2469
  if (0 < ctx.err.length || true === v?.isNil) {
@@ -2507,7 +2567,7 @@ function runGet(argv) {
2507
2567
  return 2;
2508
2568
  }
2509
2569
  const report = (0, aontu_1.get)(src, path, {
2510
- view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
2570
+ view, depth, path: file, ...verbOpts(trust, entryRootOf(file)),
2511
2571
  });
2512
2572
  if ('json' === format) {
2513
2573
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2582,7 +2642,7 @@ function runWhy(argv) {
2582
2642
  return 2;
2583
2643
  }
2584
2644
  const report = (0, aontu_1.why)(src, path, {
2585
- path: file, trust: verbTrust(trust, entryRootOf(file)),
2645
+ path: file, ...verbOpts(trust, entryRootOf(file)),
2586
2646
  });
2587
2647
  if ('json' === format) {
2588
2648
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2703,7 +2763,7 @@ function runSet(argv) {
2703
2763
  }
2704
2764
  }
2705
2765
  const report = (0, aontu_1.patch)(entrySrc, overlaySrc, assignments, {
2706
- trust: verbTrust(trust, entryRootOf(entry)),
2766
+ ...verbOpts(trust, entryRootOf(entry)),
2707
2767
  entryPath: entry,
2708
2768
  overlayPath: overlayFile,
2709
2769
  inPlace,
@@ -2823,7 +2883,7 @@ function runAgentsMd(argv) {
2823
2883
  }
2824
2884
  const report = (0, aontu_1.agentsMd)(src, {
2825
2885
  name: files[0], path: files[0],
2826
- trust: verbTrust(trust, entryRootOf(files[0])),
2886
+ ...verbOpts(trust, entryRootOf(files[0])),
2827
2887
  });
2828
2888
  if (!report.ok) {
2829
2889
  process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
@@ -2869,6 +2929,116 @@ function runAgentsMd(argv) {
2869
2929
  // on the parity-probe discipline in AGENTS.md, which derives expected
2870
2930
  // spec values by piping BOTH CLIs and comparing. A truncated pipe there
2871
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
+ }
2872
3042
  function finish(code) {
2873
3043
  process.exitCode = code;
2874
3044
  }
@@ -2876,16 +3046,16 @@ function finish(code) {
2876
3046
  // spelling, so the caller owns the usage error.
2877
3047
  function parseTrustArg(value) {
2878
3048
  if ('system' === value) {
2879
- return { kind: 'system' };
3049
+ return { kind: 'system', textExt: [] };
2880
3050
  }
2881
3051
  if ('none' === value) {
2882
- return { kind: 'none' };
3052
+ return { kind: 'none', textExt: [] };
2883
3053
  }
2884
3054
  if ('root' === value) {
2885
- return { kind: 'root' };
3055
+ return { kind: 'root', textExt: [] };
2886
3056
  }
2887
3057
  if (value.startsWith('root:') && 'root:'.length < value.length) {
2888
- return { kind: 'root', dir: value.slice('root:'.length) };
3058
+ return { kind: 'root', dir: value.slice('root:'.length), textExt: [] };
2889
3059
  }
2890
3060
  return undefined;
2891
3061
  }
@@ -2906,7 +3076,8 @@ function main(argv) {
2906
3076
  // overwritten twice. In a tool loop that reads as a passing
2907
3077
  // validation. Counting them is what lets the refusal below happen.
2908
3078
  const files = [];
2909
- let trust = { kind: 'system-warn' };
3079
+ let trust = { kind: 'system-warn', textExt: [] };
3080
+ let textExt = [];
2910
3081
  // The REPL's SESSION protocol (G7 phase 7): one JSON line per
2911
3082
  // answer, so a harness can drive the session. Named --jsonl rather
2912
3083
  // than the design's --json, which would read as the `:json` output
@@ -2933,6 +3104,9 @@ function main(argv) {
2933
3104
  if ('agentsmd' === argv[2]) {
2934
3105
  return finish(runAgentsMd(argv.slice(3)));
2935
3106
  }
3107
+ if ('fmt' === argv[2]) {
3108
+ return void Promise.resolve(runFmt(argv.slice(3))).then(finish);
3109
+ }
2936
3110
  if ('set' === argv[2]) {
2937
3111
  return finish(runSet(argv.slice(3)));
2938
3112
  }
@@ -3000,7 +3174,16 @@ function main(argv) {
3000
3174
  process.stderr.write('aontu: --include-root needs a directory\n');
3001
3175
  return finish(2);
3002
3176
  }
3003
- 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];
3004
3187
  }
3005
3188
  else if (arg.startsWith('-')) {
3006
3189
  process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`);
@@ -3024,6 +3207,10 @@ function main(argv) {
3024
3207
  ' (try --help)\n');
3025
3208
  return finish(2);
3026
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 };
3027
3214
  const file = files[0];
3028
3215
  if (null != file) {
3029
3216
  finish(runFile(file, mode, trust));