aontu 0.63.0 → 0.64.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 (61) hide show
  1. package/README.md +6 -6
  2. package/dist/aontu.d.ts +3 -3
  3. package/dist/aontu.js +4 -6
  4. package/dist/aontu.js.map +1 -1
  5. package/dist/aontumodel.js +6 -12
  6. package/dist/aontumodel.js.map +1 -1
  7. package/dist/casing.d.ts +5 -0
  8. package/dist/casing.js +90 -0
  9. package/dist/casing.js.map +1 -0
  10. package/dist/cli.d.ts +2 -2
  11. package/dist/cli.js +147 -335
  12. package/dist/cli.js.map +1 -1
  13. package/dist/helpdoc.js +1 -1
  14. package/dist/helpdoc.js.map +1 -1
  15. package/dist/hints.js +3 -0
  16. package/dist/hints.js.map +1 -1
  17. package/dist/lsp.d.ts +1 -1
  18. package/dist/mcp.js +0 -36
  19. package/dist/mcp.js.map +1 -1
  20. package/dist/mod-tool.js +16 -1
  21. package/dist/mod-tool.js.map +1 -1
  22. package/dist/profile.d.ts +9 -0
  23. package/dist/profile.js +28 -0
  24. package/dist/profile.js.map +1 -0
  25. package/dist/trace.d.ts +21 -0
  26. package/dist/trace.js +107 -0
  27. package/dist/trace.js.map +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/dist/unify.js +1 -6
  30. package/dist/unify.js.map +1 -1
  31. package/dist/val/CmpFuncVal.d.ts +2 -0
  32. package/dist/val/CmpFuncVal.js +47 -13
  33. package/dist/val/CmpFuncVal.js.map +1 -1
  34. package/dist/val/NomFuncVal.js +11 -13
  35. package/dist/val/NomFuncVal.js.map +1 -1
  36. package/dist/val/ReferFuncVal.js +3 -0
  37. package/dist/val/ReferFuncVal.js.map +1 -1
  38. package/package.json +1 -1
  39. package/skill/tasks.md +9 -7
  40. package/src/aontu.ts +3 -8
  41. package/src/aontumodel.ts +6 -12
  42. package/src/casing.ts +95 -0
  43. package/src/cli.ts +162 -359
  44. package/src/helpdoc.ts +1 -1
  45. package/src/hints.ts +3 -0
  46. package/src/mcp.ts +0 -39
  47. package/src/mod-tool.ts +18 -1
  48. package/src/profile.ts +42 -0
  49. package/src/trace.ts +157 -0
  50. package/src/unify.ts +1 -6
  51. package/src/val/CmpFuncVal.ts +66 -20
  52. package/src/val/NomFuncVal.ts +3 -5
  53. package/src/val/ReferFuncVal.ts +4 -0
  54. package/dist/lower.d.ts +0 -23
  55. package/dist/lower.js +0 -528
  56. package/dist/lower.js.map +0 -1
  57. package/dist/render.d.ts +0 -53
  58. package/dist/render.js +0 -432
  59. package/dist/render.js.map +0 -1
  60. package/src/lower.ts +0 -586
  61. package/src/render.ts +0 -580
package/src/cli.ts CHANGED
@@ -17,16 +17,14 @@ 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'
@@ -83,11 +81,10 @@ const HELP = `Usage: aontu [options] [file]
83
81
  aontu view <kind> [options] <file>...
84
82
  aontu view --views <path> [--check] [options] <file>
85
83
  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
84
  aontu template [--resugar] [--check] [--marker <token>]
90
85
  [--profile <file>] <file>
86
+ aontu trace [--at <path>] [--format json] [--marker <token>]
87
+ [--profile <file>] <file>
91
88
  aontu hash [options] <file>
92
89
  aontu mod tidy|verify|vendor|manifest [options] [dir]
93
90
  aontu get <path> [options] <file>
@@ -333,37 +330,11 @@ View exit codes: 0 rendered, 1 --check mismatch or lossy under
333
330
  --strict, 2 usage or --max-rows exceeded, 4 the document does not stand
334
331
  up on its own, or a relation, root or path that names nothing.
335
332
 
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.
333
+ A template entry file whose extension is not .aon is a GENERATOR: a
334
+ document in the target's own syntax, whose marker lines carry aontu and
335
+ whose other lines are output. It is desugared before it is evaluated,
336
+ and a language the table does not know names its marker with --marker,
337
+ or declares it once in a profile file that --profile reads.
367
338
 
368
339
  Template options:
369
340
  --resugar The file is the canonical aontu; print the template
@@ -473,11 +444,11 @@ Fmt options:
473
444
  The fmt verb prints one document in the agreed form; with no file it
474
445
  reads standard input. Several files need one of the options above.
475
446
 
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.
447
+ A file whose extension is not .aon is a GENERATOR, as it is for
448
+ template: the aontu its marker lines carry is formatted, the marker
449
+ stands at the left margin with the aontu indented after it, and every
450
+ line of output is held on a line of its own. A file with no marker line
451
+ in it is another language's, and is refused.
481
452
 
482
453
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
483
454
  a --strict finding, 2 usage, 4 a document does not parse.
@@ -2080,6 +2051,12 @@ const VIEW_USAGE_CODES = [
2080
2051
  const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
2081
2052
 
2082
2053
  function runMod(argv: string[]): number {
2054
+ const trusted = takeTrust(argv)
2055
+ if (null == trusted) {
2056
+ return 2
2057
+ }
2058
+ argv = trusted.argv
2059
+ const trust = trusted.trust
2083
2060
  const rest: string[] = []
2084
2061
  let format: SubsumeFormat = 'text'
2085
2062
  let against: string | undefined
@@ -2145,11 +2122,13 @@ function runMod(argv: string[]): number {
2145
2122
  return 2
2146
2123
  }
2147
2124
 
2125
+ const modopts = modToolOptions(trust, resolve(dir))
2126
+
2148
2127
  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)
2128
+ 'tidy' === sub ? modTidy(dir, modopts) :
2129
+ 'verify' === sub ? modVerify(dir, modopts) :
2130
+ 'vendor' === sub ? modVendor(dir, modopts) :
2131
+ modManifest(dir, modopts, against)
2153
2132
 
2154
2133
  process.stdout.write(('json' === format ?
2155
2134
  exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
@@ -2188,11 +2167,16 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2188
2167
  // The tooling's evaluator: the same standalone evaluation the module
2189
2168
  // resolver verifies with (ts/src/mod.ts), and for the same reason —
2190
2169
  // only the engine can say what a module MEANS.
2191
- function modToolOptions() {
2170
+ function modToolOptions(trust: TrustArg, entryRoot: string) {
2171
+ const opts = verbOpts(trust, entryRoot)
2172
+ // The user cache lives outside any confinement root, so a confined
2173
+ // run reads the vendor tree only -- as the evaluator's own module
2174
+ // leg already does when a root is set.
2175
+ const rooted = null != (opts.trust as any)?.include?.root
2192
2176
  return {
2193
- cache: modCacheDir(),
2177
+ ...(rooted ? {} : { cache: modCacheDir() }),
2194
2178
  eval: (src: string, path: string) => {
2195
- const a0 = new Aontu()
2179
+ const a0 = new Aontu(opts)
2196
2180
  const ctx = a0.ctx({ collect: true })
2197
2181
  const val: any = a0.unify(src, { path }, ctx)
2198
2182
  return {
@@ -2341,6 +2325,120 @@ function runRelations(argv: string[]): number {
2341
2325
  return RELATIONS_EXIT[report.verdict]
2342
2326
  }
2343
2327
 
2328
+ const TRACE_HELP =
2329
+ 'aontu trace [--at <path>] [--format json] [--marker <token>] ' +
2330
+ '[--profile <file>] <file>'
2331
+
2332
+
2333
+ // WHAT WROTE THIS LINE. Every piece a rule stamped, under the
2334
+ // component tree, with the file it reached, the rule set that wrote it
2335
+ // and the model node the dispatch matched.
2336
+ function runTrace(argv: string[]): number {
2337
+ const trusted = takeTrust(argv)
2338
+ if (null == trusted) {
2339
+ return 2
2340
+ }
2341
+ argv = trusted.argv
2342
+ const trust = trusted.trust
2343
+ const rest: string[] = []
2344
+ const profileFiles: string[] = []
2345
+ let format: 'text' | 'json' = 'text'
2346
+ let at: string | undefined = undefined
2347
+ let marker: string | undefined = undefined
2348
+
2349
+ for (let i = 0; i < argv.length; i++) {
2350
+ const arg = argv[i]
2351
+ if ('-h' === arg || '--help' === arg) {
2352
+ process.stdout.write(HELP)
2353
+ return 0
2354
+ }
2355
+ if ('--format' === arg) {
2356
+ const f = argv[++i]
2357
+ if ('text' !== f && 'json' !== f) {
2358
+ process.stderr.write('aontu: --format needs text or json\n')
2359
+ return 2
2360
+ }
2361
+ format = f
2362
+ }
2363
+ else if ('--at' === arg) {
2364
+ at = argv[++i]
2365
+ if (null == at) {
2366
+ process.stderr.write('aontu: --at needs a path\n')
2367
+ return 2
2368
+ }
2369
+ }
2370
+ else if ('--marker' === arg) {
2371
+ marker = argv[++i]
2372
+ if (null == marker) {
2373
+ process.stderr.write('aontu: --marker needs a token\n')
2374
+ return 2
2375
+ }
2376
+ }
2377
+ else if ('--profile' === arg) {
2378
+ const pf = argv[++i]
2379
+ if (null == pf) {
2380
+ process.stderr.write('aontu: --profile needs a file\n')
2381
+ return 2
2382
+ }
2383
+ profileFiles.push(pf)
2384
+ }
2385
+ else if (arg.startsWith('-')) {
2386
+ process.stderr.write(`aontu: unknown trace option ${arg} (try --help)\n`)
2387
+ return 2
2388
+ }
2389
+ else {
2390
+ rest.push(arg)
2391
+ }
2392
+ }
2393
+
2394
+ if (1 !== rest.length) {
2395
+ process.stderr.write(`aontu: trace needs one file\n${TRACE_HELP}\n`)
2396
+ return 2
2397
+ }
2398
+
2399
+ let src: string
2400
+ try {
2401
+ src = readFileSync(rest[0], 'utf8')
2402
+ }
2403
+ catch (err: any) {
2404
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
2405
+ return 2
2406
+ }
2407
+
2408
+ const declared = loadProfiles(profileFiles, trust)
2409
+ if ('number' === typeof declared) {
2410
+ return declared
2411
+ }
2412
+
2413
+ // A GENERATOR IS AN ENTRY, not a preprocessing step: the file whose
2414
+ // provenance is asked for is the one the author edits.
2415
+ if (!rest[0].endsWith('.aon')) {
2416
+ src = desugarTemplate(src, marker ??
2417
+ markerFromProfiles(declared, rest[0]) ?? markerFor(rest[0]))
2418
+ }
2419
+
2420
+ const report = traceRun(src, {
2421
+ path: rest[0], at,
2422
+ ...verbOpts(trust, entryRootOf(rest[0])),
2423
+ })
2424
+ if ('error' === report.verdict) {
2425
+ // An error report always carries its findings.
2426
+ const errors = report.errors as VetFinding[]
2427
+ process.stderr.write(errors.map(renderFinding).join('\n') + '\n')
2428
+ return 4
2429
+ }
2430
+ if ('json' === format) {
2431
+ process.stdout.write(JSON.stringify({ trace: report.trace }) + '\n')
2432
+ return 0
2433
+ }
2434
+ for (const e of report.trace) {
2435
+ process.stdout.write(
2436
+ [e.file, e.at, e.node, e.rule].join('\t') + '\n')
2437
+ }
2438
+ return 0
2439
+ }
2440
+
2441
+
2344
2442
  function runReaches(argv: string[]): number {
2345
2443
  const trusted = takeTrust(argv)
2346
2444
  if (null == trusted) {
@@ -2955,302 +3053,6 @@ function runJsonSchema(argv: string[]): number {
2955
3053
  }
2956
3054
 
2957
3055
 
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
3056
  const TEMPLATE_HELP =
3255
3057
  'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
3256
3058
 
@@ -3356,9 +3158,9 @@ function runTemplate(argv: string[]): number {
3356
3158
 
3357
3159
 
3358
3160
  // 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.
3161
+ // why not. A profile is a language declared as data: `template` and
3162
+ // `fmt` match one to a file by the extensions its `template.ext`
3163
+ // names.
3362
3164
  function loadProfiles(
3363
3165
  profileFiles: string[], trust: TrustArg
3364
3166
  ): any[] | number {
@@ -3373,7 +3175,7 @@ function loadProfiles(
3373
3175
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3374
3176
  return 2
3375
3177
  }
3376
- const loaded = renderProfile(text,
3178
+ const loaded = loadProfile(text,
3377
3179
  { path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
3378
3180
  if (undefined !== loaded.errors) {
3379
3181
  process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
@@ -4138,8 +3940,8 @@ function runFmt(argv: string[]): number | Promise<number> {
4138
3940
  }
4139
3941
  else if ('--marker' === arg) {
4140
3942
  // 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.
3943
+ // extension: `fmt` and `template` take the same option for the
3944
+ // same reason, a language the table has never seen.
4143
3945
  marker = argv[++i]
4144
3946
  if (null == marker) {
4145
3947
  process.stderr.write('aontu: --marker needs a token\n')
@@ -4616,8 +4418,8 @@ function runInit(argv: string[]): number {
4616
4418
  const KNOWN_VERBS = [
4617
4419
  'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4618
4420
  'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
4619
- 'relations', 'render', 'set', 'subsume', 'template', 'trim', 'vet',
4620
- 'view', 'why',
4421
+ 'relations', 'set', 'subsume', 'template', 'trace', 'trim',
4422
+ 'vet', 'view', 'why',
4621
4423
  ]
4622
4424
 
4623
4425
 
@@ -4758,14 +4560,15 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4758
4560
  return finish(runJsonSchema(argv.slice(3)))
4759
4561
  }
4760
4562
 
4761
- if ('render' === argv[2]) {
4762
- return finish(runRender(argv.slice(3)))
4763
- }
4764
4563
 
4765
4564
  if ('template' === argv[2]) {
4766
4565
  return finish(runTemplate(argv.slice(3)))
4767
4566
  }
4768
4567
 
4568
+ if ('trace' === argv[2]) {
4569
+ return finish(runTrace(argv.slice(3)))
4570
+ }
4571
+
4769
4572
  if ('reaches' === argv[2]) {
4770
4573
  return finish(runReaches(argv.slice(3)))
4771
4574
  }
@@ -4869,7 +4672,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4869
4672
  else {
4870
4673
  runStdin(mode, format, trust).then((code) => finish(code))
4871
4674
  }
4872
- } /* node:coverage ignore next 20 */
4675
+ } /* node:coverage ignore next 21 */
4873
4676
 
4874
4677
 
4875
4678
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -4881,8 +4684,8 @@ export {
4881
4684
  runReaches,
4882
4685
  runView,
4883
4686
  runJsonSchema,
4884
- runRender,
4885
4687
  runTemplate,
4688
+ runTrace,
4886
4689
  runMod,
4887
4690
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
4888
4691
  looksLikeVerb,
package/src/helpdoc.ts CHANGED
@@ -17,7 +17,7 @@ const HELPDOC: HelpTopic[] = [
17
17
  "topic": "tasks",
18
18
  "summary": "what do you want to do? — the verb for a job",
19
19
  "source": "docs/skill/tasks.md",
20
- "text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n## Check code against a model\n\n```\naontu render --check src/ --profile go.aon model.aon\n```\n\n`render` writes code from the model; `--check` writes nothing and\nlists what on disk differs from what the model implies. That is the\ngate: the model is the truth, the code is the claim, and drift is a\nfinding. `--coverage` reports the other direction — model paths no\noutput consumed, and rendered declarations no rule produced.\n\n## Ask what a model says, and why\n\n```\naontu get $.entity.planet.table model.aon\naontu why $.entity.planet.table model.aon\naontu get $.entity --keys model.aon\naontu get $.entity --types model.aon\n```\n\n`why` names **every** contribution to a value, with the file and line\neach was written on. It is the first thing to run when a value is not\nwhat you expected, and the second thing to run when `vet` refuses.\n\n## Change a value without editing the file\n\n```\naontu set '$.entity.planet.table=planet_v2' --entry model.aon --overlay local.aon\n```\n\nThe change is checked before it is written; a change that contradicts\na pinned value is refused, and the file is left alone.\n\n## Gate a change to the model itself\n\n```\naontu subsume old.aon new.aon # does the general admit every specific?\naontu breaking --against git#HEAD~1 model.aon\naontu hash model.aon # a pin that survives reformatting\n```\n\n## Hand a model to another agent\n\n```\naontu agentsmd --write AGENTS.md model.aon\n```\n\nSplices a derived stanza — the pin, the root keys, the shape, and the\ncommands spelled with paths that exist — between two markers, and\nleaves the rest of the file alone. Re-run it in the commit that\nchanges the model.\n\n## When something refuses\n\nRead the code in the brackets, then look it up:\n\n```\naontu explain no_scalar_unify\n```\n\nEvery finding carries a `code`, a `class`, a `path` and the **two\nsites** that disagree — the value and the constraint it failed, each\nwith its file and line. The class says what kind of answer it is:\n`conflict` (two things cannot both hold), `incomplete` (nothing is\nwrong yet, something is missing), `parse`, `reference`, `budget`.\n\nFull index: [`error-codes.md`](error-codes.md). The language on one\npage: [`grammar-card.md`](grammar-card.md). The worked ladder from\nplain JSON upward: [`examples.md`](examples.md).\n"
20
+ "text": "# What do you want to do?\n\nThe verb for a job, found by the word you arrived with. aontu's own\nvocabulary is on the right; yours is probably on the left.\n\n## Start from a document that works\n\nNothing written yet? Do not invent the first document, edit one:\n\n```\naontu init # into the working directory\naontu init model/ # or into a named one\nsh check.sh # the four questions, on what was just written\n```\n\n`init` writes `model.aon` (an entity map constrained with `&:`),\n`data.aon` (an instance of it that holds) and `check.sh` (the four\nchecks to run after every edit). It never overwrites: if any\nof the three already stands there, it refuses and writes none of them.\n\n## Describe a domain\n\nAn **ontology**, a **schema**, a **data model**, a **contract** — in\naontu these are all one thing: a document. Write the entities as a\n**map keyed by name**, and say what every entry must satisfy with the\n`&:` template:\n\n```\nentity: {\n &: {\n table: string\n fields: { &: { type: string, required: *false | boolean } }\n }\n planet: { table: \"planets\", fields: { name: { type: \"string\" } } }\n moon: { table: \"moons\", fields: { name: { type: \"string\" } } }\n}\n```\n\n`&:` is the construct to reach for first, and the one most easily\nmissed. It meets **every** key of the map it sits in. A quoted `\"*\"`\nis not a wildcard — it is a key named `*`, and a schema written that\nway constrains nothing while still reporting `valid`.\n\nPrefer a named map to a list unless the order is a fact (a migration\nsequence, a rule table tried in order). A key is an address, a name\nother parts can refer to, and a diff that shows one insertion instead\nof every following element renumbered.\n\n## Check data against a model\n\n```\naontu vet model.aon data.aon\n```\n\nExit `0` valid, `1` a contradiction, `3` incomplete (nothing\ncontradicts, but the model is not yet satisfied), `4` the model does\nnot stand up on its own. `--format json` for the machine-readable\nreport, `--closed` to refuse keys the model does not declare,\n`--max-errors <n>` to cap the list.\n\n**Make the check prove it checked something.** A check that examined\nnothing answers exactly like one that passed:\n\n```\naontu vet --coverage model.aon data.aon # what did it examine?\naontu vet --strict-coverage model.aon data.aon # exit 1 if nothing\n```\n\n`--coverage` reports how many data leaves a declaration constrained,\nthe data paths none did, and the declarations no data met.\n`--strict-coverage` exits 1 when the answer is nothing. The usual\ncause is the `\"*\"` mistake above: reach for `&:`.\n\n## Check the model is coherent with itself\n\n```\naontu relations model.aon # declared entity edges: targets resolve, no cycles\naontu reaches planet moon model.aon # does one entity reach another, at any remove?\naontu trim --check model.aon # entries whose removal changes nothing\n```\n\n## Generate code from a model, and hold it\n\n```\naontu get out gen.aon # the component tree: files, and the lines in them\naontu trace gen.aon # what rule wrote each line\n```\n\nA generator answers a **component tree** -- `file(name, [lines])` and\nits neighbours -- which a generator runtime writes to disk. Hand the\ntree to one and compare with what is committed: the model is the truth,\nthe code is the claim, and drift is a finding. `aontu trace` answers\nthe other direction: for a line of the output, the model node and the\nrule behind it.\n\n## Ask what a model says, and why\n\n```\naontu get $.entity.planet.table model.aon\naontu why $.entity.planet.table model.aon\naontu get $.entity --keys model.aon\naontu get $.entity --types model.aon\n```\n\n`why` names **every** contribution to a value, with the file and line\neach was written on. It is the first thing to run when a value is not\nwhat you expected, and the second thing to run when `vet` refuses.\n\n## Change a value without editing the file\n\n```\naontu set '$.entity.planet.table=planet_v2' --entry model.aon --overlay local.aon\n```\n\nThe change is checked before it is written; a change that contradicts\na pinned value is refused, and the file is left alone.\n\n## Gate a change to the model itself\n\n```\naontu subsume old.aon new.aon # does the general admit every specific?\naontu breaking --against git#HEAD~1 model.aon\naontu hash model.aon # a pin that survives reformatting\n```\n\n## Hand a model to another agent\n\n```\naontu agentsmd --write AGENTS.md model.aon\n```\n\nSplices a derived stanza — the pin, the root keys, the shape, and the\ncommands spelled with paths that exist — between two markers, and\nleaves the rest of the file alone. Re-run it in the commit that\nchanges the model.\n\n## When something refuses\n\nRead the code in the brackets, then look it up:\n\n```\naontu explain no_scalar_unify\n```\n\nEvery finding carries a `code`, a `class`, a `path` and the **two\nsites** that disagree — the value and the constraint it failed, each\nwith its file and line. The class says what kind of answer it is:\n`conflict` (two things cannot both hold), `incomplete` (nothing is\nwrong yet, something is missing), `parse`, `reference`, `budget`.\n\nFull index: [`error-codes.md`](error-codes.md). The language on one\npage: [`grammar-card.md`](grammar-card.md). The worked ladder from\nplain JSON upward: [`examples.md`](examples.md).\n"
21
21
  },
22
22
  {
23
23
  "topic": "language",
package/src/hints.ts CHANGED
@@ -211,6 +211,9 @@ const hints: Record<string, string> = {
211
211
 
212
212
  view_kind_unknown: 'The figure kind is not one the verb draws. The kinds are tree, matrix,\ngraph, layer, sets, layers, ladder and poset; the note lists them.',
213
213
 
214
+ // RETIRED with the `render` verb (ADR-038). Registered still,
215
+ // errcodes.tsv being append-only, so a code a released engine raised
216
+ // keeps its meaning.
214
217
  render_path: 'A unit path is written below the output directory: relative, with no\n`..` segment, and no two units the same. Rename the unit.',
215
218
  render_lang: 'A text escape carries verbatim syntax of ONE language, and it must be\nthe unit\'s: a text of another language in this unit would be written\nas if it were this one. Move it to a unit of its own language.',
216
219
  render_profile: 'Only fragments and text escapes render without a lowering, under the\ntext profile. A record, enum, alias, const or func needs a profile\nwhose language has one (typescript, go): set the unit\'s lang, or\nwrite the declaration as a fragment.',