aontu 0.63.0 → 0.65.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 (101) hide show
  1. package/README.md +8 -8
  2. package/dist/agentsmd.js +3 -3
  3. package/dist/agentsmd.js.map +1 -1
  4. package/dist/aontu.d.ts +3 -3
  5. package/dist/aontu.js +4 -6
  6. package/dist/aontu.js.map +1 -1
  7. package/dist/aontumodel.js +6 -12
  8. package/dist/aontumodel.js.map +1 -1
  9. package/dist/casing.d.ts +5 -0
  10. package/dist/casing.js +90 -0
  11. package/dist/casing.js.map +1 -0
  12. package/dist/cli.d.ts +16 -3
  13. package/dist/cli.js +653 -455
  14. package/dist/cli.js.map +1 -1
  15. package/dist/compat.d.ts +7 -0
  16. package/dist/compat.js +113 -0
  17. package/dist/compat.js.map +1 -0
  18. package/dist/helpdoc.js +5 -5
  19. package/dist/helpdoc.js.map +1 -1
  20. package/dist/hints.js +14 -5
  21. package/dist/hints.js.map +1 -1
  22. package/dist/lang.d.ts +1 -0
  23. package/dist/lang.js +8 -0
  24. package/dist/lang.js.map +1 -1
  25. package/dist/lsp.d.ts +1 -1
  26. package/dist/mcp.js +0 -36
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/mod.d.ts +18 -3
  29. package/dist/mod.js +117 -46
  30. package/dist/mod.js.map +1 -1
  31. package/dist/pkg-net.d.ts +227 -0
  32. package/dist/pkg-net.js +1598 -0
  33. package/dist/pkg-net.js.map +1 -0
  34. package/dist/pkg-zip.d.ts +8 -0
  35. package/dist/pkg-zip.js +196 -0
  36. package/dist/pkg-zip.js.map +1 -0
  37. package/dist/pkg.d.ts +159 -0
  38. package/dist/pkg.js +767 -0
  39. package/dist/pkg.js.map +1 -0
  40. package/dist/profile.d.ts +9 -0
  41. package/dist/profile.js +28 -0
  42. package/dist/profile.js.map +1 -0
  43. package/dist/report-sarif.js +1 -1
  44. package/dist/report-sarif.js.map +1 -1
  45. package/dist/subsume.d.ts +3 -1
  46. package/dist/subsume.js +2 -0
  47. package/dist/subsume.js.map +1 -1
  48. package/dist/trace.d.ts +21 -0
  49. package/dist/trace.js +107 -0
  50. package/dist/trace.js.map +1 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/dist/unify.js +1 -6
  53. package/dist/unify.js.map +1 -1
  54. package/dist/val/CmpFuncVal.d.ts +2 -0
  55. package/dist/val/CmpFuncVal.js +47 -13
  56. package/dist/val/CmpFuncVal.js.map +1 -1
  57. package/dist/val/NomFuncVal.js +11 -13
  58. package/dist/val/NomFuncVal.js.map +1 -1
  59. package/dist/val/ReferFuncVal.js +3 -0
  60. package/dist/val/ReferFuncVal.js.map +1 -1
  61. package/package.json +1 -1
  62. package/skill/SKILL.md +3 -3
  63. package/skill/error-codes.md +4 -4
  64. package/skill/examples.md +4 -4
  65. package/skill/grammar-card.md +1 -1
  66. package/skill/init/check.sh +2 -2
  67. package/skill/tasks.md +14 -12
  68. package/src/agentsmd.ts +3 -3
  69. package/src/aontu.ts +3 -8
  70. package/src/aontumodel.ts +6 -12
  71. package/src/casing.ts +95 -0
  72. package/src/cli.ts +758 -495
  73. package/src/compat.ts +148 -0
  74. package/src/helpdoc.ts +5 -5
  75. package/src/hints.ts +14 -7
  76. package/src/lang.ts +9 -1
  77. package/src/mcp.ts +0 -39
  78. package/src/mod.ts +142 -51
  79. package/src/pkg-net.ts +1945 -0
  80. package/src/pkg-zip.ts +228 -0
  81. package/src/pkg.ts +1018 -0
  82. package/src/profile.ts +42 -0
  83. package/src/report-sarif.ts +1 -1
  84. package/src/subsume.ts +2 -2
  85. package/src/trace.ts +157 -0
  86. package/src/unify.ts +1 -6
  87. package/src/val/CmpFuncVal.ts +66 -20
  88. package/src/val/NomFuncVal.ts +3 -5
  89. package/src/val/ReferFuncVal.ts +4 -0
  90. package/dist/lower.d.ts +0 -23
  91. package/dist/lower.js +0 -528
  92. package/dist/lower.js.map +0 -1
  93. package/dist/mod-tool.d.ts +0 -58
  94. package/dist/mod-tool.js +0 -403
  95. package/dist/mod-tool.js.map +0 -1
  96. package/dist/render.d.ts +0 -53
  97. package/dist/render.js +0 -432
  98. package/dist/render.js.map +0 -1
  99. package/src/lower.ts +0 -586
  100. package/src/mod-tool.ts +0 -561
  101. package/src/render.ts +0 -580
package/src/cli.ts CHANGED
@@ -17,25 +17,35 @@ import {
17
17
  hcanon, canonHash,
18
18
  get, why, patch, agentsMd,
19
19
  allow,
20
- render,
21
- renderProfile,
20
+ loadProfile,
22
21
  } from './aontu'
23
22
  import type { AllowDecision, AllowReport, AllowVerdict } from './allow'
24
- import type { RenderCoverage, RenderReport } from './render'
23
+ import { traceRun } from './trace'
25
24
  import {
26
25
  desugarTemplate, resugarTemplate, templateOutputs, markerFor,
27
26
  markerFromProfiles,
28
27
  } from './template'
29
- import { outsideRoot } from './mcp'
30
28
  import { sarifReport } from './report-sarif'
31
29
  import { main as lspMain } from './lsp-server'
32
30
  import { main as mcpMain } from './mcp-server'
33
31
  import { jsonSchema } from './jsonschema'
34
- import { modTidy, modVerify, modVendor, modManifest } from './mod-tool'
32
+ import {
33
+ pkgTidy, pkgVerify, pkgVendor, pkgManifest, pkgRefreeze, pkgTree,
34
+ versionCompare,
35
+ } from './pkg'
36
+ import {
37
+ pkgSync, pkgGet, pkgRemove, pkgWhy, pkgPublish, pkgOutdated, startServe,
38
+ defaultHttp, keygen, PUBLISH_PATH,
39
+ } from './pkg-net'
35
40
  import type {
36
- ModTidyReport, ModVerifyReport, ModVendorReport, ModManifestReport,
37
- } from './mod-tool'
38
- import { modCacheDir } from './mod'
41
+ PkgSyncReport, PkgChangeReport, PkgWhyReport, PkgPublishReport,
42
+ PkgOutdatedReport, Served, PkgHttp,
43
+ } from './pkg-net'
44
+ import type {
45
+ PkgTidyReport, PkgVerifyReport, PkgVendorReport, PkgManifestReport,
46
+ PkgRefreezeReport, PkgTreeReport, PkgToolOptions,
47
+ } from './pkg'
48
+ import { modCacheDir, PKG_FILE, LOCK_FILE, META_DIR } from './mod'
39
49
  import { VET_MAX_ERRORS } from './vet'
40
50
  import type {
41
51
  VetReport, VetFinding, VetVerdict, VetCoverage,
@@ -83,16 +93,23 @@ const HELP = `Usage: aontu [options] [file]
83
93
  aontu view <kind> [options] <file>...
84
94
  aontu view --views <path> [--check] [options] <file>
85
95
  aontu jsonschema [--at <path>] [--strict] [options] <file>
86
- aontu render [--at <path>] [--profile <file>]... [--unit <path>]
87
- [--stdout | --out <dir> | --check <dir> | --coverage]
88
- [--coverage-at <path>] [--strict] <file>
89
96
  aontu template [--resugar] [--check] [--marker <token>]
90
97
  [--profile <file>] <file>
98
+ aontu trace [--at <path>] [--format json] [--marker <token>]
99
+ [--profile <file>] <file>
91
100
  aontu hash [options] <file>
92
- aontu mod tidy|verify|vendor|manifest [options] [dir]
93
- aontu get <path> [options] <file>
94
- aontu why <path> [options] <file>
95
- aontu set <path>=<value>... --entry <file> --overlay <file>
101
+ aontu sync [--frozen] [options] [dir]
102
+ aontu add <pkg>[@<version>] [options] [dir]
103
+ aontu get <pkg>[@<version>] [options] [dir]
104
+ aontu remove <pkg> [options] [dir]
105
+ aontu why <pkg> [options] [dir]
106
+ aontu publish [--yes] [--to <dir>] [options] [dir]
107
+ aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve
108
+ [options] [dir]
109
+ aontu pkg keygen <file>
110
+ aontu model get <path> [options] <file>
111
+ aontu model why <path> [options] <file>
112
+ aontu model set <path>=<value>... --entry <file> --overlay <file>
96
113
  aontu allow --role <role> [--at <path>] <roles-file> <path>...
97
114
  aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
98
115
  aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
@@ -161,18 +178,57 @@ Options:
161
178
  named format keeps its meaning, and .js stays
162
179
  refused. Every verb takes it
163
180
 
164
- Mod options:
181
+ Package verbs (a module is imported; a package is published):
182
+ sync Make the project correct: resolve by minimum version
183
+ selection, fetch what is missing, write aontu_meta/pkg-lock.aon,
184
+ vendor, verify. --frozen refuses to change the lockfile
185
+ add Take on a dependency the project does not have, then sync;
186
+ refuses one it has and names get
187
+ get Add a dependency or raise its minimum, then sync. Without a
188
+ version, the newest version outside the cooldown
189
+ remove Drop a dependency, then sync
190
+ why Why is this package in the closure: every path of
191
+ requirements that reaches it
192
+ publish Publish this package. A dry run without --yes; --to <dir>
193
+ writes the repository layout into a directory instead
194
+
195
+ Package options:
165
196
  --format <f> text (default) or json
166
- --against <dir> manifest: a prior version's module tree, to gate on
167
-
168
- Mod subcommands:
169
- tidy Resolve the module closure by minimum version selection and
170
- rewrite aontu_meta/mod-lock.aon in canonical form
171
- verify Check every locked module still means what the lockfile
172
- pins, and change nothing (the CI gate; tidy rewrites)
197
+ --frozen sync: fail if the lockfile would change (the CI mode)
198
+ --yes publish: send it (the default is a dry run)
199
+ --to <dir> publish: write the read-path layout into a directory
200
+ --key <file> publish: the Ed25519 private key (PKCS#8 PEM) that
201
+ signs the manifest, for the key provider
202
+ --token <file> publish: the forge's OIDC token, read from a file
203
+ --base <url> the repository to read from (repeatable; overrides
204
+ pkg.aon repo.base)
205
+ --write <url> publish: the write path (overrides pkg.aon repo.write)
206
+ --against <dir> manifest: a prior version's tree, to gate on
207
+ --upstream <u> serve: fetch on miss from this repository (repeatable)
208
+ --listen <a> serve: the address to listen on (default 127.0.0.1:8017)
209
+
210
+ pkg subcommands (the rest of the package operations):
211
+ tidy Resolve the closure by minimum version selection and
212
+ rewrite aontu_meta/pkg-lock.aon in canonical form
213
+ verify Check every locked package still is and still means what
214
+ the lockfile pins, bytes before meaning, and change nothing
215
+ (the CI gate; tidy rewrites)
173
216
  vendor Materialise the locked closure into aontu_meta/vendor/
174
- manifest Print the OCI artifact a publish would push, gated on the
175
- breaking check against --against
217
+ manifest Print the manifest a publish would send, gated on the
218
+ compatibility check against --against
219
+ refreeze Recompute every canon pin and nothing else, after a
220
+ canonical-form change in the engine
221
+ tree The locked closure as a graph
222
+ outdated What could move, and what would move with it
223
+ serve Serve a repository directory; with --upstream, a caching
224
+ proxy in front of another repository
225
+ keygen Write a new Ed25519 signing key (PKCS#8 PEM) to a file,
226
+ once, and print the signer id a consumer names
227
+
228
+ model subcommands (one document, interrogated or edited):
229
+ get What the document says at a path
230
+ why Every contribution to the value at a path
231
+ set Append a path-flattened conjunct to an overlay
176
232
 
177
233
  Vet options:
178
234
  --at <path> Validate against this path of the schema ($.a.b)
@@ -251,21 +307,21 @@ Hash options:
251
307
  Hash exit codes: 0 hashed, 2 usage, 4 the document does not stand up
252
308
  on its own.
253
309
 
254
- Get options:
310
+ Model get options:
255
311
  -c, --canon Canonical-form fragment (default: generated JSON)
256
312
  --keys Keys at the node, one per line
257
313
  --types Shape view: concrete leaves lifted to their kinds
258
314
  --depth <n> Structure to depth n; deeper nodes render as top
259
315
  --format <f> text (default) or json
260
316
 
261
- Get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4 the
262
- document does not stand up on its own.
317
+ Model get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4
318
+ the document does not stand up on its own.
263
319
 
264
- Why options:
320
+ Model why options:
265
321
  --format <f> text (default) or json
266
322
 
267
- Why exit codes mirror get's: 0 explained, 1 the path names nothing,
268
- 2 usage, 4 the document does not stand up on its own.
323
+ Model why exit codes mirror get's: 0 explained, 1 the path names
324
+ nothing, 2 usage, 4 the document does not stand up on its own.
269
325
 
270
326
  View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
271
327
  ladder, poset (the poset takes several files). The figure goes to stdout, the loss
@@ -333,37 +389,11 @@ View exit codes: 0 rendered, 1 --check mismatch or lossy under
333
389
  --strict, 2 usage or --max-rows exceeded, 4 the document does not stand
334
390
  up on its own, or a relation, root or path that names nothing.
335
391
 
336
- Render options:
337
- --at <path> Render the value at this path ($.a.b); the root by
338
- default
339
- --profile <file> A profile document, aontu: render: Lang: {lang,
340
- ...}, vetted against aontu:render; repeatable,
341
- one per language
342
- --unit <path> Render only the unit with this path
343
- --stdout One unit's bytes and nothing else (with --unit when
344
- the instance has several)
345
- --out <dir> Write every unit below dir, or nothing; never deletes
346
- --check <dir> Compare every unit with dir/<path>; drift is listed
347
- --coverage Report what the render read and what it did not:
348
- model paths no output consumed, and rendered
349
- declarations no rule produced. Writes nothing
350
- --coverage-at <p> Measure coverage under this path only, instead of
351
- the document root
352
- --strict Refuse the opaque escapes (a text declaration, a raw
353
- block)
354
- --format <f> text (default) or json, the whole report; json
355
- carries the dispatch trace, one entry per emitted
356
- piece
357
-
358
- Render exit codes: 0 rendered, 1 lossy under --strict or drift under
359
- --check, 2 usage or I/O (a refused unit path included), 4 the document
360
- does not stand up or the instance is not aontu:code.
361
-
362
- A render entry file whose extension is not .aon is a TEMPLATE: a
363
- generator in the target's own syntax, whose marker lines carry aontu
364
- and whose other lines are output. It is desugared before it is
365
- evaluated, and a language the table does not know names its marker with
366
- --marker, or declares it once in a profile file that --profile reads.
392
+ A template entry file whose extension is not .aon is a GENERATOR: a
393
+ document in the target's own syntax, whose marker lines carry aontu and
394
+ whose other lines are output. It is desugared before it is evaluated,
395
+ and a language the table does not know names its marker with --marker,
396
+ or declares it once in a profile file that --profile reads.
367
397
 
368
398
  Template options:
369
399
  --resugar The file is the canonical aontu; print the template
@@ -381,7 +411,7 @@ every other line is a line of output.
381
411
 
382
412
  Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
383
413
 
384
- Set options:
414
+ Model set options:
385
415
  --entry <file> The document the change is checked against
386
416
  --overlay <file> The file the change is appended to (created if
387
417
  absent; not written when the change does not hold)
@@ -397,7 +427,7 @@ Set options:
397
427
  --format <f> text (default) or json
398
428
 
399
429
  Set exit codes are vet's verdict classes: 0 valid, 1 invalid (the
400
- change contradicts a pinned value -- aontu why locates it, and
430
+ change contradicts a pinned value -- aontu model why locates it, and
401
431
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
402
432
  stand up on its own.
403
433
 
@@ -473,11 +503,11 @@ Fmt options:
473
503
  The fmt verb prints one document in the agreed form; with no file it
474
504
  reads standard input. Several files need one of the options above.
475
505
 
476
- A file whose extension is not .aon is a GENERATOR, as it is for render:
477
- the aontu its marker lines carry is formatted, the marker stands at the
478
- left margin with the aontu indented after it, and every line of output
479
- is held on a line of its own. A file with no marker line in it is
480
- another language's, and is refused.
506
+ A file whose extension is not .aon is a GENERATOR, as it is for
507
+ template: the aontu its marker lines carry is formatted, the marker
508
+ stands at the left margin with the aontu indented after it, and every
509
+ line of output is held on a line of its own. A file with no marker line
510
+ in it is another language's, and is refused.
481
511
 
482
512
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
483
513
  a --strict finding, 2 usage, 4 a document does not parse.
@@ -630,7 +660,7 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
630
660
  }
631
661
 
632
662
 
633
- function takeTrust(argv: string[]):
663
+ function takeTrust(argv: string[], io: Io = PROCESS_IO):
634
664
  { argv: string[], trust: TrustArg } | undefined {
635
665
  const rest: string[] = []
636
666
  let trust: TrustArg = { kind: 'system-warn', textExt: [] }
@@ -640,7 +670,7 @@ function takeTrust(argv: string[]):
640
670
  if ('--trust' === arg) {
641
671
  const parsed = null == argv[i + 1] ? undefined : parseTrustArg(argv[++i])
642
672
  if (null == parsed) {
643
- process.stderr.write(
673
+ io.err(
644
674
  'aontu: --trust needs system, none, or root[:dir]\n')
645
675
  return undefined
646
676
  }
@@ -649,7 +679,7 @@ function takeTrust(argv: string[]):
649
679
  else if ('--include-root' === arg) {
650
680
  const dir = argv[++i]
651
681
  if (null == dir) {
652
- process.stderr.write('aontu: --include-root needs a directory\n')
682
+ io.err('aontu: --include-root needs a directory\n')
653
683
  return undefined
654
684
  }
655
685
  trust = { kind: 'root', dir, textExt }
@@ -657,7 +687,7 @@ function takeTrust(argv: string[]):
657
687
  else if ('--text-ext' === arg) {
658
688
  const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i])
659
689
  if (null == list) {
660
- process.stderr.write(
690
+ io.err(
661
691
  'aontu: --text-ext needs extensions, without dots' +
662
692
  ' (--text-ext md,sql)\n')
663
693
  return undefined
@@ -673,7 +703,7 @@ function takeTrust(argv: string[]):
673
703
 
674
704
 
675
705
  // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
676
- // reader who has just written `@"notes.txt"` reaches for one. An empty
706
+ // reader who has just written `@"./notes.txt"` reaches for one. An empty
677
707
  // element, or anything that is not an extension, is a usage error
678
708
  // rather than a silently ignored word: a flag that quietly does
679
709
  // nothing is how a document ends up refused with no reason visible.
@@ -2077,102 +2107,186 @@ const VIEW_USAGE_CODES = [
2077
2107
  'view_document_shape', 'view_style_profile', 'view_style_unknown',
2078
2108
  ]
2079
2109
 
2080
- const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
2110
+ const PKG_HELP =
2111
+ 'aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve [dir] | keygen <file> (try --help)'
2081
2112
 
2082
- function runMod(argv: string[]): number {
2083
- const rest: string[] = []
2084
- let format: SubsumeFormat = 'text'
2085
- let against: string | undefined
2113
+ const PKG_SUBS = [
2114
+ 'tidy', 'verify', 'vendor', 'manifest', 'refreeze', 'tree', 'outdated',
2115
+ 'serve', 'keygen',
2116
+ ]
2086
2117
 
2118
+ // The options every package verb parses, so `sync`, `get`, `publish`
2119
+ // and the `pkg` subcommands read one table.
2120
+ type PkgArgs = {
2121
+ rest: string[]
2122
+ format: SubsumeFormat
2123
+ against?: string
2124
+ frozen: boolean
2125
+ yes: boolean
2126
+ to?: string
2127
+ key?: string
2128
+ token?: string
2129
+ base: string[]
2130
+ write?: string
2131
+ upstream: string[]
2132
+ listen?: string
2133
+ }
2134
+
2135
+ function parsePkgArgs(argv: string[], verb: string, io: Io): PkgArgs | undefined {
2136
+ const out: PkgArgs = {
2137
+ rest: [], format: 'text', frozen: false, yes: false, base: [], upstream: [],
2138
+ }
2139
+ const value = (name: string, i: number): string | undefined => {
2140
+ const v = argv[i]
2141
+ if (null == v) {
2142
+ io.err(`aontu: ${name} needs a value\n`)
2143
+ }
2144
+ return v
2145
+ }
2087
2146
  for (let i = 0; i < argv.length; i++) {
2088
2147
  const arg = argv[i]
2089
2148
  if ('-h' === arg || '--help' === arg) {
2090
- process.stdout.write(HELP)
2091
- return 0
2149
+ io.out(HELP)
2150
+ return undefined
2092
2151
  }
2093
2152
  if ('--format' === arg) {
2094
2153
  const f = argv[++i]
2095
2154
  if ('text' !== f && 'json' !== f) {
2096
- process.stderr.write('aontu: --format needs text or json\n')
2097
- return 2
2155
+ io.err('aontu: --format needs text or json\n')
2156
+ return undefined
2098
2157
  }
2099
- format = f
2158
+ out.format = f
2100
2159
  }
2101
- else if ('--against' === arg) {
2102
- const a = argv[++i]
2103
- if (null == a) {
2104
- process.stderr.write('aontu: --against needs a module directory\n')
2105
- return 2
2160
+ else if ('--frozen' === arg) {
2161
+ out.frozen = true
2162
+ }
2163
+ else if ('--yes' === arg) {
2164
+ out.yes = true
2165
+ }
2166
+ else if ('--against' === arg || '--to' === arg || '--key' === arg ||
2167
+ '--token' === arg || '--write' === arg || '--listen' === arg) {
2168
+ const v = value(arg, ++i)
2169
+ if (null == v) {
2170
+ return undefined
2171
+ }
2172
+ out['--against' === arg ? 'against' : '--to' === arg ? 'to' :
2173
+ '--key' === arg ? 'key' : '--token' === arg ? 'token' :
2174
+ '--write' === arg ? 'write' : 'listen'] = v
2175
+ }
2176
+ else if ('--base' === arg || '--upstream' === arg) {
2177
+ const v = value(arg, ++i)
2178
+ if (null == v) {
2179
+ return undefined
2106
2180
  }
2107
- against = a
2181
+ out['--base' === arg ? 'base' : 'upstream'].push(v)
2108
2182
  }
2109
2183
  else if (arg.startsWith('-')) {
2110
- process.stderr.write(`aontu: unknown mod option ${arg} (try --help)\n`)
2111
- return 2
2184
+ io.err(`aontu: unknown ${verb} option ${arg} (try --help)\n`)
2185
+ return undefined
2112
2186
  }
2113
2187
  else {
2114
- rest.push(arg)
2188
+ out.rest.push(arg)
2115
2189
  }
2116
2190
  }
2191
+ return out
2192
+ }
2117
2193
 
2118
- const sub = rest[0]
2119
- const dir = rest[1] ?? '.'
2120
2194
 
2121
- if ('get' === sub || 'publish' === sub) {
2122
- process.stderr.write(
2123
- 'aontu: mod ' + sub + ' needs a registry client, which this build ' +
2124
- 'does not ship; vendor the module by hand and run ' +
2125
- "'aontu mod tidy'\n")
2195
+ // A verb that finds the older layout names the current one, once, and
2196
+ // reads nothing from it.
2197
+ function nameOldLayout(dir: string, io: Io): void {
2198
+ const old = ['aon_vendor', 'mod-lock.aon', 'mod.aon',
2199
+ join(META_DIR, 'mod-lock.aon')].filter((f) => existsSync(join(dir, f)))
2200
+ if (0 < old.length) {
2201
+ io.err(
2202
+ 'aontu: ' + old.join(', ') + ' belong to an older layout: the package ' +
2203
+ 'file is ' + PKG_FILE + ', the lockfile ' + join(META_DIR, LOCK_FILE) +
2204
+ ' and the vendor tree ' + join(META_DIR, 'vendor') + '; rename ' +
2205
+ PKG_FILE + '\'s `mod` block to `pkg`, then run aontu sync\n')
2206
+ }
2207
+ }
2208
+
2209
+
2210
+ function runPkg(argv: string[], servers: Servers): number | Promise<number> {
2211
+ const io = servers.io ?? PROCESS_IO
2212
+ const trusted = takeTrust(argv, io)
2213
+ if (null == trusted) {
2126
2214
  return 2
2127
2215
  }
2216
+ const args = parsePkgArgs(trusted.argv, 'pkg', io)
2217
+ if (null == args) {
2218
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2219
+ }
2220
+ const trust = trusted.trust
2221
+ const sub = args.rest[0]
2222
+ const dir = args.rest[1] ?? '.'
2128
2223
 
2129
- if (!MOD_SUBS.includes(sub) || 2 < rest.length) {
2130
- process.stderr.write(
2131
- `aontu: mod needs tidy, verify, vendor or manifest\n${MOD_HELP}\n`)
2224
+ if (!PKG_SUBS.includes(sub) || 2 < args.rest.length) {
2225
+ io.err(
2226
+ `aontu: pkg needs one of ${PKG_SUBS.join(', ')}\n${PKG_HELP}\n`)
2132
2227
  return 2
2133
2228
  }
2134
2229
 
2135
- if (existsSync(join(dir, 'aon_vendor')) || existsSync(join(dir, 'mod-lock.aon'))) {
2136
- process.stderr.write(
2137
- 'aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
2138
- 'move them, or run aontu mod tidy and aontu mod vendor\n')
2230
+ if ('keygen' === sub) {
2231
+ if (2 !== args.rest.length) {
2232
+ io.err('aontu: pkg keygen needs the file to write\naontu pkg keygen <file>\n')
2233
+ return 2
2234
+ }
2235
+ const made = keygen(args.rest[1])
2236
+ if (null != made.refused) {
2237
+ io.err('aontu: ' + made.refused + '\n')
2238
+ return 2
2239
+ }
2240
+ io.out('signer: ' + made.signer + '\n')
2241
+ return 0
2139
2242
  }
2140
2243
 
2141
- // `--against` gates a manifest and means nothing to the other two;
2244
+ nameOldLayout(dir, io)
2245
+
2246
+ // `--against` gates a manifest and means nothing to the others;
2142
2247
  // accepting it there would say it had been honoured.
2143
- if (null != against && 'manifest' !== sub) {
2144
- process.stderr.write('aontu: --against is a manifest option\n')
2248
+ if (null != args.against && 'manifest' !== sub) {
2249
+ io.err('aontu: --against is a manifest option\n')
2145
2250
  return 2
2146
2251
  }
2147
2252
 
2148
- const report =
2149
- 'tidy' === sub ? modTidy(dir, modToolOptions()) :
2150
- 'verify' === sub ? modVerify(dir, modToolOptions()) :
2151
- 'vendor' === sub ? modVendor(dir, modToolOptions()) :
2152
- modManifest(dir, modToolOptions(), against)
2253
+ const opts = pkgToolOptions(trust, resolve(dir))
2254
+
2255
+ if ('outdated' === sub || 'serve' === sub) {
2256
+ return runPkgNet(sub, dir, args, opts, servers, io)
2257
+ }
2153
2258
 
2154
- process.stdout.write(('json' === format ?
2155
- exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
2259
+ const report =
2260
+ 'tidy' === sub ? pkgTidy(dir, opts) :
2261
+ 'verify' === sub ? pkgVerify(dir, opts) :
2262
+ 'vendor' === sub ? pkgVendor(dir, opts) :
2263
+ 'refreeze' === sub ? pkgRefreeze(dir, opts) :
2264
+ 'tree' === sub ? pkgTree(dir, opts) :
2265
+ pkgManifest(dir, opts, args.against)
2266
+
2267
+ io.out(('json' === args.format ?
2268
+ exactJSON({ aontu: { version: version(), verb: 'pkg ' + sub }, ...report },
2156
2269
  2) :
2157
- modText(sub, report)) + '\n')
2270
+ pkgText(sub, report)) + '\n')
2158
2271
 
2159
- return MOD_EXIT[report.verdict]
2272
+ return PKG_EXIT[report.verdict]
2160
2273
  }
2161
2274
 
2162
2275
 
2163
- const MOD_SUBS = ['tidy', 'verify', 'vendor', 'manifest']
2164
-
2165
2276
  // The verdict classes: `ok` 0, a refused gate 1, an open question 3, a
2166
2277
  // document that does not stand up 4 -- `subsume`'s classes, because a
2167
2278
  // manifest gate IS a subsumption check and a caller reading exit codes
2168
2279
  // should not have to learn a second table.
2169
- type ModVerdict =
2170
- ModTidyReport['verdict'] |
2171
- ModVerifyReport['verdict'] |
2172
- ModVendorReport['verdict'] |
2173
- ModManifestReport['verdict']
2174
-
2175
- const MOD_EXIT: Record<ModVerdict, number> = {
2280
+ type PkgVerdict =
2281
+ PkgTidyReport['verdict'] |
2282
+ PkgVerifyReport['verdict'] |
2283
+ PkgVendorReport['verdict'] |
2284
+ PkgManifestReport['verdict'] |
2285
+ PkgRefreezeReport['verdict'] |
2286
+ PkgTreeReport['verdict'] |
2287
+ 'frozen' | 'refused' | 'sent' | 'dry-run' | 'outdated' | 'current'
2288
+
2289
+ const PKG_EXIT: Record<PkgVerdict, number> = {
2176
2290
  ok: 0,
2177
2291
  missing: 1,
2178
2292
  mismatch: 1,
@@ -2180,6 +2294,12 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2180
2294
  // nothing to check, which is a refusal and not a pass.
2181
2295
  unlocked: 1,
2182
2296
  breaking: 1,
2297
+ frozen: 1,
2298
+ refused: 1,
2299
+ outdated: 1,
2300
+ sent: 0,
2301
+ 'dry-run': 0,
2302
+ current: 0,
2183
2303
  undecided: 3,
2184
2304
  error: 4,
2185
2305
  }
@@ -2188,11 +2308,16 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2188
2308
  // The tooling's evaluator: the same standalone evaluation the module
2189
2309
  // resolver verifies with (ts/src/mod.ts), and for the same reason —
2190
2310
  // only the engine can say what a module MEANS.
2191
- function modToolOptions() {
2311
+ function pkgToolOptions(trust: TrustArg, entryRoot: string): PkgToolOptions {
2312
+ const opts = verbOpts(trust, entryRoot)
2313
+ // The user cache lives outside any confinement root, so a confined
2314
+ // run reads the vendor tree only -- as the evaluator's own module
2315
+ // leg already does when a root is set.
2316
+ const rooted = null != (opts.trust as any)?.include?.root
2192
2317
  return {
2193
- cache: modCacheDir(),
2318
+ ...(rooted ? {} : { cache: modCacheDir() }),
2194
2319
  eval: (src: string, path: string) => {
2195
- const a0 = new Aontu()
2320
+ const a0 = new Aontu(opts)
2196
2321
  const ctx = a0.ctx({ collect: true })
2197
2322
  const val: any = a0.unify(src, { path }, ctx)
2198
2323
  return {
@@ -2200,7 +2325,7 @@ function modToolOptions() {
2200
2325
  hash: canonHash(val),
2201
2326
  canon: val.canon,
2202
2327
  // The same question `aontu hash` asks before it will answer:
2203
- // did this document stand up ON ITS OWN? See ModToolEval.
2328
+ // did this document stand up ON ITS OWN? See PkgToolEval.
2204
2329
  ok: 0 === ctx.err.length && true !== val.isNil,
2205
2330
  }
2206
2331
  },
@@ -2208,19 +2333,54 @@ function modToolOptions() {
2208
2333
  }
2209
2334
 
2210
2335
 
2211
- function modText(sub: string, report: any): string {
2336
+ function pkgLockLines(entries: any[]): string[] {
2337
+ return entries.map((e) => e.key + ' ' + e.v + ' ' + e.canon)
2338
+ }
2339
+
2340
+ // The renderers verify, tidy and sync share, as the Go port's do.
2341
+ function pkgMismatchLines(mismatched: any[]): string[] {
2342
+ return mismatched.map((m) => 'canon' === m.pin ?
2343
+ m.key + ': pinned ' + m.want + ' but the store means ' +
2344
+ ('' === m.got ? 'nothing (it does not evaluate)' : m.got) :
2345
+ m.key + ': pinned ' + m.pin + ' ' + m.want + ' but the store holds ' + m.got)
2346
+ }
2347
+
2348
+ // NOT a fetch: the package may well be sitting in the store. What is
2349
+ // absent is the PIN, and only a sync writes one.
2350
+ function pkgUnlockedLines(unlocked: string[]): string[] {
2351
+ return unlocked.map((key) => key + ': not in the lockfile (run: aontu sync)')
2352
+ }
2353
+
2354
+ function pkgForbiddenLines(forbidden: string[]): string[] {
2355
+ return forbidden.map((f) => f + ': not admitted in a package')
2356
+ }
2357
+
2358
+
2359
+ function pkgText(sub: string, report: any): string {
2212
2360
  const lines = ['verdict: ' + report.verdict]
2213
2361
 
2214
2362
  if ('manifest' === sub) {
2215
- if ('' !== report.mod) {
2216
- lines.push(report.mod + ' ' + report.version)
2217
- lines.push('config: ' + report.config)
2218
- }
2219
- for (const key of Object.keys(report.annotations).sort()) {
2220
- lines.push(key + ': ' + report.annotations[key])
2221
- }
2222
- for (const file of report.files) {
2223
- lines.push('layer: ' + file)
2363
+ const m = report.manifest
2364
+ if (null != m) {
2365
+ lines.push(m.package + ' ' + m.version + ' ' + m.publish)
2366
+ lines.push('archive: ' + m.archive.digest + ' (' + m.archive.files.length +
2367
+ ' files, ' + m.archive.size + ' bytes)')
2368
+ for (const mod of m.modules) {
2369
+ lines.push('module: ' + mod.path + ' ' + mod.main + ' ' + mod.canon)
2370
+ }
2371
+ for (const key of Object.keys(m.deps).sort(cmpCodePoint)) {
2372
+ lines.push('dep: ' + key + ' ' + m.deps[key].v +
2373
+ (null == m.deps[key].pkg ? '' : ' (' + m.deps[key].pkg + ')'))
2374
+ }
2375
+ for (const v of m.retract ?? []) {
2376
+ lines.push('retract: ' + v)
2377
+ }
2378
+ if (null != m.moved) {
2379
+ lines.push('moved: ' + m.moved)
2380
+ }
2381
+ for (const f of m.archive.files) {
2382
+ lines.push('file: ' + f.path + ' ' + f.digest + ' ' + f.size)
2383
+ }
2224
2384
  }
2225
2385
  for (const f of report.findings) {
2226
2386
  lines.push(f.path + ': ' + f.message)
@@ -2228,48 +2388,313 @@ function modText(sub: string, report: any): string {
2228
2388
  for (const miss of report.missing) {
2229
2389
  lines.push(miss + ': missing')
2230
2390
  }
2391
+ lines.push(...pkgForbiddenLines(report.forbidden))
2231
2392
  return lines.join('\n')
2232
2393
  }
2233
2394
 
2234
2395
  if ('verify' === sub) {
2235
- for (const mod of report.verified) {
2236
- lines.push(mod + ': verified')
2396
+ for (const key of report.verified) {
2397
+ lines.push(key + ': verified')
2398
+ }
2399
+ lines.push(...pkgMismatchLines(report.mismatched), ...pkgUnlockedLines(report.unlocked))
2400
+ for (const miss of report.missing) {
2401
+ lines.push(miss + ': not fetched (run: aontu sync)')
2237
2402
  }
2238
- for (const m of report.mismatched) {
2239
- lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
2240
- ('' === m.got ? 'nothing (it does not evaluate)' : m.got))
2403
+ return lines.join('\n')
2404
+ }
2405
+
2406
+ if ('refreeze' === sub) {
2407
+ for (const r of report.repinned) {
2408
+ lines.push(r.key + ': ' + r.from + ' -> ' + r.to)
2241
2409
  }
2242
- // NOT a fetch: the module may well be sitting in the store. What
2243
- // is absent is the PIN, and only a tidy writes one.
2244
- for (const mod of report.unlocked) {
2245
- lines.push(mod + ': not in the lockfile (run: aontu mod tidy)')
2410
+ for (const key of report.unchanged) {
2411
+ lines.push(key + ': unchanged')
2412
+ }
2413
+ for (const bad of report.unevaluable) {
2414
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2246
2415
  }
2247
2416
  for (const miss of report.missing) {
2248
- lines.push(miss + ': not fetched (run: aontu mod get)')
2417
+ lines.push(miss + ': not fetched (run: aontu sync)')
2249
2418
  }
2250
2419
  return lines.join('\n')
2251
2420
  }
2252
2421
 
2253
- const done: any[] = 'tidy' === sub ? report.lock : report.vendored
2254
- for (const item of done) {
2255
- lines.push('tidy' === sub ?
2256
- item.mod + ' ' + item.v + ' ' + item.canon : '' + item)
2422
+ if ('tree' === sub) {
2423
+ const byKey = new Map(report.nodes.map((n: any) => [n.key, n]))
2424
+ const seen = new Set<string>()
2425
+ const walk = (key: string, depth: number): void => {
2426
+ const node: any = byKey.get(key)
2427
+ const again = seen.has(key)
2428
+ seen.add(key)
2429
+ lines.push(' '.repeat(depth) + key +
2430
+ (null == node || '' === node.v ? '' : ' ' + node.v) +
2431
+ (again ? ' (above)' : null == node ? ' (not locked)' : ''))
2432
+ if (again || null == node) {
2433
+ return
2434
+ }
2435
+ for (const dep of node.deps) {
2436
+ walk(dep, depth + 1)
2437
+ }
2438
+ }
2439
+ walk(report.root, 0)
2440
+ for (const miss of report.missing) {
2441
+ lines.push(miss + ': not fetched (run: aontu sync)')
2442
+ }
2443
+ return lines.join('\n')
2257
2444
  }
2258
- // A module that is PRESENT but does not stand up. Named separately
2445
+
2446
+ const done: any[] = 'tidy' === sub ? pkgLockLines(report.lock) : report.vendored
2447
+ lines.push(...done)
2448
+ // A package that is PRESENT but does not stand up. Named separately
2259
2449
  // from a missing one because the repair is different: a fetch cannot
2260
- // help, the module itself has to be fixed (or its own dependencies
2450
+ // help, the package itself has to be fixed (or its own dependencies
2261
2451
  // vendored beside it). Before the missing tail, as the Go port's
2262
2452
  // shared renderer orders them.
2263
2453
  for (const bad of report.unevaluable ?? []) {
2264
2454
  lines.push(bad + ': does not evaluate on its own; nothing to pin')
2265
2455
  }
2456
+ lines.push(...pkgForbiddenLines(report.forbidden ?? []))
2266
2457
  for (const miss of report.missing) {
2267
- lines.push(miss + ': not fetched (run: aontu mod get)')
2458
+ lines.push(miss + ': not fetched (run: aontu sync)')
2268
2459
  }
2269
2460
  return lines.join('\n')
2270
2461
  }
2271
2462
 
2272
2463
 
2464
+ // The two `pkg` subcommands that reach a repository.
2465
+ async function runPkgNet(sub: string, dir: string, args: PkgArgs,
2466
+ opts: PkgToolOptions, servers: Servers, io: Io): Promise<number> {
2467
+ if ('serve' === sub) {
2468
+ const served = await startServe({
2469
+ dir: resolve(dir), upstream: args.upstream,
2470
+ listen: args.listen ?? '127.0.0.1:8017', http: servers.http(),
2471
+ })
2472
+ io.out('serving ' + resolve(dir) + ' at ' + served.url + '\n' +
2473
+ args.upstream.map((u) => 'upstream: ' + u + '\n').join(''))
2474
+ await servers.serve(served)
2475
+ await served.close()
2476
+ return 0
2477
+ }
2478
+ if (null == opts.cache) {
2479
+ io.err(NO_CACHE)
2480
+ return 2
2481
+ }
2482
+ const report = await pkgOutdated(dir, opts, servers.http(), { base: netBase(args) })
2483
+ io.out(pkgReportText('pkg outdated', args.format, report) + '\n')
2484
+ return PKG_EXIT[report.verdict]
2485
+ }
2486
+
2487
+ const NO_CACHE = 'aontu: this verb reads and writes the user cache, which a ' +
2488
+ 'confined run (--trust root) does not reach and this host does not name ' +
2489
+ '(set XDG_CACHE_HOME or HOME)\n'
2490
+
2491
+ function netBase(args: PkgArgs): string[] | undefined {
2492
+ return 0 === args.base.length ? undefined : args.base
2493
+ }
2494
+
2495
+ const PACKAGE_VERBS = ['sync', 'add', 'get', 'remove', 'why', 'publish']
2496
+
2497
+ const PACKAGE_HELP: Record<string, string> = {
2498
+ sync: 'aontu sync [--frozen] [dir] (try --help)',
2499
+ add: 'aontu add <pkg> [dir] (try --help)',
2500
+ get: 'aontu get <pkg>[@<version>] [dir] (try --help)',
2501
+ remove: 'aontu remove <pkg> [dir] (try --help)',
2502
+ why: 'aontu why <pkg> [dir] (try --help)',
2503
+ publish: 'aontu publish [--yes] [--to <dir>] [--key <file>] [dir] (try --help)',
2504
+ }
2505
+
2506
+ // The top-level package verbs (ADR-039 part 5). Every one but `why`
2507
+ // reaches a repository, so every one runs behind the seam.
2508
+ async function runPackageVerb(verb: string, argv: string[], servers: Servers):
2509
+ Promise<number> {
2510
+ const io = servers.io ?? PROCESS_IO
2511
+ const trusted = takeTrust(argv, io)
2512
+ if (null == trusted) {
2513
+ return 2
2514
+ }
2515
+ const args = parsePkgArgs(trusted.argv, verb, io)
2516
+ if (null == args) {
2517
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2518
+ }
2519
+ const wantsPkg = 'sync' !== verb && 'publish' !== verb
2520
+ const dir = args.rest[wantsPkg ? 1 : 0] ?? '.'
2521
+ if (args.rest.length > (wantsPkg ? 2 : 1) || (wantsPkg && 0 === args.rest.length)) {
2522
+ io.err('aontu: ' + verb + (wantsPkg ? ' needs a package' : ' takes a directory') +
2523
+ '\n' + PACKAGE_HELP[verb] + '\n')
2524
+ return 2
2525
+ }
2526
+ nameOldLayout(dir, io)
2527
+ const opts = pkgToolOptions(trusted.trust, resolve(dir))
2528
+ const http = servers.http()
2529
+
2530
+ if ('why' === verb) {
2531
+ const report = pkgWhy(dir, opts, args.rest[0])
2532
+ io.out(pkgReportText('why', args.format, report) + '\n')
2533
+ return PKG_EXIT[report.verdict]
2534
+ }
2535
+
2536
+ if ('publish' === verb) {
2537
+ for (const f of [args.key, args.token]) {
2538
+ if (null != f && !existsSync(f)) {
2539
+ io.err('aontu: cannot read ' + f + '\n')
2540
+ return 2
2541
+ }
2542
+ }
2543
+ if (true === args.yes && null == args.key) {
2544
+ io.err('aontu: publish --yes needs --key <file>, the Ed25519 key that signs\n')
2545
+ return 2
2546
+ }
2547
+ const report = await pkgPublish(dir, opts, http, {
2548
+ yes: args.yes, to: null == args.to ? undefined : resolve(args.to), key: args.key,
2549
+ token: args.token, against: args.against, base: netBase(args), write: args.write,
2550
+ })
2551
+ io.out(pkgReportText('publish', args.format, report) + '\n')
2552
+ return PKG_EXIT[report.verdict]
2553
+ }
2554
+
2555
+ if (null == opts.cache) {
2556
+ io.err(NO_CACHE)
2557
+ return 2
2558
+ }
2559
+ const net = { base: netBase(args) }
2560
+ const report: PkgSyncReport | PkgChangeReport | string =
2561
+ 'sync' === verb ? await pkgSync(dir, opts, http, { ...net, frozen: args.frozen }) :
2562
+ 'remove' === verb ? await pkgRemove(dir, opts, http, args.rest[0], net) :
2563
+ await pkgGet(dir, opts, http, args.rest[0], { ...net, mode: verb as 'add' | 'get' })
2564
+ if ('string' === typeof report) {
2565
+ io.err('aontu: ' + report + '\n')
2566
+ return 2
2567
+ }
2568
+ io.out(pkgReportText(verb, args.format, report) + '\n')
2569
+ return PKG_EXIT[report.verdict]
2570
+ }
2571
+
2572
+
2573
+ function pkgReportText(verb: string, fmt: SubsumeFormat, report: any): string {
2574
+ return 'json' === fmt ?
2575
+ exactJSON({ aontu: { version: version(), verb }, ...report }, 2) :
2576
+ pkgNetText(verb, report)
2577
+ }
2578
+
2579
+
2580
+ function pkgNetText(verb: string, report: any): string {
2581
+ const lines = ['verdict: ' + report.verdict]
2582
+ const tail = (): void => {
2583
+ for (const e of report.events ?? []) {
2584
+ lines.push(e.code + ': ' + e.message)
2585
+ }
2586
+ if (null != report.refusal) {
2587
+ lines.push('refused: ' + report.refusal.code + ': ' + report.refusal.message)
2588
+ }
2589
+ }
2590
+
2591
+ if ('why' === verb) {
2592
+ const why = report as PkgWhyReport
2593
+ for (const p of why.paths) {
2594
+ lines.push(p.join(' -> '))
2595
+ }
2596
+ if ('missing' === why.verdict) {
2597
+ lines.push(why.pkg + ': not in the closure')
2598
+ }
2599
+ return lines.join('\n')
2600
+ }
2601
+
2602
+ if ('publish' === verb) {
2603
+ const pub = report as PkgPublishReport
2604
+ lines.push(...pkgText('manifest', pub).split('\n').slice(1))
2605
+ if (null != pub.digest) {
2606
+ lines.push('digest: ' + pub.digest)
2607
+ }
2608
+ if (null != pub.signer) {
2609
+ lines.push('signer: ' + pub.signer)
2610
+ }
2611
+ if (null != pub.against) {
2612
+ lines.push('against: ' + pub.against)
2613
+ }
2614
+ if (null != pub.to) {
2615
+ lines.push('to: ' + pub.to)
2616
+ }
2617
+ if (null != pub.write) {
2618
+ lines.push('write: ' + pub.write + PUBLISH_PATH)
2619
+ }
2620
+ if ('dry-run' === pub.verdict) {
2621
+ lines.push('dry run: nothing sent (add --yes)')
2622
+ }
2623
+ if ('sent' === pub.verdict) {
2624
+ lines.push('sent')
2625
+ }
2626
+ tail()
2627
+ return lines.join('\n')
2628
+ }
2629
+
2630
+ if ('pkg outdated' === verb) {
2631
+ const out = report as PkgOutdatedReport
2632
+ for (const e of out.locked) {
2633
+ if (0 < versionCompare(e.newest, e.v)) {
2634
+ lines.push(e.key + ' ' + e.v + ' -> ' + e.newest)
2635
+ for (const m of e.moves) {
2636
+ lines.push(' ' + m)
2637
+ }
2638
+ }
2639
+ else {
2640
+ lines.push(e.key + ' ' + e.v + ': current')
2641
+ }
2642
+ if (null != e.retracted) {
2643
+ lines.push(e.key + ' ' + e.v + ': retracted by ' + e.retracted)
2644
+ }
2645
+ }
2646
+ tail()
2647
+ return lines.join('\n')
2648
+ }
2649
+
2650
+ const sync = report as PkgChangeReport
2651
+ if (null != sync.change) {
2652
+ lines.push('change: ' + sync.change)
2653
+ }
2654
+ for (const f of sync.fetched) {
2655
+ lines.push('fetched: ' + f)
2656
+ }
2657
+ lines.push(...pkgLockLines(sync.lock))
2658
+ for (const bad of sync.unevaluable) {
2659
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2660
+ }
2661
+ lines.push(...pkgForbiddenLines(sync.forbidden), ...pkgMismatchLines(sync.mismatched),
2662
+ ...pkgUnlockedLines(sync.unlocked))
2663
+ for (const miss of sync.missing) {
2664
+ lines.push(miss + ': not fetched (run: aontu sync)')
2665
+ }
2666
+ for (const c of sync.changes) {
2667
+ lines.push('lockfile would change: ' + c)
2668
+ }
2669
+ tail()
2670
+ return lines.join('\n')
2671
+ }
2672
+
2673
+
2674
+ const MODEL_HELP = 'aontu model get|why|set ... (try --help)'
2675
+
2676
+ // One document, interrogated or edited (ADR-039 part 5).
2677
+ function runModel(argv: string[]): number {
2678
+ const sub = argv[0]
2679
+ if ('-h' === sub || '--help' === sub) {
2680
+ process.stdout.write(HELP)
2681
+ return 0
2682
+ }
2683
+ if ('get' === sub) {
2684
+ return runGet(argv.slice(1))
2685
+ }
2686
+ if ('why' === sub) {
2687
+ return runWhy(argv.slice(1))
2688
+ }
2689
+ if ('set' === sub) {
2690
+ return runSet(argv.slice(1))
2691
+ }
2692
+ process.stderr.write(`aontu: model needs get, why or set\n${MODEL_HELP}\n`)
2693
+ return 2
2694
+ }
2695
+
2696
+
2697
+
2273
2698
  function vacuous(what: string, why: string): void {
2274
2699
  process.stderr.write(`aontu: ${what}: ${why}\n`)
2275
2700
  }
@@ -2341,6 +2766,120 @@ function runRelations(argv: string[]): number {
2341
2766
  return RELATIONS_EXIT[report.verdict]
2342
2767
  }
2343
2768
 
2769
+ const TRACE_HELP =
2770
+ 'aontu trace [--at <path>] [--format json] [--marker <token>] ' +
2771
+ '[--profile <file>] <file>'
2772
+
2773
+
2774
+ // WHAT WROTE THIS LINE. Every piece a rule stamped, under the
2775
+ // component tree, with the file it reached, the rule set that wrote it
2776
+ // and the model node the dispatch matched.
2777
+ function runTrace(argv: string[]): number {
2778
+ const trusted = takeTrust(argv)
2779
+ if (null == trusted) {
2780
+ return 2
2781
+ }
2782
+ argv = trusted.argv
2783
+ const trust = trusted.trust
2784
+ const rest: string[] = []
2785
+ const profileFiles: string[] = []
2786
+ let format: 'text' | 'json' = 'text'
2787
+ let at: string | undefined = undefined
2788
+ let marker: string | undefined = undefined
2789
+
2790
+ for (let i = 0; i < argv.length; i++) {
2791
+ const arg = argv[i]
2792
+ if ('-h' === arg || '--help' === arg) {
2793
+ process.stdout.write(HELP)
2794
+ return 0
2795
+ }
2796
+ if ('--format' === arg) {
2797
+ const f = argv[++i]
2798
+ if ('text' !== f && 'json' !== f) {
2799
+ process.stderr.write('aontu: --format needs text or json\n')
2800
+ return 2
2801
+ }
2802
+ format = f
2803
+ }
2804
+ else if ('--at' === arg) {
2805
+ at = argv[++i]
2806
+ if (null == at) {
2807
+ process.stderr.write('aontu: --at needs a path\n')
2808
+ return 2
2809
+ }
2810
+ }
2811
+ else if ('--marker' === arg) {
2812
+ marker = argv[++i]
2813
+ if (null == marker) {
2814
+ process.stderr.write('aontu: --marker needs a token\n')
2815
+ return 2
2816
+ }
2817
+ }
2818
+ else if ('--profile' === arg) {
2819
+ const pf = argv[++i]
2820
+ if (null == pf) {
2821
+ process.stderr.write('aontu: --profile needs a file\n')
2822
+ return 2
2823
+ }
2824
+ profileFiles.push(pf)
2825
+ }
2826
+ else if (arg.startsWith('-')) {
2827
+ process.stderr.write(`aontu: unknown trace option ${arg} (try --help)\n`)
2828
+ return 2
2829
+ }
2830
+ else {
2831
+ rest.push(arg)
2832
+ }
2833
+ }
2834
+
2835
+ if (1 !== rest.length) {
2836
+ process.stderr.write(`aontu: trace needs one file\n${TRACE_HELP}\n`)
2837
+ return 2
2838
+ }
2839
+
2840
+ let src: string
2841
+ try {
2842
+ src = readFileSync(rest[0], 'utf8')
2843
+ }
2844
+ catch (err: any) {
2845
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
2846
+ return 2
2847
+ }
2848
+
2849
+ const declared = loadProfiles(profileFiles, trust)
2850
+ if ('number' === typeof declared) {
2851
+ return declared
2852
+ }
2853
+
2854
+ // A GENERATOR IS AN ENTRY, not a preprocessing step: the file whose
2855
+ // provenance is asked for is the one the author edits.
2856
+ if (!rest[0].endsWith('.aon')) {
2857
+ src = desugarTemplate(src, marker ??
2858
+ markerFromProfiles(declared, rest[0]) ?? markerFor(rest[0]))
2859
+ }
2860
+
2861
+ const report = traceRun(src, {
2862
+ path: rest[0], at,
2863
+ ...verbOpts(trust, entryRootOf(rest[0])),
2864
+ })
2865
+ if ('error' === report.verdict) {
2866
+ // An error report always carries its findings.
2867
+ const errors = report.errors as VetFinding[]
2868
+ process.stderr.write(errors.map(renderFinding).join('\n') + '\n')
2869
+ return 4
2870
+ }
2871
+ if ('json' === format) {
2872
+ process.stdout.write(JSON.stringify({ trace: report.trace }) + '\n')
2873
+ return 0
2874
+ }
2875
+ for (const e of report.trace) {
2876
+ process.stdout.write(
2877
+ [e.file, e.at, e.node, e.rule].join('\t') + '\n')
2878
+ }
2879
+ return 0
2880
+ }
2881
+
2882
+
2344
2883
  function runReaches(argv: string[]): number {
2345
2884
  const trusted = takeTrust(argv)
2346
2885
  if (null == trusted) {
@@ -2955,302 +3494,6 @@ function runJsonSchema(argv: string[]): number {
2955
3494
  }
2956
3495
 
2957
3496
 
2958
- const RENDER_HELP =
2959
- 'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
2960
- '[--stdout | --out <dir> | --check <dir> | --coverage] ' +
2961
- '[--coverage-at <path>] [--strict] [--marker <token>] <file> (try --help)'
2962
-
2963
- function runRender(argv: string[]): number {
2964
- const trusted = takeTrust(argv)
2965
- if (null == trusted) {
2966
- return 2
2967
- }
2968
- argv = trusted.argv
2969
- const trust = trusted.trust
2970
- const files: string[] = []
2971
- const profileFiles: string[] = []
2972
- let format: SubsumeFormat = 'text'
2973
- let at: string | undefined = undefined
2974
- let unit: string | undefined = undefined
2975
- let out: string | undefined = undefined
2976
- let check: string | undefined = undefined
2977
- let toStdout = false
2978
- let strict = false
2979
- let coverage = false
2980
- let coverageAt: string | undefined = undefined
2981
- let marker: string | undefined = undefined
2982
-
2983
- for (let i = 0; i < argv.length; i++) {
2984
- const arg = argv[i]
2985
- if ('-h' === arg || '--help' === arg) {
2986
- process.stdout.write(HELP)
2987
- return 0
2988
- }
2989
- if ('--format' === arg) {
2990
- const f = argv[++i]
2991
- if ('text' !== f && 'json' !== f) {
2992
- process.stderr.write('aontu: --format needs text or json\n')
2993
- return 2
2994
- }
2995
- format = f
2996
- }
2997
- else if ('--at' === arg) {
2998
- at = argv[++i]
2999
- if (null == at) {
3000
- process.stderr.write('aontu: --at needs a path\n')
3001
- return 2
3002
- }
3003
- }
3004
- else if ('--unit' === arg) {
3005
- unit = argv[++i]
3006
- if (null == unit) {
3007
- process.stderr.write('aontu: --unit needs a unit path\n')
3008
- return 2
3009
- }
3010
- }
3011
- else if ('--profile' === arg) {
3012
- const pf = argv[++i]
3013
- if (null == pf) {
3014
- process.stderr.write('aontu: --profile needs a file\n')
3015
- return 2
3016
- }
3017
- profileFiles.push(pf)
3018
- }
3019
- else if ('--out' === arg) {
3020
- out = argv[++i]
3021
- if (null == out) {
3022
- process.stderr.write('aontu: --out needs a directory\n')
3023
- return 2
3024
- }
3025
- }
3026
- else if ('--check' === arg) {
3027
- check = argv[++i]
3028
- if (null == check) {
3029
- process.stderr.write('aontu: --check needs a directory\n')
3030
- return 2
3031
- }
3032
- }
3033
- else if ('--stdout' === arg) {
3034
- toStdout = true
3035
- }
3036
- else if ('--coverage' === arg) {
3037
- coverage = true
3038
- }
3039
- else if ('--marker' === arg) {
3040
- marker = argv[++i]
3041
- if (null == marker) {
3042
- process.stderr.write('aontu: --marker needs a token\n')
3043
- return 2
3044
- }
3045
- }
3046
- else if ('--coverage-at' === arg) {
3047
- coverageAt = argv[++i]
3048
- if (null == coverageAt) {
3049
- process.stderr.write('aontu: --coverage-at needs a path\n')
3050
- return 2
3051
- }
3052
- }
3053
- else if ('--strict' === arg) {
3054
- strict = true
3055
- }
3056
- else if (arg.startsWith('-')) {
3057
- process.stderr.write(
3058
- `aontu: unknown render option ${arg} (try --help)\n`)
3059
- return 2
3060
- }
3061
- else {
3062
- files.push(arg)
3063
- }
3064
- }
3065
-
3066
- if (1 !== files.length) {
3067
- process.stderr.write(`aontu: render needs one file\n${RENDER_HELP}\n`)
3068
- return 2
3069
- }
3070
- const modes = [toStdout, undefined !== out, undefined !== check, coverage]
3071
- .filter((on) => on).length
3072
- if (1 < modes) {
3073
- process.stderr.write(
3074
- 'aontu: render takes one of --stdout, --out, --check or --coverage\n')
3075
- return 2
3076
- }
3077
- // A NARROWER MEASURE NEEDS SOMETHING TO NARROW. `--coverage-at`
3078
- // without `--coverage` asks for a report the run does not compute,
3079
- // and answering silently would be the wrong half of the request.
3080
- if (undefined !== coverageAt && !coverage) {
3081
- process.stderr.write('aontu: --coverage-at needs --coverage\n')
3082
- return 2
3083
- }
3084
-
3085
- let src: string
3086
- try {
3087
- src = readFileSync(files[0], 'utf8')
3088
- }
3089
- catch (err: any) {
3090
- process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3091
- return 2
3092
- }
3093
-
3094
- const loadedProfiles = loadProfiles(profileFiles, trust)
3095
- if ('number' === typeof loadedProfiles) {
3096
- return loadedProfiles
3097
- }
3098
- const profiles = loadedProfiles
3099
-
3100
- if (!files[0].endsWith('.aon')) {
3101
- src = desugarTemplate(src, marker ??
3102
- markerFromProfiles(profiles, files[0]) ?? markerFor(files[0]))
3103
- }
3104
-
3105
- // A RENDER WITH NO PROFILE PRODUCES NO UNITS, and said so with zero
3106
- // bytes and exit 0. The profile is what maps a model onto a
3107
- // language, so without one there is nothing for the renderer to
3108
- // write -- which is a usable answer only if the caller is told.
3109
- const noProfiles = 0 === profiles.length
3110
- const report = render(src, {
3111
- at, unit, strict, profiles, path: files[0],
3112
- coverage, coverageAt,
3113
- // THE JSON REPORT CARRIES THE TRACE (D9), which is what the shape
3114
- // there has always said; a text run computes it only when the
3115
- // coverage report needs it.
3116
- trace: 'json' === format,
3117
- ...verbOpts(trust, entryRootOf(files[0])),
3118
- })
3119
-
3120
- // Said once, whatever the format: stdout stays the report.
3121
- if ('error' !== report.verdict && 0 === report.units.length) {
3122
- vacuous('nothing was rendered',
3123
- noProfiles
3124
- ? 'no profile was given, and the document declares none' +
3125
- ' (see aontu help tasks)'
3126
- : 'the document produced no units under this profile')
3127
- }
3128
-
3129
- if ('json' === format) {
3130
- process.stdout.write(exactJSON({
3131
- aontu: { version: version(), verb: 'render' },
3132
- verdict: report.verdict,
3133
- units: report.units,
3134
- lossy: report.lossy,
3135
- ...(null == report.errors ? {} : { errors: report.errors }),
3136
- ...(null == report.trace ? {} : { trace: report.trace }),
3137
- ...(null == report.coverage ? {} : { coverage: report.coverage }),
3138
- }, 2) + '\n')
3139
- return renderExit(report, 0)
3140
- }
3141
- if ('error' === report.verdict) {
3142
- process.stderr.write(
3143
- (report.errors as VetFinding[]).map(renderFinding).join('\n') + '\n')
3144
- return renderExit(report, 0)
3145
- }
3146
-
3147
- let drift = 0
3148
- if (toStdout) {
3149
- // ONE UNIT'S BYTES AND NOTHING ELSE, so the output can be piped
3150
- // into a formatter or a file.
3151
- if (1 !== report.units.length) {
3152
- process.stderr.write(
3153
- 'aontu: --stdout needs exactly one unit, and the instance has ' +
3154
- `${report.units.length}; --unit names one\n`)
3155
- return 2
3156
- }
3157
- process.stdout.write(report.units[0].text)
3158
- }
3159
- else if (undefined !== out) {
3160
- // EVERY UNIT BELOW <dir>, OR NOTHING: every unit rendered first
3161
- // (the report above), and no file touched unless all did. The
3162
- // directory is realpath-confined; a unit path is already a relative
3163
- // descent (render_path refuses the rest), and the check here is
3164
- // against the symlink inside it. render never deletes.
3165
- for (const u of report.units) {
3166
- if (outsideRoot(out, resolve(out, u.path))) {
3167
- process.stderr.write(`aontu: ${u.path} escapes ${out}\n`)
3168
- return 2
3169
- }
3170
- }
3171
- for (const u of report.units) {
3172
- const full = resolve(out, u.path)
3173
- try {
3174
- mkdirSync(dirname(full), { recursive: true })
3175
- writeFileSync(full, u.text, 'utf8')
3176
- }
3177
- catch (err: any) {
3178
- process.stderr.write(`aontu: cannot write ${u.path}: ${err.message}\n`)
3179
- return 2
3180
- }
3181
- process.stderr.write(`wrote ${u.path}\n`)
3182
- }
3183
- }
3184
- else if (undefined !== check) {
3185
- // RENDER AND COMPARE: a unit whose bytes differ from the file at
3186
- // <dir>/<path>, or whose file is absent, is drift, listed by path.
3187
- // The CI form.
3188
- for (const u of report.units) {
3189
- let have: string | undefined = undefined
3190
- try {
3191
- have = readFileSync(resolve(check, u.path), 'utf8')
3192
- }
3193
- catch {
3194
- // Absent is drift, reported below.
3195
- }
3196
- if (undefined === have) {
3197
- drift++
3198
- process.stderr.write(`aontu: ${u.path} is missing from ${check}\n`)
3199
- }
3200
- else if (have !== u.text) {
3201
- drift++
3202
- process.stderr.write(`aontu: ${u.path} differs from the rendered unit\n`)
3203
- }
3204
- }
3205
- }
3206
- else if (coverage) {
3207
- // THE COVERAGE REPORT (P7), one line per finding and a count at
3208
- // the end: dead model first, then the declarations no rule
3209
- // produced. A clean report is the count line alone.
3210
- const cov = report.coverage as RenderCoverage
3211
- for (const d of cov.dead) {
3212
- process.stdout.write(`dead: ${d}\n`)
3213
- }
3214
- for (const u of cov.unruled) {
3215
- process.stdout.write(`unruled: ${u.unit} ${u.path}\n`)
3216
- }
3217
- process.stdout.write(
3218
- `coverage: ${cov.read.length} path(s) read, ${cov.dead.length} ` +
3219
- `no output consumed, ${cov.unruled.length} declaration(s) ` +
3220
- 'no rule produced\n')
3221
- }
3222
- else {
3223
- // THE SUMMARY: one line per unit -- its path, its language and its
3224
- // size -- since several units have no one text to print.
3225
- for (const u of report.units) {
3226
- process.stdout.write(`${u.path}\t${u.lang}\t${u.text.length} bytes\n`)
3227
- }
3228
- }
3229
- for (const l of report.lossy) {
3230
- process.stderr.write(
3231
- `lossy: ${l.unit} ${l.path} tier ${l.tier} ${l.construct}: ${l.reason}\n`)
3232
- }
3233
- return renderExit(report, drift)
3234
- }
3235
-
3236
- // D8's exit table over a report: a refused unit path is usage (2), a
3237
- // strict refusal is lossy (1), any other error is the document's (4);
3238
- // drift under --check is 1.
3239
- function renderExit(report: RenderReport, drift: number): number {
3240
- if ('error' === report.verdict) {
3241
- const errors = report.errors as VetFinding[]
3242
- if (errors.every((f) => 'render_path' === f.code)) {
3243
- return 2
3244
- }
3245
- if (errors.every((f) => 'render_strict' === f.code)) {
3246
- return 1
3247
- }
3248
- return 4
3249
- }
3250
- return 0 < drift ? 1 : 0
3251
- }
3252
-
3253
-
3254
3497
  const TEMPLATE_HELP =
3255
3498
  'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
3256
3499
 
@@ -3356,9 +3599,9 @@ function runTemplate(argv: string[]): number {
3356
3599
 
3357
3600
 
3358
3601
  // The profiles named by --profile, vetted, or the exit code that says
3359
- // why not. A profile is a language declared as data: `render` matches
3360
- // one to a unit by `lang`, and `template` and `fmt` match one to a file
3361
- // by the extensions its `template.ext` names.
3602
+ // why not. A profile is a language declared as data: `template` and
3603
+ // `fmt` match one to a file by the extensions its `template.ext`
3604
+ // names.
3362
3605
  function loadProfiles(
3363
3606
  profileFiles: string[], trust: TrustArg
3364
3607
  ): any[] | number {
@@ -3373,7 +3616,7 @@ function loadProfiles(
3373
3616
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3374
3617
  return 2
3375
3618
  }
3376
- const loaded = renderProfile(text,
3619
+ const loaded = loadProfile(text,
3377
3620
  { path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
3378
3621
  if (undefined !== loaded.errors) {
3379
3622
  process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
@@ -3470,7 +3713,7 @@ function runHash(argv: string[]): number {
3470
3713
  }
3471
3714
 
3472
3715
 
3473
- const GET_HELP = 'aontu get <path> <file> (try --help)'
3716
+ const GET_HELP = 'aontu model get <path> <file> (try --help)'
3474
3717
 
3475
3718
  function runGet(argv: string[]): number {
3476
3719
  const trusted = takeTrust(argv)
@@ -3516,7 +3759,7 @@ function runGet(argv: string[]): number {
3516
3759
  format = f
3517
3760
  }
3518
3761
  else if (arg.startsWith('-')) {
3519
- process.stderr.write(`aontu: unknown get option ${arg} (try --help)\n`)
3762
+ process.stderr.write(`aontu: unknown model get option ${arg} (try --help)\n`)
3520
3763
  return 2
3521
3764
  }
3522
3765
  else {
@@ -3525,7 +3768,7 @@ function runGet(argv: string[]): number {
3525
3768
  }
3526
3769
 
3527
3770
  if (2 !== rest.length) {
3528
- process.stderr.write(`aontu: get needs a path and one file\n${GET_HELP}\n`)
3771
+ process.stderr.write(`aontu: model get needs a path and one file\n${GET_HELP}\n`)
3529
3772
  return 2
3530
3773
  }
3531
3774
  const [path, file] = rest
@@ -3553,7 +3796,7 @@ function runGet(argv: string[]): number {
3553
3796
  })
3554
3797
  if ('json' === format) {
3555
3798
  process.stdout.write(exactJSON({
3556
- aontu: { version: version(), verb: 'get' },
3799
+ aontu: { version: version(), verb: 'model get' },
3557
3800
  findings: report.findings,
3558
3801
  ok: report.ok,
3559
3802
  out: report.out,
@@ -3582,7 +3825,7 @@ function runGet(argv: string[]): number {
3582
3825
  // The positive twin of the vet report: errors explain what failed to
3583
3826
  // unify, this explains what did.
3584
3827
 
3585
- const WHY_HELP = 'aontu why <path> <file> (try --help)'
3828
+ const WHY_HELP = 'aontu model why <path> <file> (try --help)'
3586
3829
 
3587
3830
  function runWhy(argv: string[]): number {
3588
3831
  const trusted = takeTrust(argv)
@@ -3609,7 +3852,7 @@ function runWhy(argv: string[]): number {
3609
3852
  format = f
3610
3853
  }
3611
3854
  else if (arg.startsWith('-')) {
3612
- process.stderr.write(`aontu: unknown why option ${arg} (try --help)\n`)
3855
+ process.stderr.write(`aontu: unknown model why option ${arg} (try --help)\n`)
3613
3856
  return 2
3614
3857
  }
3615
3858
  else {
@@ -3618,7 +3861,7 @@ function runWhy(argv: string[]): number {
3618
3861
  }
3619
3862
 
3620
3863
  if (2 !== rest.length) {
3621
- process.stderr.write(`aontu: why needs a path and one file\n${WHY_HELP}\n`)
3864
+ process.stderr.write(`aontu: model why needs a path and one file\n${WHY_HELP}\n`)
3622
3865
  return 2
3623
3866
  }
3624
3867
  const [path, file] = rest
@@ -3637,7 +3880,7 @@ function runWhy(argv: string[]): number {
3637
3880
  })
3638
3881
  if ('json' === format) {
3639
3882
  process.stdout.write(exactJSON({
3640
- aontu: { version: version(), verb: 'why' },
3883
+ aontu: { version: version(), verb: 'model why' },
3641
3884
  findings: report.findings,
3642
3885
  ok: report.ok,
3643
3886
  ...(null == report.record ? {} : { record: report.record }),
@@ -3675,7 +3918,7 @@ function renderWhyText(record: WhyRecord): string {
3675
3918
 
3676
3919
 
3677
3920
  const SET_HELP =
3678
- 'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)'
3921
+ 'aontu model set <path>=<value> --entry <file> --overlay <file> (try --help)'
3679
3922
 
3680
3923
  function runSet(argv: string[]): number {
3681
3924
  const trusted = takeTrust(argv)
@@ -3718,7 +3961,7 @@ function runSet(argv: string[]): number {
3718
3961
  format = f
3719
3962
  }
3720
3963
  else if (arg.startsWith('-')) {
3721
- process.stderr.write(`aontu: unknown set option ${arg} (try --help)\n`)
3964
+ process.stderr.write(`aontu: unknown model set option ${arg} (try --help)\n`)
3722
3965
  return 2
3723
3966
  }
3724
3967
  else {
@@ -3728,7 +3971,7 @@ function runSet(argv: string[]): number {
3728
3971
 
3729
3972
  if (0 === assignments.length || null == entry || null == overlayFile) {
3730
3973
  process.stderr.write(
3731
- `aontu: set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
3974
+ `aontu: model set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
3732
3975
  return 2
3733
3976
  }
3734
3977
 
@@ -3780,7 +4023,7 @@ function runSet(argv: string[]): number {
3780
4023
 
3781
4024
  if ('json' === format) {
3782
4025
  process.stdout.write(exactJSON({
3783
- aontu: { version: version(), verb: 'set' },
4026
+ aontu: { version: version(), verb: 'model set' },
3784
4027
  appended: report.appended,
3785
4028
  findings: report.findings,
3786
4029
  overlay: report.overlay,
@@ -4138,8 +4381,8 @@ function runFmt(argv: string[]): number | Promise<number> {
4138
4381
  }
4139
4382
  else if ('--marker' === arg) {
4140
4383
  // THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
4141
- // extension: `render` and `template` take the same option for
4142
- // the same reason, a language the table has never seen.
4384
+ // extension: `fmt` and `template` take the same option for the
4385
+ // same reason, a language the table has never seen.
4143
4386
  marker = argv[++i]
4144
4387
  if (null == marker) {
4145
4388
  process.stderr.write('aontu: --marker needs a token\n')
@@ -4280,17 +4523,38 @@ function fmtOne(
4280
4523
  }
4281
4524
 
4282
4525
 
4526
+ // Where a verb writes. The package verbs are asynchronous, so a test
4527
+ // cannot capture the process streams around them; it hands these in.
4528
+ type Io = { out: (s: string) => void, err: (s: string) => void }
4529
+
4530
+ const PROCESS_IO: Io = {
4531
+ out: (s) => void process.stdout.write(s),
4532
+ err: (s) => void process.stderr.write(s),
4533
+ }
4534
+
4283
4535
  type Servers = {
4284
4536
  lsp: () => void
4285
4537
  mcp: (argv: string[]) => void
4538
+ // `pkg serve` runs until this settles: the process's SIGINT, or a
4539
+ // test's say-so.
4540
+ serve: (served: Served) => Promise<void>
4541
+ http: () => PkgHttp
4542
+ io?: Io
4543
+ }
4544
+
4545
+ // `pkg serve` runs until the process is interrupted.
4546
+ function serveUntilInterrupted(): Promise<void> {
4547
+ return new Promise((done) => process.once('SIGINT', () => done()))
4286
4548
  }
4287
4549
 
4288
4550
  // Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
4289
4551
  // drives each server through a child process instead.
4290
- /* node:coverage ignore next 4 */
4552
+ /* node:coverage ignore next 6 */
4291
4553
  const SERVERS: Servers = {
4292
4554
  lsp: () => void lspMain(),
4293
4555
  mcp: (argv) => void mcpMain(undefined, undefined, undefined, undefined, argv),
4556
+ serve: serveUntilInterrupted,
4557
+ http: defaultHttp,
4294
4558
  }
4295
4559
 
4296
4560
  // undefined: the server took the process; a number: an answer the CLI
@@ -4614,10 +4878,10 @@ function runInit(argv: string[]): number {
4614
4878
 
4615
4879
 
4616
4880
  const KNOWN_VERBS = [
4617
- 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4618
- 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
4619
- 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
4620
- 'view', 'why',
4881
+ 'add', 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4882
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'model', 'pkg', 'publish',
4883
+ 'reaches', 'relations', 'remove', 'subsume', 'sync', 'template', 'trace',
4884
+ 'trim', 'vet', 'view', 'why',
4621
4885
  ]
4622
4886
 
4623
4887
 
@@ -4712,20 +4976,22 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4712
4976
  return servers.mcp(argv.slice(3))
4713
4977
  }
4714
4978
 
4715
- if ('set' === argv[2]) {
4716
- return finish(runSet(argv.slice(3)))
4717
- }
4718
4979
 
4719
4980
  if ('allow' === argv[2]) {
4720
4981
  return finish(runAllow(argv.slice(3)))
4721
4982
  }
4722
4983
 
4723
- if ('why' === argv[2]) {
4724
- return finish(runWhy(argv.slice(3)))
4984
+ if ('model' === argv[2]) {
4985
+ return finish(runModel(argv.slice(3)))
4725
4986
  }
4726
4987
 
4727
- if ('get' === argv[2]) {
4728
- return finish(runGet(argv.slice(3)))
4988
+ if ('pkg' === argv[2]) {
4989
+ const r = runPkg(argv.slice(3), servers)
4990
+ return 'number' === typeof r ? finish(r) : void r.then(finish)
4991
+ }
4992
+
4993
+ if (PACKAGE_VERBS.includes(argv[2])) {
4994
+ return void runPackageVerb(argv[2], argv.slice(3), servers).then(finish)
4729
4995
  }
4730
4996
 
4731
4997
  if ('hash' === argv[2]) {
@@ -4746,10 +5012,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4746
5012
  return finish(runInit(argv.slice(3)))
4747
5013
  }
4748
5014
 
4749
- if ('mod' === argv[2]) {
4750
- return finish(runMod(argv.slice(3)))
4751
- }
4752
-
4753
5015
  if ('relations' === argv[2]) {
4754
5016
  return finish(runRelations(argv.slice(3)))
4755
5017
  }
@@ -4758,14 +5020,15 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4758
5020
  return finish(runJsonSchema(argv.slice(3)))
4759
5021
  }
4760
5022
 
4761
- if ('render' === argv[2]) {
4762
- return finish(runRender(argv.slice(3)))
4763
- }
4764
5023
 
4765
5024
  if ('template' === argv[2]) {
4766
5025
  return finish(runTemplate(argv.slice(3)))
4767
5026
  }
4768
5027
 
5028
+ if ('trace' === argv[2]) {
5029
+ return finish(runTrace(argv.slice(3)))
5030
+ }
5031
+
4769
5032
  if ('reaches' === argv[2]) {
4770
5033
  return finish(runReaches(argv.slice(3)))
4771
5034
  }
@@ -4869,7 +5132,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4869
5132
  else {
4870
5133
  runStdin(mode, format, trust).then((code) => finish(code))
4871
5134
  }
4872
- } /* node:coverage ignore next 20 */
5135
+ } /* node:coverage ignore next 21 */
4873
5136
 
4874
5137
 
4875
5138
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -4881,9 +5144,9 @@ export {
4881
5144
  runReaches,
4882
5145
  runView,
4883
5146
  runJsonSchema,
4884
- runRender,
4885
5147
  runTemplate,
4886
- runMod,
5148
+ runTrace,
5149
+ runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
4887
5150
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
4888
5151
  looksLikeVerb,
4889
5152
  KNOWN_VERBS,