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/src/cli.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import { evalFailure } from './query'
13
13
  // __importStar downlevel helper, whose branches no supported Node takes.
14
14
  import {
15
- mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,
15
+ existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,
16
16
  } from 'node:fs'
17
17
  import { basename, dirname, join, resolve } from 'node:path'
18
18
  import { tmpdir } from 'node:os'
@@ -25,6 +25,8 @@ import {
25
25
  get, why, patch, agentsMd,
26
26
  } from './aontu'
27
27
  import { sarifReport } from './report-sarif'
28
+ import { main as lspMain } from './lsp-server'
29
+ import { main as mcpMain } from './mcp-server'
28
30
  import { jsonSchema } from './jsonschema'
29
31
  import { modTidy, modVerify, modVendor, modManifest } from './mod-tool'
30
32
  import type {
@@ -48,6 +50,9 @@ import type {
48
50
  import type { QueryView } from './query'
49
51
  import type { WhyRecord } from './provenance'
50
52
  import { agentsMdSplice } from './agentsmd'
53
+ import { format, unifiedDiff } from './format'
54
+ import { includeOpts } from './utility'
55
+ import type { IncludeOptions } from './utility'
51
56
 
52
57
 
53
58
  type Mode = 'json' | 'canon'
@@ -69,6 +74,9 @@ const HELP = `Usage: aontu [options] [file]
69
74
  aontu why <path> [options] <file>
70
75
  aontu set <path>=<value>... --entry <file> --overlay <file>
71
76
  aontu agentsmd [--write <AGENTS.md>] <file>
77
+ aontu fmt [-w|-l|--check|-d|--lint] <file>...
78
+ aontu lsp
79
+ aontu mcp [--root <dir>]
72
80
 
73
81
  Evaluate an Aontu source file and print the result as JSON.
74
82
  With no file on an interactive terminal, start a REPL.
@@ -91,6 +99,10 @@ Options:
91
99
  Every verb takes it too, and a bare root means the
92
100
  document's own directory
93
101
  --include-root <dir> Shorthand for --trust root:<dir>
102
+ --text-ext <e> Read these extensions as text too, comma-separated
103
+ and without dots (md,sql). .txt needs no flag; a
104
+ named format keeps its meaning, and .js stays
105
+ refused. Every verb takes it
94
106
 
95
107
  Mod options:
96
108
  --format <f> text (default) or json
@@ -98,10 +110,10 @@ Mod options:
98
110
 
99
111
  Mod subcommands:
100
112
  tidy Resolve the module closure by minimum version selection and
101
- rewrite mod-lock.aon in canonical form
102
- verify Check every locked module still means what mod-lock.aon
113
+ rewrite aontu_meta/mod-lock.aon in canonical form
114
+ verify Check every locked module still means what the lockfile
103
115
  pins, and change nothing (the CI gate; tidy rewrites)
104
- vendor Materialise the locked closure into aon_vendor/
116
+ vendor Materialise the locked closure into aontu_meta/vendor/
105
117
  manifest Print the OCI artifact a publish would push, gated on the
106
118
  breaking check against --against
107
119
 
@@ -182,8 +194,8 @@ Why options:
182
194
  Why exit codes mirror get's: 0 explained, 1 the path names nothing,
183
195
  2 usage, 4 the document does not stand up on its own.
184
196
 
185
- View kinds: tree, matrix, graph, layer, sets, layers, ladder, poset
186
- (the poset takes several files). The figure goes to stdout, the loss
197
+ View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
198
+ ladder, poset (the poset takes several files). The figure goes to stdout, the loss
187
199
  report to stderr. With --views it draws every figure a document
188
200
  declares as data, from one evaluation: each declaration names its own
189
201
  kind and out file, nothing is written unless every figure rendered,
@@ -191,13 +203,13 @@ and --check gates the committed set.
191
203
 
192
204
  View options:
193
205
  --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
206
+ lattice, tree, matrix, sets and layers draw text
207
+ (default) or svg; graph draws mermaid (default),
208
+ dot or er; layer draws text (default), mermaid or
209
+ svg; ladder and poset draw mermaid (default) or dot
198
210
  --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
211
+ subtree doc draws; the subtree the lattice counts;
212
+ the path the ladder draws; where the poset compares
201
213
  --views <path> Draw every figure the document declares at this
202
214
  path, one evaluation, all or nothing; each
203
215
  declaration names its own kind and out file
@@ -276,6 +288,32 @@ Agentsmd options:
276
288
  Agentsmd exit codes: 0 generated, 2 usage, 4 the document does not
277
289
  stand up on its own.
278
290
 
291
+ Fmt options:
292
+ -w, --write Rewrite each file in place, when its form would change
293
+ -l, --list Print the name of each file whose form would change
294
+ --check Like --list, and exit 1 when any would: the CI gate
295
+ -d, --diff Print a unified diff for each file whose form would
296
+ change
297
+ --lint Report the style findings, key case and repeated
298
+ shapes, on standard error, and print nothing else
299
+ --strict With --lint, and exit 1 when there is a finding
300
+
301
+ The fmt verb prints one document in the agreed form; with no file it
302
+ reads standard input. Several files need one of the options above.
303
+
304
+ Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
305
+ a --strict finding, 2 usage, 4 a document does not parse.
306
+
307
+ The lsp verb runs the language server over standard input and output
308
+ (LSP: JSON-RPC with Content-Length framing) until the client exits;
309
+ editors launch it with no arguments. The standalone aontu-lsp binary
310
+ runs the same server.
311
+
312
+ The mcp verb runs the Model Context Protocol server over standard
313
+ input and output (newline-delimited JSON-RPC), confined below --root
314
+ when one is given. It is part of the npm build, as the standalone
315
+ aontu-mcp binary is.
316
+
279
317
  REPL commands:
280
318
  :help Show REPL help
281
319
  :load <file> Evaluate a document and hold it for the commands below
@@ -333,11 +371,18 @@ function evalSource(
333
371
  // entry root or goes through package resolution prints a one-line
334
372
  // stderr warning naming the flag a future default will require
335
373
  // (phase 6, the staged flip).
336
- type TrustArg =
374
+ type TrustArg = (
337
375
  | { kind: 'system-warn' }
338
376
  | { kind: 'system' }
339
377
  | { kind: 'none' }
340
378
  | { kind: 'root', dir?: string }
379
+ // EXTENSIONS READ AS TEXT ride with the capability rather than
380
+ // beside it: both answer "what may an include read", both are
381
+ // stripped by takeTrust before a verb parses its own tail, and a
382
+ // verb that threads one and not the other is the G5 defect again --
383
+ // `aontu vet` running under a flag the bare command honoured and it
384
+ // did not.
385
+ ) & { textExt: string[] }
341
386
 
342
387
 
343
388
  // The one-line warning of the staged default flip. Once per (kind,
@@ -366,15 +411,16 @@ function makeTrustWarn(): (kind: 'escape' | 'pkg', path: string) => void {
366
411
  // entryRoot (the entry file's directory, or the working directory for
367
412
  // stdin/REPL).
368
413
  function trustOpts(trust: TrustArg, entryRoot: string): any {
414
+ const text = 0 === trust.textExt.length ? {} : { textExt: trust.textExt }
369
415
  switch (trust.kind) {
370
416
  case 'none':
371
- return { trust: { include: 'none' } }
417
+ return { ...text, trust: { include: 'none' } }
372
418
  case 'root':
373
- return { trust: { include: { root: trust.dir ?? entryRoot } } }
419
+ return { ...text, trust: { include: { root: trust.dir ?? entryRoot } } }
374
420
  case 'system':
375
- return {}
421
+ return { ...text }
376
422
  default: // system-warn: today's default plus the warning window
377
- return { trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot }
423
+ return { ...text, trustWarn: makeTrustWarn(), trustWarnRoot: entryRoot }
378
424
  }
379
425
  }
380
426
 
@@ -392,7 +438,8 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
392
438
  function takeTrust(argv: string[]):
393
439
  { argv: string[], trust: TrustArg } | undefined {
394
440
  const rest: string[] = []
395
- let trust: TrustArg = { kind: 'system-warn' }
441
+ let trust: TrustArg = { kind: 'system-warn', textExt: [] }
442
+ let textExt: string[] = []
396
443
  for (let i = 0; i < argv.length; i++) {
397
444
  const arg = argv[i]
398
445
  if ('--trust' === arg) {
@@ -410,21 +457,47 @@ function takeTrust(argv: string[]):
410
457
  process.stderr.write('aontu: --include-root needs a directory\n')
411
458
  return undefined
412
459
  }
413
- trust = { kind: 'root', dir }
460
+ trust = { kind: 'root', dir, textExt }
461
+ }
462
+ else if ('--text-ext' === arg) {
463
+ const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i])
464
+ if (null == list) {
465
+ process.stderr.write(
466
+ 'aontu: --text-ext needs extensions, without dots' +
467
+ ' (--text-ext md,sql)\n')
468
+ return undefined
469
+ }
470
+ textExt = [...textExt, ...list]
414
471
  }
415
472
  else {
416
473
  rest.push(arg)
417
474
  }
418
475
  }
419
- return { argv: rest, trust }
476
+ return { argv: rest, trust: { ...trust, textExt } }
477
+ }
478
+
479
+
480
+ // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
481
+ // reader who has just written `@"notes.txt"` reaches for one. An empty
482
+ // element, or anything that is not an extension, is a usage error
483
+ // rather than a silently ignored word: a flag that quietly does
484
+ // nothing is how a document ends up refused with no reason visible.
485
+ function parseTextExt(arg: string): string[] | undefined {
486
+ const out: string[] = []
487
+ for (const raw of arg.split(',')) {
488
+ const ext = raw.trim().replace(/^\./, '').toLowerCase()
489
+ if ('' === ext || !/^[a-z0-9]+$/.test(ext)) {
490
+ return undefined
491
+ }
492
+ out.push(ext)
493
+ }
494
+ return out
420
495
  }
421
496
 
422
497
 
423
498
  // The evaluator options a REPL session's capability means.
424
499
  function replTrust(state: ReplState, entryRoot: string): any {
425
- const capability = verbTrust(
426
- state.trust ?? { kind: 'system-warn' }, entryRoot)
427
- return null == capability ? {} : { trust: capability }
500
+ return verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] }, entryRoot)
428
501
  }
429
502
 
430
503
 
@@ -444,6 +517,20 @@ function verbTrust(trust: TrustArg, entryRoot: string): any {
444
517
  }
445
518
 
446
519
 
520
+ // THE INCLUDE OPTIONS A VERB RUNS UNDER, spread into its engine call:
521
+ // the capability above, and the extensions `--text-ext` widened. Both
522
+ // are absent when unset rather than present-and-undefined, so a verb's
523
+ // options bag is byte-identical to what it was before either flag
524
+ // existed and no engine sees a key it has to ignore.
525
+ function verbOpts(trust: TrustArg, entryRoot: string): any {
526
+ const include = verbTrust(trust, entryRoot)
527
+ return {
528
+ ...(undefined === include ? {} : { trust: include }),
529
+ ...(0 === trust.textExt.length ? {} : { textExt: trust.textExt }),
530
+ }
531
+ }
532
+
533
+
447
534
  // The directory a bare `--trust root` confines to for a verb: the
448
535
  // primary document's own, matching the bare command's entry root.
449
536
  function entryRootOf(file: string | undefined): string {
@@ -607,7 +694,7 @@ export function replCommand(
607
694
  if (':why' === cmd) {
608
695
  const report = why(src, path, {
609
696
  path: state.name,
610
- trust: verbTrust(state.trust ?? { kind: 'system-warn' },
697
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] },
611
698
  entryRootOf(state.name)),
612
699
  })
613
700
  return report.ok
@@ -618,7 +705,7 @@ export function replCommand(
618
705
  ? 'keys' : 'canon' === state.mode ? 'canon' : 'json'
619
706
  const report = get(src, path, {
620
707
  view, path: state.name,
621
- trust: verbTrust(state.trust ?? { kind: 'system-warn' },
708
+ ...verbOpts(state.trust ?? { kind: 'system-warn', textExt: [] },
622
709
  entryRootOf(state.name)),
623
710
  })
624
711
  return report.ok
@@ -901,7 +988,7 @@ function vetOnce(args: VetArgs, trust: TrustArg): number {
901
988
 
902
989
  for (const source of sources) {
903
990
  const report = vet(schemaSrc, source.src, {
904
- trust: verbTrust(trust, entryRootOf(args.schema)),
991
+ ...verbOpts(trust, entryRootOf(args.schema)),
905
992
  at: args.at,
906
993
  closed: args.closed,
907
994
  partial: args.partial,
@@ -1193,7 +1280,7 @@ function runSubsume(argv: string[]): number {
1193
1280
  }
1194
1281
 
1195
1282
  const report = subsume(generalSrc, specificSrc, {
1196
- trust: verbTrust(trust, entryRootOf(args.general)),
1283
+ ...verbOpts(trust, entryRootOf(args.general)),
1197
1284
  profile: args.profile,
1198
1285
  at: args.at,
1199
1286
  generalUrl: args.general,
@@ -1417,15 +1504,18 @@ function oldVersion(spec: string, file: string): OldVersion | undefined {
1417
1504
  // a disjunction whose default is the declared mode. Undefined when the
1418
1505
  // key is absent or does not spell a mode.
1419
1506
  function policyCompat(
1420
- newSrc: string, path: string, trust: any): BreakingMode | undefined {
1507
+ newSrc: string, path: string, include: IncludeOptions
1508
+ ): BreakingMode | undefined {
1421
1509
  const aontu = new Aontu()
1422
1510
  const ctx = aontu.ctx({ collect: true })
1423
1511
  // 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)
1512
+ // runs the include resolver too and has to run it under BOTH of the
1513
+ // verb's include options -- a `breaking --trust none` that read its
1514
+ // own mode through an unconfined resolver would confine the
1515
+ // comparison and not the question (use-cases/REVIEW.md finding G),
1516
+ // and one that took the capability alone read no mode at all when
1517
+ // the declaration arrived through a `--text-ext` include.
1518
+ const v: any = aontu.unify(newSrc, { path, ...includeOpts(include) }, ctx)
1429
1519
  if (0 < ctx.err.length || true === v?.isNil) {
1430
1520
  return undefined
1431
1521
  }
@@ -1528,7 +1618,8 @@ function runBreaking(argv: string[]): number {
1528
1618
  // stay valid).
1529
1619
  const mode: BreakingMode =
1530
1620
  args.mode ??
1531
- policyCompat(newSrc, args.file, verbTrust(trust, entryRootOf(args.file))) ??
1621
+ policyCompat(newSrc, args.file,
1622
+ verbOpts(trust, entryRootOf(args.file))) ??
1532
1623
  'backward'
1533
1624
 
1534
1625
  if ('none' === mode) {
@@ -1578,7 +1669,7 @@ function runBreaking(argv: string[]): number {
1578
1669
 
1579
1670
  for (const check of checks) {
1580
1671
  const report = subsume(check.general[0], check.specific[0], {
1581
- trust: verbTrust(trust, entryRootOf(args.file)),
1672
+ ...verbOpts(trust, entryRootOf(args.file)),
1582
1673
  at: args.at,
1583
1674
  generalUrl: check.general[1],
1584
1675
  specificUrl: check.specific[1],
@@ -1724,7 +1815,7 @@ function runTrim(argv: string[]): number {
1724
1815
  }
1725
1816
 
1726
1817
  const report = trimCheck(src, {
1727
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
1818
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
1728
1819
  })
1729
1820
  const text = 'json' === format
1730
1821
  ? renderTrimJson(report)
@@ -1788,8 +1879,8 @@ const VIEW_HELP =
1788
1879
  'aontu view <kind> [options] <file>... (try --help)'
1789
1880
 
1790
1881
  const VIEW_KINDS: ViewKind[] =
1791
- ['doc', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers', 'ladder',
1792
- 'poset']
1882
+ ['doc', 'lattice', 'tree', 'matrix', 'graph', 'layer', 'sets', 'layers',
1883
+ 'ladder', 'poset']
1793
1884
 
1794
1885
  const VIEW_PROFILES: ViewProfile[] = ['text', 'mermaid', 'dot', 'er', 'svg']
1795
1886
 
@@ -1920,6 +2011,16 @@ function runMod(argv: string[]): number {
1920
2011
  return 2
1921
2012
  }
1922
2013
 
2014
+ // THE OLD LAYOUT IS NAMED, NOT READ. The lockfile and the vendored
2015
+ // closure moved under aontu_meta/; a project that still carries them
2016
+ // at its root would otherwise look untouched by any of these verbs,
2017
+ // which is the one silence worth breaking.
2018
+ if (existsSync(join(dir, 'aon_vendor')) || existsSync(join(dir, 'mod-lock.aon'))) {
2019
+ process.stderr.write(
2020
+ 'aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
2021
+ 'move them, or run aontu mod tidy and aontu mod vendor\n')
2022
+ }
2023
+
1923
2024
  // `--against` gates a manifest and means nothing to the other two;
1924
2025
  // accepting it there would say it had been honoured.
1925
2026
  if (null != against && 'manifest' !== sub) {
@@ -2110,7 +2211,7 @@ function runRelations(argv: string[]): number {
2110
2211
  }
2111
2212
 
2112
2213
  const report = relationCheck(src, {
2113
- path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2214
+ path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2114
2215
  })
2115
2216
  const text = 'json' === format
2116
2217
  ? renderRelationsJson(report)
@@ -2178,7 +2279,7 @@ function runReaches(argv: string[]): number {
2178
2279
 
2179
2280
  const report = reachCheck(src, rest[0], rest[1], {
2180
2281
  path: rest[2], relation,
2181
- trust: verbTrust(trust, entryRootOf(rest[2])),
2282
+ ...verbOpts(trust, entryRootOf(rest[2])),
2182
2283
  })
2183
2284
  const text = 'json' === format
2184
2285
  ? renderReachesJson(report)
@@ -2424,7 +2525,7 @@ function runView(argv: string[]): number {
2424
2525
  kind,
2425
2526
  path: files[0],
2426
2527
  roots,
2427
- trust: verbTrust(trust, entryRootOf(files[0])),
2528
+ ...verbOpts(trust, entryRootOf(files[0])),
2428
2529
  docs: files.slice(1).map((path, i) => ({ src: srcs[i + 1], path })),
2429
2530
  })
2430
2531
 
@@ -2503,7 +2604,7 @@ function runViewSet(
2503
2604
  }
2504
2605
 
2505
2606
  const report = viewSet(src, {
2506
- ...opts, path: file, trust: verbTrust(trust, entryRootOf(file)),
2607
+ ...opts, path: file, ...verbOpts(trust, entryRootOf(file)),
2507
2608
  })
2508
2609
 
2509
2610
  if ('json' === how.format) {
@@ -2726,7 +2827,7 @@ function runJsonSchema(argv: string[]): number {
2726
2827
  }
2727
2828
 
2728
2829
  const report = jsonSchema(src, {
2729
- at, path: files[0], trust: verbTrust(trust, entryRootOf(files[0])),
2830
+ at, path: files[0], ...verbOpts(trust, entryRootOf(files[0])),
2730
2831
  })
2731
2832
 
2732
2833
  if ('json' === format) {
@@ -2819,9 +2920,7 @@ function runHash(argv: string[]): number {
2819
2920
 
2820
2921
  // The file's own directory is the include base, as every verb
2821
2922
  // 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 })
2923
+ const aontu = new Aontu(verbOpts(trust, entryRootOf(files[0])))
2825
2924
  const ctx = aontu.ctx({ collect: true })
2826
2925
  const v: any = aontu.unify(src, { path: files[0] }, ctx)
2827
2926
  if (0 < ctx.err.length || true === v?.isNil) {
@@ -2937,7 +3036,7 @@ function runGet(argv: string[]): number {
2937
3036
  }
2938
3037
 
2939
3038
  const report = get(src, path, {
2940
- view, depth, path: file, trust: verbTrust(trust, entryRootOf(file)),
3039
+ view, depth, path: file, ...verbOpts(trust, entryRootOf(file)),
2941
3040
  })
2942
3041
  if ('json' === format) {
2943
3042
  process.stdout.write(exactJSON({
@@ -3021,7 +3120,7 @@ function runWhy(argv: string[]): number {
3021
3120
  }
3022
3121
 
3023
3122
  const report = why(src, path, {
3024
- path: file, trust: verbTrust(trust, entryRootOf(file)),
3123
+ path: file, ...verbOpts(trust, entryRootOf(file)),
3025
3124
  })
3026
3125
  if ('json' === format) {
3027
3126
  process.stdout.write(exactJSON({
@@ -3156,7 +3255,7 @@ function runSet(argv: string[]): number {
3156
3255
  }
3157
3256
 
3158
3257
  const report = patch(entrySrc, overlaySrc, assignments, {
3159
- trust: verbTrust(trust, entryRootOf(entry)),
3258
+ ...verbOpts(trust, entryRootOf(entry)),
3160
3259
  entryPath: entry,
3161
3260
  overlayPath: overlayFile,
3162
3261
  inPlace,
@@ -3291,7 +3390,7 @@ function runAgentsMd(argv: string[]): number {
3291
3390
 
3292
3391
  const report = agentsMd(src, {
3293
3392
  name: files[0], path: files[0],
3294
- trust: verbTrust(trust, entryRootOf(files[0])),
3393
+ ...verbOpts(trust, entryRootOf(files[0])),
3295
3394
  })
3296
3395
  if (!report.ok) {
3297
3396
  process.stderr.write(
@@ -3343,6 +3442,188 @@ function runAgentsMd(argv: string[]): number {
3343
3442
  // on the parity-probe discipline in AGENTS.md, which derives expected
3344
3443
  // spec values by piping BOTH CLIs and comparing. A truncated pipe there
3345
3444
  // reads as a port divergence.
3445
+ // ---------------------------------------------------------------------
3446
+ // The source formatter (docs/design/FMT.0.md): one agreed form, in the
3447
+ // tradition of gofmt. The verb prints, lists, checks, diffs or rewrites;
3448
+ // the form itself is the library's (ts/src/format.ts), and the two
3449
+ // ports agree on it row by row in test/spec/fmt.tsv.
3450
+
3451
+ const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] <file>... (try --help)'
3452
+
3453
+ type FmtFlags = {
3454
+ write: boolean, list: boolean, check: boolean, diff: boolean, lint: boolean, strict: boolean,
3455
+ }
3456
+
3457
+ function runFmt(argv: string[]): number | Promise<number> {
3458
+ const files: string[] = []
3459
+ const flags: FmtFlags = {
3460
+ write: false, list: false, check: false, diff: false, lint: false, strict: false,
3461
+ }
3462
+
3463
+ for (const arg of argv) {
3464
+ if ('-h' === arg || '--help' === arg) {
3465
+ process.stdout.write(HELP)
3466
+ return 0
3467
+ }
3468
+ if ('-w' === arg || '--write' === arg) {
3469
+ flags.write = true
3470
+ }
3471
+ else if ('-l' === arg || '--list' === arg) {
3472
+ flags.list = true
3473
+ }
3474
+ else if ('--check' === arg) {
3475
+ flags.check = true
3476
+ }
3477
+ else if ('-d' === arg || '--diff' === arg) {
3478
+ flags.diff = true
3479
+ }
3480
+ else if ('--lint' === arg) {
3481
+ flags.lint = true
3482
+ }
3483
+ else if ('--strict' === arg) {
3484
+ flags.lint = true
3485
+ flags.strict = true
3486
+ }
3487
+ else if (arg.startsWith('-')) {
3488
+ process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
3489
+ return 2
3490
+ }
3491
+ else {
3492
+ files.push(arg)
3493
+ }
3494
+ }
3495
+
3496
+ if (0 === files.length) {
3497
+ // Standard input: formatted onto standard output, or listed,
3498
+ // checked and diffed under the name <stdin>. It cannot be written
3499
+ // back.
3500
+ if (flags.write) {
3501
+ process.stderr.write(`aontu: --write needs a file\n${FMT_HELP}\n`)
3502
+ return 2
3503
+ }
3504
+ return new Promise((resolve) => {
3505
+ let src = ''
3506
+ process.stdin.setEncoding('utf8')
3507
+ process.stdin.on('data', (d) => (src += d))
3508
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)))
3509
+ })
3510
+ }
3511
+
3512
+ // Several files onto standard output would be one stream nobody can
3513
+ // split again (the note's X-6): the verb refuses unless an option
3514
+ // says what to do with each.
3515
+ if (1 < files.length && !fmtQuiet(flags)) {
3516
+ process.stderr.write(
3517
+ `aontu: fmt prints one file; with ${files.length}, say --write, ` +
3518
+ `--list, --check, --diff or --lint\n${FMT_HELP}\n`)
3519
+ return 2
3520
+ }
3521
+
3522
+ let worst = 0
3523
+ for (const file of files) {
3524
+ let src: string
3525
+ try {
3526
+ src = readFileSync(file, 'utf8')
3527
+ }
3528
+ catch (err: any) {
3529
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3530
+ return 2
3531
+ }
3532
+ worst = Math.max(worst, fmtOne(file, src, flags))
3533
+ }
3534
+ return worst
3535
+ }
3536
+
3537
+ // An option that says what to do with a file, in place of printing
3538
+ // it: what to do when its form would change, or the lint.
3539
+ function fmtQuiet(flags: FmtFlags): boolean {
3540
+ return flags.write || flags.list || flags.check || flags.diff || flags.lint
3541
+ }
3542
+
3543
+ // One document: 0 printed, clean or done; 1 a --check that would
3544
+ // change, or a --strict finding; 2 a file that cannot be written; 4 a
3545
+ // document that does not format, with the finding that says why. The
3546
+ // style findings go to standard error, one line each, in the shape
3547
+ // every linter prints: `file:line:col: rule: message`.
3548
+ function fmtOne(name: string, src: string, flags: FmtFlags): number {
3549
+ const report = format(src, { path: name, lint: flags.lint })
3550
+ if ('error' === report.verdict) {
3551
+ process.stderr.write(`aontu: ${name} was not formatted\n` +
3552
+ report.errors.map(renderFinding).join('\n') + '\n')
3553
+ return 4
3554
+ }
3555
+ for (const f of report.findings) {
3556
+ process.stderr.write(`${name}:${f.line}:${f.col}: ${f.rule}: ${f.message}\n`)
3557
+ }
3558
+ const strict = flags.strict && 0 < report.findings.length ? 1 : 0
3559
+ if (!fmtQuiet(flags)) {
3560
+ process.stdout.write(report.text)
3561
+ return 0
3562
+ }
3563
+ if (!report.changed) {
3564
+ return strict
3565
+ }
3566
+ if (flags.list || flags.check) {
3567
+ process.stdout.write(name + '\n')
3568
+ }
3569
+ if (flags.diff) {
3570
+ process.stdout.write(unifiedDiff(name, src, report.text))
3571
+ }
3572
+ if (flags.write) {
3573
+ try {
3574
+ writeFileSync(name, report.text)
3575
+ }
3576
+ catch (err: any) {
3577
+ process.stderr.write(`aontu: cannot write ${name}: ${err.message}\n`)
3578
+ return 2
3579
+ }
3580
+ }
3581
+ return flags.check ? 1 : strict
3582
+ }
3583
+
3584
+
3585
+ // ---------------------------------------------------------------------
3586
+ // The servers as verbs: `aontu lsp` runs the language server and
3587
+ // `aontu mcp` the MCP server over the CLI's own streams, so the one
3588
+ // command on PATH is the editor's and the agent's server too, and a
3589
+ // version manager (docs/design/ENV.0.md) has one thing to resolve. The
3590
+ // standalone bins (aontu-lsp, aontu-mcp) run the same functions. Each
3591
+ // server owns its exit; the CLI only dispatches. The pair is a
3592
+ // parameter of main so that a test can see the dispatch without a
3593
+ // server taking the test's own stdin.
3594
+
3595
+ type Servers = {
3596
+ lsp: () => void
3597
+ mcp: (argv: string[]) => void
3598
+ }
3599
+
3600
+ // The real pair takes the process's own stdin and stdout, which no
3601
+ // in-process test can lend it; the executable-entry tests in
3602
+ // cli.test.ts run each through a child process instead, so these two
3603
+ // lines are excluded from the in-process count, as the stdio wiring
3604
+ // of lsp-server.ts is.
3605
+ /* node:coverage ignore next 4 */
3606
+ const SERVERS: Servers = {
3607
+ lsp: () => void lspMain(),
3608
+ mcp: (argv) => void mcpMain(undefined, undefined, undefined, undefined, argv),
3609
+ }
3610
+
3611
+ // undefined: the server took the process; a number: an answer the CLI
3612
+ // gives itself, --help or a usage error.
3613
+ function runLsp(argv: string[], servers: Servers): number | undefined {
3614
+ for (const arg of argv) {
3615
+ if ('-h' === arg || '--help' === arg) {
3616
+ process.stdout.write(HELP)
3617
+ return 0
3618
+ }
3619
+ process.stderr.write(`aontu: lsp takes no arguments (try --help)\n`)
3620
+ return 2
3621
+ }
3622
+ servers.lsp()
3623
+ return undefined
3624
+ }
3625
+
3626
+
3346
3627
  function finish(code: number): void {
3347
3628
  process.exitCode = code
3348
3629
  }
@@ -3352,22 +3633,22 @@ function finish(code: number): void {
3352
3633
  // spelling, so the caller owns the usage error.
3353
3634
  function parseTrustArg(value: string): TrustArg | undefined {
3354
3635
  if ('system' === value) {
3355
- return { kind: 'system' }
3636
+ return { kind: 'system', textExt: [] }
3356
3637
  }
3357
3638
  if ('none' === value) {
3358
- return { kind: 'none' }
3639
+ return { kind: 'none', textExt: [] }
3359
3640
  }
3360
3641
  if ('root' === value) {
3361
- return { kind: 'root' }
3642
+ return { kind: 'root', textExt: [] }
3362
3643
  }
3363
3644
  if (value.startsWith('root:') && 'root:'.length < value.length) {
3364
- return { kind: 'root', dir: value.slice('root:'.length) }
3645
+ return { kind: 'root', dir: value.slice('root:'.length), textExt: [] }
3365
3646
  }
3366
3647
  return undefined
3367
3648
  }
3368
3649
 
3369
3650
 
3370
- function main(argv: string[]): void {
3651
+ function main(argv: string[], servers: Servers = SERVERS): void {
3371
3652
  // COLOUR OFF WHEN THE DESTINATION IS NOT A TERMINAL. Error frames
3372
3653
  // hardcoded their ANSI escapes, so a piped report and a `--jsonl`
3373
3654
  // answer carried terminal control codes into whatever read them (the
@@ -3385,7 +3666,8 @@ function main(argv: string[]): void {
3385
3666
  // overwritten twice. In a tool loop that reads as a passing
3386
3667
  // validation. Counting them is what lets the refusal below happen.
3387
3668
  const files: string[] = []
3388
- let trust: TrustArg = { kind: 'system-warn' }
3669
+ let trust: TrustArg = { kind: 'system-warn', textExt: [] }
3670
+ let textExt: string[] = []
3389
3671
  // The REPL's SESSION protocol (G7 phase 7): one JSON line per
3390
3672
  // answer, so a harness can drive the session. Named --jsonl rather
3391
3673
  // than the design's --json, which would read as the `:json` output
@@ -3413,6 +3695,16 @@ function main(argv: string[]): void {
3413
3695
  if ('agentsmd' === argv[2]) {
3414
3696
  return finish(runAgentsMd(argv.slice(3)))
3415
3697
  }
3698
+ if ('fmt' === argv[2]) {
3699
+ return void Promise.resolve(runFmt(argv.slice(3))).then(finish)
3700
+ }
3701
+ if ('lsp' === argv[2]) {
3702
+ const code = runLsp(argv.slice(3), servers)
3703
+ return undefined === code ? undefined : finish(code)
3704
+ }
3705
+ if ('mcp' === argv[2]) {
3706
+ return servers.mcp(argv.slice(3))
3707
+ }
3416
3708
 
3417
3709
  if ('set' === argv[2]) {
3418
3710
  return finish(runSet(argv.slice(3)))
@@ -3492,7 +3784,17 @@ function main(argv: string[]): void {
3492
3784
  process.stderr.write('aontu: --include-root needs a directory\n')
3493
3785
  return finish(2)
3494
3786
  }
3495
- trust = { kind: 'root', dir }
3787
+ trust = { kind: 'root', dir, textExt }
3788
+ }
3789
+ else if ('--text-ext' === arg) {
3790
+ const list = null == args[i + 1] ? undefined : parseTextExt(args[++i])
3791
+ if (null == list) {
3792
+ process.stderr.write(
3793
+ 'aontu: --text-ext needs extensions, without dots' +
3794
+ ' (--text-ext md,sql)\n')
3795
+ return finish(2)
3796
+ }
3797
+ textExt = [...textExt, ...list]
3496
3798
  }
3497
3799
  else if (arg.startsWith('-')) {
3498
3800
  process.stderr.write(`aontu: unknown option ${arg} (try --help)\n`)
@@ -3519,6 +3821,11 @@ function main(argv: string[]): void {
3519
3821
  return finish(2)
3520
3822
  }
3521
3823
 
3824
+ // The extensions ride with the capability from here on, so the three
3825
+ // entry shapes below (file, REPL, stdin) each get them by threading
3826
+ // the one value they already thread.
3827
+ trust = { ...trust, textExt }
3828
+
3522
3829
  const file = files[0]
3523
3830
  if (null != file) {
3524
3831
  finish(runFile(file, mode, trust))
@@ -3547,6 +3854,6 @@ export {
3547
3854
  runJsonSchema,
3548
3855
  runMod,
3549
3856
  runHash, runGet,
3550
- runWhy, renderWhyText, runSet, runAgentsMd,
3857
+ runWhy, renderWhyText, runSet, runAgentsMd, runFmt,
3551
3858
  watchChange, watchSignature, vetWaiter, deprecatedAt,
3552
3859
  }