aontu 0.55.0 → 0.57.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 (128) 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 +4 -2
  6. package/dist/aontu.js +5 -2
  7. package/dist/aontu.js.map +1 -1
  8. package/dist/cli.d.ts +10 -3
  9. package/dist/cli.js +311 -53
  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/escape.d.ts +5 -0
  15. package/dist/escape.js +455 -0
  16. package/dist/escape.js.map +1 -0
  17. package/dist/format.d.ts +26 -0
  18. package/dist/format.js +1489 -0
  19. package/dist/format.js.map +1 -0
  20. package/dist/hints.js +39 -1
  21. package/dist/hints.js.map +1 -1
  22. package/dist/jsonschema.d.ts +1 -0
  23. package/dist/jsonschema.js +3 -2
  24. package/dist/jsonschema.js.map +1 -1
  25. package/dist/lang.js +84 -6
  26. package/dist/lang.js.map +1 -1
  27. package/dist/lsp.d.ts +2 -1
  28. package/dist/lsp.js +4 -4
  29. package/dist/lsp.js.map +1 -1
  30. package/dist/mcp-server.js +2 -2
  31. package/dist/mcp-server.js.map +1 -1
  32. package/dist/mcp.js +6 -4
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/mod-tool.js +8 -7
  35. package/dist/mod-tool.js.map +1 -1
  36. package/dist/mod.js +6 -6
  37. package/dist/mod.js.map +1 -1
  38. package/dist/patch.d.ts +1 -0
  39. package/dist/patch.js +1 -0
  40. package/dist/patch.js.map +1 -1
  41. package/dist/query.d.ts +1 -0
  42. package/dist/query.js +4 -3
  43. package/dist/query.js.map +1 -1
  44. package/dist/reach.d.ts +1 -0
  45. package/dist/reach.js +3 -2
  46. package/dist/reach.js.map +1 -1
  47. package/dist/relation.d.ts +1 -0
  48. package/dist/relation.js +3 -2
  49. package/dist/relation.js.map +1 -1
  50. package/dist/sigdecl.js +1 -1
  51. package/dist/sigdecl.js.map +1 -1
  52. package/dist/std.js +2 -1
  53. package/dist/std.js.map +1 -1
  54. package/dist/subsume.d.ts +1 -0
  55. package/dist/subsume.js +3 -2
  56. package/dist/subsume.js.map +1 -1
  57. package/dist/trim.d.ts +1 -0
  58. package/dist/trim.js +3 -2
  59. package/dist/trim.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/type.d.ts +1 -0
  62. package/dist/type.js.map +1 -1
  63. package/dist/utility.d.ts +8 -2
  64. package/dist/utility.js +9 -1
  65. package/dist/utility.js.map +1 -1
  66. package/dist/val/EachFuncVal.js +1 -2
  67. package/dist/val/EachFuncVal.js.map +1 -1
  68. package/dist/val/EmitFuncVal.d.ts +23 -0
  69. package/dist/val/EmitFuncVal.js +261 -0
  70. package/dist/val/EmitFuncVal.js.map +1 -0
  71. package/dist/val/FilterFuncVal.js +1 -2
  72. package/dist/val/FilterFuncVal.js.map +1 -1
  73. package/dist/val/FuncBaseVal.d.ts +1 -0
  74. package/dist/val/FuncBaseVal.js +16 -0
  75. package/dist/val/FuncBaseVal.js.map +1 -1
  76. package/dist/val/PackFuncVal.js +1 -2
  77. package/dist/val/PackFuncVal.js.map +1 -1
  78. package/dist/val/PlaceVal.d.ts +3 -1
  79. package/dist/val/PlaceVal.js +8 -6
  80. package/dist/val/PlaceVal.js.map +1 -1
  81. package/dist/val/StrFuncVal.d.ts +32 -0
  82. package/dist/val/StrFuncVal.js +292 -0
  83. package/dist/val/StrFuncVal.js.map +1 -0
  84. package/dist/vet.d.ts +1 -0
  85. package/dist/vet.js +1 -1
  86. package/dist/vet.js.map +1 -1
  87. package/dist/view.d.ts +2 -1
  88. package/dist/view.js +379 -8
  89. package/dist/view.js.map +1 -1
  90. package/grammar/aontu.abnf +156 -0
  91. package/grammar/aontu.gbnf +4 -3
  92. package/grammar/aontu.lark +4 -3
  93. package/grammar/aontu.tmLanguage.json +1 -1
  94. package/package.json +2 -1
  95. package/skill/grammar-card.md +5 -2
  96. package/src/agentsmd.ts +15 -3
  97. package/src/aontu.ts +8 -1
  98. package/src/cli.ts +365 -58
  99. package/src/diff.ts +7 -2
  100. package/src/escape.ts +371 -0
  101. package/src/format.ts +1728 -0
  102. package/src/hints.ts +53 -1
  103. package/src/jsonschema.ts +7 -1
  104. package/src/lang.ts +93 -6
  105. package/src/lsp.ts +8 -6
  106. package/src/mcp-server.ts +3 -2
  107. package/src/mcp.ts +6 -4
  108. package/src/mod-tool.ts +9 -8
  109. package/src/mod.ts +6 -6
  110. package/src/patch.ts +7 -0
  111. package/src/query.ts +8 -4
  112. package/src/reach.ts +7 -2
  113. package/src/relation.ts +7 -2
  114. package/src/sigdecl.ts +1 -1
  115. package/src/std.ts +2 -1
  116. package/src/subsume.ts +7 -2
  117. package/src/trim.ts +7 -2
  118. package/src/type.ts +8 -0
  119. package/src/utility.ts +27 -2
  120. package/src/val/EachFuncVal.ts +1 -3
  121. package/src/val/EmitFuncVal.ts +401 -0
  122. package/src/val/FilterFuncVal.ts +1 -3
  123. package/src/val/FuncBaseVal.ts +18 -0
  124. package/src/val/PackFuncVal.ts +1 -3
  125. package/src/val/PlaceVal.ts +8 -6
  126. package/src/val/StrFuncVal.ts +334 -0
  127. package/src/vet.ts +9 -3
  128. 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;
@@ -39,6 +40,8 @@ const node_os_1 = require("node:os");
39
40
  const node_readline_1 = require("node:readline");
40
41
  const aontu_1 = require("./aontu");
41
42
  const report_sarif_1 = require("./report-sarif");
43
+ const lsp_server_1 = require("./lsp-server");
44
+ const mcp_server_1 = require("./mcp-server");
42
45
  const jsonschema_1 = require("./jsonschema");
43
46
  const mod_tool_1 = require("./mod-tool");
44
47
  const mod_1 = require("./mod");
@@ -46,6 +49,8 @@ const vet_1 = require("./vet");
46
49
  const reach_1 = require("./reach");
47
50
  const view_1 = require("./view");
48
51
  const agentsmd_1 = require("./agentsmd");
52
+ const format_1 = require("./format");
53
+ const utility_1 = require("./utility");
49
54
  const HELP = `Usage: aontu [options] [file]
50
55
  aontu vet [options] <schema> <data> [more-data...]
51
56
  aontu subsume [options] <general> <specific>
@@ -62,6 +67,9 @@ const HELP = `Usage: aontu [options] [file]
62
67
  aontu why <path> [options] <file>
63
68
  aontu set <path>=<value>... --entry <file> --overlay <file>
64
69
  aontu agentsmd [--write <AGENTS.md>] <file>
70
+ aontu fmt [-w|-l|--check|-d|--lint] <file>...
71
+ aontu lsp
72
+ aontu mcp [--root <dir>]
65
73
 
66
74
  Evaluate an Aontu source file and print the result as JSON.
67
75
  With no file on an interactive terminal, start a REPL.
@@ -84,6 +92,10 @@ Options:
84
92
  Every verb takes it too, and a bare root means the
85
93
  document's own directory
86
94
  --include-root <dir> Shorthand for --trust root:<dir>
95
+ --text-ext <e> Read these extensions as text too, comma-separated
96
+ and without dots (md,sql). .txt needs no flag; a
97
+ named format keeps its meaning, and .js stays
98
+ refused. Every verb takes it
87
99
 
88
100
  Mod options:
89
101
  --format <f> text (default) or json
@@ -91,10 +103,10 @@ Mod options:
91
103
 
92
104
  Mod subcommands:
93
105
  tidy Resolve the module closure by minimum version selection and
94
- rewrite mod-lock.aon in canonical form
95
- verify Check every locked module still means what mod-lock.aon
106
+ rewrite aontu_meta/mod-lock.aon in canonical form
107
+ verify Check every locked module still means what the lockfile
96
108
  pins, and change nothing (the CI gate; tidy rewrites)
97
- vendor Materialise the locked closure into aon_vendor/
109
+ vendor Materialise the locked closure into aontu_meta/vendor/
98
110
  manifest Print the OCI artifact a publish would push, gated on the
99
111
  breaking check against --against
100
112
 
@@ -175,8 +187,8 @@ Why options:
175
187
  Why exit codes mirror get's: 0 explained, 1 the path names nothing,
176
188
  2 usage, 4 the document does not stand up on its own.
177
189
 
178
- View kinds: tree, matrix, graph, layer, sets, layers, ladder, poset
179
- (the poset takes several files). The figure goes to stdout, the loss
190
+ View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
191
+ ladder, poset (the poset takes several files). The figure goes to stdout, the loss
180
192
  report to stderr. With --views it draws every figure a document
181
193
  declares as data, from one evaluation: each declaration names its own
182
194
  kind and out file, nothing is written unless every figure rendered,
@@ -184,13 +196,13 @@ and --check gates the committed set.
184
196
 
185
197
  View options:
186
198
  --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
199
+ lattice, tree, matrix, sets and layers draw text
200
+ (default) or svg; graph draws mermaid (default),
201
+ dot or er; layer draws text (default), mermaid or
202
+ svg; ladder and poset draw mermaid (default) or dot
191
203
  --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
204
+ subtree doc draws; the subtree the lattice counts;
205
+ the path the ladder draws; where the poset compares
194
206
  --views <path> Draw every figure the document declares at this
195
207
  path, one evaluation, all or nothing; each
196
208
  declaration names its own kind and out file
@@ -269,6 +281,32 @@ Agentsmd options:
269
281
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
270
282
  stand up on its own.
271
283
 
284
+ Fmt options:
285
+ -w, --write Rewrite each file in place, when its form would change
286
+ -l, --list Print the name of each file whose form would change
287
+ --check Like --list, and exit 1 when any would: the CI gate
288
+ -d, --diff Print a unified diff for each file whose form would
289
+ change
290
+ --lint Report the style findings, key case and repeated
291
+ shapes, on standard error, and print nothing else
292
+ --strict With --lint, and exit 1 when there is a finding
293
+
294
+ The fmt verb prints one document in the agreed form; with no file it
295
+ reads standard input. Several files need one of the options above.
296
+
297
+ Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
298
+ a --strict finding, 2 usage, 4 a document does not parse.
299
+
300
+ The lsp verb runs the language server over standard input and output
301
+ (LSP: JSON-RPC with Content-Length framing) until the client exits;
302
+ editors launch it with no arguments. The standalone aontu-lsp binary
303
+ runs the same server.
304
+
305
+ The mcp verb runs the Model Context Protocol server over standard
306
+ input and output (newline-delimited JSON-RPC), confined below --root
307
+ when one is given. It is part of the npm build, as the standalone
308
+ aontu-mcp binary is.
309
+
272
310
  REPL commands:
273
311
  :help Show REPL help
274
312
  :load <file> Evaluate a document and hold it for the commands below
@@ -333,15 +371,16 @@ function makeTrustWarn() {
333
371
  // entryRoot (the entry file's directory, or the working directory for
334
372
  // stdin/REPL).
335
373
  function trustOpts(trust, entryRoot) {
374
+ const text = 0 === trust.textExt.length ? {} : { textExt: trust.textExt };
336
375
  switch (trust.kind) {
337
376
  case 'none':
338
- return { trust: { include: 'none' } };
377
+ return { ...text, trust: { include: 'none' } };
339
378
  case 'root':
340
- return { trust: { include: { root: trust.dir ?? entryRoot } } };
379
+ return { ...text, trust: { include: { root: trust.dir ?? entryRoot } } };
341
380
  case 'system':
342
- return {};
381
+ return { ...text };
343
382
  default: // system-warn: today's default plus the warning window
344
- return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
383
+ return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot };
345
384
  }
346
385
  }
347
386
  // EVERY VERB honours the include capability, not just the bare
@@ -356,7 +395,8 @@ function trustOpts(trust, entryRoot) {
356
395
  // already printed: the caller answers the usage class.
357
396
  function takeTrust(argv) {
358
397
  const rest = [];
359
- let trust = { kind: 'system-warn' };
398
+ let trust = { kind: 'system-warn', textExt: [] };
399
+ let textExt = [];
360
400
  for (let i = 0; i < argv.length; i++) {
361
401
  const arg = argv[i];
362
402
  if ('--trust' === arg) {
@@ -373,18 +413,42 @@ function takeTrust(argv) {
373
413
  process.stderr.write('aontu: --include-root needs a directory\n');
374
414
  return undefined;
375
415
  }
376
- trust = { kind: 'root', dir };
416
+ trust = { kind: 'root', dir, textExt };
417
+ }
418
+ else if ('--text-ext' === arg) {
419
+ const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i]);
420
+ if (null == list) {
421
+ process.stderr.write('aontu: --text-ext needs extensions, without dots' +
422
+ ' (--text-ext md,sql)\n');
423
+ return undefined;
424
+ }
425
+ textExt = [...textExt, ...list];
377
426
  }
378
427
  else {
379
428
  rest.push(arg);
380
429
  }
381
430
  }
382
- return { argv: rest, trust };
431
+ return { argv: rest, trust: { ...trust, textExt } };
432
+ }
433
+ // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
434
+ // reader who has just written `@"notes.txt"` reaches for one. An empty
435
+ // element, or anything that is not an extension, is a usage error
436
+ // rather than a silently ignored word: a flag that quietly does
437
+ // nothing is how a document ends up refused with no reason visible.
438
+ function parseTextExt(arg) {
439
+ const out = [];
440
+ for (const raw of arg.split(',')) {
441
+ const ext = raw.trim().replace(/^\./, '').toLowerCase();
442
+ if ('' === ext || !/^[a-z0-9]+$/.test(ext)) {
443
+ return undefined;
444
+ }
445
+ out.push(ext);
446
+ }
447
+ return out;
383
448
  }
384
449
  // The evaluator options a REPL session's capability means.
385
450
  function replTrust(state, entryRoot) {
386
- const capability = verbTrust(state.trust ?? { kind: 'system-warn' }, entryRoot);
387
- return null == capability ? {} : { trust: capability };
451
+ return verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRoot);
388
452
  }
389
453
  // The capability a verb's engine runs under. `system` and the staged
390
454
  // warning default both mean today's behaviour (no option); the warning
@@ -400,6 +464,18 @@ function verbTrust(trust, entryRoot) {
400
464
  return undefined;
401
465
  }
402
466
  }
467
+ // THE INCLUDE OPTIONS A VERB RUNS UNDER, spread into its engine call:
468
+ // the capability above, and the extensions `--text-ext` widened. Both
469
+ // are absent when unset rather than present-and-undefined, so a verb's
470
+ // options bag is byte-identical to what it was before either flag
471
+ // existed and no engine sees a key it has to ignore.
472
+ function verbOpts(trust, entryRoot) {
473
+ const include = verbTrust(trust, entryRoot);
474
+ return {
475
+ ...(undefined === include ? {} : { trust: include }),
476
+ ...(0 === trust.textExt.length ? {} : { textExt: trust.textExt }),
477
+ };
478
+ }
403
479
  // The directory a bare `--trust root` confines to for a verb: the
404
480
  // primary document's own, matching the bare command's entry root.
405
481
  function entryRootOf(file) {
@@ -505,7 +581,7 @@ function replCommand(state, line, read) {
505
581
  if (':why' === cmd) {
506
582
  const report = (0, aontu_1.why)(src, path, {
507
583
  path: state.name,
508
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
584
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
509
585
  });
510
586
  return report.ok
511
587
  ? answer(renderWhyText(report.record))
@@ -515,7 +591,7 @@ function replCommand(state, line, read) {
515
591
  ? 'keys' : 'canon' === state.mode ? 'canon' : 'json';
516
592
  const report = (0, aontu_1.get)(src, path, {
517
593
  view, path: state.name,
518
- trust: verbTrust(state.trust ?? { kind: 'system-warn' }, entryRootOf(state.name)),
594
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRootOf(state.name)),
519
595
  });
520
596
  return report.ok
521
597
  ? answer(report.out)
@@ -746,7 +822,7 @@ function vetOnce(args, trust) {
746
822
  const findings = [];
747
823
  for (const source of sources) {
748
824
  const report = (0, aontu_1.vet)(schemaSrc, source.src, {
749
- trust: verbTrust(trust, entryRootOf(args.schema)),
825
+ ...verbOpts(trust, entryRootOf(args.schema)),
750
826
  at: args.at,
751
827
  closed: args.closed,
752
828
  partial: args.partial,
@@ -985,7 +1061,7 @@ function runSubsume(argv) {
985
1061
  return 2;
986
1062
  }
987
1063
  const report = (0, aontu_1.subsume)(generalSrc, specificSrc, {
988
- trust: verbTrust(trust, entryRootOf(args.general)),
1064
+ ...verbOpts(trust, entryRootOf(args.general)),
989
1065
  profile: args.profile,
990
1066
  at: args.at,
991
1067
  generalUrl: args.general,
@@ -1165,15 +1241,17 @@ function oldVersion(spec, file) {
1165
1241
  // The document's own compatibility declaration: `$.aontu_policy.compat`,
1166
1242
  // a disjunction whose default is the declared mode. Undefined when the
1167
1243
  // key is absent or does not spell a mode.
1168
- function policyCompat(newSrc, path, trust) {
1244
+ function policyCompat(newSrc, path, include) {
1169
1245
  const aontu = new aontu_1.Aontu();
1170
1246
  const ctx = aontu.ctx({ collect: true });
1171
1247
  // 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);
1248
+ // runs the include resolver too and has to run it under BOTH of the
1249
+ // verb's include options -- a `breaking --trust none` that read its
1250
+ // own mode through an unconfined resolver would confine the
1251
+ // comparison and not the question (use-cases/REVIEW.md finding G),
1252
+ // and one that took the capability alone read no mode at all when
1253
+ // the declaration arrived through a `--text-ext` include.
1254
+ const v = aontu.unify(newSrc, { path, ...(0, utility_1.includeOpts)(include) }, ctx);
1177
1255
  if (0 < ctx.err.length || true === v?.isNil) {
1178
1256
  return undefined;
1179
1257
  }
@@ -1266,7 +1344,7 @@ function runBreaking(argv) {
1266
1344
  // neither means backward, the index's framing (v1-valid documents
1267
1345
  // stay valid).
1268
1346
  const mode = args.mode ??
1269
- policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
1347
+ policyCompat(newSrc, args.file, verbOpts(trust, entryRootOf(args.file))) ??
1270
1348
  'backward';
1271
1349
  if ('none' === mode) {
1272
1350
  // The document declares no compatibility promise: nothing to check.
@@ -1309,7 +1387,7 @@ function runBreaking(argv) {
1309
1387
  const oldPath = old.path;
1310
1388
  for (const check of checks) {
1311
1389
  const report = (0, aontu_1.subsume)(check.general[0], check.specific[0], {
1312
- trust: verbTrust(trust, entryRootOf(args.file)),
1390
+ ...verbOpts(trust, entryRootOf(args.file)),
1313
1391
  at: args.at,
1314
1392
  generalUrl: check.general[1],
1315
1393
  specificUrl: check.specific[1],
@@ -1439,7 +1517,7 @@ function runTrim(argv) {
1439
1517
  return 2;
1440
1518
  }
1441
1519
  const report = (0, aontu_1.trimCheck)(src, {
1442
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1520
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1443
1521
  });
1444
1522
  const text = 'json' === format
1445
1523
  ? renderTrimJson(report)
@@ -1490,8 +1568,8 @@ const REACHES_EXIT = {
1490
1568
  error: 4,
1491
1569
  };
1492
1570
  const VIEW_HELP = 'aontu view <kind> [options] <file>... (try --help)';
1493
- const VIEW_KINDS = ['doc', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers', 'ladder',
1494
- 'poset'];
1571
+ const VIEW_KINDS = ['doc', 'lattice', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers',
1572
+ 'ladder', 'poset'];
1495
1573
  const VIEW_PROFILES = ['text', 'mermaid', 'dot', 'er', 'svg'];
1496
1574
  const VIEW_EDGES = ['upward', 'all', 'none'];
1497
1575
  // The styles the CLI accepts (VIEWS.0.md, "7. Styling"). `auto` is
@@ -1605,6 +1683,14 @@ function runMod(argv) {
1605
1683
  process.stderr.write(`aontu: mod needs tidy, verify, vendor or manifest\n${MOD_HELP}\n`);
1606
1684
  return 2;
1607
1685
  }
1686
+ // THE OLD LAYOUT IS NAMED, NOT READ. The lockfile and the vendored
1687
+ // closure moved under aontu_meta/; a project that still carries them
1688
+ // at its root would otherwise look untouched by any of these verbs,
1689
+ // which is the one silence worth breaking.
1690
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'aon_vendor')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'mod-lock.aon'))) {
1691
+ process.stderr.write('aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
1692
+ 'move them, or run aontu mod tidy and aontu mod vendor\n');
1693
+ }
1608
1694
  // `--against` gates a manifest and means nothing to the other two;
1609
1695
  // accepting it there would say it had been honoured.
1610
1696
  if (null != against && 'manifest' !== sub) {
@@ -1764,7 +1850,7 @@ function runRelations(argv) {
1764
1850
  return 2;
1765
1851
  }
1766
1852
  const report = (0, aontu_1.relationCheck)(src, {
1767
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1853
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1768
1854
  });
1769
1855
  const text = 'json' === format
1770
1856
  ? renderRelationsJson(report)
@@ -1825,7 +1911,7 @@ function runReaches(argv) {
1825
1911
  }
1826
1912
  const report = (0, reach_1.reachCheck)(src, rest[0], rest[1], {
1827
1913
  path: rest[2], relation,
1828
- trust: verbTrust(trust, entryRootOf(rest[2])),
1914
+ ...verbOpts(trust, entryRootOf(rest[2])),
1829
1915
  });
1830
1916
  const text = 'json' === format
1831
1917
  ? renderReachesJson(report)
@@ -2052,7 +2138,7 @@ function runView(argv) {
2052
2138
  kind,
2053
2139
  path: files[0],
2054
2140
  roots,
2055
- trust: verbTrust(trust, entryRootOf(files[0])),
2141
+ ...verbOpts(trust, entryRootOf(files[0])),
2056
2142
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2057
2143
  });
2058
2144
  if ('json' === format) {
@@ -2122,7 +2208,7 @@ function runViewSet(rest, opts, trust, how) {
2122
2208
  return 2;
2123
2209
  }
2124
2210
  const report = (0, view_1.viewSet)(src, {
2125
- ...opts, path: file, trust: verbTrust(trust, entryRootOf(file)),
2211
+ ...opts, path: file, ...verbOpts(trust, entryRootOf(file)),
2126
2212
  });
2127
2213
  if ('json' === how.format) {
2128
2214
  process.stdout.write(renderViewSetJson(report) + '\n');
@@ -2319,7 +2405,7 @@ function runJsonSchema(argv) {
2319
2405
  return 2;
2320
2406
  }
2321
2407
  const report = (0, jsonschema_1.jsonSchema)(src, {
2322
- at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2408
+ at, path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2323
2409
  });
2324
2410
  if ('json' === format) {
2325
2411
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2402,8 +2488,7 @@ function runHash(argv) {
2402
2488
  }
2403
2489
  // The file's own directory is the include base, as every verb
2404
2490
  // 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 });
2491
+ const aontu = new aontu_1.Aontu(verbOpts(trust, entryRootOf(files[0])));
2407
2492
  const ctx = aontu.ctx({ collect: true });
2408
2493
  const v = aontu.unify(src, { path: files[0] }, ctx);
2409
2494
  if (0 < ctx.err.length || true === v?.isNil) {
@@ -2507,7 +2592,7 @@ function runGet(argv) {
2507
2592
  return 2;
2508
2593
  }
2509
2594
  const report = (0, aontu_1.get)(src, path, {
2510
- view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
2595
+ view, depth, path: file, ...verbOpts(trust, entryRootOf(file)),
2511
2596
  });
2512
2597
  if ('json' === format) {
2513
2598
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2582,7 +2667,7 @@ function runWhy(argv) {
2582
2667
  return 2;
2583
2668
  }
2584
2669
  const report = (0, aontu_1.why)(src, path, {
2585
- path: file, trust: verbTrust(trust, entryRootOf(file)),
2670
+ path: file, ...verbOpts(trust, entryRootOf(file)),
2586
2671
  });
2587
2672
  if ('json' === format) {
2588
2673
  process.stdout.write((0, aontu_1.exactJSON)({
@@ -2703,7 +2788,7 @@ function runSet(argv) {
2703
2788
  }
2704
2789
  }
2705
2790
  const report = (0, aontu_1.patch)(entrySrc, overlaySrc, assignments, {
2706
- trust: verbTrust(trust, entryRootOf(entry)),
2791
+ ...verbOpts(trust, entryRootOf(entry)),
2707
2792
  entryPath: entry,
2708
2793
  overlayPath: overlayFile,
2709
2794
  inPlace,
@@ -2823,7 +2908,7 @@ function runAgentsMd(argv) {
2823
2908
  }
2824
2909
  const report = (0, aontu_1.agentsMd)(src, {
2825
2910
  name: files[0], path: files[0],
2826
- trust: verbTrust(trust, entryRootOf(files[0])),
2911
+ ...verbOpts(trust, entryRootOf(files[0])),
2827
2912
  });
2828
2913
  if (!report.ok) {
2829
2914
  process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n');
@@ -2869,6 +2954,155 @@ function runAgentsMd(argv) {
2869
2954
  // on the parity-probe discipline in AGENTS.md, which derives expected
2870
2955
  // spec values by piping BOTH CLIs and comparing. A truncated pipe there
2871
2956
  // reads as a port divergence.
2957
+ // ---------------------------------------------------------------------
2958
+ // The source formatter (docs/design/FMT.0.md): one agreed form, in the
2959
+ // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
2960
+ // the form itself is the library's (ts/src/format.ts), and the two
2961
+ // ports agree on it row by row in test/spec/fmt.tsv.
2962
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] <file>... (try --help)';
2963
+ function runFmt(argv) {
2964
+ const files = [];
2965
+ const flags = {
2966
+ write: false, list: false, check: false, diff: false, lint: false, strict: false,
2967
+ };
2968
+ for (const arg of argv) {
2969
+ if ('-h' === arg || '--help' === arg) {
2970
+ process.stdout.write(HELP);
2971
+ return 0;
2972
+ }
2973
+ if ('-w' === arg || '--write' === arg) {
2974
+ flags.write = true;
2975
+ }
2976
+ else if ('-l' === arg || '--list' === arg) {
2977
+ flags.list = true;
2978
+ }
2979
+ else if ('--check' === arg) {
2980
+ flags.check = true;
2981
+ }
2982
+ else if ('-d' === arg || '--diff' === arg) {
2983
+ flags.diff = true;
2984
+ }
2985
+ else if ('--lint' === arg) {
2986
+ flags.lint = true;
2987
+ }
2988
+ else if ('--strict' === arg) {
2989
+ flags.lint = true;
2990
+ flags.strict = true;
2991
+ }
2992
+ else if (arg.startsWith('-')) {
2993
+ process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`);
2994
+ return 2;
2995
+ }
2996
+ else {
2997
+ files.push(arg);
2998
+ }
2999
+ }
3000
+ if (0 === files.length) {
3001
+ // Standard input: formatted onto standard output, or listed,
3002
+ // checked and diffed under the name <stdin>. It cannot be written
3003
+ // back.
3004
+ if (flags.write) {
3005
+ process.stderr.write(`aontu: --write needs a file\n${FMT_HELP}\n`);
3006
+ return 2;
3007
+ }
3008
+ return new Promise((resolve) => {
3009
+ let src = '';
3010
+ process.stdin.setEncoding('utf8');
3011
+ process.stdin.on('data', (d) => (src += d));
3012
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)));
3013
+ });
3014
+ }
3015
+ // Several files onto standard output would be one stream nobody can
3016
+ // split again (the note's X-6): the verb refuses unless an option
3017
+ // says what to do with each.
3018
+ if (1 < files.length && !fmtQuiet(flags)) {
3019
+ process.stderr.write(`aontu: fmt prints one file; with ${files.length}, say --write, ` +
3020
+ `--list, --check, --diff or --lint\n${FMT_HELP}\n`);
3021
+ return 2;
3022
+ }
3023
+ let worst = 0;
3024
+ for (const file of files) {
3025
+ let src;
3026
+ try {
3027
+ src = (0, node_fs_1.readFileSync)(file, 'utf8');
3028
+ }
3029
+ catch (err) {
3030
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`);
3031
+ return 2;
3032
+ }
3033
+ worst = Math.max(worst, fmtOne(file, src, flags));
3034
+ }
3035
+ return worst;
3036
+ }
3037
+ // An option that says what to do with a file, in place of printing
3038
+ // it: what to do when its form would change, or the lint.
3039
+ function fmtQuiet(flags) {
3040
+ return flags.write || flags.list || flags.check || flags.diff || flags.lint;
3041
+ }
3042
+ // One document: 0 printed, clean or done; 1 a --check that would
3043
+ // change, or a --strict finding; 2 a file that cannot be written; 4 a
3044
+ // document that does not format, with the finding that says why. The
3045
+ // style findings go to standard error, one line each, in the shape
3046
+ // every linter prints: `file:line:col: rule: message`.
3047
+ function fmtOne(name, src, flags) {
3048
+ const report = (0, format_1.format)(src, { path: name, lint: flags.lint });
3049
+ if ('error' === report.verdict) {
3050
+ process.stderr.write(`aontu: ${name} was not formatted\n` +
3051
+ report.errors.map(renderFinding).join('\n') + '\n');
3052
+ return 4;
3053
+ }
3054
+ for (const f of report.findings) {
3055
+ process.stderr.write(`${name}:${f.line}:${f.col}: ${f.rule}: ${f.message}\n`);
3056
+ }
3057
+ const strict = flags.strict && 0 < report.findings.length ? 1 : 0;
3058
+ if (!fmtQuiet(flags)) {
3059
+ process.stdout.write(report.text);
3060
+ return 0;
3061
+ }
3062
+ if (!report.changed) {
3063
+ return strict;
3064
+ }
3065
+ if (flags.list || flags.check) {
3066
+ process.stdout.write(name + '\n');
3067
+ }
3068
+ if (flags.diff) {
3069
+ process.stdout.write((0, format_1.unifiedDiff)(name, src, report.text));
3070
+ }
3071
+ if (flags.write) {
3072
+ try {
3073
+ (0, node_fs_1.writeFileSync)(name, report.text);
3074
+ }
3075
+ catch (err) {
3076
+ process.stderr.write(`aontu: cannot write ${name}: ${err.message}\n`);
3077
+ return 2;
3078
+ }
3079
+ }
3080
+ return flags.check ? 1 : strict;
3081
+ }
3082
+ // The real pair takes the process's own stdin and stdout, which no
3083
+ // in-process test can lend it; the executable-entry tests in
3084
+ // cli.test.ts run each through a child process instead, so these two
3085
+ // lines are excluded from the in-process count, as the stdio wiring
3086
+ // of lsp-server.ts is.
3087
+ /* node:coverage ignore next 4 */
3088
+ const SERVERS = {
3089
+ lsp: () => void (0, lsp_server_1.main)(),
3090
+ mcp: (argv) => void (0, mcp_server_1.main)(undefined, undefined, undefined, undefined, argv),
3091
+ };
3092
+ // undefined: the server took the process; a number: an answer the CLI
3093
+ // gives itself, --help or a usage error.
3094
+ function runLsp(argv, servers) {
3095
+ for (const arg of argv) {
3096
+ if ('-h' === arg || '--help' === arg) {
3097
+ process.stdout.write(HELP);
3098
+ return 0;
3099
+ }
3100
+ process.stderr.write(`aontu: lsp takes no arguments (try --help)\n`);
3101
+ return 2;
3102
+ }
3103
+ servers.lsp();
3104
+ return undefined;
3105
+ }
2872
3106
  function finish(code) {
2873
3107
  process.exitCode = code;
2874
3108
  }
@@ -2876,20 +3110,20 @@ function finish(code) {
2876
3110
  // spelling, so the caller owns the usage error.
2877
3111
  function parseTrustArg(value) {
2878
3112
  if ('system' === value) {
2879
- return { kind: 'system' };
3113
+ return { kind: 'system', textExt: [] };
2880
3114
  }
2881
3115
  if ('none' === value) {
2882
- return { kind: 'none' };
3116
+ return { kind: 'none', textExt: [] };
2883
3117
  }
2884
3118
  if ('root' === value) {
2885
- return { kind: 'root' };
3119
+ return { kind: 'root', textExt: [] };
2886
3120
  }
2887
3121
  if (value.startsWith('root:') && 'root:'.length < value.length) {
2888
- return { kind: 'root', dir: value.slice('root:'.length) };
3122
+ return { kind: 'root', dir: value.slice('root:'.length), textExt: [] };
2889
3123
  }
2890
3124
  return undefined;
2891
3125
  }
2892
- function main(argv) {
3126
+ function main(argv, servers = SERVERS) {
2893
3127
  // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
2894
3128
  // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
2895
3129
  // answer carried terminal control codes into whatever read them (the
@@ -2906,7 +3140,8 @@ function main(argv) {
2906
3140
  // overwritten twice. In a tool loop that reads as a passing
2907
3141
  // validation. Counting them is what lets the refusal below happen.
2908
3142
  const files = [];
2909
- let trust = { kind: 'system-warn' };
3143
+ let trust = { kind: 'system-warn', textExt: [] };
3144
+ let textExt = [];
2910
3145
  // The REPL's SESSION protocol (G7 phase 7): one JSON line per
2911
3146
  // answer, so a harness can drive the session. Named --jsonl rather
2912
3147
  // than the design's --json, which would read as the `:json` output
@@ -2933,6 +3168,16 @@ function main(argv) {
2933
3168
  if ('agentsmd' === argv[2]) {
2934
3169
  return finish(runAgentsMd(argv.slice(3)));
2935
3170
  }
3171
+ if ('fmt' === argv[2]) {
3172
+ return void Promise.resolve(runFmt(argv.slice(3))).then(finish);
3173
+ }
3174
+ if ('lsp' === argv[2]) {
3175
+ const code = runLsp(argv.slice(3), servers);
3176
+ return undefined === code ? undefined : finish(code);
3177
+ }
3178
+ if ('mcp' === argv[2]) {
3179
+ return servers.mcp(argv.slice(3));
3180
+ }
2936
3181
  if ('set' === argv[2]) {
2937
3182
  return finish(runSet(argv.slice(3)));
2938
3183
  }
@@ -3000,7 +3245,16 @@ function main(argv) {
3000
3245
  process.stderr.write('aontu: --include-root needs a directory\n');
3001
3246
  return finish(2);
3002
3247
  }
3003
- trust = { kind: 'root', dir };
3248
+ trust = { kind: 'root', dir, textExt };
3249
+ }
3250
+ else if ('--text-ext' === arg) {
3251
+ const list = null == args[i + 1] ? undefined : parseTextExt(args[++i]);
3252
+ if (null == list) {
3253
+ process.stderr.write('aontu: --text-ext needs extensions, without dots' +
3254
+ ' (--text-ext md,sql)\n');
3255
+ return finish(2);
3256
+ }
3257
+ textExt = [...textExt, ...list];
3004
3258
  }
3005
3259
  else if (arg.startsWith('-')) {
3006
3260
  process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`);
@@ -3024,6 +3278,10 @@ function main(argv) {
3024
3278
  ' (try --help)\n');
3025
3279
  return finish(2);
3026
3280
  }
3281
+ // The extensions ride with the capability from here on, so the three
3282
+ // entry shapes below (file, REPL, stdin) each get them by threading
3283
+ // the one value they already thread.
3284
+ trust = { ...trust, textExt };
3027
3285
  const file = files[0];
3028
3286
  if (null != file) {
3029
3287
  finish(runFile(file, mode, trust));