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/src/cli.ts CHANGED
@@ -48,6 +48,9 @@ import type {
48
48
  import type { QueryView } from './query'
49
49
  import type { WhyRecord } from './provenance'
50
50
  import { agentsMdSplice } from './agentsmd'
51
+ import { format, unifiedDiff } from './format'
52
+ import { includeOpts } from './utility'
53
+ import type { IncludeOptions } from './utility'
51
54
 
52
55
 
53
56
  type Mode = 'json' | 'canon'
@@ -69,6 +72,7 @@ const HELP = `Usage: aontu [options] [file]
69
72
  aontu why <path> [options] <file>
70
73
  aontu set <path>=<value>... --entry <file> --overlay <file>
71
74
  aontu agentsmd [--write <AGENTS.md>] <file>
75
+ aontu fmt [-w|-l|--check|-d] <file>...
72
76
 
73
77
  Evaluate an Aontu source file and print the result as JSON.
74
78
  With no file on an interactive terminal, start a REPL.
@@ -91,6 +95,10 @@ Options:
91
95
  Every verb takes it too, and a bare root means the
92
96
  document's own directory
93
97
  --include-root <dir> Shorthand for --trust root:<dir>
98
+ --text-ext <e> Read these extensions as text too, comma-separated
99
+ and without dots (md,sql). .txt needs no flag; a
100
+ named format keeps its meaning, and .js stays
101
+ refused. Every verb takes it
94
102
 
95
103
  Mod options:
96
104
  --format <f> text (default) or json
@@ -182,8 +190,8 @@ Why options:
182
190
  Why exit codes mirror get's: 0 explained, 1 the path names nothing,
183
191
  2 usage, 4 the document does not stand up on its own.
184
192
 
185
- View kinds: tree, matrix, graph, layer, sets, layers, ladder, poset
186
- (the poset takes several files). The figure goes to stdout, the loss
193
+ View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
194
+ ladder, poset (the poset takes several files). The figure goes to stdout, the loss
187
195
  report to stderr. With --views it draws every figure a document
188
196
  declares as data, from one evaluation: each declaration names its own
189
197
  kind and out file, nothing is written unless every figure rendered,
@@ -191,13 +199,13 @@ and --check gates the committed set.
191
199
 
192
200
  View options:
193
201
  --as <profile> text | mermaid | dot | er | svg, per kind: doc,
194
- tree, matrix, sets and layers draw text (default)
195
- or svg; graph draws mermaid (default), dot or er;
196
- layer draws text (default), mermaid or svg; ladder
197
- and poset draw mermaid (default) or dot
202
+ lattice, tree, matrix, sets and layers draw text
203
+ (default) or svg; graph draws mermaid (default),
204
+ dot or er; layer draws text (default), mermaid or
205
+ svg; ladder and poset draw mermaid (default) or dot
198
206
  --at <path> Restrict the figure to nodes under this path; the
199
- subtree doc draws; the path the ladder draws;
200
- where the poset compares
207
+ subtree doc draws; the subtree the lattice counts;
208
+ the path the ladder draws; where the poset compares
201
209
  --views <path> Draw every figure the document declares at this
202
210
  path, one evaluation, all or nothing; each
203
211
  declaration names its own kind and out file
@@ -276,6 +284,19 @@ Agentsmd options:
276
284
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
277
285
  stand up on its own.
278
286
 
287
+ Fmt options:
288
+ -w, --write Rewrite each file in place, when its form would change
289
+ -l, --list Print the name of each file whose form would change
290
+ --check Like --list, and exit 1 when any would: the CI gate
291
+ -d, --diff Print a unified diff for each file whose form would
292
+ change
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,
298
+ 2 usage, 4 a document does not parse.
299
+
279
300
  REPL commands:
280
301
  :help Show REPL help
281
302
  :load <file> Evaluate a document and hold it for the commands below
@@ -333,11 +354,18 @@ function evalSource(
333
354
  // entry root or goes through package resolution prints a one-line
334
355
  // stderr warning naming the flag a future default will require
335
356
  // (phase 6, the staged flip).
336
- type TrustArg =
357
+ type TrustArg = (
337
358
  | { kind: 'system-warn' }
338
359
  | { kind: 'system' }
339
360
  | { kind: 'none' }
340
361
  | { kind: 'root', dir?: string }
362
+ // EXTENSIONS READ AS TEXT ride with the capability rather than
363
+ // beside it: both answer "what may an include read", both are
364
+ // stripped by takeTrust before a verb parses its own tail, and a
365
+ // verb that threads one and not the other is the G5 defect again --
366
+ // `aontu vet` running under a flag the bare command honoured and it
367
+ // did not.
368
+ ) & { textExt: string[] }
341
369
 
342
370
 
343
371
  // The one-line warning of the staged default flip. Once per (kind,
@@ -366,15 +394,16 @@ function makeTrustWarn(): (kind: 'escape' | 'pkg', path: string) => void {
366
394
  // entryRoot (the entry file's directory, or the working directory for
367
395
  // stdin/REPL).
368
396
  function trustOpts(trust: TrustArg, entryRoot: string): any {
397
+ const text = 0 === trust.textExt.length ? {} : { textExt: trust.textExt }
369
398
  switch (trust.kind) {
370
399
  case 'none':
371
- return { trust: { include: 'none' } }
400
+ return { ...text, trust: { include: 'none' } }
372
401
  case 'root':
373
- return { trust: { include: { root: trust.dir ?? entryRoot } } }
402
+ return { ...text, trust: { include: { root: trust.dir ?? entryRoot } } }
374
403
  case 'system':
375
- return {}
404
+ return { ...text }
376
405
  default: // system-warn: today's default plus the warning window
377
- return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot }
406
+ return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot }
378
407
  }
379
408
  }
380
409
 
@@ -392,7 +421,8 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
392
421
  function takeTrust(argv: string[]):
393
422
  { argv: string[], trust: TrustArg } | undefined {
394
423
  const rest: string[] = []
395
- let trust: TrustArg = { kind: 'system-warn' }
424
+ let trust: TrustArg = { kind: 'system-warn', textExt: [] }
425
+ let textExt: string[] = []
396
426
  for (let i = 0; i < argv.length; i++) {
397
427
  const arg = argv[i]
398
428
  if ('--trust' === arg) {
@@ -410,21 +440,47 @@ function takeTrust(argv: string[]):
410
440
  process.stderr.write('aontu: --include-root needs a directory\n')
411
441
  return undefined
412
442
  }
413
- trust = { kind: 'root', dir }
443
+ trust = { kind: 'root', dir, textExt }
444
+ }
445
+ else if ('--text-ext' === arg) {
446
+ const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i])
447
+ if (null == list) {
448
+ process.stderr.write(
449
+ 'aontu: --text-ext needs extensions, without dots' +
450
+ ' (--text-ext md,sql)\n')
451
+ return undefined
452
+ }
453
+ textExt = [...textExt, ...list]
414
454
  }
415
455
  else {
416
456
  rest.push(arg)
417
457
  }
418
458
  }
419
- return { argv: rest, trust }
459
+ return { argv: rest, trust: { ...trust, textExt } }
460
+ }
461
+
462
+
463
+ // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
464
+ // reader who has just written `@"notes.txt"` reaches for one. An empty
465
+ // element, or anything that is not an extension, is a usage error
466
+ // rather than a silently ignored word: a flag that quietly does
467
+ // nothing is how a document ends up refused with no reason visible.
468
+ function parseTextExt(arg: string): string[] | undefined {
469
+ const out: string[] = []
470
+ for (const raw of arg.split(',')) {
471
+ const ext = raw.trim().replace(/^\./, '').toLowerCase()
472
+ if ('' === ext || !/^[a-z0-9]+$/.test(ext)) {
473
+ return undefined
474
+ }
475
+ out.push(ext)
476
+ }
477
+ return out
420
478
  }
421
479
 
422
480
 
423
481
  // The evaluator options a REPL session's capability means.
424
482
  function replTrust(state: ReplState, entryRoot: string): any {
425
- const capability = verbTrust(
426
- state.trust ?? { kind: 'system-warn' }, entryRoot)
427
- return null == capability ? {} : { trust: capability }
483
+ return verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRoot)
428
484
  }
429
485
 
430
486
 
@@ -444,6 +500,20 @@ function verbTrust(trust: TrustArg, entryRoot: string): any {
444
500
  }
445
501
 
446
502
 
503
+ // THE INCLUDE OPTIONS A VERB RUNS UNDER, spread into its engine call:
504
+ // the capability above, and the extensions `--text-ext` widened. Both
505
+ // are absent when unset rather than present-and-undefined, so a verb's
506
+ // options bag is byte-identical to what it was before either flag
507
+ // existed and no engine sees a key it has to ignore.
508
+ function verbOpts(trust: TrustArg, entryRoot: string): any {
509
+ const include = verbTrust(trust, entryRoot)
510
+ return {
511
+ ...(undefined === include ? {} : { trust: include }),
512
+ ...(0 === trust.textExt.length ? {} : { textExt: trust.textExt }),
513
+ }
514
+ }
515
+
516
+
447
517
  // The directory a bare `--trust root` confines to for a verb: the
448
518
  // primary document's own, matching the bare command's entry root.
449
519
  function entryRootOf(file: string | undefined): string {
@@ -607,7 +677,7 @@ export function replCommand(
607
677
  if (':why' === cmd) {
608
678
  const report = why(src, path, {
609
679
  path: state.name,
610
- trust: verbTrust(state.trust ?? { kind: 'system-warn' },
680
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] },
611
681
  entryRootOf(state.name)),
612
682
  })
613
683
  return report.ok
@@ -618,7 +688,7 @@ export function replCommand(
618
688
  ? 'keys' : 'canon' === state.mode ? 'canon' : 'json'
619
689
  const report = get(src, path, {
620
690
  view, path: state.name,
621
- trust: verbTrust(state.trust ?? { kind: 'system-warn' },
691
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] },
622
692
  entryRootOf(state.name)),
623
693
  })
624
694
  return report.ok
@@ -901,7 +971,7 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
901
971
 
902
972
  for (const source of sources) {
903
973
  const report = vet(schemaSrc, source.src, {
904
- trust: verbTrust(trust, entryRootOf(args.schema)),
974
+ ...verbOpts(trust, entryRootOf(args.schema)),
905
975
  at: args.at,
906
976
  closed: args.closed,
907
977
  partial: args.partial,
@@ -1193,7 +1263,7 @@ function runSubsume(argv: string[]): number {
1193
1263
  }
1194
1264
 
1195
1265
  const report = subsume(generalSrc, specificSrc, {
1196
- trust: verbTrust(trust, entryRootOf(args.general)),
1266
+ ...verbOpts(trust, entryRootOf(args.general)),
1197
1267
  profile: args.profile,
1198
1268
  at: args.at,
1199
1269
  generalUrl: args.general,
@@ -1417,15 +1487,18 @@ function oldVersion(spec: string, file: string): OldVersion | undefined {
1417
1487
  // a disjunction whose default is the declared mode. Undefined when the
1418
1488
  // key is absent or does not spell a mode.
1419
1489
  function policyCompat(
1420
- newSrc: string, path: string, trust: any): BreakingMode | undefined {
1490
+ newSrc: string, path: string, include: IncludeOptions
1491
+ ): BreakingMode | undefined {
1421
1492
  const aontu = new Aontu()
1422
1493
  const ctx = aontu.ctx({ collect: true })
1423
1494
  // The declaration is read by EVALUATING the document, so this leg
1424
- // runs the include resolver too and has to run it under the verb's
1425
- // capability -- a `breaking --trust none` that read its own mode
1426
- // through an unconfined resolver would confine the comparison and
1427
- // not the question (use-cases/REVIEW.md finding G).
1428
- const v: any = aontu.unify(newSrc, { path, ...(null == trust ? {} : { trust }) }, ctx)
1495
+ // runs the include resolver too and has to run it under BOTH of the
1496
+ // verb's include options -- a `breaking --trust none` that read its
1497
+ // own mode through an unconfined resolver would confine the
1498
+ // comparison and not the question (use-cases/REVIEW.md finding G),
1499
+ // and one that took the capability alone read no mode at all when
1500
+ // the declaration arrived through a `--text-ext` include.
1501
+ const v: any = aontu.unify(newSrc, { path, ...includeOpts(include) }, ctx)
1429
1502
  if (0 < ctx.err.length || true === v?.isNil) {
1430
1503
  return undefined
1431
1504
  }
@@ -1528,7 +1601,8 @@ function runBreaking(argv: string[]): number {
1528
1601
  // stay valid).
1529
1602
  const mode: BreakingMode =
1530
1603
  args.mode ??
1531
- policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
1604
+ policyCompat(newSrc, args.file,
1605
+ verbOpts(trust, entryRootOf(args.file))) ??
1532
1606
  'backward'
1533
1607
 
1534
1608
  if ('none' === mode) {
@@ -1578,7 +1652,7 @@ function runBreaking(argv: string[]): number {
1578
1652
 
1579
1653
  for (const check of checks) {
1580
1654
  const report = subsume(check.general[0], check.specific[0], {
1581
- trust: verbTrust(trust, entryRootOf(args.file)),
1655
+ ...verbOpts(trust, entryRootOf(args.file)),
1582
1656
  at: args.at,
1583
1657
  generalUrl: check.general[1],
1584
1658
  specificUrl: check.specific[1],
@@ -1724,7 +1798,7 @@ function runTrim(argv: string[]): number {
1724
1798
  }
1725
1799
 
1726
1800
  const report = trimCheck(src, {
1727
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1801
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1728
1802
  })
1729
1803
  const text = 'json' === format
1730
1804
  ? renderTrimJson(report)
@@ -1788,8 +1862,8 @@ const VIEW_HELP =
1788
1862
  'aontu view <kind> [options] <file>... (try --help)'
1789
1863
 
1790
1864
  const VIEW_KINDS: ViewKind[] =
1791
- ['doc', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers', 'ladder',
1792
- 'poset']
1865
+ ['doc', 'lattice', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers',
1866
+ 'ladder', 'poset']
1793
1867
 
1794
1868
  const VIEW_PROFILES: ViewProfile[] = ['text', 'mermaid', 'dot', 'er', 'svg']
1795
1869
 
@@ -2110,7 +2184,7 @@ function runRelations(argv: string[]): number {
2110
2184
  }
2111
2185
 
2112
2186
  const report = relationCheck(src, {
2113
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2187
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2114
2188
  })
2115
2189
  const text = 'json' === format
2116
2190
  ? renderRelationsJson(report)
@@ -2178,7 +2252,7 @@ function runReaches(argv: string[]): number {
2178
2252
 
2179
2253
  const report = reachCheck(src, rest[0], rest[1], {
2180
2254
  path: rest[2], relation,
2181
- trust: verbTrust(trust, entryRootOf(rest[2])),
2255
+ ...verbOpts(trust, entryRootOf(rest[2])),
2182
2256
  })
2183
2257
  const text = 'json' === format
2184
2258
  ? renderReachesJson(report)
@@ -2424,7 +2498,7 @@ function runView(argv: string[]): number {
2424
2498
  kind,
2425
2499
  path: files[0],
2426
2500
  roots,
2427
- trust: verbTrust(trust, entryRootOf(files[0])),
2501
+ ...verbOpts(trust, entryRootOf(files[0])),
2428
2502
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2429
2503
  })
2430
2504
 
@@ -2503,7 +2577,7 @@ function runViewSet(
2503
2577
  }
2504
2578
 
2505
2579
  const report = viewSet(src, {
2506
- ...opts, path: file, trust: verbTrust(trust, entryRootOf(file)),
2580
+ ...opts, path: file, ...verbOpts(trust, entryRootOf(file)),
2507
2581
  })
2508
2582
 
2509
2583
  if ('json' === how.format) {
@@ -2726,7 +2800,7 @@ function runJsonSchema(argv: string[]): number {
2726
2800
  }
2727
2801
 
2728
2802
  const report = jsonSchema(src, {
2729
- at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2803
+ at, path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2730
2804
  })
2731
2805
 
2732
2806
  if ('json' === format) {
@@ -2819,9 +2893,7 @@ function runHash(argv: string[]): number {
2819
2893
 
2820
2894
  // The file's own directory is the include base, as every verb
2821
2895
  // resolves a named file (vet's aontuForPath rule).
2822
- const capability = verbTrust(trust, entryRootOf(files[0]))
2823
- const aontu = new Aontu(
2824
- null == capability ? undefined : { trust: capability })
2896
+ const aontu = new Aontu(verbOpts(trust, entryRootOf(files[0])))
2825
2897
  const ctx = aontu.ctx({ collect: true })
2826
2898
  const v: any = aontu.unify(src, { path: files[0] }, ctx)
2827
2899
  if (0 < ctx.err.length || true === v?.isNil) {
@@ -2937,7 +3009,7 @@ function runGet(argv: string[]): number {
2937
3009
  }
2938
3010
 
2939
3011
  const report = get(src, path, {
2940
- view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
3012
+ view, depth, path: file, ...verbOpts(trust, entryRootOf(file)),
2941
3013
  })
2942
3014
  if ('json' === format) {
2943
3015
  process.stdout.write(exactJSON({
@@ -3021,7 +3093,7 @@ function runWhy(argv: string[]): number {
3021
3093
  }
3022
3094
 
3023
3095
  const report = why(src, path, {
3024
- path: file, trust: verbTrust(trust, entryRootOf(file)),
3096
+ path: file, ...verbOpts(trust, entryRootOf(file)),
3025
3097
  })
3026
3098
  if ('json' === format) {
3027
3099
  process.stdout.write(exactJSON({
@@ -3156,7 +3228,7 @@ function runSet(argv: string[]): number {
3156
3228
  }
3157
3229
 
3158
3230
  const report = patch(entrySrc, overlaySrc, assignments, {
3159
- trust: verbTrust(trust, entryRootOf(entry)),
3231
+ ...verbOpts(trust, entryRootOf(entry)),
3160
3232
  entryPath: entry,
3161
3233
  overlayPath: overlayFile,
3162
3234
  inPlace,
@@ -3291,7 +3363,7 @@ function runAgentsMd(argv: string[]): number {
3291
3363
 
3292
3364
  const report = agentsMd(src, {
3293
3365
  name: files[0], path: files[0],
3294
- trust: verbTrust(trust, entryRootOf(files[0])),
3366
+ ...verbOpts(trust, entryRootOf(files[0])),
3295
3367
  })
3296
3368
  if (!report.ok) {
3297
3369
  process.stderr.write(
@@ -3343,6 +3415,129 @@ function runAgentsMd(argv: string[]): number {
3343
3415
  // on the parity-probe discipline in AGENTS.md, which derives expected
3344
3416
  // spec values by piping BOTH CLIs and comparing. A truncated pipe there
3345
3417
  // reads as a port divergence.
3418
+ // ---------------------------------------------------------------------
3419
+ // The source formatter (docs/design/FMT.0.md): one agreed form, in the
3420
+ // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
3421
+ // the form itself is the library's (ts/src/format.ts), and the two
3422
+ // ports agree on it row by row in test/spec/fmt.tsv.
3423
+
3424
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d] <file>... (try --help)'
3425
+
3426
+ type FmtFlags = { write: boolean, list: boolean, check: boolean, diff: boolean }
3427
+
3428
+ function runFmt(argv: string[]): number | Promise<number> {
3429
+ const files: string[] = []
3430
+ const flags: FmtFlags = { write: false, list: false, check: false, diff: false }
3431
+
3432
+ for (const arg of argv) {
3433
+ if ('-h' === arg || '--help' === arg) {
3434
+ process.stdout.write(HELP)
3435
+ return 0
3436
+ }
3437
+ if ('-w' === arg || '--write' === arg) {
3438
+ flags.write = true
3439
+ }
3440
+ else if ('-l' === arg || '--list' === arg) {
3441
+ flags.list = true
3442
+ }
3443
+ else if ('--check' === arg) {
3444
+ flags.check = true
3445
+ }
3446
+ else if ('-d' === arg || '--diff' === arg) {
3447
+ flags.diff = true
3448
+ }
3449
+ else if (arg.startsWith('-')) {
3450
+ process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
3451
+ return 2
3452
+ }
3453
+ else {
3454
+ files.push(arg)
3455
+ }
3456
+ }
3457
+
3458
+ if (0 === files.length) {
3459
+ // Standard input: formatted onto standard output, or listed,
3460
+ // checked and diffed under the name <stdin>. It cannot be written
3461
+ // back.
3462
+ if (flags.write) {
3463
+ process.stderr.write(`aontu: --write needs a file\n${FMT_HELP}\n`)
3464
+ return 2
3465
+ }
3466
+ return new Promise((resolve) => {
3467
+ let src = ''
3468
+ process.stdin.setEncoding('utf8')
3469
+ process.stdin.on('data', (d) => (src += d))
3470
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)))
3471
+ })
3472
+ }
3473
+
3474
+ // Several files onto standard output would be one stream nobody can
3475
+ // split again (the note's X-6): the verb refuses unless an option
3476
+ // says what to do with each.
3477
+ if (1 < files.length && !fmtQuiet(flags)) {
3478
+ process.stderr.write(
3479
+ `aontu: fmt prints one file; with ${files.length}, say --write, ` +
3480
+ `--list, --check or --diff\n${FMT_HELP}\n`)
3481
+ return 2
3482
+ }
3483
+
3484
+ let worst = 0
3485
+ for (const file of files) {
3486
+ let src: string
3487
+ try {
3488
+ src = readFileSync(file, 'utf8')
3489
+ }
3490
+ catch (err: any) {
3491
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3492
+ return 2
3493
+ }
3494
+ worst = Math.max(worst, fmtOne(file, src, flags))
3495
+ }
3496
+ return worst
3497
+ }
3498
+
3499
+ // An option that says what to do with a file that would change, in
3500
+ // place of printing it.
3501
+ function fmtQuiet(flags: FmtFlags): boolean {
3502
+ return flags.write || flags.list || flags.check || flags.diff
3503
+ }
3504
+
3505
+ // One document: 0 printed, clean or done; 1 a --check that would
3506
+ // change; 2 a file that cannot be written; 4 a document that does not
3507
+ // format, with the finding that says why.
3508
+ function fmtOne(name: string, src: string, flags: FmtFlags): number {
3509
+ const report = format(src, { path: name })
3510
+ if ('error' === report.verdict) {
3511
+ process.stderr.write(`aontu: ${name} was not formatted\n` +
3512
+ report.errors.map(renderFinding).join('\n') + '\n')
3513
+ return 4
3514
+ }
3515
+ if (!fmtQuiet(flags)) {
3516
+ process.stdout.write(report.text)
3517
+ return 0
3518
+ }
3519
+ if (!report.changed) {
3520
+ return 0
3521
+ }
3522
+ if (flags.list || flags.check) {
3523
+ process.stdout.write(name + '\n')
3524
+ }
3525
+ if (flags.diff) {
3526
+ process.stdout.write(unifiedDiff(name, src, report.text))
3527
+ }
3528
+ if (flags.write) {
3529
+ try {
3530
+ writeFileSync(name, report.text)
3531
+ }
3532
+ catch (err: any) {
3533
+ process.stderr.write(`aontu: cannot write ${name}: ${err.message}\n`)
3534
+ return 2
3535
+ }
3536
+ }
3537
+ return flags.check ? 1 : 0
3538
+ }
3539
+
3540
+
3346
3541
  function finish(code: number): void {
3347
3542
  process.exitCode = code
3348
3543
  }
@@ -3352,16 +3547,16 @@ function finish(code: number): void {
3352
3547
  // spelling, so the caller owns the usage error.
3353
3548
  function parseTrustArg(value: string): TrustArg | undefined {
3354
3549
  if ('system' === value) {
3355
- return { kind: 'system' }
3550
+ return { kind: 'system', textExt: [] }
3356
3551
  }
3357
3552
  if ('none' === value) {
3358
- return { kind: 'none' }
3553
+ return { kind: 'none', textExt: [] }
3359
3554
  }
3360
3555
  if ('root' === value) {
3361
- return { kind: 'root' }
3556
+ return { kind: 'root', textExt: [] }
3362
3557
  }
3363
3558
  if (value.startsWith('root:') && 'root:'.length < value.length) {
3364
- return { kind: 'root', dir: value.slice('root:'.length) }
3559
+ return { kind: 'root', dir: value.slice('root:'.length), textExt: [] }
3365
3560
  }
3366
3561
  return undefined
3367
3562
  }
@@ -3385,7 +3580,8 @@ function main(argv: string[]): void {
3385
3580
  // overwritten twice. In a tool loop that reads as a passing
3386
3581
  // validation. Counting them is what lets the refusal below happen.
3387
3582
  const files: string[] = []
3388
- let trust: TrustArg = { kind: 'system-warn' }
3583
+ let trust: TrustArg = { kind: 'system-warn', textExt: [] }
3584
+ let textExt: string[] = []
3389
3585
  // The REPL's SESSION protocol (G7 phase 7): one JSON line per
3390
3586
  // answer, so a harness can drive the session. Named --jsonl rather
3391
3587
  // than the design's --json, which would read as the `:json` output
@@ -3413,6 +3609,9 @@ function main(argv: string[]): void {
3413
3609
  if ('agentsmd' === argv[2]) {
3414
3610
  return finish(runAgentsMd(argv.slice(3)))
3415
3611
  }
3612
+ if ('fmt' === argv[2]) {
3613
+ return void Promise.resolve(runFmt(argv.slice(3))).then(finish)
3614
+ }
3416
3615
 
3417
3616
  if ('set' === argv[2]) {
3418
3617
  return finish(runSet(argv.slice(3)))
@@ -3492,7 +3691,17 @@ function main(argv: string[]): void {
3492
3691
  process.stderr.write('aontu: --include-root needs a directory\n')
3493
3692
  return finish(2)
3494
3693
  }
3495
- trust = { kind: 'root', dir }
3694
+ trust = { kind: 'root', dir, textExt }
3695
+ }
3696
+ else if ('--text-ext' === arg) {
3697
+ const list = null == args[i + 1] ? undefined : parseTextExt(args[++i])
3698
+ if (null == list) {
3699
+ process.stderr.write(
3700
+ 'aontu: --text-ext needs extensions, without dots' +
3701
+ ' (--text-ext md,sql)\n')
3702
+ return finish(2)
3703
+ }
3704
+ textExt = [...textExt, ...list]
3496
3705
  }
3497
3706
  else if (arg.startsWith('-')) {
3498
3707
  process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`)
@@ -3519,6 +3728,11 @@ function main(argv: string[]): void {
3519
3728
  return finish(2)
3520
3729
  }
3521
3730
 
3731
+ // The extensions ride with the capability from here on, so the three
3732
+ // entry shapes below (file, REPL, stdin) each get them by threading
3733
+ // the one value they already thread.
3734
+ trust = { ...trust, textExt }
3735
+
3522
3736
  const file = files[0]
3523
3737
  if (null != file) {
3524
3738
  finish(runFile(file, mode, trust))
@@ -3547,6 +3761,6 @@ export {
3547
3761
  runJsonSchema,
3548
3762
  runMod,
3549
3763
  runHash, runGet,
3550
- runWhy, renderWhyText, runSet, runAgentsMd,
3764
+ runWhy, renderWhyText, runSet, runAgentsMd, runFmt,
3551
3765
  watchChange, watchSignature, vetWaiter, deprecatedAt,
3552
3766
  }
package/src/diff.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+ import { includeOpts } from './utility'
2
3
 
3
4
  // PATH-ADDRESSED DIFF (G7 phase 6,
4
5
  // docs/capability-review/g7-machine-access.md): what changed, at which
@@ -61,6 +62,11 @@ export type DiffOptions = {
61
62
  // extension -- but reading is enough.) A server passes
62
63
  // `{include:'none'}`.
63
64
  trust?: TrustOptions
65
+
66
+ // Extensions additionally read as text (the CLI's `--text-ext`).
67
+ // Rides beside `trust` because it is the other half of what an
68
+ // include may read.
69
+ textExt?: string[]
64
70
  }
65
71
 
66
72
 
@@ -182,8 +188,7 @@ function evalSide(
182
188
  export function diff(
183
189
  leftSrc: string, rightSrc: string, opts?: DiffOptions): DiffReport {
184
190
  const options = opts ?? {}
185
- const aontu = new Aontu(
186
- null == options.trust ? undefined : { trust: options.trust })
191
+ const aontu = new Aontu(includeOpts(options))
187
192
 
188
193
  const l = evalSide(aontu, leftSrc, options.leftPath, options.at)
189
194
  const r = evalSide(aontu, rightSrc, options.rightPath, options.at)