aontu 0.57.0 → 0.58.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 (117) hide show
  1. package/README.md +2 -2
  2. package/dist/agentsmd.js +1 -1
  3. package/dist/alias.d.ts +3 -0
  4. package/dist/alias.js +59 -0
  5. package/dist/alias.js.map +1 -0
  6. package/dist/aontu.d.ts +4 -2
  7. package/dist/aontu.js +11 -3
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +3 -1
  10. package/dist/cli.js +475 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +2 -0
  13. package/dist/ctx.js +1 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/format.js +8 -2
  16. package/dist/format.js.map +1 -1
  17. package/dist/hints.js +40 -9
  18. package/dist/hints.js.map +1 -1
  19. package/dist/lang.js +354 -57
  20. package/dist/lang.js.map +1 -1
  21. package/dist/lower.d.ts +20 -0
  22. package/dist/lower.js +575 -0
  23. package/dist/lower.js.map +1 -0
  24. package/dist/lsp.d.ts +1 -1
  25. package/dist/lsp.js +1 -1
  26. package/dist/lsp.js.map +1 -1
  27. package/dist/mcp-server.js +1 -1
  28. package/dist/mcp.d.ts +1 -0
  29. package/dist/mcp.js +40 -3
  30. package/dist/mcp.js.map +1 -1
  31. package/dist/render.d.ts +53 -0
  32. package/dist/render.js +542 -0
  33. package/dist/render.js.map +1 -0
  34. package/dist/sigdecl.js +1 -1
  35. package/dist/sigdecl.js.map +1 -1
  36. package/dist/std.d.ts +2 -0
  37. package/dist/std.js +498 -2
  38. package/dist/std.js.map +1 -1
  39. package/dist/template.d.ts +5 -0
  40. package/dist/template.js +257 -0
  41. package/dist/template.js.map +1 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/dist/unify.js +43 -0
  44. package/dist/unify.js.map +1 -1
  45. package/dist/val/AggFuncVal.d.ts +1 -1
  46. package/dist/val/AggFuncVal.js +10 -21
  47. package/dist/val/AggFuncVal.js.map +1 -1
  48. package/dist/val/BagVal.js +1 -1
  49. package/dist/val/BagVal.js.map +1 -1
  50. package/dist/val/ConstraintVal.js +1 -1
  51. package/dist/val/EachFuncVal.d.ts +1 -1
  52. package/dist/val/EachFuncVal.js +8 -13
  53. package/dist/val/EachFuncVal.js.map +1 -1
  54. package/dist/val/EmitFuncVal.d.ts +23 -4
  55. package/dist/val/EmitFuncVal.js +298 -28
  56. package/dist/val/EmitFuncVal.js.map +1 -1
  57. package/dist/val/FilterFuncVal.js +8 -4
  58. package/dist/val/FilterFuncVal.js.map +1 -1
  59. package/dist/val/FormFuncVal.d.ts +14 -0
  60. package/dist/val/FormFuncVal.js +55 -0
  61. package/dist/val/FormFuncVal.js.map +1 -0
  62. package/dist/val/MapVal.d.ts +2 -1
  63. package/dist/val/MapVal.js +2 -1
  64. package/dist/val/MapVal.js.map +1 -1
  65. package/dist/val/PackFuncVal.d.ts +1 -1
  66. package/dist/val/PackFuncVal.js +22 -18
  67. package/dist/val/PackFuncVal.js.map +1 -1
  68. package/dist/val/PlaceVal.js +2 -1
  69. package/dist/val/PlaceVal.js.map +1 -1
  70. package/dist/val/RefVal.d.ts +3 -0
  71. package/dist/val/RefVal.js +156 -17
  72. package/dist/val/RefVal.js.map +1 -1
  73. package/dist/val/Val.d.ts +7 -1
  74. package/dist/val/Val.js +12 -1
  75. package/dist/val/Val.js.map +1 -1
  76. package/dist/val/members.d.ts +9 -0
  77. package/dist/val/members.js +52 -0
  78. package/dist/val/members.js.map +1 -0
  79. package/grammar/aontu.abnf +1 -1
  80. package/grammar/aontu.gbnf +1 -1
  81. package/grammar/aontu.lark +1 -1
  82. package/grammar/aontu.tmLanguage.json +1 -1
  83. package/package.json +1 -1
  84. package/skill/SKILL.md +4 -4
  85. package/skill/error-codes.md +1 -1
  86. package/skill/examples.md +1 -1
  87. package/skill/grammar-card.md +1 -1
  88. package/src/agentsmd.ts +1 -1
  89. package/src/alias.ts +112 -0
  90. package/src/aontu.ts +19 -2
  91. package/src/cli.ts +517 -3
  92. package/src/ctx.ts +13 -0
  93. package/src/format.ts +12 -2
  94. package/src/hints.ts +47 -9
  95. package/src/lang.ts +406 -62
  96. package/src/lower.ts +636 -0
  97. package/src/lsp.ts +1 -1
  98. package/src/mcp-server.ts +1 -1
  99. package/src/mcp.ts +43 -4
  100. package/src/render.ts +727 -0
  101. package/src/sigdecl.ts +1 -1
  102. package/src/std.ts +506 -1
  103. package/src/template.ts +291 -0
  104. package/src/unify.ts +47 -0
  105. package/src/val/AggFuncVal.ts +10 -21
  106. package/src/val/BagVal.ts +1 -1
  107. package/src/val/ConstraintVal.ts +1 -1
  108. package/src/val/EachFuncVal.ts +8 -16
  109. package/src/val/EmitFuncVal.ts +376 -39
  110. package/src/val/FilterFuncVal.ts +11 -4
  111. package/src/val/FormFuncVal.ts +119 -0
  112. package/src/val/MapVal.ts +3 -2
  113. package/src/val/PackFuncVal.ts +23 -19
  114. package/src/val/PlaceVal.ts +2 -1
  115. package/src/val/RefVal.ts +167 -18
  116. package/src/val/Val.ts +42 -1
  117. package/src/val/members.ts +86 -0
package/src/cli.ts CHANGED
@@ -23,7 +23,12 @@ import {
23
23
  exactJSON, vet, subsume, trimCheck, relationCheck,
24
24
  hcanon, canonHash,
25
25
  get, why, patch, agentsMd,
26
+ render,
27
+ renderProfile,
26
28
  } from './aontu'
29
+ import type { RenderCoverage, RenderReport } from './render'
30
+ import { desugarTemplate, resugarTemplate, markerFor } from './template'
31
+ import { outsideRoot } from './mcp'
27
32
  import { sarifReport } from './report-sarif'
28
33
  import { main as lspMain } from './lsp-server'
29
34
  import { main as mcpMain } from './mcp-server'
@@ -68,6 +73,10 @@ const HELP = `Usage: aontu [options] [file]
68
73
  aontu view <kind> [options] <file>...
69
74
  aontu view --views <path> [--check] [options] <file>
70
75
  aontu jsonschema [--at <path>] [--strict] [options] <file>
76
+ aontu render [--at <path>] [--profile <file>]... [--unit <path>]
77
+ [--stdout | --out <dir> | --check <dir> | --coverage]
78
+ [--coverage-at <path>] [--strict] <file>
79
+ aontu template [--resugar] [--check] [--marker <token>] <file>
71
80
  aontu hash [options] <file>
72
81
  aontu mod tidy|verify|vendor|manifest [options] [dir]
73
82
  aontu get <path> [options] <file>
@@ -78,7 +87,7 @@ const HELP = `Usage: aontu [options] [file]
78
87
  aontu lsp
79
88
  aontu mcp [--root <dir>]
80
89
 
81
- Evaluate an Aontu source file and print the result as JSON.
90
+ Evaluate an aontu source file and print the result as JSON.
82
91
  With no file on an interactive terminal, start a REPL.
83
92
  With no file and piped input, read the source from stdin.
84
93
 
@@ -260,6 +269,51 @@ View exit codes: 0 rendered, 1 --check mismatch or lossy under
260
269
  --strict, 2 usage or --max-rows exceeded, 4 the document does not stand
261
270
  up on its own, or a relation, root or path that names nothing.
262
271
 
272
+ Render options:
273
+ --at <path> Render the value at this path ($.a.b); the root by
274
+ default
275
+ --profile <file> A profile document, profile: {lang, ...}, vetted
276
+ against aontu:profile; repeatable, one per language
277
+ --unit <path> Render only the unit with this path
278
+ --stdout One unit's bytes and nothing else (with --unit when
279
+ the instance has several)
280
+ --out <dir> Write every unit below dir, or nothing; never deletes
281
+ --check <dir> Compare every unit with dir/<path>; drift is listed
282
+ --coverage Report what the render read and what it did not:
283
+ model paths no output consumed, and rendered
284
+ declarations no rule produced. Writes nothing
285
+ --coverage-at <p> Measure coverage under this path only, instead of
286
+ the document root
287
+ --strict Refuse the opaque escapes (a text declaration, a raw
288
+ block)
289
+ --format <f> text (default) or json, the whole report; json
290
+ carries the dispatch trace, one entry per emitted
291
+ piece
292
+
293
+ Render exit codes: 0 rendered, 1 lossy under --strict or drift under
294
+ --check, 2 usage or I/O (a refused unit path included), 4 the document
295
+ does not stand up or the instance is not aontu:code.
296
+
297
+ A render entry file whose extension is not .aon is a TEMPLATE: a
298
+ generator in the target's own syntax, whose marker lines carry aontu
299
+ and whose other lines are output. It is desugared before it is
300
+ evaluated, and --marker names the marker for a language the table does
301
+ not know.
302
+
303
+ Template options:
304
+ --resugar The file is the canonical aontu; print the template
305
+ form instead of reading one
306
+ --check Desugar and resugar, and exit 1 if the file is not
307
+ what the round trip answers
308
+ --marker <t> The marker, when the extension does not name it
309
+ (default //-, and #- --- /*- by extension)
310
+
311
+ The template verb prints the canonical aontu form of a generator
312
+ written in the target's own syntax: a marked line is aontu source, and
313
+ every other line is a line of output.
314
+
315
+ Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
316
+
263
317
  Set options:
264
318
  --entry <file> The document the change is checked against
265
319
  --overlay <file> The file the change is appended to (created if
@@ -729,7 +783,7 @@ function runRepl(initialMode: Mode, jsonl: boolean, trust: TrustArg): void {
729
783
 
730
784
  if (!jsonl) {
731
785
  process.stdout.write(
732
- `Aontu v${version()} REPL — :help for commands, :quit to exit\n`)
786
+ `aontu v${version()} REPL — :help for commands, :quit to exit\n`)
733
787
  }
734
788
  rl.prompt()
735
789
 
@@ -2857,6 +2911,439 @@ function runJsonSchema(argv: string[]): number {
2857
2911
  strict && 'lossy' === report.verdict ? 1 : 0
2858
2912
  }
2859
2913
 
2914
+ // ---------------------------------------------------------------------
2915
+ // THE RENDER VERB (docs/design/RENDER.0.md D8): evaluate a document,
2916
+ // vet the value at --at against aontu:code, fold code.units into bytes,
2917
+ // and put them where the flag says -- one unit on stdout, every unit
2918
+ // below --out (all or nothing), or compared against --check. Exit codes
2919
+ // mirror jsonschema's: 0 ok; 1 lossy under --strict or drift under
2920
+ // --check; 2 usage or I/O, a refused unit path included; 4 the
2921
+ // document does not stand up or the instance is not aontu:code.
2922
+
2923
+ const RENDER_HELP =
2924
+ 'aontu render [--at <path>] [--profile <file>]... [--unit <path>] ' +
2925
+ '[--stdout | --out <dir> | --check <dir> | --coverage] ' +
2926
+ '[--coverage-at <path>] [--strict] [--marker <token>] <file> (try --help)'
2927
+
2928
+ function runRender(argv: string[]): number {
2929
+ const trusted = takeTrust(argv)
2930
+ if (null == trusted) {
2931
+ return 2
2932
+ }
2933
+ argv = trusted.argv
2934
+ const trust = trusted.trust
2935
+ const files: string[] = []
2936
+ const profileFiles: string[] = []
2937
+ let format: SubsumeFormat = 'text'
2938
+ let at: string | undefined = undefined
2939
+ let unit: string | undefined = undefined
2940
+ let out: string | undefined = undefined
2941
+ let check: string | undefined = undefined
2942
+ let toStdout = false
2943
+ let strict = false
2944
+ let coverage = false
2945
+ let coverageAt: string | undefined = undefined
2946
+ let marker: string | undefined = undefined
2947
+
2948
+ for (let i = 0; i < argv.length; i++) {
2949
+ const arg = argv[i]
2950
+ if ('-h' === arg || '--help' === arg) {
2951
+ process.stdout.write(HELP)
2952
+ return 0
2953
+ }
2954
+ if ('--format' === arg) {
2955
+ const f = argv[++i]
2956
+ if ('text' !== f && 'json' !== f) {
2957
+ process.stderr.write('aontu: --format needs text or json\n')
2958
+ return 2
2959
+ }
2960
+ format = f
2961
+ }
2962
+ else if ('--at' === arg) {
2963
+ at = argv[++i]
2964
+ if (null == at) {
2965
+ process.stderr.write('aontu: --at needs a path\n')
2966
+ return 2
2967
+ }
2968
+ }
2969
+ else if ('--unit' === arg) {
2970
+ unit = argv[++i]
2971
+ if (null == unit) {
2972
+ process.stderr.write('aontu: --unit needs a unit path\n')
2973
+ return 2
2974
+ }
2975
+ }
2976
+ else if ('--profile' === arg) {
2977
+ const pf = argv[++i]
2978
+ if (null == pf) {
2979
+ process.stderr.write('aontu: --profile needs a file\n')
2980
+ return 2
2981
+ }
2982
+ profileFiles.push(pf)
2983
+ }
2984
+ else if ('--out' === arg) {
2985
+ out = argv[++i]
2986
+ if (null == out) {
2987
+ process.stderr.write('aontu: --out needs a directory\n')
2988
+ return 2
2989
+ }
2990
+ }
2991
+ else if ('--check' === arg) {
2992
+ check = argv[++i]
2993
+ if (null == check) {
2994
+ process.stderr.write('aontu: --check needs a directory\n')
2995
+ return 2
2996
+ }
2997
+ }
2998
+ else if ('--stdout' === arg) {
2999
+ toStdout = true
3000
+ }
3001
+ else if ('--coverage' === arg) {
3002
+ coverage = true
3003
+ }
3004
+ else if ('--marker' === arg) {
3005
+ marker = argv[++i]
3006
+ if (null == marker) {
3007
+ process.stderr.write('aontu: --marker needs a token\n')
3008
+ return 2
3009
+ }
3010
+ }
3011
+ else if ('--coverage-at' === arg) {
3012
+ coverageAt = argv[++i]
3013
+ if (null == coverageAt) {
3014
+ process.stderr.write('aontu: --coverage-at needs a path\n')
3015
+ return 2
3016
+ }
3017
+ }
3018
+ else if ('--strict' === arg) {
3019
+ strict = true
3020
+ }
3021
+ else if (arg.startsWith('-')) {
3022
+ process.stderr.write(
3023
+ `aontu: unknown render option ${arg} (try --help)\n`)
3024
+ return 2
3025
+ }
3026
+ else {
3027
+ files.push(arg)
3028
+ }
3029
+ }
3030
+
3031
+ if (1 !== files.length) {
3032
+ process.stderr.write(`aontu: render needs one file\n${RENDER_HELP}\n`)
3033
+ return 2
3034
+ }
3035
+ const modes = [toStdout, undefined !== out, undefined !== check, coverage]
3036
+ .filter((on) => on).length
3037
+ if (1 < modes) {
3038
+ process.stderr.write(
3039
+ 'aontu: render takes one of --stdout, --out, --check or --coverage\n')
3040
+ return 2
3041
+ }
3042
+ // A NARROWER MEASURE NEEDS SOMETHING TO NARROW. `--coverage-at`
3043
+ // without `--coverage` asks for a report the run does not compute,
3044
+ // and answering silently would be the wrong half of the request.
3045
+ if (undefined !== coverageAt && !coverage) {
3046
+ process.stderr.write('aontu: --coverage-at needs --coverage\n')
3047
+ return 2
3048
+ }
3049
+
3050
+ let src: string
3051
+ try {
3052
+ src = readFileSync(files[0], 'utf8')
3053
+ }
3054
+ catch (err: any) {
3055
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3056
+ return 2
3057
+ }
3058
+
3059
+ // THE ENTRY MAY BE A TEMPLATE (TEMPLATE.0.md; P8), and its EXTENSION
3060
+ // decides, as an include's extension decides what the include is
3061
+ // (ADR-012): a generator is a file in the target's own syntax, so it
3062
+ // carries the target's extension and never `.aon`. Desugared here
3063
+ // rather than anywhere deeper, because a template is an entry
3064
+ // spelling and not a value: an include is still aontu.
3065
+ if (!files[0].endsWith('.aon')) {
3066
+ src = desugarTemplate(src, marker ?? markerFor(files[0]))
3067
+ }
3068
+
3069
+ // THE PROFILES (D5): each --profile file is a document whose root is
3070
+ // `profile: {lang, ...}`, evaluated under the verb's trust and vetted
3071
+ // against aontu:profile as a settled value before the fold reads it
3072
+ // (renderProfile, which also fills the defaults). Two files claiming
3073
+ // one lang is a usage error: the fold could not choose.
3074
+ const profiles: any[] = []
3075
+ const langs = new Map<string, string>()
3076
+ for (const pf of profileFiles) {
3077
+ let text: string
3078
+ try {
3079
+ text = readFileSync(pf, 'utf8')
3080
+ }
3081
+ catch (err: any) {
3082
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3083
+ return 2
3084
+ }
3085
+ const loaded = renderProfile(text,
3086
+ { path: resolve(pf), ...verbOpts(trust, entryRootOf(pf)) })
3087
+ if (undefined !== loaded.errors) {
3088
+ process.stderr.write(loaded.errors.map(renderFinding).join('\n') + '\n')
3089
+ return 4
3090
+ }
3091
+ const profile = loaded.profile
3092
+ const prev = langs.get(profile.lang)
3093
+ if (undefined !== prev) {
3094
+ process.stderr.write(
3095
+ `aontu: two profiles claim ${profile.lang}: ${prev} and ${pf}\n`)
3096
+ return 2
3097
+ }
3098
+ langs.set(profile.lang, pf)
3099
+ profiles.push(profile)
3100
+ }
3101
+
3102
+ const report = render(src, {
3103
+ at, unit, strict, profiles, path: files[0],
3104
+ coverage, coverageAt,
3105
+ // THE JSON REPORT CARRIES THE TRACE (D9), which is what the shape
3106
+ // there has always said; a text run computes it only when the
3107
+ // coverage report needs it.
3108
+ trace: 'json' === format,
3109
+ ...verbOpts(trust, entryRootOf(files[0])),
3110
+ })
3111
+
3112
+ if ('json' === format) {
3113
+ process.stdout.write(exactJSON({
3114
+ aontu: { version: version(), verb: 'render' },
3115
+ verdict: report.verdict,
3116
+ units: report.units,
3117
+ lossy: report.lossy,
3118
+ ...(null == report.errors ? {} : { errors: report.errors }),
3119
+ ...(null == report.trace ? {} : { trace: report.trace }),
3120
+ ...(null == report.coverage ? {} : { coverage: report.coverage }),
3121
+ }, 2) + '\n')
3122
+ return renderExit(report, 0)
3123
+ }
3124
+ if ('error' === report.verdict) {
3125
+ process.stderr.write(
3126
+ (report.errors as VetFinding[]).map(renderFinding).join('\n') + '\n')
3127
+ return renderExit(report, 0)
3128
+ }
3129
+
3130
+ let drift = 0
3131
+ if (toStdout) {
3132
+ // ONE UNIT'S BYTES AND NOTHING ELSE, so the output can be piped
3133
+ // into a formatter or a file.
3134
+ if (1 !== report.units.length) {
3135
+ process.stderr.write(
3136
+ 'aontu: --stdout needs exactly one unit, and the instance has ' +
3137
+ `${report.units.length}; --unit names one\n`)
3138
+ return 2
3139
+ }
3140
+ process.stdout.write(report.units[0].text)
3141
+ }
3142
+ else if (undefined !== out) {
3143
+ // EVERY UNIT BELOW <dir>, OR NOTHING: every unit rendered first
3144
+ // (the report above), and no file touched unless all did. The
3145
+ // directory is realpath-confined; a unit path is already a relative
3146
+ // descent (render_path refuses the rest), and the check here is
3147
+ // against the symlink inside it. render never deletes.
3148
+ for (const u of report.units) {
3149
+ if (outsideRoot(out, resolve(out, u.path))) {
3150
+ process.stderr.write(`aontu: ${u.path} escapes ${out}\n`)
3151
+ return 2
3152
+ }
3153
+ }
3154
+ for (const u of report.units) {
3155
+ const full = resolve(out, u.path)
3156
+ try {
3157
+ mkdirSync(dirname(full), { recursive: true })
3158
+ writeFileSync(full, u.text, 'utf8')
3159
+ }
3160
+ catch (err: any) {
3161
+ process.stderr.write(`aontu: cannot write ${u.path}: ${err.message}\n`)
3162
+ return 2
3163
+ }
3164
+ process.stderr.write(`wrote ${u.path}\n`)
3165
+ }
3166
+ }
3167
+ else if (undefined !== check) {
3168
+ // RENDER AND COMPARE: a unit whose bytes differ from the file at
3169
+ // <dir>/<path>, or whose file is absent, is drift, listed by path.
3170
+ // The CI form.
3171
+ for (const u of report.units) {
3172
+ let have: string | undefined = undefined
3173
+ try {
3174
+ have = readFileSync(resolve(check, u.path), 'utf8')
3175
+ }
3176
+ catch {
3177
+ // Absent is drift, reported below.
3178
+ }
3179
+ if (undefined === have) {
3180
+ drift++
3181
+ process.stderr.write(`aontu: ${u.path} is missing from ${check}\n`)
3182
+ }
3183
+ else if (have !== u.text) {
3184
+ drift++
3185
+ process.stderr.write(`aontu: ${u.path} differs from the rendered unit\n`)
3186
+ }
3187
+ }
3188
+ }
3189
+ else if (coverage) {
3190
+ // THE COVERAGE REPORT (P7), one line per finding and a count at
3191
+ // the end: dead model first, then the declarations no rule
3192
+ // produced. A clean report is the count line alone.
3193
+ const cov = report.coverage as RenderCoverage
3194
+ for (const d of cov.dead) {
3195
+ process.stdout.write(`dead: ${d}\n`)
3196
+ }
3197
+ for (const u of cov.unruled) {
3198
+ process.stdout.write(`unruled: ${u.unit} ${u.path}\n`)
3199
+ }
3200
+ process.stdout.write(
3201
+ `coverage: ${cov.read.length} path(s) read, ${cov.dead.length} ` +
3202
+ `no output consumed, ${cov.unruled.length} declaration(s) ` +
3203
+ 'no rule produced\n')
3204
+ }
3205
+ else {
3206
+ // THE SUMMARY: one line per unit -- its path, its language and its
3207
+ // size -- since several units have no one text to print.
3208
+ for (const u of report.units) {
3209
+ process.stdout.write(`${u.path}\t${u.lang}\t${u.text.length} bytes\n`)
3210
+ }
3211
+ }
3212
+ for (const l of report.lossy) {
3213
+ process.stderr.write(
3214
+ `lossy: ${l.unit} ${l.path} tier ${l.tier} ${l.construct}: ${l.reason}\n`)
3215
+ }
3216
+ return renderExit(report, drift)
3217
+ }
3218
+
3219
+ // D8's exit table over a report: a refused unit path is usage (2), a
3220
+ // strict refusal is lossy (1), any other error is the document's (4);
3221
+ // drift under --check is 1.
3222
+ function renderExit(report: RenderReport, drift: number): number {
3223
+ if ('error' === report.verdict) {
3224
+ const errors = report.errors as VetFinding[]
3225
+ if (errors.every((f) => 'render_path' === f.code)) {
3226
+ return 2
3227
+ }
3228
+ if (errors.every((f) => 'render_strict' === f.code)) {
3229
+ return 1
3230
+ }
3231
+ return 4
3232
+ }
3233
+ return 0 < drift ? 1 : 0
3234
+ }
3235
+
3236
+
3237
+ // ---------------------------------------------------------------------
3238
+ // THE TEMPLATE SURFACE (docs/design/TEMPLATE.0.md; RENDER.0.md P8): the
3239
+ // two transforms and the round trip between them. `render` reads a
3240
+ // template directly, by its extension; this verb is for seeing the
3241
+ // canonical form, for writing one by hand and sugaring it, and for the
3242
+ // check that keeps a committed template and its meaning in agreement.
3243
+
3244
+ const TEMPLATE_HELP =
3245
+ 'aontu template [--resugar] [--check] [--marker <token>] <file> (try --help)'
3246
+
3247
+ function runTemplate(argv: string[]): number {
3248
+ const files: string[] = []
3249
+ let resugar = false
3250
+ let check = false
3251
+ let marker: string | undefined = undefined
3252
+
3253
+ for (let i = 0; i < argv.length; i++) {
3254
+ const arg = argv[i]
3255
+ if ('-h' === arg || '--help' === arg) {
3256
+ process.stdout.write(HELP)
3257
+ return 0
3258
+ }
3259
+ else if ('--resugar' === arg) {
3260
+ resugar = true
3261
+ }
3262
+ else if ('--check' === arg) {
3263
+ check = true
3264
+ }
3265
+ else if ('--marker' === arg) {
3266
+ marker = argv[++i]
3267
+ if (null == marker) {
3268
+ process.stderr.write('aontu: --marker needs a token\n')
3269
+ return 2
3270
+ }
3271
+ }
3272
+ else if (arg.startsWith('-')) {
3273
+ process.stderr.write(
3274
+ `aontu: unknown template option ${arg} (try --help)\n`)
3275
+ return 2
3276
+ }
3277
+ else {
3278
+ files.push(arg)
3279
+ }
3280
+ }
3281
+
3282
+ if (1 !== files.length) {
3283
+ process.stderr.write(`aontu: template needs one file\n${TEMPLATE_HELP}\n`)
3284
+ return 2
3285
+ }
3286
+ // THE TWO ARE DIRECTIONS, NOT MODES THAT COMPOSE: `--check` reads a
3287
+ // template and asks whether the round trip answers it back, and
3288
+ // `--resugar` reads the canonical form instead. A run cannot be both
3289
+ // at once, because the file is one thing or the other.
3290
+ if (resugar && check) {
3291
+ process.stderr.write(
3292
+ 'aontu: template takes one of --resugar or --check\n')
3293
+ return 2
3294
+ }
3295
+
3296
+ let src: string
3297
+ try {
3298
+ src = readFileSync(files[0], 'utf8')
3299
+ }
3300
+ catch (err: any) {
3301
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3302
+ return 2
3303
+ }
3304
+
3305
+ const mark = marker ?? markerFor(files[0])
3306
+
3307
+ if (check) {
3308
+ // THE ROUND TRIP IS THE CHECK (D6): the file held to the spelling
3309
+ // the two transforms answer. What that names is a marker line the
3310
+ // transform would not have written -- one without its space, or one
3311
+ // whose aontu is indented after the marker rather than before it,
3312
+ // since the marker keeps its own indentation. It does NOT name a
3313
+ // changed body line: a template's whitespace is output, so a
3314
+ // trimmed trailing space is still a valid template and it is
3315
+ // `render --check` against the committed files that catches it.
3316
+ // The first line that differs is the report, since a whole diff of
3317
+ // a generator is the file again.
3318
+ const back = resugarTemplate(desugarTemplate(src, mark), mark)
3319
+ if (back === src) {
3320
+ return 0
3321
+ }
3322
+ const want = back.split('\n')
3323
+ const have = src.split('\n')
3324
+ let n = 0
3325
+ while (n < want.length && n < have.length && want[n] === have[n]) {
3326
+ n++
3327
+ }
3328
+ // THE TWO ARE THE SAME LENGTH, always: each transform maps one
3329
+ // line to one line and applies the same trailing-newline rule, so
3330
+ // `back` has as many lines as `src`. The loop above therefore stops
3331
+ // at a real difference rather than by running out of either -- an
3332
+ // equal prefix all the way to the end IS `back === src`, which
3333
+ // returned above. So both indexes are in range here.
3334
+ process.stderr.write(
3335
+ `aontu: ${files[0]}:${n + 1} is not what the round trip answers\n` +
3336
+ ` have: ${JSON.stringify(have[n])}\n` +
3337
+ ` want: ${JSON.stringify(want[n])}\n`)
3338
+ return 1
3339
+ }
3340
+
3341
+ process.stdout.write(resugar ?
3342
+ resugarTemplate(src, mark) : desugarTemplate(src, mark))
3343
+ return 0
3344
+ }
3345
+
3346
+
2860
3347
  // ---------------------------------------------------------------------
2861
3348
  // The canon-hash (G6 phase 1): the pin an agent, a lockfile or a
2862
3349
  // registry stores for "this module, this meaning". The hash covers the
@@ -3521,6 +4008,23 @@ function runFmt(argv: string[]): number | Promise<number> {
3521
4008
 
3522
4009
  let worst = 0
3523
4010
  for (const file of files) {
4011
+ // FMT FORMATS AONTU SOURCE, AND THE EXTENSION SAYS WHAT A FILE IS
4012
+ // (ADR-012's rule, and the one `render` reads a template by).
4013
+ // A TEMPLATE FILE IS NOT AONTU (docs/design/TEMPLATE.0.md; P8):
4014
+ // its marker lines are fragments of a document and its other lines
4015
+ // are the target's, so there is nothing here to format that would
4016
+ // not also rewrite the output. Refused rather than attempted, and
4017
+ // refused BY NAME rather than by a parse failure, because a `#-`
4018
+ // template parses: `#` opens a comment, so every marker line
4019
+ // vanishes and what is left is read as a document that was never
4020
+ // written. The verb answered `0` over one, having understood none
4021
+ // of it.
4022
+ if (!/[.](aon|aontu)$/.test(file)) {
4023
+ process.stderr.write(
4024
+ `aontu: ${file} is not aontu source (.aon, .aontu); a generator ` +
4025
+ 'written in the target\'s own syntax is aontu template\'s\n')
4026
+ return 2
4027
+ }
3524
4028
  let src: string
3525
4029
  try {
3526
4030
  src = readFileSync(file, 'utf8')
@@ -3734,6 +4238,14 @@ function main(argv: string[], servers: Servers = SERVERS): void {
3734
4238
  return finish(runJsonSchema(argv.slice(3)))
3735
4239
  }
3736
4240
 
4241
+ if ('render' === argv[2]) {
4242
+ return finish(runRender(argv.slice(3)))
4243
+ }
4244
+
4245
+ if ('template' === argv[2]) {
4246
+ return finish(runTemplate(argv.slice(3)))
4247
+ }
4248
+
3737
4249
  if ('reaches' === argv[2]) {
3738
4250
  return finish(runReaches(argv.slice(3)))
3739
4251
  }
@@ -3840,7 +4352,7 @@ function main(argv: string[], servers: Servers = SERVERS): void {
3840
4352
  else {
3841
4353
  runStdin(mode, trust).then((code) => finish(code))
3842
4354
  }
3843
- } /* node:coverage ignore next 16 */
4355
+ } /* node:coverage ignore next 18 */
3844
4356
 
3845
4357
 
3846
4358
  // No require.main guard here: bin/aontu.js is the executable entry and
@@ -3852,6 +4364,8 @@ export {
3852
4364
  runReaches,
3853
4365
  runView,
3854
4366
  runJsonSchema,
4367
+ runRender,
4368
+ runTemplate,
3855
4369
  runMod,
3856
4370
  runHash, runGet,
3857
4371
  runWhy, renderWhyText, runSet, runAgentsMd, runFmt,
package/src/ctx.ts CHANGED
@@ -26,6 +26,14 @@ type AontuContextConfig = {
26
26
  // uninstrumented run. Shared by reference down every descend, as the
27
27
  // error list is, so one run has one record.
28
28
  prov?: any
29
+
30
+ // THE READ SET (RENDER.0.md P7), or absent for an uninstrumented
31
+ // run: every tree path a reference resolved to, in one shared set.
32
+ // It is what `render --coverage` measures the model against -- a
33
+ // path no read reached is model the transform never consumed -- and
34
+ // its presence is also what switches the two render riders on
35
+ // (Val.origin, Val.emitted), so one flag turns the whole record on.
36
+ reads?: Set<string>
29
37
  fs?: any
30
38
  path?: string[]
31
39
  root?: Val
@@ -94,6 +102,10 @@ class AontuContext {
94
102
  // context through the prototype chain, so one run has one record.
95
103
  prov?: any
96
104
 
105
+ // The read set (RENDER.0.md P7), or undefined for an uninstrumented
106
+ // run. Inherited exactly as `prov` is.
107
+ reads?: Set<string>
108
+
97
109
  // errlist: Omit<NilVal[], "push"> // Nil error log of current unify.
98
110
  err: any[]
99
111
  explain: any[] | null
@@ -181,6 +193,7 @@ class AontuContext {
181
193
 
182
194
  this.collect = cfg.collect ?? null != cfg.err
183
195
  this.prov = cfg.prov
196
+ this.reads = cfg.reads
184
197
 
185
198
  this.err = cfg.err ?? []
186
199
  this.explain = Array.isArray(cfg.explain) ? cfg.explain : null
package/src/format.ts CHANGED
@@ -148,6 +148,10 @@ type Node = {
148
148
  // value; spread: the value.
149
149
  key?: string
150
150
  opt?: boolean
151
+ // pair: written with `=`, the alias declaration operator, rather than
152
+ // a colon. The spelling is the parse's -- a colon after an alias name
153
+ // is a refused document, and the formatter keeps it one.
154
+ alias?: boolean
151
155
  value?: Node
152
156
 
153
157
  // map, list: the entries, and the comment on the opener's line.
@@ -349,8 +353,9 @@ class Reader {
349
353
  if (this.atKey()) {
350
354
  const tok = this.T[this.i]
351
355
  const opt = '#QM' === this.name(1)
356
+ const alias = '=' === this.T[this.i + (opt ? 2 : 1)].src
352
357
  this.i += opt ? 3 : 2
353
- return { t: 'pair', key: keyText(tok), opt, value: this.value(), at }
358
+ return { t: 'pair', key: keyText(tok), opt, alias, value: this.value(), at }
354
359
  }
355
360
  return this.value()
356
361
  }
@@ -590,6 +595,11 @@ function width(s: string): number {
590
595
  }
591
596
 
592
597
  function pairHead(node: Node, tight: boolean): string {
598
+ // An alias declaration is `%name = value` at every width: the `=` is
599
+ // an operator, and operators are spaced (§3.2).
600
+ if (node.alias) {
601
+ return node.key! + ' = '
602
+ }
593
603
  return node.key! + (node.opt ? '?' : '') + (tight ? ':' : ': ')
594
604
  }
595
605
 
@@ -1104,7 +1114,7 @@ function mergeRuns(body: Node[]): Node[] {
1104
1114
  continue
1105
1115
  }
1106
1116
  out.push({
1107
- t: 'pair', key: first.key, opt: first.opt,
1117
+ t: 'pair', key: first.key, opt: first.opt, alias: first.alias,
1108
1118
  value: { t: 'map', body: mergeRuns(merged) }, orig: group,
1109
1119
  })
1110
1120
  i = j - carry.length