aontu 0.65.0 → 0.66.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.
@@ -1,9 +1,9 @@
1
1
  # Aontu — published grammar (GBNF), for constrained decoding.
2
2
  #
3
- # G7 phase 6, docs/capability-review/g7-machine-access.md. This
4
- # grammar covers the DOCUMENTED EMISSION SURFACE: what a model should
5
- # be allowed to write, which is a superset of JSON plus the operators,
6
- # constraints and marks the language's own canonical form emits.
3
+ # This grammar covers the DOCUMENTED EMISSION SURFACE: what a model
4
+ # should be allowed to write, which is a superset of JSON plus the
5
+ # operators, constraints and marks the language's own canonical form
6
+ # emits.
7
7
  #
8
8
  # It is CONSERVATIVE BY CONSTRUCTION: it may accept less than the
9
9
  # parser does, never more. Two deliberate exclusions:
@@ -21,19 +21,9 @@
21
21
 
22
22
  root ::= ws value ws
23
23
 
24
- # No pipe. `|>` was parse-time sugar (G8 phase 4); ADR-018 removed it,
25
- # `x |> f(a)` being written `f(x, a)`.
26
- #
27
- # That ADR says "the operator from both grammars", and both kept it
28
- # anyway — the engine, the suite, the error code and the docs were
29
- # done, these two files were not. Nothing caught it, and nothing here
30
- # could have: the parity discipline above runs one way, requiring every
31
- # canon output to PARSE, so a rule canon never emits is unreachable and
32
- # a test that only reads this file cannot tell a dead rule from a live
33
- # one. What it cost was the direction that matters to this file's
34
- # consumer — a decoder constrained by the old text could emit `|>` and
35
- # have the engine refuse it with `aontu/unexpected`. Fixed before the
36
- # next release, so no published grammar ever disagreed with its engine.
24
+ # No pipe. `|>` is not in the language; `x |> f(a)` is written
25
+ # `f(x, a)`. A decoder constrained by this file cannot emit a spelling
26
+ # the engine refuses with `aontu/unexpected`.
37
27
  value ::= disjunct
38
28
 
39
29
  disjunct ::= conjunct ( ws "|" ws conjunct )*
@@ -108,7 +98,7 @@ ref ::= "$" ( "." segment )+ | ( "." segment )+
108
98
  # `a:6-2` as a parse error), so admitting it over-approximated.
109
99
  segment ::= ( [a-zA-Z0-9_] )+
110
100
 
111
- # The placeholder: a hole a call is filled through (G8 phase 3). BARE
101
+ # The placeholder: a hole a call is filled through. BARE
112
102
  # only -- `"_"` is an ordinary string, and a longer bare word
113
103
  # containing it is ordinary text.
114
104
  place ::= "_"
@@ -1,9 +1,8 @@
1
1
  // Aontu — published grammar (lark), for constrained decoding.
2
2
  //
3
- // G7 phase 6, docs/capability-review/g7-machine-access.md. The same
4
- // rules as grammar/aontu.gbnf, for the other consumer: keep the two in
5
- // step, and note that ts/test/grammar.test.ts checks that every rule
6
- // named there is named here.
3
+ // The same rules as grammar/aontu.gbnf, for the other consumer: keep
4
+ // the two in step, and note that ts/test/grammar.test.ts checks that
5
+ // every rule named there is named here.
7
6
  //
8
7
  // The gbnf file is the one the parity test INTERPRETS against the
9
8
  // shared suite's canonical output; this file is its transcription, so
@@ -15,16 +14,9 @@
15
14
 
16
15
  root: value
17
16
 
18
- // No pipe. `|>` was parse-time sugar (G8 phase 4); ADR-018 removed it,
19
- // `x |> f(a)` being written `f(x, a)`.
20
- //
21
- // That ADR says "the operator from both grammars", and both kept it
22
- // anyway — the engine, the suite, the error code and the docs were
23
- // done, these two files were not. Nothing caught it: a rule nothing
24
- // derives never matches, so every canon still parsed and every test
25
- // stayed green while the grammars offered a spelling the parser
26
- // refuses with `aontu/unexpected`. Fixed before the next release, so
27
- // no published grammar ever disagreed with its engine.
17
+ // No pipe. `|>` is not in the language; `x |> f(a)` is written
18
+ // `f(x, a)`. The engine refuses the old spelling with
19
+ // `aontu/unexpected`, and all three published grammars agree with it.
28
20
  value: disjunct
29
21
 
30
22
  disjunct: conjunct ("|" conjunct)*
@@ -14,16 +14,14 @@
14
14
  "rejects AS THOUGH it were valid, which is why the builtin list below is",
15
15
  "the engine's own set and not a hand-picked sample of it.",
16
16
  "",
17
- "THE BUILTIN LIST IS A COPY, AND A COPY DRIFTS. aontu.lark carried",
18
- "`same` — never a builtin — and was missing `acyclic`, `inverse` and",
19
- "`rel` for two releases, with every test green. So the same guard the",
17
+ "THE BUILTIN LIST IS A COPY, AND A COPY DRIFTS, so the same guard the",
20
18
  "other two grammars have covers this file: ts/test/grammar.test.ts,",
21
19
  "`the-textmate-grammar-names-exactly-the-engine-builtins`, asserts this",
22
20
  "list against BUILTIN_FUNCS in BOTH directions. Add a builtin to the",
23
21
  "engine without adding it here and that test fails.",
24
22
  "",
25
- "No `|>`: ADR-018 removed the pipe from the language, and all three",
26
- "published grammars now agree about that."
23
+ "No `|>`: the pipe is not in the language, and all three published",
24
+ "grammars agree about that."
27
25
  ],
28
26
  "name": "aontu",
29
27
  "displayName": "Aontu",
@@ -130,7 +128,7 @@
130
128
  "match": "\\b(true|false|null)\\b"
131
129
  },
132
130
  {
133
- "comment": "`_` is a literal of the language as of G8 phase 3, not text.",
131
+ "comment": "`_` is a literal of the language, not text.",
134
132
  "name": "constant.language.placeholder.aontu",
135
133
  "match": "(?<![A-Za-z0-9_])_(?![A-Za-z0-9_])"
136
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aontu",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "main": "dist/aontu.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/aontu.d.ts",
@@ -78,7 +78,8 @@
78
78
  "@tabnas/parser": "0.9.7",
79
79
  "@tabnas/path": "0.3.2",
80
80
  "@tabnas/toml": "0.5.7",
81
- "@tabnas/yaml": "0.5.7"
81
+ "@tabnas/yaml": "0.5.7",
82
+ "jostraca": "0.38.0"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@tabnas/railroad": "0.3.6",
package/skill/tasks.md CHANGED
@@ -82,16 +82,21 @@ aontu trim --check model.aon # entries whose removal changes nothing
82
82
  ## Generate code from a model, and hold it
83
83
 
84
84
  ```
85
- aontu model get out gen.aon # the component tree: files, and the lines in them
86
- aontu trace gen.aon # what rule wrote each line
85
+ aontu render gen.aon build # write the files below build/
86
+ aontu render --check gen.aon build # write nothing; exit 1 when build/ has drifted
87
+ aontu render gen/ build # every generator directly in gen/, as one run
88
+ aontu model get out gen.aon # the component tree: files, and the lines in them
89
+ aontu trace gen.aon # what rule wrote each line
87
90
  ```
88
91
 
89
92
  A generator answers a **component tree** -- `file(name, [lines])` and
90
- its neighbours -- which a generator runtime writes to disk. Hand the
91
- tree to one and compare with what is committed: the model is the truth,
92
- the code is the claim, and drift is a finding. `aontu trace` answers
93
- the other direction: for a line of the output, the model node and the
94
- rule behind it.
93
+ its neighbours -- which `aontu render` hands to jostraca, the generator
94
+ runtime, to write. A tree that is one file is written to the path
95
+ itself, and a folder of generators is one run. `--check` compares with
96
+ what is committed: the model is the
97
+ truth, the code is the claim, and drift is a finding. `aontu trace`
98
+ answers the other direction: for a line of the output, the model node
99
+ and the rule behind it.
95
100
 
96
101
  ## Ask what a model says, and why
97
102
 
package/src/aontu.ts CHANGED
@@ -35,7 +35,7 @@ import { format, unifiedDiff } from './format'
35
35
  export type { LintFinding, FormatReport, FormatOptions } from './format'
36
36
 
37
37
 
38
- const VERSION = '0.65.0'
38
+ const VERSION = '0.66.0'
39
39
 
40
40
 
41
41
  function genQuiet(val: any, aontu: Aontu): any {
package/src/cli.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  import { evalFailure } from './query'
6
6
  // __importStar downlevel helper, whose branches no supported Node takes.
7
7
  import {
8
- existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,
8
+ existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync,
9
+ writeFileSync,
9
10
  } from 'node:fs'
11
+ import type { Dirent } from 'node:fs'
10
12
  import { basename, dirname, join, resolve } from 'node:path'
11
13
  import { tmpdir } from 'node:os'
12
14
  import { createInterface } from 'node:readline'
@@ -97,6 +99,9 @@ const HELP = `Usage: aontu [options] [file]
97
99
  [--profile <file>] <file>
98
100
  aontu trace [--at <path>] [--format json] [--marker <token>]
99
101
  [--profile <file>] <file>
102
+ aontu render [--check] [--at <path>] [--format json]
103
+ [--marker <token>] [--profile <file>]
104
+ <file|folder> <path>
100
105
  aontu hash [options] <file>
101
106
  aontu sync [--frozen] [options] [dir]
102
107
  aontu add <pkg>[@<version>] [options] [dir]
@@ -411,6 +416,31 @@ every other line is a line of output.
411
416
 
412
417
  Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
413
418
 
419
+ Render options:
420
+ --check Compare what the generator writes with what <path>
421
+ holds, write nothing, and exit 1 on drift
422
+ --at <path> Where the component tree lives in the document
423
+ (default $.out)
424
+ --format json Print the files written, or the drift, as JSON
425
+ --marker <t> The file is a generator, and this is its marker
426
+ (default //-, and #- --- /*- <!--- by extension)
427
+ --profile <f> A profile file, whose template.ext names the
428
+ extensions it marks and template.marker the marker
429
+
430
+ The render verb writes the component tree a generator answers. The
431
+ tree is handed to jostraca, the generator runtime, which writes the
432
+ files: a tree that is one file is written to <path> itself, unless
433
+ <path> is a directory, and any other tree is written below <path>.
434
+ With --check nothing is written and <path> is compared with what the
435
+ generator writes, one "kind: file" line per difference.
436
+ A folder as the generator is a set: every regular file directly in
437
+ it, dotfiles aside, in name order, and their trees are written below
438
+ <path> as one run, so a path two of them claim is refused. A file with
439
+ no marker line in it is refused by name.
440
+
441
+ Render exit codes: 0 written or clean, 1 --check drift, 2 usage or
442
+ I/O, 4 the document does not stand up, or --at names nothing.
443
+
414
444
  Model set options:
415
445
  --entry <file> The document the change is checked against
416
446
  --overlay <file> The file the change is appended to (created if
@@ -2880,6 +2910,211 @@ function runTrace(argv: string[]): number {
2880
2910
  }
2881
2911
 
2882
2912
 
2913
+ // ---------------------------------------------------------------------
2914
+ // The writer (ADR-040). A generator answers a component tree; this verb
2915
+ // hands it to jostraca, which writes the files below a path or holds
2916
+ // them to it.
2917
+
2918
+ const RENDER_HELP =
2919
+ 'aontu render [--check] [--at <path>] [--format json] ' +
2920
+ '[--marker <token>] [--profile <file>] <file|folder> <path> (try --help)'
2921
+
2922
+
2923
+ // Loaded at the call, so every other verb starts without it.
2924
+ function generatorRuntime(): any {
2925
+ return require('jostraca')
2926
+ }
2927
+
2928
+
2929
+ function isDirectory(path: string): boolean {
2930
+ return true === statSync(path, { throwIfNoEntry: false })?.isDirectory()
2931
+ }
2932
+
2933
+
2934
+ async function runRender(argv: string[]): Promise<number> {
2935
+ const trusted = takeTrust(argv)
2936
+ if (null == trusted) {
2937
+ return 2
2938
+ }
2939
+ argv = trusted.argv
2940
+ const trust = trusted.trust
2941
+ const rest: string[] = []
2942
+ const profileFiles: string[] = []
2943
+ let format: SubsumeFormat = 'text'
2944
+ let at: string | undefined = undefined
2945
+ let marker: string | undefined = undefined
2946
+ let check = false
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 ('--check' === arg) {
2955
+ check = true
2956
+ }
2957
+ else if ('--format' === arg) {
2958
+ const f = argv[++i]
2959
+ if ('text' !== f && 'json' !== f) {
2960
+ process.stderr.write('aontu: --format needs text or json\n')
2961
+ return 2
2962
+ }
2963
+ format = f
2964
+ }
2965
+ else if ('--at' === arg) {
2966
+ at = argv[++i]
2967
+ if (null == at || '' === at) {
2968
+ process.stderr.write('aontu: --at needs a path\n')
2969
+ return 2
2970
+ }
2971
+ }
2972
+ else if ('--marker' === arg) {
2973
+ marker = argv[++i]
2974
+ if (null == marker || '' === marker) {
2975
+ process.stderr.write('aontu: --marker needs a token\n')
2976
+ return 2
2977
+ }
2978
+ }
2979
+ else if ('--profile' === arg) {
2980
+ const pf = argv[++i]
2981
+ if (null == pf || '' === pf) {
2982
+ process.stderr.write('aontu: --profile needs a file\n')
2983
+ return 2
2984
+ }
2985
+ profileFiles.push(pf)
2986
+ }
2987
+ else if (arg.startsWith('-')) {
2988
+ process.stderr.write(`aontu: unknown render option ${arg} (try --help)\n`)
2989
+ return 2
2990
+ }
2991
+ else {
2992
+ rest.push(arg)
2993
+ }
2994
+ }
2995
+
2996
+ if (2 !== rest.length) {
2997
+ process.stderr.write(`aontu: render needs a file and a path\n${RENDER_HELP}\n`)
2998
+ return 2
2999
+ }
3000
+ const [file, dest] = rest
3001
+
3002
+ const declared = loadProfiles(profileFiles, trust)
3003
+ if ('number' === typeof declared) {
3004
+ return declared
3005
+ }
3006
+
3007
+ // A FOLDER IS A SET OF GENERATORS: every regular file directly in it,
3008
+ // dotfiles aside, in code-point order, written as one tree.
3009
+ let entries: Dirent[] | undefined
3010
+ try {
3011
+ entries = readdirSync(file, { withFileTypes: true })
3012
+ }
3013
+ catch (err: any) {
3014
+ entries = undefined
3015
+ }
3016
+ const set = undefined !== entries
3017
+ const files = undefined === entries ? [file] :
3018
+ entries.filter((e) => e.isFile() && !e.name.startsWith('.'))
3019
+ .map((e) => e.name).sort(cmpCodePoint).map((n) => join(file, n))
3020
+ if (0 === files.length) {
3021
+ process.stderr.write(`aontu: ${file} holds no generator\n`)
3022
+ return 2
3023
+ }
3024
+
3025
+ const trees: any[] = []
3026
+ for (const f of files) {
3027
+ let src: string
3028
+ try {
3029
+ src = readFileSync(f, 'utf8')
3030
+ }
3031
+ catch (err: any) {
3032
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3033
+ return 2
3034
+ }
3035
+ if (!/[.](aon|aontu)$/.test(f)) {
3036
+ const mark = marker ?? markerFromProfiles(declared, f) ?? markerFor(f)
3037
+ if (!templateOutputs(src, mark).some((out) => !out)) {
3038
+ process.stderr.write(`aontu: ${f} carries no ${mark} marker line, ` +
3039
+ 'so there is no aontu in it to render\n')
3040
+ return 2
3041
+ }
3042
+ src = desugarTemplate(src, mark)
3043
+ }
3044
+ const report = get(src, at ?? '$.out', {
3045
+ view: 'json', path: f, ...verbOpts(trust, entryRootOf(f)),
3046
+ })
3047
+ if (!report.ok) {
3048
+ process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n')
3049
+ return 4
3050
+ }
3051
+ const tree = JSON.parse(report.out)
3052
+ // A `File` without a name is refused: the runtime ports disagree
3053
+ // about it.
3054
+ if ('File' === tree?.cmp && 'string' !== typeof tree.props?.name) {
3055
+ process.stderr.write(`aontu: ${f}: the file at ${at ?? '$.out'} has no name\n`)
3056
+ return 4
3057
+ }
3058
+ trees.push(tree)
3059
+ }
3060
+
3061
+ // ONE FILE GOES TO THE PATH ITSELF, unless the path is a directory; a
3062
+ // set is written below the path whatever its trees are.
3063
+ let folder = dest
3064
+ const tree: any = set ? trees.flatMap((t) => Array.isArray(t) ? t : [t]) : trees[0]
3065
+ if (!set && 'File' === tree?.cmp && !isDirectory(dest)) {
3066
+ tree.props.name = basename(dest)
3067
+ folder = dirname(dest)
3068
+ }
3069
+
3070
+ const { cmpTree, Jostraca } = generatorRuntime()
3071
+ let root: any
3072
+ try {
3073
+ root = cmpTree(tree, { raw: true })
3074
+ }
3075
+ catch (err: any) {
3076
+ process.stderr.write(`aontu: ${file}: ${err.message}\n`)
3077
+ return 4
3078
+ }
3079
+ const runtime = Jostraca()
3080
+
3081
+ try {
3082
+ if (check) {
3083
+ const res = await runtime.check({ folder }, root)
3084
+ const drift = res.drift.map((d: any) => ({ kind: d.kind, path: d.path }))
3085
+ if ('json' === format) {
3086
+ process.stdout.write(exactJSON({
3087
+ aontu: { version: version(), verb: 'render' },
3088
+ verdict: 0 === drift.length ? 'ok' : 'drift',
3089
+ checked: res.checked,
3090
+ drift,
3091
+ }, 2) + '\n')
3092
+ }
3093
+ else {
3094
+ for (const d of drift) {
3095
+ process.stdout.write(`${d.kind}: ${d.path}\n`)
3096
+ }
3097
+ }
3098
+ return 0 === drift.length ? 0 : 1
3099
+ }
3100
+
3101
+ const res = await runtime.generate({ folder }, root)
3102
+ if ('json' === format) {
3103
+ process.stdout.write(exactJSON({
3104
+ aontu: { version: version(), verb: 'render' },
3105
+ verdict: 'ok',
3106
+ files: res.files,
3107
+ }, 2) + '\n')
3108
+ }
3109
+ return 0
3110
+ }
3111
+ catch (err: any) {
3112
+ process.stderr.write(`aontu: ${err.message}\n`)
3113
+ return 2
3114
+ }
3115
+ }
3116
+
3117
+
2883
3118
  function runReaches(argv: string[]): number {
2884
3119
  const trusted = takeTrust(argv)
2885
3120
  if (null == trusted) {
@@ -4880,8 +5115,8 @@ function runInit(argv: string[]): number {
4880
5115
  const KNOWN_VERBS = [
4881
5116
  'add', 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4882
5117
  'help', 'init', 'jsonschema', 'lsp', 'mcp', 'model', 'pkg', 'publish',
4883
- 'reaches', 'relations', 'remove', 'subsume', 'sync', 'template', 'trace',
4884
- 'trim', 'vet', 'view', 'why',
5118
+ 'reaches', 'relations', 'remove', 'render', 'subsume', 'sync', 'template',
5119
+ 'trace', 'trim', 'vet', 'view', 'why',
4885
5120
  ]
4886
5121
 
4887
5122
 
@@ -5029,6 +5264,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
5029
5264
  return finish(runTrace(argv.slice(3)))
5030
5265
  }
5031
5266
 
5267
+ if ('render' === argv[2]) {
5268
+ return void runRender(argv.slice(3)).then(finish)
5269
+ }
5270
+
5032
5271
  if ('reaches' === argv[2]) {
5033
5272
  return finish(runReaches(argv.slice(3)))
5034
5273
  }
@@ -5146,6 +5385,7 @@ export {
5146
5385
  runJsonSchema,
5147
5386
  runTemplate,
5148
5387
  runTrace,
5388
+ runRender,
5149
5389
  runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
5150
5390
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
5151
5391
  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## Generate code from a model, and hold it\n\n```\naontu model 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 model get $.entity.planet.table model.aon\naontu model why $.entity.planet.table model.aon\naontu model get $.entity --keys model.aon\naontu model 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 model 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 render gen.aon build # write the files below build/\naontu render --check gen.aon build # write nothing; exit 1 when build/ has drifted\naontu render gen/ build # every generator directly in gen/, as one run\naontu model 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 `aontu render` hands to jostraca, the generator\nruntime, to write. A tree that is one file is written to the path\nitself, and a folder of generators is one run. `--check` compares with\nwhat is committed: the model is the\ntruth, the code is the claim, and drift is a finding. `aontu trace`\nanswers the other direction: for a line of the output, the model node\nand the rule behind it.\n\n## Ask what a model says, and why\n\n```\naontu model get $.entity.planet.table model.aon\naontu model why $.entity.planet.table model.aon\naontu model get $.entity --keys model.aon\naontu model 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 model 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",