---
title: "API reference"
description: "The API and the command line: every verb, every flag, every exit code, and the MCP server."
source: "https://aontu.dev/docs/reference-api/"
---

# API reference

Rendered from [`docs/reference-api.md`](https://github.com/aontu-lang/aontu/blob/main/docs/reference-api.md) in the engine repository, where a correction belongs, and where the test suite executes every example on this page.

The programming interfaces of both implementations, plus the command-line tool. For the language itself see the [Language reference](https://aontu.dev/docs/reference-language).

## Contents

-   [Command-line interface](#command-line-interface)
-   [Evaluation consumes the tree](#evaluation-consumes-the-tree)
-   [TypeScript API](#typescript-api)
    -   [`Aontu`](#class-aontu)
    -   [`AontuOptions`](#aontuoptions)
    -   [`AontuContext`](#aontucontext)
    -   [`Val`](#val-typescript)
    -   [Exact numbers and `exactJSON`](#exact-numbers-and-exactjson)
    -   [Variables](#variables)
    -   [Exports](#exports)
-   [Go API](#go-api)
    -   [`Aontu`](#type-aontu)
    -   [`Val`](#val-go)
    -   [Exact numbers in Go](#exact-numbers-in-go)
    -   [`Ctx` and errors](#ctx-and-errors)
    -   [Variables in Go](#variables-in-go)
-   [Behavioural parity](#behavioural-parity)

---

## Command-line interface

Both implementations ship the same `aontu` command. It evaluates a source file (or stdin) and prints the result, or starts a REPL when run interactively with no file. The synopsis, from the command itself:

```sh
$ aontu --help
Usage: aontu [options] [file]
       aontu vet [options] <schema> <data> [more-data...]
       aontu subsume [options] <general> <specific>
       aontu breaking --against <file|git#rev> [options] <file>
       aontu trim --check [options] <file>
       aontu relations [options] <file>
       aontu reaches <from> <to> [--relation <name>] [options] <file>
       aontu view <kind> [options] <file>...
       aontu view --views <path> [--check] [options] <file>
       aontu jsonschema [--at <path>] [--strict] [options] <file>
       aontu template [--resugar] [--check] [--marker <token>]
                      [--profile <file>] <file>
       aontu trace [--at <path>] [--format json] [--marker <token>]
                   [--profile <file>] <file>
       aontu render [--check] [--at <path>] [--format json]
                    [--marker <token>] [--profile <file>]
                    <file|folder> <path>
       aontu hash [options] <file>
       aontu sync [--frozen] [options] [dir]
       aontu add <pkg>[@<version>] [options] [dir]
       aontu get <pkg>[@<version>] [options] [dir]
       aontu remove <pkg> [options] [dir]
       aontu why <pkg> [options] [dir]
       aontu publish [--yes] [--to <dir>] [options] [dir]
       aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve
                 [options] [dir]
       aontu pkg keygen <file>
       aontu model get <path> [options] <file>
       aontu model why <path> [options] <file>
       aontu model set <path>=<value>... --entry <file> --overlay <file>
       aontu allow --role <role> [--at <path>] <roles-file> <path>...
       aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
       aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
                 [--profile <file>] <file>...
       aontu help [topic] [--format text|json]
       aontu explain <code> | --list [--format text|json]
       aontu init [dir]
       aontu lsp
       aontu mcp [--root <dir>]

Evaluate an aontu source file and print the result as JSON.
With no file on an interactive terminal, start a REPL.
With no file and piped input, read the source from stdin.
...
```

The elided remainder lists every option; the per-verb sections below carry the same lists. Three options apply to every verb that reads a document: `--trust <t>` (the include capability: `system`, `none`, or `root[:dir]`; see [`AontuOptions`](#aontuoptions) for what each admits), its shorthand `--include-root <dir>`, and `--text-ext <ext>[,<ext>]` (extensions an include additionally reads as text; repeatable, and a leading dot is optional). The last rides WITH the capability rather than beside it: both answer what an include may read. Five verbs take none of the three: [`help`](#aontu-help), [`explain`](#aontu-explain) and [`init`](#aontu-init), which read no document; [`lsp`](#aontu-lsp), which takes no arguments at all; and [the MCP server](#the-mcp-server), which confines with `--root <dir>` instead. [`fmt`](#aontu-fmt) takes all three, and they govern the document it evaluates, which is the one `--profile` names rather than the one it formats. `sync`, `add`, `get` and `remove` take them and then refuse a `root` capability, because a confined run cannot reach the package cache they write.

**Behaviour**

-   **File:** `aontu config.aontu` reads, unifies and prints the file. Relative `@"file"` loads inside it resolve against the file’s own directory, so it works from any working directory.
-   **Exactly one file.** The bare form is `aontu [options] [file]`, singular, and a second filename is a usage error (exit 2) rather than a silent discard. This is what makes a MISTYPED VERB fail loudly: `vet2` matches no subcommand, so it falls through as a file name, and `aontu vet2 schema.aontu good.json` is a usage error rather than a plausible pass: in the one place a tool loop reads the exit code to decide whether the data is good. A file genuinely named like a verb is still reachable as `./vet`.
-   **One mistyped verb is diagnosed too.** A single argument that cannot be read and is shaped like a bare word (no separator, no extension) is reported as a verb rather than as a missing file, with the nearest verb named: `aontu vett` answers ``aontu: `vett` is not a file, and not a verb this port knows`` and suggests `aontu vet`, at exit 2. A path-shaped argument (`./help`, `help.aontu`, `/tmp/help`) keeps the file diagnosis and its exit 1, which is the same escape hatch the subcommand dispatch uses.
-   **Stdin:** `echo 'a:1 b:$.a' | aontu` reads source from the pipe.
-   **REPL:** `aontu` with no file on a terminal starts an interactive loop; each line is evaluated and printed.
-   Output is pretty-printed JSON by default, or canonical form with `--canon`.
-   **Exact numbers keep their digits.** A document using the `0d` exact literals prints them in full, at any magnitude: `x:0d9007199254740993` prints `9007199254740993`, not a rounded `…992`. The TypeScript CLI gets this from the library’s [`exactJSON`](#exact-numbers-and-exactjson) export, the Go CLI from a `json.Encoder` over the [marshalling types](#exact-numbers-in-go), with HTML escaping **off** in both, so `<`, `>` and `&` stay literal and the two CLIs print the same bytes.
-   Results go to **stdout**; errors go to **stderr** with a non-zero exit status (`1` for an evaluation error, `2` for a bad option).
-   **`--format json` makes the answer an object**, so the default entry point reports like every other verb rather than like a stream of prose: `{aontu, findings, ok, out}` on **stdout**, whether the document evaluated or not. `out` is the text the default form prints (the generated JSON, or the canonical form under `--canon`), empty when it did not evaluate; `findings` then carries one finding with the `code` to hand to [`aontu explain`](#aontu-explain), its `class` from the registry, and the headline as `message`. The finding names no site and carries no hint: the frames under the headline are drawn for a person, and hint prose is deliberately outside cross-port parity. Exit codes are unchanged.

### `aontu vet`

Validate data documents against a schema document. This is the emit → validate → repair loop’s entry point: an agent writes a document, `vet` says what does not hold and where, and the exit code says which kind of “no” it was.

```plaintext
aontu vet [options] <schema> <data> [more-data...]

  --at <path>       Validate against this path of the schema ($.a.b)
  --closed          Refuse keys the anchor does not declare
  --partial         Residue is reported but does not fail the run
  --max-errors <n>  Cap the finding list (default 20)
  --coverage        Report what the check examined
  --strict-coverage --coverage, and exit 1 when the run was vacuous
  --coverage-at <p> Measure coverage under this path of the data only
  --format <f>      text (default), json or sarif
  --watch           Re-run whenever a watched file changes
```

**Exit codes are verdict classes**, not a pass/fail bit, because the three ways to fail call for three different responses:

| Exit | Verdict | Meaning |
| --- | --- | --- |
| 0 | `valid` | the data unifies and is concrete (or `--partial`) |
| 1 | `invalid` | the data does not hold: a contradiction it can never satisfy, or a document that would not parse |
| 1 | `valid` | a VACUOUS run under `--strict-coverage`: the unification held and measured nothing, so the verdict stands and only the status says so (below) |
| 2 | : | usage: a bad option, or a file that cannot be read |
| 3 | `incomplete` | no contradiction, but the truth is not yet satisfied |
| 4 | `error` | the run could not be set up from the schema side: an unusable schema, or an `--at` that names nothing: never the data’s fault |

Each data file is vetted separately, and the worst verdict wins: two data files are two candidates for the same truth, not one merged candidate. `--max-errors` caps the whole report, not each file, and says so with `truncated`.

**A data file that will not parse is the data’s fault**, and is reported as one `parse`\-class finding with a site in that file: not as a broken schema. The distinction matters to the loop the verb exists for: exit 1 says “repair what you emitted”, exit 4 says “the truth you were given is unusable, stop”.

#### What the check examined

A check that examined **nothing** and a check that **passed** answer the same. That is not a bug in the unifier, which answered correctly about the document it was given, but it is a hole in a gate: the caller reads exit 0 and reports success.

The usual cause is one construct. A schema written with the wildcard other tools use is not a wildcard here:

```plaintext
entity: { "*": { table: string } }
```

`"*"` is a key **named** `*`. It declares an entity called `*`, meets no data key, and constrains nothing, so data with `table: 42` vets `valid`. The template is [`&:`](https://aontu.dev/docs/reference-language), which meets every key of the map it sits in.

`--coverage` adds a `coverage` object to the report, and the report is the answer:

| field | is |
| --- | --- |
| `checked` | data **leaves** a schema declaration constrained |
| `leaves` | data leaves in all, under `--coverage-at` when given |
| `declared` | declarations the schema makes under the anchor: a map key, a list index, or a template, at every depth |
| `unchecked` | the shallowest data paths no declaration constrained |
| `unused` | the shallowest declarations no data path met |
| `vacuous` | no data leaf was constrained, over a document that has leaves |

**Leaves, not paths.** A leaf is where a value lives, and matching a container constrains no value: a schema saying only “there is a key called `entity`” has checked nothing, and `checked` is the number that says so. A document with no leaves is not vacuous either, because there was nothing to examine.

The lists name the **shallowest** paths: a subtree nothing constrained is named once rather than once per leaf. The text form prints the first ten of each and counts the rest; the JSON form carries every one.

`--strict-coverage` makes a vacuous run **exit 1**. The verdict word is unchanged (the unification really did hold), so nothing that passes today starts failing, and the reason goes to stderr while stdout stays a report contract. It is the flag a CI gate and an agent loop both want, and it implies `--coverage`, because a gate cannot fire on what was never measured.

Across several data files the schema side is counted once and the data side adds up: a declaration one file exercised is not unused, and `vacuous` means no file constrained anything.

The accounting is **structural**: what the schema declares about the data, rather than a reading of the meet. A meet-based reading would count a value the data supplied to itself as covered, which is the opposite of the question. It costs one extra evaluation of the data document, and only when asked for.

**A parse failure is located.** Its single site carries the parser’s own row and column, 1-based: the same position the human renderer draws its caret under. A document whose second line is `b: ]` reports `row: 2, col: 4`, in both ports.

**A site has an extent, so a finding can be repaired.** Beside `row` and `col` a site carries `len` (the span in UTF-16 code units, the units `col` is already counted in) and `src`, the source text that span covers. Both are `-1` and `""` when unknown, and a consumer must not edit a site that says so.

The extent is not optional detail, because **`value` is the canon and not the source text**. Vetting `port: 0x1F` reports `value: "31"` at column 7, so replacing `(col, value.length)` writes `port: 90001F`. With the span the edit is `(col, len)` (`(7, 4)`) and lands exactly on `0x1F`.

`src` is what makes the span **verifiable**: read the document at `(row, col, len)`, compare it to `src`, and refuse if they differ. That check matters most where the span is correct but partial. A site names the token it points at, exactly as `row` and `col` always have, so a scalar reports its whole literal while a compound reports its opening token: `min(1)` reports `src: "min"`, a map reports `src: "{"`. Seeing `min` where it expected `min(1)`, a consumer refuses rather than replacing the name and orphaning the arguments.

To see the site shape, pin `port` in a one-line `schema.aontu`:

```aontu
port: 8080
```

and vet a `data.aontu` that spells a different port in hex:

```aontu
port: 0x1F
```

```sh
$ aontu vet --format json schema.aontu data.aontu
{
  "aontu": {
    "verb": "vet",
...
      "sites": [
        {
          "col": 7,
          "file": "data.aontu",
          "len": 4,
          "role": "data",
          "row": 1,
          "src": "0x1F",
          "value": "31"
        },
...
$ echo $?
1
```

`value` says `31`, `src` says `0x1F`, and `len` says the span is four code units: the three facts an editing consumer needs, together.

`aontu model why` carries the same pair: each conjunct in the record has the `len` on its site and the contribution’s own `src` beside its `canon`.

**A site names the file whose text it excerpts**, which for a modular document is not the entry file. A constraint written in `lib/types.aontu` and reached through `@"./lib/types.aontu"` is reported at `lib/types.aontu` with that file’s row and column: never at the entry with the included file’s coordinates, which is a real filename against a line it may not have.

The name is the one the CALLER’S OWN spelling reaches: `vet contract.aontu` names `types.aontu`, `vet a/b/contract.aontu` names `a/b/types.aontu`, and an absolute entry keeps absolute includes. So a site can be opened from wherever the command was run, and a report stays repo-relative, which is what a SARIF upload needs. Identity is still the resolved path underneath: two documents loading one library by different relative spellings are one file, not two.

**Every verdict carries its finding, `error` included.** A schema that does not stand up (a contradiction inside it, a document that will not parse, a merge marker) reports what failed and where, while the verdict stays `error`: whose fault it is and what the fault is are two separate answers, and the report gives both. Every site is in the schema (role `schema`), and a contradiction names both of its operands, exactly as one in the data would. A report that said only `error` was the one a repair loop could do nothing with.

**`--at` takes a structural path**: map keys and list indices, the same thing a reference means by `$.a.b`, with an index spelled as a plain decimal integer. A path that names nothing is verdict `error`, carrying the same `no_path` finding `get` and `why` give, including the “did you mean” note when a near key exists.

**Relative `@"file"` loads inside either document** resolve from that document’s own directory, exactly as they do for `aontu <file>`.

**A finding names both sides.** Sites are labelled by provenance (`data` first, because that is the one to edit) rather than by the source-order heuristic a single-document error uses. Write a closed schema as `service.aontu`:

```aontu
service: close({ name:string port: *8080|integer replicas:integer })
```

and a `deploy.json` with one mistyped key and one string where an integer belongs:

```json
{"service": {"name": "checkout", "prot": 8080,
  "replicas": "3"}}
```

```sh
$ aontu vet service.aontu deploy.json
verdict: invalid

$.service.prot: closed [conflict]
  [aontu/closed]: Cannot resolve value at path $.service.prot
  data: deploy.json:1:42 (8080)
$.service.replicas: no_scalar_unify [conflict]
  [aontu/no_scalar_unify]: Cannot unify values at path $.service.replicas
  data: deploy.json:2:15 ("3")
  schema: service.aontu:1:59 (integer)
$ echo $?
1
```

The `closed` finding has no schema site (there is no line that refuses `prot`, only a `close()` that never declared it) and the `no_scalar_unify` finding names both.

`--format json` emits the same report as an object, with an `aontu` stanza naming the producer, so a report read from a pipe says which version and verb made it. Where the constraint algebra knows what would have unified, the finding carries it as `expected`/`actual`, and a `must()` check’s author message rides along as `note`.

**A finding carries the repair beside the diagnosis.** `message` is the headline and stays one line (that is what makes it comparable and greppable) so a finding also carries `hint`: the engine’s own explanation of the failure class, with the offending values filled in. It is the text a human sees under the error frame, and for several codes it is the only place the FIX is written down. A lossy integer literal is the clearest case (abridged):

```json
{ "code": "lossy_integer_literal",
  "message": "[aontu/lossy_integer_literal]: Cannot resolve value at path $.port",
  "hint": "This integer literal, 9007199254740993, is not exactly representable in\nbinary64 ... write it as a `0d`\nliteral to get the exact integer." }
```

The field is absent, not empty, for a code that has no hint text. Hint prose, like `message`, is deliberately outside cross-port parity.

**Colour is a decision about the destination.** Error frames are coloured for a terminal and plain everywhere else: `NO_COLOR` (set, to anything) turns colour off for every caller of the library, the command additionally turns it off when its own stderr is not a terminal, and `--jsonl` turns it off unconditionally: a JSONL answer is machine-read by definition. A piped report therefore never carries terminal control codes into a log, a CI annotation or a parser.

`--format sarif` emits the report as SARIF 2.1.0, the interchange form CI systems ingest (GitHub code scanning upload, PR annotation): a minimal profile: one run, one result per finding, the data site as the primary location, the schema site under `relatedLocations`, and the whole native finding embedded in `properties`, so a SARIF consumer still holds the native contract. Severities map to SARIF levels (`info` → `note`). The renderer is library API in both ports (`sarifReport(report, version)` from `aontu`; `aontu.SarifReport` in Go), and its bytes are held to cross-port parity by the golden in [`test/spec/files/vet-sarif/`](https://github.com/aontu-lang/aontu/blob/main/test/spec/files/vet-sarif/README.md). A ready-made GitHub Action wrapping the verb ships in this repository: [`vet-action/`](https://github.com/aontu-lang/aontu/blob/main/vet-action/README.md).

`--watch` re-runs the whole vet whenever a watched file (the schema or any data file) changes, streaming one report per run: non-incremental by design: parsed trees are single-use, so every run is a full re-parse and re-unify, bounded by the fixpoint’s pass budget. A file that is briefly unreadable mid-save reports and keeps watching.

#### Vetting a recursive schema

A [recursive schema](https://aontu.dev/docs/reference-language#recursive-references-fixpoints) needs nothing extra from `vet`: the definition expands one level per [meet](https://aontu.dev/docs/unification) with concrete data, so the checks descend exactly as far as the data does, and a finding at depth is located there. The vocabulary below is a trimmed version of [use-cases/13-recursive-schema](https://aontu.dev/use-cases/13-recursive-schema). Write it as `chain.aontu`:

```aontu
spec: hide({
  Step: {
    approver: string & re("^[a-z]+@acme[.]example$")
    decision: *pending|pending|approved|rejected
    then?: $.spec.Step
  }
})
```

`--at` anchors the run at the definition (`hide()` keeps `spec` out of generated output but not out of the path) so a data document is a candidate `Step`, not a candidate whole file. A two-level chain in `request.json` holds:

```json
{"approver": "lead@acme.example", "decision": "approved",
 "then": {"approver": "cfo@acme.example"}}
```

```sh
$ aontu vet --at $.spec.Step chain.aontu request.json
verdict: valid
```

A chain whose third level breaks the `approver` pattern, as `request-deep.json` does, is refused **at that depth**:

```json
{"approver": "lead@acme.example", "decision": "approved",
 "then": {"approver": "cfo@acme.example",
          "then": {"approver": "EXTERNAL@other.example"}}}
```

```sh
$ aontu vet --at $.spec.Step chain.aontu request-deep.json
verdict: invalid

$.spec.Step.then.then.approver: constraint [conflict]
  [aontu/constraint]: Cannot unify values at path $.spec.Step.then.then.approver
  expected: re("^[a-z]+@acme[.]example$")
  actual:   "EXTERNAL@other.example"
  data: request-deep.json:3:32 ("EXTERNAL@other.example")
  schema: chain.aontu:3:24 (re("^[a-z]+@acme[.]example$"))
$ echo $?
1
```

The finding’s path is the unrolled position (`$.spec.Step.then.then.approver`), while its schema site is the one `re()` the author wrote: the definition is written once and applies at every depth, and the report says both. For the recipe form see [Define a recursive schema](https://aontu.dev/how-to/define-a-recursive-schema).

### `aontu subsume`

The subsumption query as a command ([docs/reference-language.md, “Subsumption”](https://aontu.dev/docs/reference-language#subsumption)): does the general document admit every instance the specific one admits?

```plaintext
aontu subsume [--profile values|defaults|gen] [--at <path>]
              [--format text|json] <general.aontu> <specific.aontu>
```

The exit code is the verdict class: `0` subsumes, `1` does not subsume (the findings carry the witness: path, codes, both sites), `3` undecided (always with a `sub_*` reason), `4` a document that does not stand up on its own, `2` usage. The report reuses vet’s finding object and renderers, class `compat`.

**An unexpanded recursive position is `undecided`, never guessed.** A [recursive reference](https://aontu.dev/docs/reference-language#recursive-references-fixpoints) expands only against concrete data, and subsumption compares two documents with no data on either side, so at the recursive position there is no rule to apply, and the query says so rather than answering from hope. Write a `general.aontu`:

```aontu
spec: hide({ Step: { label:string then?:$.spec.Step } })
doc: $.spec.Step
```

and a `specific.aontu` whose step recurses into a DIFFERENT definition:

```aontu
spec: hide({ Step: { label:"start" then?:$.spec.Other } Other:label:string })
doc: $.spec.Step
```

```sh
$ aontu subsume general.aontu specific.aontu
verdict: undecided

$.spec.Step.then: sub_unresolved [compat]
  no subsumption rule covers this pair of value formers
  expected: $.spec.Step
  actual:   {"label":string}
  general: general.aontu:1:41 ($.spec.Step)
  specific: specific.aontu:1:63 ({"label":string})
$.doc: sub_unresolved [compat]
  no subsumption rule covers this pair of value formers
  expected: $.spec.Step
  actual:   {"label":"start","then"?:{"label":string}}
  general: general.aontu:2:6 ($.spec.Step)
  specific: specific.aontu:1:20 ({"label":"start","then"?:{"label":string}})
$ echo $?
3
```

**The same recursion on both sides is decided**, and decided by identity: a document that recurses, declares a relation or shares a template by reference admits itself, because two values with the same **hash form** are the same value. The rule runs only where the answer would otherwise be `undecided`, so it narrows nothing else, and without it a contract could not be gated against its own earlier version at all.

This is the verdict [`breaking`](#aontu-breaking) fails on by default: a gate that cannot decide a recursive contract reports `undecided` and stops, and `--allow-undecided` is the deliberate downgrade.

### `aontu breaking`

The evolution gate built on the same query: compare a document against its own earlier versions.

```plaintext
aontu breaking --against <file|git#rev> [--at <path>]
               [--mode backward|forward|full]
               [--allow-undecided] [--format text|json] <file.aontu>
```

-   `--against` takes a file path or `git#<rev>`, and is repeatable. A `git#<rev>` spelling is the old version of the **whole tree**, not of the entry file alone: the revision’s includable sources (`.aontu`, `.aontu`, `.jsonic`, `.json`) are materialised into a temporary directory by shelling out to git (no embedded git) and the old document is evaluated from there, so a change inside an `@"…"`\-included file is part of the comparison. The temporary tree is removed when the run ends. Sources outside the revision (package includes under `node_modules`, the bundled `aontu:system`) resolve as they always do; their versions travel with the lockfile rather than with this comparison. A file the revision does not carry is a usage failure naming it, not a comparison against nothing.
-   `--at <path>` compares that path of **both** versions, the same anchor [`subsume`](#aontu-subsume) takes, and findings are reported from it. A module’s top level carries the things that are _supposed_ to change between releases (the version string, the `aontu_policy` block) so the whole-document comparison answers about those rather than about the contract, and a release that bumps only its version self-breaks the gate. Anchoring at the contract is the fix; splitting the file was the workaround.
-   Modes: **backward** (the default) checks the new document subsumes the old: documents valid under v1 stay valid; **forward** checks the old subsumes the new; **full** checks both.
-   The document can declare its own promise: `aontu_policy: hide({compat: *backward | forward | full | none})`: `breaking` reads `$.aontu_policy.compat` from the new document, and `--mode` overrides it. `none` declares no promise: nothing is checked.
-   Exit codes mirror `subsume`’s: `0` compatible, `1` breaking, `3` undecided, `4` error, `2` usage. Undecided **fails** the gate by default (a gate that shrugs is not a gate) downgradable with `--allow-undecided`.
-   `--allow-deprecated-removal` downgrades a finding about a value the old version already `deprecate()`d to a warning (still reported, no longer failing): deprecate-then-remove is the supported rename path.

### `aontu trim`

Report redundant map entries (entries whose removal leaves the evaluated result unchanged, the spread-implied case included) as paths.

```plaintext
aontu trim --check [--format text|json] <file.aontu>
```

-   The test is **evaluate-and-compare**: for each candidate entry the source is re-parsed, the entry deleted from the parsed tree, and the canon compared to the baseline. This covers everything the fixpoint can see (spread templates, references, duplicate-key merges), and a removal that _errors_ is not redundant: the document does not stand up without that entry.
-   Candidates are map entries at every depth; **list elements are not candidates** (removing one shifts every later index: a different document, not the same one minus a redundancy). A child of a redundant parent is skipped: removing the parent already covers it.
-   `--check` is **required**: trim only reports, and `aontu trim f.aontu` doing something other than trimming silently would be worse than saying so. It is not blocked on the machinery [`set --in-place`](#aontu-model-set) now has (a splice needs no format-preserving editor) but deleting an entry is a different edit from replacing one: a statement’s span does not say which surrounding blank line or trailing comment went with it, and guessing wrong silently rewrites the file’s shape.
-   Exit codes: `0` clean, `1` redundant entries found, `4` the document itself does not evaluate, `2` usage.
-   **A verdict of `error` says why.** A document that does not evaluate has no redundancy to report, but it does have a reason: the report carries `errors`, the engine’s own first failure in the same finding shape [`vet`](#aontu-vet) reports in: code, class, path, sites with file, row, column and extent, and the repair `hint`. The field is present only on that verdict, and the text renderer prints the finding under the verdict line.

### `aontu relations`

Run the [declared-relation](https://aontu.dev/docs/reference-language#declared-relations) checks (acyclicity and inverse consistency) over one finished model.

```plaintext
aontu relations [--format text|json] <file.aontu>
```

The vocabulary is declared once, at the field; the model lists plain names. The files below are a trimmed version of [use-cases/12-relations](https://aontu.dev/use-cases/12-relations). Write the vocabulary as `spec.aontu`:

```aontu
spec: hide({
  Service: {
    kind: service
    dependsOn?: rel($.spec.ServiceShape) & acyclic() & inverse(usedBy)
    usedBy?: rel($.spec.ServiceShape)
  }
  ServiceShape: kind: service
})
```

A model whose edges hold, `system.aontu`, passes:

```aontu
@"./spec.aontu"

services: { &: $.spec.Service }
services: web: dependsOn: [path($.services.billing)]
services: billing: {
  dependsOn: [path($.services.ledger)]
  usedBy: [path($.services.web)]
}
services: ledger: usedBy: [path($.services.billing)]
```

```sh
$ aontu relations system.aontu
verdict: pass
```

A `bad-system.aontu` whose two services depend on each other, with neither inverse written out, fails on every count at once:

```aontu
@"./spec.aontu"

services: { &: $.spec.Service }
services: auth: dependsOn: [path($.services.billing)]
services: billing: dependsOn: [path($.services.auth)]
```

```sh
$ aontu relations bad-system.aontu
verdict: fail

$.services.auth.dependsOn.0  dependsOn: cycle $.services.auth -> $.services.billing -> $.services.auth
$.services.auth.dependsOn.0  dependsOn: $.services.billing does not list $.services.auth under usedBy
$.services.billing.dependsOn.0  dependsOn: $.services.auth does not list $.services.billing under usedBy
$ echo $?
1
```

-   Relations are declared **at the field**, by [`rel(t)` and the graph atoms](https://aontu.dev/docs/reference-language#declared-relations): `acyclic()` and `inverse(name)` register the declaration during unification, and the verb reports the verdict over the finished model’s edge set. There is no reserved `relations:` key: a document that writes one has written ordinary data.
-   **These are not lattice constraints, deliberately.** Both properties are global and non-monotone (one more edge makes an acyclic graph cyclic) so they are facts about a finished model rather than something unification may hold. Generation enforces the same verdict (a located `relation_cycle` / `relation_inverse_missing` at the offending edge); the verb reports it without generating. The [language reference](https://aontu.dev/docs/reference-language#declared-relations) states the rule; the [explanation](https://aontu.dev/docs/explanation#why-there-is-a-verb-surface) argues it.
-   A finding carries `at` (the position of the offending edge), `code` (`relation_cycle` or `relation_inverse_missing`), `relation`, and `detail`: for a cycle, the node paths it runs through in order; for a missing inverse, `[from, to, inverseName]`. Findings are **sorted by `at`**, so the report diffs cleanly.
-   **The endpoint type is `rel(t)`’s flow**: declared once on the field, it flows into each far end at the site, so a conflict or a hole is an ordinary located evaluation error rather than a report row, and a document with a wrong-typed far end answers `verdict: error` here, because it does not stand up at all. (The old `target:` declaration and its `relation_target_unmet` finding are retired with the `relations:` key.)
-   `--format json` wraps the same findings with the `aontu` producer block (`verb`, `version`) that every machine-readable report carries.
-   Exit codes: `0` `pass`, `1` `fail`, `4` `error` (the document does not evaluate), `2` usage. Note these are the verb’s own three verdicts, not [`vet`](#aontu-vet)’s five classes: there is no schema on the other side of this question, so `incomplete` has nothing to mean.
-   **A verdict of `error` says why.** A document that does not stand up has no graph, so it has no relation findings, but the report carries `errors`, the engine’s own first failure in the same finding shape [`vet`](#aontu-vet) reports in. `findings` stays the graph’s own vocabulary; the two lists answer two different questions, and the `errors` field is present only on the `error` verdict.
-   The library form is `relationCheck(src)` in TypeScript and `Aontu.RelationCheck(src)` in Go, returning the identical `{verdict, findings}` record (plus `errors` on a failed run); the derived graph the checks run over is `result.graph` / `Aontu.Graph`, described under [the TypeScript API](#class-aontu).

### `aontu reaches`

Ask whether one node **reaches** another over the link graph, at any remove. Endpoints are `$.dotted` node paths.

```plaintext
aontu reaches <from> <to> [--relation <name>] [--format text|json] <file>
```

[`relations`](#aontu-relations) asks about the edge set as a whole. This asks the question that needs the **closure**: does anything `from` links to, at any remove, end up at `to`? That is the shape of every blast-radius question an operator asks (“if the billing database goes, what falls over?”) and every containment question a policy asks (“nothing in the public tier may reach the ledger”), and neither can be put one edge at a time. Ask it of the `system.aontu` model above:

```sh
$ aontu reaches $.services.web $.services.ledger system.aontu
verdict: reaches

$.services.web -> $.services.billing -> $.services.ledger
$ echo $?
0
```

-   **The path is the answer**, not decoration: “yes” is worth little to an operator asking what a failure would take out, and the chain is what they act on. It is a **shortest** path, and among shortest ones the first in code-point order, so it is the same path in both ports. A `no` carries none: there is no evidence for a negative answer.
-   **Transitive, not reflexive-transitive.** `reaches a a` is true only when a path of one or more edges returns to `a`, which says the graph has a cycle through `a` rather than saying nothing.
-   `--relation <name>` follows only edges under that relation: the difference between “can this reach that at all” and “can it reach it _this way_”.
-   A link into part of a node (`$.services.auth.ports.http`) reaches **that node**, not the one above it: reachability is between tree positions, and there is no declared boundary to widen it to. Same rule [`relations`](#aontu-relations) uses, and it has to be, or the two verbs would disagree about what an edge connects.
-   Exit codes: `0` `reaches`, `1` `unreachable`, `4` `error`, `2` usage. An unreachable pair is a **failed check**, not an error: the question was answered, and the answer was no.
-   **An endpoint that names no node is a refusal**, reported as `refer_unresolved` with the linked nodes listed: answering `no` would report a typo as a fact about the model. An endpoint that exists but has no edges is a perfectly good question with the answer `unreachable`.
-   Like acyclicity, this is a verb and **not a lattice constraint**: reachability is global and non-monotone, so a citizen asserting _non_\-reachability could be true and then false as one more edge arrives.
-   The library form is `reachCheck(src, from, to, options?)` in TypeScript and `Aontu.Reach(src, from, to, options)` in Go, returning the identical `{verdict, path?}` record (plus `errors` on a failed run).

In `system.aontu` the `usedBy` inverses run the other way, so the ledger reaches the web service in general but not along `dependsOn`:

```sh
$ aontu reaches $.services.ledger $.services.web system.aontu
verdict: reaches

$.services.ledger -> $.services.billing -> $.services.web
$ aontu reaches $.services.ledger $.services.web --relation dependsOn system.aontu
verdict: unreachable

$.services.ledger does not reach $.services.web
$ echo $?
1
```

And an endpoint that names no node refuses rather than answering:

```sh
$ aontu reaches $.services.web $.services.ledgr system.aontu
verdict: error

$: refer_unresolved [reference]
  $.services.ledgr names no node in this document.
  note: nodes with links: $.services.billing, $.services.ledger, $.services.web
$ echo $?
4
```

### `aontu view`

Draw a **figure** of a finished model, as deterministic text a golden diff can check: no coordinate is computed, nodes and edges are sorted by code point, and both ports emit the same bytes.

```plaintext
aontu view <kind> [--as <profile>] [--at <path>] [--out <file> [--check]]
           [--strict] [--max-rows <n>] [--depth <n>] [--style <s>]
           [--format text|json] [options] <file>...
```

Ten kinds. Eight read a report the engine already produces; `doc` and `lattice` read the document itself, which is the one thing no report holds:

| kind | draws | reads | profiles |
| --- | --- | --- | --- |
| `doc` | the document’s own key tree, to a depth | the anchor walk, as `get --keys --types` reads it | `text`, `svg` |
| `lattice` | the language’s value lattice, with a count at every node the document reaches | the anchor walk, and each value’s own kind | `text`, `svg` |
| `tree` | the dependency tree of a relation: roots derived, repeats elided, cycles marked | the edge set | `text`, `svg` |
| `matrix` | the dependency-structure matrix over one relation, in `canon` or `partition` order, with `--closure` | the edge set and the relation declarations | `text`, `svg` |
| `graph` | the node-link drawing, grouped and labelled by fields of the nodes | the edge set, the declarations, the node values | `mermaid`, `dot`, `er` |
| `layer` | the architecture layers: one band per value of `--group-by`, upward edges named | the edge set and the node values | `text`, `mermaid`, `svg` |
| `sets` | the set-intersection panel over a family of sets (UpSet) | the generated value | `text`, `svg` |
| `layers` | which document contributed which path | the provenance record | `text`, `svg` |
| `ladder` | the meet ladder at one path: every contribution as a rung, in rank order | the [`why`](#aontu-model-why) record | `mermaid`, `dot` |
| `poset` | the subsumption order over several documents | [`subsume`](#aontu-subsume), pairwise | `mermaid`, `dot` |

The first profile listed is the kind’s default; asking for another is a refusal (`view_profile_unknown`), because there is no text form of a node-link drawing and no Mermaid form of a matrix.

`--as svg` draws the cell-based kinds (every kind whose text form is a grid of character cells) as a standalone SVG with the same geometry: 8 units per character and 20 per line, every coordinate a whole number, so no font is measured and both ports emit the same bytes. The figure carries its own style block (dropped by `--style none`, below); a host page sets the colours through CSS variables (`--av-ink`, `--av-muted`, `--av-bg`, `--av-rule`, `--av-rule-faint`, `--av-closure`, `--av-warn`, `--av-alert`, `--av-bar`), and the defaults stand where it sets none. The matrix fills a direct cell, tints a closure cell, marks an unmirrored edge, and rules the diagonal; the layers draw each upward edge as a dashed arrow; the panel draws its bars and dots. The node-link kinds stay Mermaid and DOT, whose renderers lay them out.

Draw the `system.aontu` model above over its `dependsOn` relation, first as the tree, then as the matrix in partition order:

```sh
$ aontu view tree --relation dependsOn system.aontu
web
└── billing
    └── ledger
```

```sh
$ aontu view matrix --relation dependsOn --order partition --closure system.aontu
          1 2 3
ledger  1 \ . .
billing 2 X \ .
web     3 + X \
# above-diagonal direct cells: 0
```

A cell at (row, column) is set when the row depends on the column: `X` a direct edge, `!` a direct edge whose declared inverse is not written back, `+` reachable only transitively, `.` absent, `\` the diagonal. In partition order an acyclic relation is a lower triangle, and the footer’s count of cells above the diagonal is the acyclicity proof in the picture’s own shape; a cycle survives every ordering as an above-diagonal cell, and is reported as `cycle_block`.

The same edges as a node-link drawing, in Mermaid:

```sh
$ aontu view graph --relation dependsOn system.aontu
flowchart LR
  n_billing["billing"]
  n_ledger["ledger"]
  n_web["web"]
  n_billing -->|"dependsOn"| n_ledger
  n_web -->|"dependsOn"| n_billing
```

The `lattice` kind draws the LANGUAGE rather than the model. The shape is always the same ([the value lattice](https://aontu.dev/docs/reference-language#the-value-lattice), whatever the document holds) and what the document adds is a count at each node its own values landed on. Write `ports.aontu`:

```aontu
host: "0.0.0.0"
port: 8080
tls: true
timeout: 1.5
retries: integer
backoff: integer & min(1)
```

```sh
$ aontu view lattice ports.aontu
                                           top
                                            │
      ┌────────────────────────────────┬────┴───────────────────────────┬─────────┐
 string (1)                         number                         boolean (1)  null
      │                                │                                │         │
      ├─────────────┬────────────┬─────┴─────┬────────────┐             │         │
   path()      integer (2)   float (1)  biginteger   bigdecimal         │         │
      │             │            │           │            │             │         │
      └─────────────┴────────────┴──────────┬┴────────────┴─────────────┴─────────┘
                                           nil
```

A concrete scalar sits at its kind (`8080` counts at `integer`) and a kind written as a schema sits _at_ that node, which is why `retries: integer` is the second of the two. `backoff` is at neither: `integer & min(1)` is a region of the lattice rather than a point, so the figure declines to draw it anywhere and names it instead:

```plaintext
lattice_unplaced  1  $.backoff
```

An unresolved disjunction is unplaced for the same reason: `*8080 | 9090` is two places at once, and a node that claimed either would be claiming something the document does not say.

-   **The loss report.** Every run prints, on stderr, what the figure could not draw or drew differently from the model, one line per code with a count: `hidden_contribution` (an edge inside a `hide()` subtree, not drawn, because a committed figure discloses what it draws), `edges_in_disjunct` (a link under an unresolved disjunction, which is not a fact, so the figure reports it rather than picking an arm), `unresolved_field` (a node without a value for `--group-by` or `--label`), `lattice_unplaced` (a value at no single point of the lattice, named rather than drawn), `cycle_block`, `cols_elided`, and for the poset `order_undecided`, `order_maybe_equal` and `order_intransitive`. Any of these makes the verdict `lossy`, which `--strict` turns into exit `1`. Three codes are informational and leave the verdict `rendered`: `edges_deduped` (a model declaring each entity at two positions writes each edge twice), `inverse_suppressed` (a declared mirror, implied by the edge drawn) and `crossings` (a property of the emitted order).

-   **`--style <s>`** says how a figure carries the MEANING of its marks. Every mark has a reason the extractor established (a direct cell, a closure cell, an unmirrored edge, an upward edge, a repeated subtree) and each profile has one way to show it: SGR escapes for `text`, CSS classes for `svg`. `auto`, the default, picks that mechanism where the destination can carry it: escapes only when **stdout** is a terminal and `NO_COLOR` is unset (stdout, because that is where the figure goes: the error frames’ own colour decision is about stderr), and an SVG keeps the stylesheet that makes it standalone. `none` drops both: on `svg` the classes stay (they are structure, not style) and only the embedded stylesheet goes, which is what a host page wants once it has bound `--av-ink` and its kin and is embedding several figures. `ansi` and `css` name a mechanism outright, and asking for one on a profile that cannot carry it is a usage error (`view_style_profile`) rather than a silent no-op. Escapes are never written to a file: `--out` with `--style ansi` is refused, and `auto` resolves to no escapes there.

    Neither mechanism states a colour, and a figure still cannot name one: SGR 31 means the colour the reader’s terminal calls red, and a CSS class states nothing at all. A hex triple in a figure stays refused, and so does `style` in a view document: a declaration says which projection, never how it looks.

-   **`--out <file>` and `--check`.** The figure is written to the file instead of stdout; with `--check` nothing is written and the exit is `1` when the file differs from what would be drawn, which is the CI gate for a committed figure.

-   **`--max-rows <n>`** (default 60) is a refusal, exit `2`, not a truncation; the message names the narrowing options.

-   `doc`: the shape of the model, before any of its values mean anything. Every other kind here needs the document to HAVE something (links, contributions, peers) and draws nothing from one that does not; this draws what is in the document and how it is arranged, which is what a reader meeting a model wants first. `--at` names the subtree (default `$`) and `--depth <n>` how many levels of key below it (default 3). Map keys are in code-point order and list indices in order, exactly as `get --keys` lists them. A leaf carries its canon, cut at 32 characters: the kind of thing it is, not its value. A container the depth bound stops at carries the number of keys not drawn, and they are counted into the loss report as `depth_elided`: a tree that stopped without saying so would be the one thing a structural drawing must not be.

-   **`--at <path>`** restricts the edge-derived kinds to nodes under the path, names the subtree `doc` draws, the provenance panel to paths under it, and names the path the ladder draws (required) and where the poset compares.

-   `tree`: `--relation` draws one relation; without it every relation is drawn, each branch naming its own. `--root <path>` (repeatable) draws one subtree; a root that is not a node of the drawn graph is refused (`refer_unresolved`). Roots are derived as the nodes nothing depends on; a shared subtree is expanded once and marked `(*)` after; a closing edge is `(cycle)`; labels are the shortest path suffix unique in the drawing.

-   `matrix`: `--relation` is required unless exactly one relation has edges (`view_relation_ambiguous` otherwise); `--order canon` (label order, the default) or `partition`; `--closure` marks the transitively reachable cells. Ten or more rows stack the index digits in the header.

-   `graph`: `--relation` (repeatable) keeps only those predicates; a declared inverse’s mirror is suppressed and counted. `--group-by <field>` puts each node in a subgraph named by that field’s value (ids `g0`, `g1` and so on, in label order); `--label <field>` labels the node with it, a number or boolean as its canon. Node ids encode the label injectively: `n_` + the name when it is an ASCII identifier, else `nq_` + the name with every other code point as `_` and its hex (`cust-1` is `nq_cust_2d1`, so a name such as `end` or `graph` can never collide with a keyword). Text is escaped per code point: Mermaid as numeric entities (`#34;` for `"`, `#124;` for `|`), DOT as `\"` and `\\`. A label holding a line terminator is refused (`view_line_break`). `--as er` draws Mermaid’s `erDiagram`, every relationship many-to-many because the model states no cardinality.

-   `layer`: `--group-by <field>` (required, `view_group_required`) names each node’s layer; bands are stacked in the partition order of the layer-level graph, reversed, so the layer nothing depends on is on top. `--layers a,b,c` fixes the order (top first) for a model whose upward edge makes the layer graph cyclic. The footer counts the relation’s downward, sideways and upward edges, and names each edge `--edges` shows. `--edges upward|all|none` chooses which of them the figure draws over the bands: `upward` is the violations, and the default for the fixed grids (`text`, `svg`), because the bands already say which way the rest go; `all` draws the relation itself, which is what a reader tracing one module’s dependencies wants, and is `mermaid`’s default since it lays edges out itself; `none` leaves the bands alone. In SVG an upward edge is dashed and alert-coloured, a downward one runs from the bottom of its box to the top of the one it names, and a sideways one dips below the boxes of its band.

-   `sets`: `--sets <path>` names a map whose keys are the sets, `--member <key>` the field holding each set’s members (a list of strings), `--universe <path>` a map or list of every element, so the covered-by-nothing column exists. A member written as an address (`path($.perms.read)`) meets a universe map’s key on that address and is shown by its shortest unique suffix. Columns are the exact membership signatures, by degree, then cardinality, then name; `--min-degree <n>` drops the low ones and `--max-cols <n>` elides the rest (`cols_elided`). Both need the document to generate.

-   `layers`: the same panel over the provenance record, sets being the documents and elements the paths each document wrote into. Files are shown relative to the entry document; `--min-size <n>` drops the small intersections.

-   `ladder`: the `why` record at `--at`, one rung per contribution, sorted by rank descending (weakest first, so the winner is the last rung before the value), then by site.

-   `poset`: several files; each pair is compared with `subsume` at `--at` under `--profile values|defaults|gen` (default `defaults`). Documents that subsume each other are one node, labelled `a = b`; an edge is a cover of the transitive closure, upward toward the more general document; an undecided pair with no proven order is a dashed edge labelled with the `sub_*` reason. Labels are the filenames without `.aontu`.

-   Exit codes: `0` rendered or lossy, `1` a `--check` mismatch or lossy under `--strict`, `2` usage (an unknown kind or profile, a missing required option, `--max-rows` exceeded), `4` error (a document that does not stand up, a relation, root or path that names nothing). A document with nothing to draw renders an empty figure and exits `0`. In text form the figure is all that stdout carries, so a redirect is a golden file; `--format json` wraps the whole report as `{kind, verdict, text, loss}` under the usual `aontu` envelope, and a refusal carries `errors` in place of `text`.

-   The library form is `view(src, options)` in TypeScript (`viewTree` remains for the tree) and `Aontu.View(src, options)` in Go, returning the identical `{verdict, kind, text?, loss, errors?}` record; the poset’s further documents ride `options.docs` as `{src, path?, name?}`.

**The view document.** A projection that runs in CI belongs in a file. `--views <path>` names a map, in an ordinary document that includes the model, whose values declare figures: one evaluation, N figures, one exit code. The keys of a declaration are the view options (the flags without the dashes) and every declaration names its `kind` and the `out` file it draws into. `views` is the author’s key; nothing in the engine knows the name, which is why the path is given. Write a `views.aontu` beside the `system.aontu` above:

```aontu
@"./system.aontu"

views: arch: {
  kind: matrix
  relation: dependsOn
  order: partition
  closure: true
  out: "arch.dsm.txt"
}
views: map: {
  kind: graph
  relation: dependsOn
  groupBy: owner
  as: mermaid
  out: "arch.mmd"
}
```

```sh
$ aontu view --views '$.views' views.aontu
$ aontu view --views '$.views' --check views.aontu
$ echo $?
0
```

Each `out` is resolved against the **view document’s own directory**, so the gate passes from any working directory. Nothing is written unless every figure rendered: N figures of one model are only meaningful together, so a set whose third figure refuses leaves the first two off disk, and its exit code is the worst of the figures’. `--check` compares the whole set and names every difference; `--strict` turns any figure’s loss into exit `1`. A declaration that names an option that is not one, gives a value of the wrong shape, or leaves out `kind` or `out` is `view_document_shape`, reported for every faulty declaration at once and before anything is drawn. The `poset` is refused there: it compares several documents and a view document declares figures of the one it includes. The library form is `viewSet(src, options)` in TypeScript and `Aontu.ViewSet(src, options)` in Go, returning `{verdict, views, errors?}` where each view is `{name, kind, out, verdict, text?, loss, errors?}`: the caller writes the files.

`@"aontu:view"` is the bundled schema for a declaration, so the same mistakes are refused when the document is EVALUATED rather than when the verb reads it. Write a `views-typed.aontu`:

```aontu
@"aontu:view"
@"./system.aontu"

views: { &: $.aontu.View.Figure } & {
  arch: {
    kind: matrix
    relation: dependsOn
    order: partition
    closure: true
    out: "arch.dsm.txt"
  }
}
```

```sh
$ aontu view --views '$.views' --check views-typed.aontu
$ echo $?
0
```

`$.aontu.View.Figure` types every option, and a kind that is not a kind, an order that is not an order or a count below zero is an ordinary unification failure naming `aontu:view` as the other operand. It is optional: a view document that does not include it is read exactly the same way, and refused by `view_document_shape` instead. The source is served from the engine, as `aontu:system` is, so it needs no filesystem and resolves under every include capability but `'none'`.

The use cases pin one figure of each kind as a golden: [01-service-catalog](https://aontu.dev/use-cases/01-service-catalog) the graph and the matrix, [04-schema-evolution](https://aontu.dev/use-cases/04-schema-evolution) the poset, [08-feature-flags](https://aontu.dev/use-cases/08-feature-flags) the ladder, [12-relations](https://aontu.dev/use-cases/12-relations) the graph and the ER diagram, and [16-module-deps](https://aontu.dev/use-cases/16-module-deps) the tree, the matrix and the layers; 16 also declares all seven of its figures in a `views.aontu` that its `check.sh` gates in one run.

### `aontu jsonschema`

Export a document as a **JSON Schema** (draft 2020-12), and say what could not be carried.

```plaintext
aontu jsonschema [--at <path>] [--strict] [--format text|json] <file>
```

This is the interop bridge. Every major LLM provider’s structured-output API constrains generation to JSON Schema and to nothing else, so the shape an enterprise actually deploys is: export the model, let the provider generate under it, then [`vet`](#aontu-vet) the result against the model itself: the schema narrows what is _produced_, the model decides what is _true_. An MCP tool’s `inputSchema`, which the protocol requires to be JSON Schema, is the same export.

**The schema goes to stdout and the losses to stderr**, so `aontu jsonschema x.aontu > schema.json` writes a usable schema and still tells the reader what it left behind. Write a `contract.aontu`:

```aontu
spec: name: string & re("^[a-z][a-z0-9-]{2,39}$")
spec: tier: *internal|standard|critical
```

```sh
$ aontu jsonschema --at spec contract.aontu
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "name": {
      "pattern": "^[a-z][a-z0-9-]{2,39}$",
      "type": "string"
    },
    "tier": {
      "default": "internal",
      "enum": [
        "internal",
        "standard",
        "critical"
      ]
    }
  },
  "required": [
    "name",
    "tier"
  ],
  "type": "object"
}
```

-   It exports the **unified** value, not the parse: what a document MEANS is what a consumer should be constrained to.
-   `--at <path>` names the subtree to export: the same anchor [`vet --at`](#aontu-vet) takes, so `--at spec` means the same thing in both.
-   `--format json` prints the whole report (`schema`, `lossy`, `verdict`) under the usual `aontu: {version, verb}` envelope.
-   Exit codes: `0` exported, `1` lossy **under `--strict`**, `2` usage, `4` the document does not stand up on its own. Without `--strict` a lossy export is still an export and exits 0.

**What crosses exactly.** Kinds become `type`; a concrete scalar becomes `const`; a disjunction of scalars becomes `enum`, and its preference becomes `default`; bounds become `minimum`/`maximum`, with the open endpoints as 2020-12’s `exclusiveMinimum`/`exclusiveMaximum`; `re` becomes `pattern` (aontu’s portable subset is a subset of ECMA-262, which is what JSON Schema reads, so no translation happens); `neq` becomes `not: {enum: …}`; `length` becomes `minLength`/`maxLength` on a string and `minItems`/`maxItems` otherwise; `unique()` becomes `uniqueItems`; an optional key is simply absent from `required`. A spread is `additionalProperties: <template>`, which is what a spread means. A written list is a **tuple**, so `prefixItems` plus `items: false`.

**And `close()` is `additionalProperties: false`**: the one thing the two languages say identically, and the reason the export is worth having at all: the closedness an agent’s output must respect crosses without loss.

**What does not cross is REPORTED, never dropped in silence.** A converter that silently lost a constraint would hand its caller a schema that admits _more_ than the model does, which is the failure this language exists to refuse. So each loss carries its path, the aontu construct’s own name, and one sentence saying what the schema says instead:

```plaintext
lossy: $.spec.total must: an evaluate-only check is opaque by
  construction … so it is DROPPED and the schema admits values `vet`
  refuses
```

The losses, and why each is one:

| Construct | Why JSON Schema cannot say it |
| --- | --- |
| `must(c, m)` | Band B is opaque by construction: it carries the author’s own message and the algebra never reasons about it |
| `unique(k)` | there is no uniqueness-by-property keyword; `uniqueItems` compares whole items |
| `biginteger`, `bigdecimal`, and exact literals | JSON has one number type and it is binary64, so the exactness these leaves exist for has no receiver |
| `hide(x)` | a hidden entry is not generated, so it is not part of the value a consumer produces |
| `&:` on a closed map | the template constrains keys that cannot exist |
| a `length` with no domain | no keyword counts a string _or_ a container, so it is exported as `minItems`/`maxItems` |
| residue: an unresolved reference, a waiting call | not a property constraint at all; guessing one would be inventing a promise |

The exact-leaf loss is the one with a way around it. Money carried as a **decimal string** with a conversion mark exports without loss (the pattern and the mark both cross) and stays exact on the aontu side: see [Carry exact money over JSON](https://aontu.dev/how-to/carry-exact-money-over-json).

**A recursive position is residue, and exports as residue.** JSON Schema can spell recursion (`$defs` plus `$ref`), but this exporter does not mint it: a [recursive reference](https://aontu.dev/docs/reference-language#recursive-references-fixpoints) that has met no data is unresolved, so it crosses as the empty schema `{}` (a position that admits _anything_) and is reported under `lossy` as `unresolved`, like any other residue. Two consequences follow. Anchor the export at a definition kept **un-hidden**, because a `hide()` mark propagates and a hidden entry is omitted from the export entirely; and treat the exported schema as wider than the model at the recursive position: [`vet`](#aontu-vet) the produced value against the model, which does check every depth. `--strict` turns the loss into exit 1. Write a recursive `steps.aontu`:

```aontu
Step: { approver:string & re("^[a-z]+@acme[.]example$") then?:$.Step }
```

```sh
$ aontu jsonschema --strict --at Step steps.aontu
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "approver": {
      "pattern": "^[a-z]+@acme[.]example$",
      "type": "string"
    },
    "then": {}
  },
  "required": [
    "approver"
  ],
  "type": "object"
}
lossy: $.Step.then unresolved: this is not a value yet, so there is nothing to constrain a consumer to; the schema admits anything here
$ echo $?
1
```

Everything above `then` crosses intact; the tail is the gap it reports. Without `--strict` the same export exits 0.

-   The library form is `jsonSchema(src, options?)` in TypeScript and `Aontu.JSONSchema(src, at)` in Go, returning the identical `{verdict, schema, lossy}` record (plus `errors` on a failed run).

### `aontu model get`

Select one node of an evaluated document by path and render it: the task-sized slice, instead of the whole file as one JSON blob.

```plaintext
aontu model get <path> [-c|--canon] [--keys] [--types] [--depth <n>]
          [--format text|json] <file.aontu>
```

-   **Evaluation is global.** Unification has no partial mode: the whole document is evaluated and then one node is selected. What `get` buys is the size of the _answer_, not the cost of producing it.
-   The path is what a reference means by `$.a.b`: map keys and canonical-decimal list indices, and nothing else, so `$.a.01` names nothing here exactly as it names nothing there. A key that _contains_ a dot is likewise unreachable, as it is to a reference.
-   Default output is the fragment’s generated JSON; `--canon` is its canonical form, and for the root path that is byte-identical to `aontu --canon`.
-   Exit codes: `0` rendered, `1` the path names nothing (the finding carries a nearest-key suggestion), `2` usage, `4` the document does not stand up on its own, including a node that is not concrete, for which there is no JSON to print.

Write an `app.aontu` whose spread template supplies defaults:

```aontu
services: { &: { replicas: *1|integer port: *8080|integer } }
services: auth: replicas: 3
services: billing: {}
```

```sh
$ aontu model get $.services.auth app.aontu
{
  "port": 8080,
  "replicas": 3
}
$ aontu model get --keys $.services app.aontu
auth
billing
$ aontu model get $.services.auht app.aontu
$.services.auht: no_path [reference]
  The path $.services.auht names nothing in this document.
  note: did you mean auth?
$ echo $?
1
```

The mistyped path is exit 1 with a suggestion, not an empty render: a missing key and an empty value are different answers.

**The projections are lattice abstractions.** Each view is a valid aontu document that _subsumes the truth_: generalisation, never distortion:

| flag | view |
| --- | --- |
| `--types` | every concrete leaf lifted to its own kind: `{"replicas":3}` becomes `{"replicas":integer}` |
| `--depth n` | structure to depth n; every elided subtree renders as `top`: “no further information at this tier” |
| `--keys` | the node’s own key names (or list indices), one per line |

On `app.aontu` the shape view erases the concrete leaves:

```sh
$ aontu model get --types $.services.auth app.aontu
{"port":*integer|integer,"replicas":integer}
```

That claim is checked rather than asserted: every projection row of `test/spec/query.tsv` runs [`subsume`](#aontu-subsume)`(view, truth)` in both implementations and requires `subsumes`. It runs under the **values** profile, deliberately: a shape view _erases defaults_ (`*8080|integer` becomes `*integer|integer`, as above), which the `defaults` profile would rightly call a compatibility break. The claim projections make is about the values admitted, not about which one is generated.

Kinds are lifted through the lattice’s own `superior()`, so the view follows the type system rather than a table of the renderer’s opinions; a value that is _already_ an abstraction (a kind marker, a constraint, an unresolved reference) is left alone rather than generalised twice. Projections are not canonical form and are never fed to [`aontu hash`](#aontu-hash): the flags are distinct from `--canon` to keep that unambiguous.

### `aontu model why`

Provenance: what _contributed_ to the value at a path, in order, with the site each contribution was written at. The positive twin of [`vet`](#aontu-vet)’s report: errors explain what failed to unify, `why` explains what did.

```plaintext
aontu model why <path> [--format text|json] <file.aontu>
```

Ask it about the `app.aontu` above:

```sh
$ aontu model why $.services.auth.replicas app.aontu
$.services.auth.replicas = 3
  1. *1|integer  app.aontu:1:28  (spread)
  2. 3  app.aontu:2:27
```

-   A **contribution** is a value the author _wrote_ that met something at this path. Values the engine mints on the way (a kind lifted from a leaf while a disjunction trials its members, a fold’s intermediate) are not contributions, and neither are the members _inside_ one written value, which meet at the same path as that value resolves. A **conjunct** is the exception in the other direction: `a & b`, or the merge of two duplicate keys, is the statement that several separately written values must all hold, so it expands into one contribution each.
-   **Roles**: `literal`, `spread` (a template applied to this key), `ref` (the reference itself, whose canon names its target) and `pref`. A preference _inside_ a spread template reports as `spread`, which is the thing the author needs to be told.
-   Contributions are listed in **source order** (file, then row, then column) not in the order the fixpoint happened to meet them, which is an engine detail.
-   **Provenance travels with a clone.** A value that reached this path by being copied from somewhere else (a spread template applied per key, a `pack()` generator’s child, a `$ref`, a `refer(t)` flow) is reported as the value the author wrote, at the line they wrote it on. That is the whole audit question: _which file set this?_ A clone of a written value is that written value somewhere else, so it is named; a value the engine mints on the way is not.
-   **The value that stands at a path is a contribution when nothing met there.** A generator places a value without meeting anything, and a path with no meets still has a source.
-   A value the author never wrote and no template supplied has **no contributions**, and says so. That is a fact about the document, not a failure.
-   `--format json` emits the record: `{path, value, conjuncts: [{canon, role, site}]}`, with sites in the same shape the vet report uses. Exit codes mirror `get`’s: `0` explained, `1` the path names nothing, `2` usage, `4` the document does not stand up.
-   **Cost**: the recorder rides the context and is off by default: uninstrumented evaluation pays one property load per meet. An instrumented run pays site materialisation, one map entry per path met, and the spread walk that marks a template’s application.

### `aontu model set`

Change a document by **appending to an overlay**: or, with `--in-place`, by rewriting the literal inside that same overlay. `--overlay` is required either way, and the entry document is never written.

```plaintext
aontu model set <path>=<value>... --entry <file> --overlay <file>
         [--in-place] [--dry-run] [--format text|json]
```

Write an `entry.aontu` that constrains `owner` and pins `replicas`:

```aontu
services: auth: { owner:string replicas:3 }
```

```sh
$ aontu model set '$.services.auth.owner="identity-2"' --entry entry.aontu --overlay changes.aontu
verdict: valid
wrote: changes.aontu
```

A pinned value refuses the append and the overlay is left unchanged:

```sh
$ aontu model set '$.services.auth.replicas=5' --entry entry.aontu --overlay changes.aontu
verdict: invalid

$.services.auth.replicas: scalar_value [conflict]
  [aontu/scalar_value]: Cannot unify values at path $.services.auth.replicas
  data: changes.aontu:2:33 (5)
  schema: entry.aontu:1:41 (3)
$ echo $?
1
```

A literal the overlay itself pinned is `--in-place`’s case: the span is rewritten where it was written, and the report says so as source text:

```sh
$ aontu model set '$.services.auth.owner="identity-3"' --entry entry.aontu --overlay changes.aontu --in-place
verdict: valid
replaced: changes.aontu:1:30 "identity-2" -> "identity-3"
wrote: changes.aontu
```

-   The assignment becomes a **path-flattened conjunct**: `$.a.b=1` is appended as `"a": "b": 1`, keys quoted so a segment may be a word the grammar spells otherwise, a number, or hold a space. The text is split at the _first_ `=`; everything after it is aontu source, so a value may contain one.
-   This needs no rewriter, and damages nothing: an overlay entry is just another conjunct, and unification is order-independent, so appending to a second file is the same value as writing into the first. The shared suite asserts that equivalence for every row rather than claiming it.
-   **Appending cannot change a pinned value.** The lattice refuses `5` against `3`, the verdict is `invalid`, and the finding names the pinning site, which [`aontu model why`](#aontu-model-why) then explains. `--in-place` closes that loop.
-   **A path reached through a reference is refused** (`patch_not_editable`). `n: $.base` against `base: 7` is pinned by `base`’s line, not by `n`’s: splicing there would rewrite the referent for every reader of it and leave the named path where it was. The assignment is appended instead, exactly as it would be without the flag.
-   **`--in-place` rewrites the literal where the author wrote it.** The span at `(row, col, len)` is replaced and nothing else is touched, so comments and layout survive, including a comment on the edited line. Nothing is re-serialised, which is why no CST is needed: a targeted splice never reads the bytes it does not replace.
-   **The span is verified before a byte is written.** A site carries `src`, the source text it claims to cover, and the text at the span must equal it. That is what makes `port: 0x1F` safe to rewrite even though its value is `31`: the span is four code units and says so.
-   **An overlay that loads another document is refused outright.** A literal reached through `@"..."` cannot be told apart from the overlay’s own by position (an include holding `a: 42` at 1:4 and an overlay holding `x: 42` at 1:4 give the same site and the same text) so the evaluation that decides what to edit denies loads, and what resolves is what the overlay says by itself.
-   **It rewrites only a single editable literal, and appends otherwise.** The contribution must be one `literal`\-role conjunct in _this_ overlay whose `src`, parsed alone, means the contribution’s own canon, which refuses a compound, because a site names a compound’s _opening token_ (`min` for `min(1)`, `1` for `1+2`, `{` for a map). It must also be concrete: `a: integer` is a constraint, not a pin, and appending narrows it without discarding what it says. Anything else appends exactly as plain `set` would, plus one **warning** naming the case: `patch_not_editable`, `patch_ambiguous` or `patch_span_mismatch`. Warnings never move a verdict, so `--in-place` cannot turn a run that would have held into one that does not.
-   A default (`a: *1`) is left alone with no warning: appending already overrides a default correctly.
-   **The text form says `would replace:` when nothing was written**, and `replaced:` only when the file changed: a run can have one replaceable assignment and another that refuses it as a whole. A run that HOLDS writes its status to stdout whether or not it carries warnings; the warnings go to stderr beside it.
-   The report gains `replaced`, one entry per rewrite, carrying the path, the site, and `from`/`to` as **source text**: replacing `0x1F` with `31` is a different edit from replacing it with `0x1F`, and only the spelling says which.
-   **The overlay is written only when the change holds.** An `invalid` or `error` verdict leaves the file exactly as it was: a change the author still has to think about should not sit in their configuration while they do. `--dry-run` writes nothing either way and prints what would have been written.
-   A missing overlay file is the empty overlay, and is created.
-   Exit codes are [`vet`](#aontu-vet)’s verdict classes: `0` valid, `1` invalid, `2` usage, `3` incomplete, `4` the entry does not stand up on its own.

### `aontu allow`

Ask a role model whether a role may modify every one of the given subtrees, and answer before the change is made: the gate an agent runs in front of [`set`](#aontu-model-set), from a document that is itself aontu.

```plaintext
aontu allow --role <role> [--at <path>] [--format text|json]
            <roles-file> <path>...
```

-   **The role model is an aontu document**: one entry per role, each carrying `allow` (the subtrees it may modify) and optionally `deny` (the ones it may not), as path strings. The map lives at `$.roles` unless `--at` names another path, and a role is whatever identifier its author writes.
-   **A path is allowed when an `allow` entry is at or above it.** Being allowed `$.services` allows `$.services.auth` and `$.services.auth.replicas`; it does not allow `$`, because a change at the root reaches every sibling too.
-   **A path is refused when a `deny` entry is at, above or below it, whatever the order.** Denied `$.services.*.tier` refuses `$.services.auth.tier`, and refuses `$.services.auth` and `$.services` as well, because a change at either could rewrite the tier. Deny wins over allow however the entries were written.
-   `*` in a path matches any one key. It is the only pattern character; every other segment is a map key or a list index compared for equality, as a reference compares them.
-   A role with no `allow` list allows nothing, and so does a role the model does not declare: the verdict is `refused`, and a `no_path` finding at the role’s path names the nearest declared role when one is close.
-   Every asked path is answered, and the verdict is `allowed` only when every one of them is. Each answer names the entry that decided it as a path into the role model, so [`aontu model why`](#aontu-model-why) locates the rule and the line that wrote it.
-   Every path starts with `$`, and may be spelled as `set`’s assignment, `<path>=<value>`, so a skill can hand the gate the very arguments the write will get. The value must be one value: `set` appends it as source after the flattened path, so a value carrying a second pair (`3 secrets: key: "x"`) would write a sibling of the overlay root, a subtree the gate was never asked about. Such an argument, an empty one, or a second filename in a path’s place is a usage error, exit 2.
-   `--format json` emits `{aontu, findings, paths, role, verdict}`. Each entry of `paths` carries `path`, `allowed` and `reason` (`allow`, `deny`, `uncovered` or `no_role`), plus `by` and `pattern` when an entry decided it.
-   Exit codes: `0` allowed (every path), `1` refused (at least one path, or a role the model does not declare), `2` usage, `4` the role model does not stand up on its own, in which case the engine’s own finding follows the verdict after a blank line.

The shape of a role is aontu too. The model meets it at evaluation, as data meets a schema under [`vet`](#aontu-vet): a spread template over the roles map, `roles: { &: { allow: [&: Entry] deny?: [&: Entry] } }` where `Entry` is `string & re("^[$]") & re("[^.]$")`, so a malformed role (`allow: "$.a"`, `deny: [1]`, an entry that is empty or does not start at `$` or ends in a dot, a roles map that is a number) is refused with the engine’s own code and site, exit 4. A role model that `close()`s its role vocabulary must declare `deny?` in it, or the template’s optional key is refused as `[aontu/closed]` and no role can be asked about. The lists are read from the written tree rather than the generated document, so a `hide()`d `deny` still denies; each entry must be one concrete string, and a kind (`string`) in an entry’s place is refused as `no_gen`.

Three limits follow from what the gate compares:

-   A key that contains a dot is unreachable, as it is for [`get`](#aontu-model-get): an asked path and an entry are both split at every dot, quotes included, so neither side can name such a key. A role is one key, looked up as written and never split, and the command refuses a dotted role name, because the entry paths it reports could not then be followed by `why`.
-   The gate answers about the path, and says nothing about where the value is written. A path reached through a reference is `set`’s business: the append lands at the named path, and `--in-place` is refused there.
-   The verb is TypeScript-only for now: the Go CLI does not have it.

Write a `roles.aontu` that declares three roles:

```aontu
roles: admin: allow: ["$"]
roles: dev: {
  allow: ["$.services" "$.deploy.*.replicas"]
  deny: ["$.services.*.tier"]
}
roles: qa: allow: ["$.tests"]
```

`dev` may change any service, and the replica count of any region:

```sh
$ aontu allow --role dev roles.aontu $.services.auth.replicas $.deploy.eu1.replicas
verdict: allowed
role: dev
$.services.auth.replicas: allowed by $.roles.dev.allow.0 ($.services)
$.deploy.eu1.replicas: allowed by $.roles.dev.allow.1 ($.deploy.*.replicas)
```

A service’s tier is denied, and so is the service above it, because a change there could rewrite the tier:

```sh
$ aontu allow --role dev roles.aontu $.services.auth
verdict: refused
role: dev
$.services.auth: refused by $.roles.dev.deny.0 ($.services.*.tier)
$ echo $?
1
```

A role the model does not declare may change nothing, and the finding says which path was looked for:

```sh
$ aontu allow --role ops roles.aontu $.services.auth
verdict: refused
role: ops
$.services.auth: refused (role ops is not declared)

$.roles.ops: no_path [reference]
  The role ops is not declared at $.roles in this document.
$ echo $?
1
```

The deciding entry is a path into the role model, and `why` names the line that wrote it:

```sh
$ aontu model why $.roles.dev.deny.0 roles.aontu
$.roles.dev.deny.0 = "$.services.*.tier"
  1. "$.services.*.tier"  roles.aontu:4:10
```

### `aontu agentsmd`

Generate the AGENTS.md stanza for a definition: the prose entrypoint, derived from the formal source so it cannot drift from it.

```plaintext
aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file.aontu>
```

The stanza names the document, its [canon-hash](#aontu-hash) pin, its root keys and its shape, spells the `get` / `why` / `vet` / `set` commands with a path that actually exists in it, and points at [`aontu help language`](#aontu-help) for the language the document is written in. `--write` splices it into a file between `<!-- aontu:begin -->` and `<!-- aontu:end -->`, appending the markers when they are absent: everything outside them is left exactly as it was, so the verb is safe to re-run and safe to point at a file someone else writes prose in.

`--depth <n>` is how deep the **shape** line projects, default `2`. Two levels name the root keys and say `top` under them, which says what the document is about rather than what is in it; `--depth 4` on an entity map reaches the fields. The default is unchanged because the stanza is spliced into a file people read.

Exit codes: `0` generated, `2` usage, `4` the document does not stand up on its own.

### `aontu fmt`

The source formatter, in the tradition of `gofmt`: one agreed form for aontu source, so that layout is never argued about and a diff shows only what changed.

```plaintext
aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
          [--profile <file>] <file>...
aontu fmt < in.aontu > out.aontu
```

| option | does |
| --- | --- |
| (none) | print the formatted text; one file, or standard input |
| `-w`, `--write` | rewrite each file in place, when its form would change |
| `-l`, `--list` | print the name of each file whose form would change |
| `--check` | like `--list`, and exit 1 when any would: the CI gate |
| `-d`, `--diff` | print a unified diff for each file whose form would change |
| `--lint` | report the style findings, key case and repeated shapes, on standard error, and print nothing else |
| `--strict` | `--lint`, and exit 1 when there is a finding |
| `--marker <t>` | the file is a generator, and this is its marker (default `//-`, and `#-` `---` `/*-` `<!---` by extension) |
| `--profile <f>` | a profile file, whose `template.ext` names the extensions it marks and `template.marker` the marker |

-   **The form.** Two-space indentation. `key: value`, the colon tight to the key. No commas between entries (a call’s argument list keeps them). Braces only where the language needs them: a one-pair map is a chain, `a: b: c: 1`, and a one-key map in a list is a pair element, `[a:1 b:2]`. A container on one line when it fits 80 columns, padded inside braces and with the colons tight, `{ a:1 b:2 }`, and as a block when it does not. Keys bare where they can be; a single-quoted string double-quoted unless it holds a double quote; numbers as written. Every comment and every blank line between groups stays, and a comment that ends a line of code sits two spaces behind it, `port: 8080 # the admin port`, normalised from whatever the author left. The formatter never breaks a line, and keeps the author’s line breaks inside an expression, at their operators.
-   **The prefix is repeated.** A map that does not fit on one line is written as one statement per entry, each carrying its key again: `server: host: "0.0.0.0"` / `server: port: 8080`. A key written twice is a meet, so the two spellings are one document. The prefix reaches through nested maps and stops at a RECORD (a map of several entries, all of them values) which is written as a braced block under the prefix instead, `field: id: {` and its facts one level in. Adjacent statements naming one key are the same map to the formatter, and are written as one line when that fits. Only a plain map in statement position: a map that is an argument, an operand or a list element keeps its braces, because splitting it would change the document.
-   **It reads the file it is given and no other.** An `@"..."` include is a token like any other, so `--trust` has nothing to confine in it. The verb does take the flag, and it governs the `--profile` document, which is evaluated: `--trust none` beside a profile that includes a file refuses with `include_denied`.
-   **A new tree at the top level stands apart.** One blank line above a top-level statement that takes more than one line to write, and one below it, above its own comments so that a note travels with what it describes. What counts as a tree is measured rather than guessed: the statement did not fit on one line. `a: 1` beside `b: 2` is left where it is, and the rule is the root’s alone; below it a blank line is the author’s.
-   **A generator is formatted as the document it carries.** A file whose extension is not `.aontu` is a generator written in the target’s own syntax ([`aontu template`](#aontu-template)), as it is for `trace`: it is desugared, formatted and resugared, so what comes back is a generator. The marker stands at the left margin with the aontu indented **after** it, so the tree the marker lines carry has a shape on the page, and every line of output is held on a line of its own, which the packing budget would otherwise fold three strings into. `--marker` names the marker for a language the table does not know. A file with no marker line in it is another language’s, a `.json`, `.yaml` or `.toml` include, and is refused by name, exit 2.
-   **It checks its own work.** Before a byte is returned the formatted text is parsed again and compared with the input, tree to tree; a disagreement is refused as the formatter’s own defect (`format_check`), and nothing is written. Each repeated or merged statement is also evaluated both ways in isolation, and a rewrite the engine evaluates differently is not made: that statement keeps its braces.
-   **It points at style, and touches nothing.** `--lint` reports on standard error, one line per finding as `file:line:col: rule: message`, and changes neither the document nor the exit code. `style/key-case` is a bare key holding an underscore or beginning with two capitals, `credit_cents`, `HTTP_PORT`, with the spelling that would follow the form, `creditCents`, `httpPort`; a quoted key that must stay quoted is a deliberate spelling. `style/repeat` is a map or list whose shape is written two or more times in the file and is 40 columns or wider, reported once, at its first site, with the other sites, because a value written twice can drift and an alias names it once. A chain and the braces it stands for are one shape, the order of a map’s entries is not part of it, and a repeat inside a repeat is the outer one’s. `--strict` is `--lint` with exit 1 when there is a finding.
-   With no file it reads standard input; with several files it needs `--write`, `--list`, `--check`, `--diff` or `--lint`, rather than print them as one stream.
-   Exit codes: `0` formatted or clean, `1` a `--check` file would change or a `--strict` finding, `2` usage, `4` a document does not parse.

A configuration written as it came from JSON, `config.aontu`:

```aontu
{
  "server": { "host": "0.0.0.0", "port": 8080, "tls": { "enabled": true, "cert": "/etc/tls/cert.pem" } },
  "features": ["auth", "metrics"],
  "limits": { "rps": 100, "burst": 200 }
}
```

```sh
$ aontu fmt config.aontu
server: host: "0.0.0.0"
server: port: 8080
server: tls: { enabled:true cert:"/etc/tls/cert.pem" }

features: ["auth" "metrics"]
limits: { rps:100 burst:200 }
```

The formatted text is the same document: `aontu hash` of the two agrees, and formatting the formatted text changes nothing.

### `aontu template`

Read a generator written in the **target’s own syntax**, and print the aontu it means.

```sh
aontu template [--resugar] [--check] [--marker <token>]
               [--profile <file>] <file>
```

**One rule: a marked line is aontu source, and every other line is a line of output.** The marker is the target’s comment token plus a dash (`//-`, `#-`, `---`, or a block form such as `/*- … */` and markdown’s `<!--- … -->` where the language has no line comment) so the file stays valid in its own language, an editor highlights it, and the target’s compiler parses it.

**A language the table does not know says so once.** `--marker` names the marker for one call; a profile names it for every call, because a profile is a language declared as data and `aontu template` and `aontu fmt` read the same file. A marker carries its own closer after a space when the opener does not imply one:

```aontu
@"aontu:profile"

aontu: Lang: lang: "ocaml"
aontu: Lang: indent: { unit:" " width:2 }
aontu: Lang: template: { marker:"(*-" close:"*)" ext: ["ml" "mli"] }
```

```sh
aontu template --profile ocaml.aontu unit.ml
```

Write a `greet.ts`:

```typescript
//- who: { world: {}, moon: {} }
//- svc: $.who & pack($.who, { name: key() })
//- out: emit($.svc, {
//- match: { name: string }
//- body: [file("greet-" + .name + ".ts", emit([_], {
//- match: { name: string }, replace: { NAME: .name }, body: [
export function greet() {
  console.log(`hello, NAME`)
}
//- ]}))]
//- })
```

The three unmarked lines are the output. They are ordinary TypeScript, indented where they belong, and **a value reaches them through `replace` rather than through a hole**: `NAME` is a string the body already holds, and there is no delimiter to collide with the target’s own syntax: the backtick template literal above survives untouched. The inner dispatch is what makes `replace` reach those lines: a rule substitutes into the body it wrote, so the lines of a file and the map that names the file are two rules, not one.

**The verb prints the canonical form**, which is what the marker lines mean once the output lines are quoted:

```sh
$ aontu template greet.ts
who: { world: {}, moon: {} }
svc: $.who & pack($.who, { name: key() })
out: emit($.svc, {
match: { name: string }
body: [file("greet-" + .name + ".ts", emit([_], {
match: { name: string }, replace: { NAME: .name }, body: [
`export function greet() {`
"  console.log(`hello, NAME`)"
`}`
]}))]
})
```

Each output line is one string, and **the quote is chosen per line**: a backtick, which carries `"` and `'` without escaping, unless the line holds a backtick itself, in which case the double quote is used and `"` is escaped. A backslash is escaped in either.

**Every verb that reads a generator reads the template directly**, so the canonical form is something to look at rather than something to keep. [`aontu trace`](#aontu-trace) answers over the file the author edits:

```sh
$ aontu trace greet.ts
greet-moon.ts	$.0	$.who.moon	#0
greet-moon.ts	$.0.children.0	$.who.moon	#0
greet-moon.ts	$.0.children.1	$.who.moon	#0
greet-moon.ts	$.0.children.2	$.who.moon	#0
greet-world.ts	$.1	$.who.world	#0
greet-world.ts	$.1.children.0	$.who.world	#0
greet-world.ts	$.1.children.1	$.who.world	#0
greet-world.ts	$.1.children.2	$.who.world	#0
```

**`--resugar` is the other direction**, and `--check` is the round trip between them: it holds the file to the spelling the two transforms answer, and names the first line that is not it. What that catches is a marker line the transform would not have written: one without its space, or one indented to match the code around it, since the marker stands at the left margin with the aontu indented after it. **A template’s whitespace is output**, so its bytes are the artifact: a body line’s trailing space shows up as a changed byte in the generated tree, which is what a drift check against the committed files catches.

[`aontu fmt`](#aontu-fmt) writes that spelling, and formats the aontu the marker lines carry while it is there.

```sh
$ aontu template --check greet.ts
```

The round trip is a **fixpoint**, not a table of escapes: a canonical line becomes an output line only when desugaring the rebuilt line answers the canonical line back. A line that could not survive (one beginning with the marker itself) fails that test and stays aontu, which is how a generator emits its own marker with no new syntax.

Exit codes: `0` written, `1` a `--check` file that is not what the round trip answers, `2` usage or I/O.

### `aontu trace`

Ask **what wrote this line**. Every piece a rule stamped under the component tree, with the file it reached, the model node the dispatch matched, and the rule set that wrote it.

```sh
aontu trace [--at <path>] [--format json] [--marker <token>]
            [--profile <file>] <file>
```

A generator answers a tree of files, and a reader looking at one line of the output has no way back to the rule that produced it. This is that way back. The anchor is `$.out` unless `--at` names another.

```sh
$ aontu trace gen.aontu
handlers/chat.ts	$.0.children.0	$.services.chat	$.%handler#0
```

Four columns: the **file**, the **address** of the piece in the document, the **model node** the dispatch matched, and the **rule**. `--format json` answers the same entries as one object.

**A rule read through a name is addressed by it.** `$.%handler#0` is the first template of the `%handler` rule set. A table written inline at the call has no address of its own and is `#0`, `#1` and so on, so the two cases are told apart by whether an address precedes the hash.

**A piece that reached no file is not traced.** The tree is what the entries attribute to, so a rule whose output never landed in a file has nothing to name.

### `aontu render`

Write the files a generator answers.

```sh
aontu render [--check] [--at <path>] [--format json] [--marker <token>]
             [--profile <file>] <file|folder> <path>
```

A generator answers a **component tree** (`file`, `folder`, `line` and the rest) as an ordinary value at `$.out`. `render` hands that tree to [jostraca](https://github.com/jostraca/jostraca), the generator runtime both implementations depend on, and jostraca writes the files. aontu decides which files exist and what each line of them is; the runtime decides how the bytes reach a disk. Write a `gen.aontu`:

```aontu
foo: "BAR"

out: file("zed.txt", ["foo = " + $.foo])
```

**A tree that is one file is written to `<path>` itself**, under that name, unless `<path>` is a directory that already exists:

```sh
$ aontu render gen.aontu zed.txt
```

Nothing is printed when the write succeeds, unless a file was skipped (below). `--format json` reports the files written, in the seven groups the runtime sorts them into, and an eighth aontu reads from the run’s own record:

```sh
$ aontu render --format json gen.aontu zed.txt
{
  "aontu": {
...
  "files": {
...
    "unchanged": [
      "zed.txt"
    ],
    "written": []
  },
  "verdict": "ok"
}
```

**A file the runtime declines to touch is named as skipped.** A copy on disk carrying the `JOSTRACA_PROTECT` marker is left as it is, so a hand edit survives the next run. Those seven groups say what was done to a file, and nothing was done to this one, so aontu reads the run’s own record under `.jostraca/` and reports `skipped: <path>` in text, or the `skipped` group in JSON. `--check` still counts it as `content` drift: the bytes differ from what the generator answers, whoever chose that.

**Any other tree is written below `<path>`**: a `project`, a `folder`, or a list of files, with the paths the tree spells. A one-file tree that should land in a directory names the file: `aontu render gen.aontu build/zed.txt`.

**A folder is a set of generators.** Every regular file directly in it is one (a name beginning with a dot is not), taken in name order: a `.aontu` as it is, any other file by its marker, as below. Their trees are written below `<path>` as one run, so one `--check` holds the whole set, and two generators claiming one path are refused by the runtime. A file with no marker line in it is refused by name rather than skipped. Write a `gen/a.aontu`:

```aontu
out: file("a.txt", ["a"])
```

and a `gen/b.rb`, a generator in the target’s own syntax:

```ruby
#- out: folder("lib", [file("b.rb", [
puts "b"
#- ])])
```

```sh
$ aontu render gen out
$ aontu render --check gen out
```

`out/a.txt` and `out/lib/b.rb` are on disk, `a.txt` under its own name: in a set the one-file rule above does not apply.

**`--check` is the CI form.** Nothing is written; `<path>` is compared with what the generator writes, one `kind: file` line per difference (`content`, `missing`, or `mode` where the tree declared one), and the exit code is 1 on drift:

```sh
$ aontu render --check gen.aontu zed.txt
```

Edit the file by hand, as `zed.txt`:

```text
foo = edited
```

```sh
$ aontu render --check gen.aontu zed.txt
content: zed.txt
$ echo $?
1
```

**A template entry is read directly.** As [`aontu trace`](#aontu-trace) does, `render` reads a `<file>` whose extension is not `.aontu` as a generator in the target’s own syntax (see [`aontu template`](#aontu-template)), desugared by the marker its extension names, `--marker`, or a profile’s. `--at` names another anchor than `$.out`.

Beside the files it writes, jostraca keeps a record of its own in a `.jostraca/` folder under the output directory (for a one-file render, the directory that holds the file), with a `.gitignore` in it. That folder, and what the runtime does with a file that already exists, is jostraca’s contract; [Trust and determinism](https://aontu.dev/docs/trust#clause-4-sandboxing) records what stays aontu’s.

Exit codes: `0` written or clean, `1` `--check` drift, `2` usage or I/O (a path that cannot be made, a `fragment` whose source is not there), `4` the document does not stand up, or `--at` names nothing.

### `aontu hash`

The canon-hash: one string that pins what a document _means_, so a lockfile, a registry or an agent can say “this module, this meaning” and have the claim survive reformatting.

```plaintext
aontu hash [--form] [--format text|json] <file.aontu>
```

-   The hash is `"aon1-" + base64url(SHA-256(UTF-8(hcanon(unify(file)))))`, where `hcanon` is the **hash form**: see below. `aon1-` is a scheme id, so a future semantically stronger normal form is an upgrade rather than a breakage.
-   The document is evaluated **standalone**: its own `@"file"` closure resolved and unified at its own root, before any consumer context. That is what makes the pin transitive: an edit two includes deep changes the unified root, hence the hash.
-   **The pin survives** comments, whitespace, formatting, key reordering, and splitting one file into several includes: any refactor that leaves the unified value identical. **It breaks on** any semantic change in the transitive closure: a default flipped, a field added, a map closed, a constraint tightened.
-   `--form` prints the hashed TEXT instead of the digest, which is what to diff when a pin moves. `--format json` prints both under `hash` and `form`.
-   Exit codes: `0` hashed, `2` usage, `4` the document does not evaluate on its own: a broken document has no meaning to pin, and a hash of the wreck would agree with every other wreck.

To see the pin hold still, write `svc.aontu`:

```aontu
service: { name:"checkout" port: *8080|integer }
```

and `svc-reformat.aontu`, the same meaning re-ordered under a comment:

```aontu
# the same meaning, reordered and commented
service: port: *8080 | integer
service: name: "checkout"
```

```sh
$ aontu hash svc.aontu
aon1-nSY9noXFhWc_dtcRrErhCS9bZVtNfTJb0vVoCE9W1CM
$ aontu hash svc-reformat.aontu
aon1-nSY9noXFhWc_dtcRrErhCS9bZVtNfTJb0vVoCE9W1CM
$ aontu hash --form svc.aontu
{"service":{"name":"checkout","port":*8080|integer}}
```

Two spellings, one meaning, one pin, and `--form` prints the exact text the digest is taken over.

**The hash form (`hcanon`)**

Exactly the unify-level [canon](#val-typescript) with the two additions that close its semantic gaps:

|  | canon | hash form |
| --- | --- | --- |
| a closed map or list | `{"a":1}` | `close({"a":1})` |
| a `type`\- or `hide`\-marked value | `1` | `type(1)`, `hide(1)` |

Both reuse existing parseable syntax, so the hash form is itself valid aontu source and round-trips: `hcanon(unify(parse(hcanon(v)))) == hcanon(v)` is asserted for every row of `test/spec/hcanon.tsv`, in both implementations. Marks propagate to every descendant at unification, so a wrapper is emitted only where a mark _starts_. User-facing `canon` is unchanged.

This is a _canonical-text_ hash, not a hash of semantic equivalence classes: canon is deterministic syntax, not a unique normal form, so `number|integer` and `number` denote the same value set and hash differently. The failure direction is the safe one: a false “changed” forces a needless re-review, while a false “unchanged” is impossible provided the hash form is semantically complete, which is exactly why the `close`/mark additions are part of the definition rather than an optimisation.

### `aontu sync`

Make the project correct: resolve its declared dependencies by minimum version selection, fetch what no local store holds, write the lockfile, materialise the vendor tree, and verify every pin. It is idempotent, and it is the one package verb a reader needs to know.

```plaintext
aontu sync [--frozen] [--base <url>]... [--format text|json] [dir]
```

`dir` is the project root, the directory holding `pkg.aontu`, and defaults to the working directory.

**What a project declares.** `pkg.aontu` names the package and what it depends on. A dependency is a minimum version; an `alias:<name>` entry holds a second version of one package under a name the imports use:

```aontu
pkg: { path:"corp.example/checkout" main:"main.aontu" }

dep: "corp.example/schemas/service": v: "1.4.2"
dep: "alias:legacy": { pkg:"corp.example/schemas/service" v:"1.2.0" }
```

**Resolution is minimum version selection.** Every package is taken at the highest of the minima anyone in the closure asked for, and never higher, so a resolve is reproducible and adding one dependency cannot move another. The closure is walked from the project’s own file through each dependency’s `pkg.aontu`, fetching a package the stores lack as it is reached.

**What a fetch checks, in order.** The repository is trusted for availability and for nothing else. For each package the client reads the version list, then the manifest, then the proof, then the archive, and refuses at the first check that does not hold:

1.  the manifest is well formed and names the package and version asked for;
2.  the proof signs the manifest’s digest, by a signer the project’s trust configuration accepts for that name;
3.  the archive’s digest is the one the manifest declares, before a byte of it is parsed;
4.  every file in the archive has the digest and size the manifest lists, no file is missing and none is extra, every path obeys the element rules (at most 32 elements, none a name a platform reserves such as `con` or `nul`), and nothing outside the allowlist is present;
5.  the module, evaluated standalone with its own dependencies at the minima it declares, means what the manifest’s canon-hash pins.

A refusal names its code and the package, and nothing is written:

```plaintext
verdict: refused
fetched: corp.example/schemas/common 1.0.0
refused: archive_digest_mismatch: the archive for corp.example/schemas/service 1.4.2 is sha256:45e3…, not sha256:9127…
```

**The lockfile pins three things.** `aontu_meta/pkg-lock.aontu` is one canonical line, and each entry carries the version, the canon-hash of the module’s meaning, the digest of its canonical archive and, for a package acquired from a repository, the digest of its signed manifest:

```aontu
# pkg-lock.aontu (generated by `aontu sync`; do not edit)
{"lock":{"corp.example/schemas/service":{"archive":"sha256:9127…","canon":"aon1-zFHn…","manifest":"sha256:f72c…","v":"1.4.2"}}}
```

**The vendor tree is the build.** `aontu_meta/vendor/<package-path>/` holds every package of the closure as a real copy, one directory per `/`\-element, uppercase escaped as `!x`, with the served manifest and proof kept beside it under the package’s own `aontu_meta/`. A synced project evaluates with no cache and no network. Commit the lockfile and the vendor tree together.

**`--frozen` is the CI mode.** It resolves, fetches what a locked version needs, and refuses to change the lockfile: a dependency the lock does not name, a version that would move, a pin that would be recomputed, all exit 1 and name themselves as `lockfile would change`.

**Where packages come from.** `repo` in `pkg.aontu` is the project’s trust configuration, and it travels with the project:

```aontu
repo: base: ["https://pkg.aontu.dev"]
repo: private: ["corp.example/*"]
repo: private_base: ["https://pkg.corp.example"]
repo: trust: "corp.example/*": { signer:"ed25519:q520…" inclusion:none }
```

`base` lists the repositories to read from, in order; `--base <url>` on the command line replaces the list. A base is `https`, or `http` on a loopback host, which is how a local `aontu pkg serve` is reached. A name matching `private` is never sent to a public base: it is read from `private_base`, or refused when none is configured. `trust` maps a pattern (a package path, a path followed by `/*`, or `*`) to the signer accepted for it: a named Ed25519 key, or `forge` for a signing identity proved through Sigstore. The default entry, `*`, names the forge signer and requires log inclusion. This build verifies key proofs; a name whose entry names the forge is refused rather than accepted unverified.

**Cooldown.** A version is selectable by `get` and `outdated` only once the repository has held it for 72 hours, timed from the repository’s own first-seen time, and a version that a later version retracted is never selected. A version named explicitly, in `pkg.aontu` or on the command line, is taken as it is; a private name skips the cooldown.

**Exit codes.** `0` the project is correct; `1` a refusal, a frozen lockfile that would change, a missing package or a pin that does not hold; `2` usage; `4` a module in the store that does not evaluate on its own.

`--format json` prints the report: `verdict`, `fetched`, `lock`, `vendored`, `missing`, `unevaluable`, `forbidden`, `mismatched`, `unlocked`, `changes`, `events` and, on a refusal, `refusal` with its `code`, `message` and `pkg`.

### `aontu add`, `aontu get`, `aontu remove`

Change what the project depends on, then sync.

```plaintext
aontu add <pkg> [dir]
aontu get <pkg>[@<version>] [dir]
aontu remove <pkg> [dir]
```

-   **`add`** takes on a dependency the project does not have. It refuses one already declared and names `get`.
-   **`get`** adds a dependency or raises its minimum: the superset of `add`. Without a version it takes the newest selectable version.
-   **`remove`** drops a dependency and the vendor tree it held.

Each edits `pkg.aontu` by the smallest text change that keeps the file the author’s: a line appended, a version literal rewritten where it stands, a one-line entry removed. An entry the verb cannot edit that way is left to the author, and the verb says so. Each then runs a full `sync`, and a change the sync cannot carry is taken back, so neither leaves the project half-changed:

```plaintext
$ aontu get corp.example/schemas/service@1.4.3
verdict: ok
change: raised corp.example/schemas/service 1.4.2 -> 1.4.3
fetched: corp.example/schemas/service 1.4.3
corp.example/schemas/service 1.4.3 aon1-JstX…
```

Exit codes are `sync`’s, plus `2` for a spelling the verb cannot act on.

### `aontu why`

Why is this package in the closure? Every chain of dependencies from the project to it, read from the lockfile and the package files in the store. No network.

```plaintext
aontu why <pkg> [--format text|json] [dir]
```

```plaintext
$ aontu why corp.example/schemas/common
verdict: ok
corp.example/checkout -> corp.example/schemas/service -> corp.example/schemas/common
```

Exit `0` when a chain exists, `1` when the package is not in the closure.

### `aontu publish`

Publish this package. A dry run without `--yes`.

```plaintext
aontu publish [--yes] [--key <file>] [--token <file>] [--to <dir>]
              [--write <url>] [--against <dir>] [--format text|json] [dir]
```

A package declares its path, its version and its entry in `pkg.aontu`, and whether it may leave the machine:

```aontu
pkg: path: "corp.example/schemas/service"
pkg: version: "1.4.2"
pkg: main: "service.aontu"

publish: public
```

`publish` defaults to `private`, and a private package is refused by a public write path; a project-level `publish: public` is what allows it out.

**What a publish sends** is the manifest, its proof, and the canonical archive of the source tree. The archive holds exactly what the allowlist admits: aontu source, the data formats the include table reads, `.md` and `.txt`, `LICENSE` and `NOTICE`, and nothing under `aontu_meta/`. It is a zip whose entries are sorted, stored uncompressed and undated, so one tree has one digest in both implementations. It is held to the caps every consumer applies at acquisition (16 MiB compressed, 64 MiB of files, 4096 files, 8 MiB a file) before it is minted, so nothing leaves the machine that every consumer would refuse; the package path and version in `pkg.aontu` must be a package path and a version, and the entry must be a path inside the tree.

**The gate runs first.** The highest version the repository already holds is fetched, checked as any acquisition is, and the candidate is compared against it. Compatibility has three components, and all three must hold: **acceptance**, every document the old version admitted the new one admits, which is the subsumption query’s [`subsume`](#aontu-subsume) with its findings; **determination**, every position the old version resolved to a value with nothing supplied the new one still resolves (`compat_undetermined`, as when `port: 8080` loosens to `port: integer` and a consumer who supplied nothing had a value and now has an error); and **agreement**, where both resolve a position they resolve it to the same value (`compat_outcome_changed`). A change that fails any of them is refused, and there is no major version to bump past the gate; a breaking change ships as a new package path, and the old path’s last version declares `moved: <new path>`. `--against <dir>` names a prior tree to gate against instead.

```plaintext
$ aontu publish --key key.pem
verdict: dry-run
corp.example/schemas/service 1.4.3 public
archive: sha256:b4b8… (2 files, 1006 bytes)
module: corp.example/schemas/service service.aontu aon1-JstX…
file: pkg.aontu sha256:bc26… 133
file: service.aontu sha256:0188… 663
digest: sha256:acb6…
signer: ed25519:q520…
against: corp.example/schemas/service 1.4.2
write: https://publish.aontu.dev/v1/publish
dry run: nothing sent (add --yes)
```

**The proof.** `--key <file>` names a PKCS#8 PEM Ed25519 private key; any other kind of key, or a file that is not a PEM private key, is refused `key_invalid` before anything is signed. The proof signs the manifest’s digest under it, and the signer’s id, `ed25519:` followed by the public key, is what a consumer names in its `trust` entry. `--token <file>` carries the forge’s OIDC token to the write path, which decides admission from it; the token’s claims are copied into the manifest as its `publisher` block.

**`--to <dir>`** writes the read-path layout straight into a directory instead of sending anything: the archive, manifest and proof under `pkg/<path>/@v/`, the version list and `@latest`, and the advisory derived from `retract` declarations. That directory is a repository, which `aontu pkg serve` serves and a consumer reads over a loopback base. `--write <url>` names another write path.

**Exit codes.** `0` sent or dry run; `1` refused, or breaking; `2` usage; `3` the gate is undecided; `4` the package cannot be minted.

### `aontu pkg`

The remaining package operations, each the right verb when you want exactly that step and no other.

```plaintext
aontu pkg tidy     [--format text|json] [dir]
aontu pkg verify   [--format text|json] [dir]
aontu pkg vendor   [--format text|json] [dir]
aontu pkg manifest [--against <dir>] [--format text|json] [dir]
aontu pkg refreeze [--format text|json] [dir]
aontu pkg tree     [--format text|json] [dir]
aontu pkg outdated [--base <url>]... [--format text|json] [dir]
aontu pkg serve    [--listen <addr>] [--upstream <url>]... [dir]
aontu pkg keygen   <file>
```

**`tidy`** resolves the closure against the local stores and rewrites the lockfile, fetching nothing. A package the stores do not hold is reported and **the lockfile is not written at all**: a partial lock claims a closure that was never resolved. A package that is present but does not evaluate on its own is refused the same way (`verdict: error`, exit 4), because a pin computed from a module with no meaning is the same string for every broken module.

**`verify`** asks whether every locked package still is, and still means, what the lockfile pins, and **changes nothing**. It is the CI gate. Bytes are checked before meaning: the archive digest and each file against the kept manifest first, then the one evaluation and the canon-hash. A pinned manifest the tree no longer keeps is a mismatch, not a pass.

```plaintext
$ aontu pkg verify
verdict: mismatch
corp.example/schemas/service: pinned archive sha256:9127… but the store holds sha256:45e3…
```

Verdicts: `ok`; `mismatch`, a store no longer is or means what is pinned; `unlocked`, the lockfile does not name a dependency the project declares; `missing`, a locked package is in no store. Exit `0`, and `1` for each of the three refusals: nothing to check is not a pass.

**`vendor`** copies every locked package from the stores into `aontu_meta/vendor/`, as whole source trees, replacing whatever stood at each destination. The cache is keyed by canon-hash, and a consumer’s pin is computed against its own closure, so `vendor` looks under that pin and under the canon the repository’s manifest for the locked version pins.

**`manifest`** prints the manifest a publish would send, and gates it with `--against <dir>` exactly as `publish` does: acceptance, determination and agreement.

**`refreeze`** recomputes every canon pin and nothing else, which is what a canonical-form change in the engine needs.

**`tree`** draws the locked closure as a graph, each node’s edges read from its own package file in the store.

**`outdated`** asks the repository, for every locked package, what the newest selectable version is and what a resolution taking it would move with it: minimum version selection walked through the repository’s manifests to the closure, so a dependency’s own raises are listed too. A pinned version that a later one retracted is named too; the lock keeps working.

```plaintext
$ aontu pkg outdated
verdict: outdated
corp.example/schemas/common 1.0.0 -> 1.1.0
corp.example/schemas/service 1.0.0 -> 1.1.0
  corp.example/schemas/common 1.0.0 -> 1.1.0
corp.example/schemas/service 1.0.0: retracted by 1.1.0
```

**`keygen`** writes a new Ed25519 signing key to a file, as PKCS#8 PEM and once only, and prints the signer id, `ed25519:` followed by the public key, which is what a consumer names in a `trust` entry and what `publish --key` signs under.

**`serve`** serves a repository directory over HTTP: what `publish --to` wrote, byte for byte. With `--upstream <url>` it is a caching proxy in front of another repository, fetching an object on a miss and keeping it, and it says when it served a copy no upstream would refresh. The default address is `127.0.0.1:8017`. A proxy relaxes nothing: every consumer verifies what it serves as it verifies anything else.

**The user cache** is `$XDG_CACHE_HOME/aontu/pkg` where that is set, otherwise `~/.cache/aontu/pkg` (`%LOCALAPPDATA%\aontu\pkg` on Windows), and holds three trees: `download/<package>/@v/` keyed by package and version, `store/<canon-hash>/<package>/` keyed by canon-hash and package path, and `seen/<package>/`, the client’s own first-seen records, from which a repository that drops a version this client saw is detected. Under a root trust capability the cache is not consulted at all: a confined run reads the vendor tree only, and the verbs that write the cache refuse to run.

**`aontu_meta/` is where a project’s generated state lives**: the lockfile, the vendored closure, and whatever later tooling writes. `pkg.aontu` and the documents stay at the root, since they are authored; nothing under `aontu_meta/` is. The verbs name an older layout, a `mod.aon` or a root `mod-lock.aon`, once when they find it, and read nothing from it.

-   `--format json` prints every report as an object with the usual `aontu: {version, verb}` envelope and a `verdict`.

**REPL commands**

| Command | Effect |
| --- | --- |
| `:help` | show help |
| `:load <file>` | evaluate a document and hold it for the commands below |
| `:get [path]` | what the held document says at a path |
| `:keys [path]` | the keys at a path of the held document |
| `:why <path>` | every contribution to the value at a path |
| `:canon` | switch to canonical-form output |
| `:json` | switch to JSON output |
| `:quit`, `:exit` | leave (or press Ctrl-D) |

`:load` holds the document’s **source**, not its evaluated tree (parsed trees are single-use) so every later question re-evaluates from the text. `:get` and `:keys` are the [query](#aontu-model-get) surface and `:why` is the [provenance](#aontu-model-why) surface, answering about the held document.

**`--jsonl` makes the session machine-drivable**: no banner, no prompt, and every command answers as one JSON line (`{"ok":true,"out":"…"}`), so a harness can drive the REPL the way it drives the CLI. Human-readable output stays the default.

```plaintext
$ aontu
aontu v0.56.0 REPL — :help for commands, :quit to exit
aontu> port: *8080 | integer
{
  "port": 8080
}
aontu> :canon
canon output
aontu> a:1|2|3
{"a":1|2|3}
aontu> :quit
```

### `aontu help`

Print the embedded teaching pack: the **language**, where `--help` documents the **tool**.

```plaintext
aontu help [topic] [--format text|json]
```

With no topic it lists them. The corpus travels inside the binary, so it answers with no network, no checkout and no documentation site, which is the condition it exists for.

| topic | is |
| --- | --- |
| `tasks` | which verb does the job you have, indexed by the word you arrived with |
| `language` | the grammar card: everything the language spells, on one page |
| `examples` | the ladder, from plain JSON upward |
| `codes` | what a refusal means, and what to do about it |
| `grammar` | the published [ABNF](#the-published-grammar) |

The corpus is **generated** from [`docs/skill/`](https://github.com/aontu-lang/aontu/blob/main/docs/skill/) and `grammar/aontu.abnf` by `ts/scripts/helpdoc.cjs` (`make helpdoc`, which `make build-ts` runs), into `ts/src/helpdoc.ts` and `go/cmd/aontu/helpdoc/`. Both suites assert the embedded copy is byte-identical with its source, so the pack cannot drift from the published files: a Go binary needs a copy inside its own package directory, because `//go:embed` cannot read above one, and a generated copy that nothing compares is a second source of truth.

`--format json` answers `{aontu, topics}` for the index and `{aontu, source, summary, text, topic}` for a topic.

Exit codes: `0` printed, `2` an unknown topic (the topics are listed) or a bad option.

### `aontu explain`

Explain one error code, from the same table the engine attaches to a finding.

```plaintext
aontu explain <code> [--format text|json]
aontu explain --list [--format text|json]
```

Every code in [the registry](#behavioural-parity) resolves, so a code read out of a report always answers:

```plaintext
$ aontu explain mapval_no_gen
code:  mapval_no_gen
class: incomplete

This value was present after unification, and cannot be generated
because it is not a literal value.
```

The list is the **registry**, not the hint table. The registry is in cross-port parity (`test/spec/errcodes.tsv`, asserted set-equal with the engine’s class map in both ports), and listing from the hint table would make the verb answer about something other than what either port can report. Every registered code carries explanation text, and a gate in both suites holds it so. A registered code carrying none would be marked `(no text)` in the listing and say so when asked, rather than printing an empty block.

The code may be written bare or with the `aontu/` prefix a report prints in its brackets: `aontu explain constraint` and `aontu explain aontu/constraint` answer identically, under the registered spelling.

A dynamic code is registered through the prefix it extends, and carries that prefix’s text: `func:upper` answers with the `func:` explanation, because the suffix names the operator and the explanation is the prefix’s.

An unknown code exits 2 and names the nearest registered match.

Exit codes: `0` explained, `2` an unknown code or a bad option.

### `aontu init`

Write a working model, an instance of it, and a check script into a directory.

```plaintext
aontu init [dir]
```

The reason this verb exists is that writing a **first** document is the most expensive thing to get wrong. A model that reaches for `"*"`, where the language spells the template [`&:`](https://aontu.dev/docs/reference-language#spreads-), constrains nothing, and [`vet`](#aontu-vet) over it reports `valid`. A known-good document makes the next step an edit rather than an invention.

| file | is |
| --- | --- |
| `model.aontu` | the truth: an entity map, constrained with `&:` |
| `data.aontu` | an instance of it that holds |
| `check.sh` | the four checks to run after every edit, `vet --strict-coverage` first |

```plaintext
$ aontu init
model.aontu
data.aontu
check.sh

A model, an instance of it, and the four questions to ask.
Run the checks:  sh check.sh
Learn the language:  aontu help language
```

With no directory it writes into the working one, and it creates the directory if it is not there. `check.sh` is written executable and takes the binary from `$AONTU`, so a checkout with no installed `aontu` runs it with `AONTU=./aontu sh check.sh`.

It **never overwrites**. If any member of the trio already stands in the directory, it refuses before writing any of them, so a refused run never leaves the directory half written.

The trio is **generated** into both ports from `docs/skill/init/` by the generator that stages the teaching pack (`make helpdoc`), on the [`aontu help`](#aontu-help) precedent. Both suites assert the staged bytes are identical with their sources, and that the emitted documents pass their own `check.sh`.

Exit codes: `0` written, `2` a standing file, a directory it cannot write, more than one directory, or a bad option.

### `aontu lsp`

```plaintext
aontu lsp
```

The language server, over standard input and output: LSP, which is JSON-RPC with Content-Length framing, until the client says exit. Both builds serve it, byte-for-byte alike from a client’s point of view; editors launch it with no arguments beyond the verb, and `--help` is the only option. The standalone `aontu-lsp` binary runs the same server. What it reports, and the library beneath it, are on [the LSP page](https://aontu.dev/docs/lsp); wiring a client is in [wire your editor](https://aontu.dev/how-to/wire-your-editor).

### The MCP server

```plaintext
aontu mcp [--root <dir>]
```

A Model Context Protocol server over stdio (newline-delimited JSON-RPC 2.0), the `mcp` verb of the npm build’s CLI; the standalone `aontu-mcp` binary of the package runs the same server. It follows the language server’s three-layer split ([docs/lsp.md](https://aontu.dev/docs/lsp)): the tools and the protocol are a transport-free library (`ts/src/mcp.ts`), the verb is stdio and nothing else.

| Tool | Answers |
| --- | --- |
| `vet` | the [vet](#aontu-vet) report for a schema and a data document |
| `get` | the [query](#aontu-model-get) surface: a path, and a view of it |
| `why` | the [provenance](#aontu-model-why) record for a path |
| `diff` | what changed at which paths between two documents |
| `canon` | a document’s canonical form |
| `summary` | the pin, the root keys and the top-tier shape: the first tier of progressive disclosure, expanded by calling `get` |
| `subsume` | the [subsume](#aontu-subsume) report: does the general document admit every instance the specific one admits? |
| `breaking` | the [breaking](#aontu-breaking) verdict (`compatible` | `breaking` | `undecided` | `error`) plus the mode checked: the `mode` argument, else the document’s own `$.aontu_policy.compat`, else `backward` |
| `set` | the [set](#aontu-model-set) report **plus the new overlay text**: assignments arrive as `{path, value}` pairs (with optional `inPlace`), and the server never writes files: the caller owns the write |
| `relations` | the [relations](#aontu-relations) report: acyclicity and inverse consistency over the entity edge set |
| `hash` | the [canon-hash](#aontu-hash) pin `{hash}` (plus the hash-form text when `form: true`) |
| `trim` | the [`trim --check`](#aontu-trim) report: redundant entries as paths |
| `reaches` | the [reachability check](#aontu-reaches): the verdict and, when it reaches, a shortest path: the closure question `relations` cannot ask one edge at a time |
| `view` | a [figure](#aontu-view) of the document as text: the tree, matrix, graph (mermaid, dot, er), layer, sets, layers, ladder, doc or lattice kind, with the loss report; the poset takes several files and is CLI only |
| `jsonschema` | the [JSON Schema export](#aontu-jsonschema): the schema, and the `lossy` list naming what it could not say: the bridge to a structured-output API, and to an MCP tool’s own `inputSchema` |

Every tool returns **the same JSON contract the CLI prints**, so a report read from one is the report read from the other. A tool that _refuses_ (an invalid document, a path that names nothing, a document the served trust profile cannot read) answers with its own report and `isError: false`, because the report is the answer; `isError` is reserved for a call that could not be made at all (an unknown tool, a malformed argument, a file argument the server cannot serve).

Served evaluation is **confined** ([the trust contract](https://aontu.dev/docs/trust)): the source arrives from a caller, and `@"..."` is exactly what a server must not run unconfined. By default every include is denied (`{ include: 'none' }`). Started with **`--root <dir>`**, the server takes the CLI’s `--trust root:<dir>` posture instead: includes resolve confined below the (realpath’d) root, and every tool’s document arguments gain `<name>Path` alternatives (`schemaPath`, `srcPath`, `sourcePath`, `generalPath`, …) naming files below that root, checked by the same realpath-then-prefix rule the include resolver applies, so a symlink escape is an escape. Without `--root`, path arguments are refused with `isError: true`; the `initialize` handshake’s `instructions` field says which mode the server is in, and `tools/list` advertises the `<name>Path` properties only when they are served. The package-resolver leg is enabled by neither posture.

The Go port ships no MCP server: its `aontu mcp` says so and exits 2. Its role is embedding the same library calls, and `Get`, `Why`, `Diff` and `AgentsMd` are in the Go API for that.

### The published grammar

Three files, one grammar. [`grammar/aontu.gbnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.gbnf) and [`grammar/aontu.lark`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.lark) publish the **emission surface** for constrained decoding; [`grammar/aontu.abnf`](https://github.com/aontu-lang/aontu/blob/main/grammar/aontu.abnf) says the same thing in RFC 5234 notation, for a reader rather than a decoder, and is the one the [language reference draws its railroad diagrams](https://aontu.dev/docs/reference-language#the-published-grammar) from.

All three are conservative by construction (they accept less than the parser does, never more) and all three deliberately exclude `@"..."` includes, because generated documents should describe values rather than reach for files.

The grammar is not a document that drifts: `ts/test/grammar.test.ts` reads `aontu.gbnf` and `aontu.abnf`, interprets both, and requires each to accept **every canonical-form output in the shared spec suite**. The function-name list in all three, and in the TextMate grammar the editors ship, is checked against the engine’s own registry in both directions.

### The skill

[`docs/skill/`](https://github.com/aontu-lang/aontu/blob/main/docs/skill/) holds the agent-facing sources: a trigger stub, a task-to-verb index, a one-page grammar card, a JSON-first example ladder, and the error-code index for repair loops. Every example document in the ladder is evaluated by `ts/test/skill.test.ts`, so a skill that teaches something the engine no longer does fails the build.

They reach a caller by three routes, and the third is the one that works when nothing was configured. The npm tarball stages them as `skill/` (`ts/scripts/prepack.js`, which rewrites the links that would otherwise escape the package). A harness can mount the directory as a skill. And [`aontu help`](#aontu-help) serves them **from inside the binary**, which is what an agent holding the command and no network has.

### LSP hover provenance

The language server can append a value’s **contributions** to its hover (what met at that path, in source order, with each site) the same record [`aontu model why`](#aontu-model-why) prints. It is off unless an editor asks for it:

```json
{ "initializationOptions": { "aontu": { "provenance": true } } }
```

Hover already re-unifies the document per request, so an editor that asks for this pays a second, instrumented evaluation knowingly, and one that does not pays nothing. Diagnostics are unchanged either way.

**Getting the command**

-   **TypeScript:** the npm package declares a `bin` named `aontu` (`bin/aontu.js`), so `npm install -g aontu` (or `npx aontu`) provides it. From a clone: `make install-ts`, an `npm link` of the checkout, or `node ts/bin/aontu.js …` without installing.
-   **Go:** `go install github.com/aontu-lang/aontu/go/cmd/aontu@latest`, or from a clone: `make install-go`, a `go install` of `aontu` and `aontu-lsp`, or `go run ./cmd/aontu …` inside `go/`.

`make install` at the root of a clone does both. Both commands accept the same options and produce the same results.

---

## Evaluation consumes the tree

A parsed `Val` tree is **single-use**, in both implementations. `unify`/`generate` refine the tree in place (children are written back, junction and reference nodes advance their own state), which is safe only because a tree is unified once and never shared. Do not cache, reuse, or unify the same parsed `Val` (or any node reachable from it) in two different evaluations: the second run starts from mutated state and the result is nondeterministic. Parse again (or clone first) for every independent evaluation. The string entry points (`generate(src)`, `unify(src)`) parse per call and are always safe.

This is a named rule of the [trust contract](https://aontu.dev/docs/trust)’s determinism clause, not a performance note: violating it produces wrong answers, not slow ones.

## TypeScript API

Package `aontu` (canonical). Entry point `dist/aontu.js`, types `dist/aontu.d.ts`. Requires Node ≥ 22.

```ts
import { Aontu } from 'aontu'          // named
import Aontu from 'aontu'              // default (same class)
```

### class `Aontu`

```ts
new Aontu(opts?: AontuOptions)
```

Constructs an instance and its parser (`Lang`). One instance can process many sources.

| Method | Signature | Returns | Notes |
| --- | --- | --- | --- |
| `parse` | `parse(src: string, opts?, ctx?)` | `Val | undefined` | Parses to an unresolved AST. Does not unify. |
| `unify` | `unify(src: string | Val, opts?, ctx?)` | `Val` | Parses (if given a string) and runs the fixpoint to a fully unified `Val`. |
| `generate` | `generate(src: string, opts?, ctx?)` | `any` | Parse → unify → emit a native JS value. **Throws `AontuError`** on conflict or an unresolved result. Serialise the result with [`exactJSON`](#exact-numbers-and-exactjson), not `JSON.stringify`. |
| `ctx` | `ctx(cfg?: AontuContextConfig)` | `AontuContext` | Creates a context (for variables, error collection, a custom `fs`, etc.). |

```ts
const aontu = new Aontu()
aontu.parse('a:number')                  // Val (AST)
aontu.unify('a:1 a:number').canon        // '{"a":1}'
aontu.generate('a:1 b:$.a')              // { a: 1, b: 1 }
aontu.generate('a:1 a:2')                // throws AontuError: Cannot unify value: 2 with value: 1
```

`unify` accepts a previously parsed `Val`, so a caller that wants the AST first need not re-parse: `const p = aontu.parse(src); aontu.unify(p)`. But note that the parsed tree is **single-use** (see [Evaluation consumes the tree](#evaluation-consumes-the-tree)) so each parse feeds at most one unify/generate.

### `AontuOptions`

Passed to the constructor, to any method’s `opts` argument, or merged into a context.

| Option | Type | Purpose |
| --- | --- | --- |
| `src` | `string` | Source text (usually passed positionally instead). |
| `path` | `string` | Path of the entry file (for `@"…"` relative resolution and error sites). |
| `base` | `string` | Base path for the resolver. |
| `resolver` | `Resolver` | Custom source resolver for `@"…"` loading. |
| `fs` | `typeof fs` | Filesystem implementation: for example a `memfs` volume for tests. |
| `collect` | `boolean` | Collect errors onto `result.err` instead of throwing. |
| `err` | `any[]` | Pre-existing array to accumulate errors into (implies `collect`). |
| `explain` | `any[]` | Capture a structured trace of the unification. |
| `debug` / `trace` | `boolean` | Enable parser debug / parse tracing. |
| `deps` | `object` | Dependency record populated by `@"…"` loads. |
| `log` | `number` | Parser log verbosity. |

`@"…"` resolution tries an **in-memory** resolver, then the **filesystem**, then **package** resolution, in that order. The chain is unconfined by default (a relative include follows any path the process can read) so **treat opening an untrusted source as running it**. Confinement is the **`trust` option** ([the trust contract](https://aontu.dev/docs/trust)), in both implementations:

```ts
const aontu = new Aontu({
  trust: {
    // include capability, one of:
    //   'none'              — @"…" always denied
    //   { mem: {...} }      — a virtual file set only
    //   { root: '/models' } — real files, realpath-confined below root;
    //                         no package resolution
    //   'system'            — the full chain (today's default)
    include: { root: '/models' },
    budget: { passes: 9, depth: 1000 },  // integer engine-event counts
  },
})
```

Go mirrors it as `Aontu.Trust` (`TrustOptions`: `IncludeNone`, `IncludeMem`, `IncludeRoot`, `Budget`). A denied resolution is the parse-stage `include_denied` error, pinned by `test/spec/include-trust.tsv` in both runners. Confinement is realpath-then-prefix-check on the resolved file, so a symlink inside the root pointing outside it is denied. An **empty** `root` denies every include rather than resolving to the process directory; Go’s flat `IncludeRoot` has no spelling for it, its zero value being an absent root. Note `fs` is _not_ a sandbox: it supplies source text for parsing and error context, and the file and package legs read through their own channels; the trust profile is the confinement surface.

**The include manifest.** After a parse, the resolved include closure is observable as sorted, deduplicated `{ path, capability }` entries (`result.deps` in TypeScript, `Aontu.IncludeDeps` in Go) hermeticity’s “file set” as data (capability is `mem`, `file` or `pkg`). Content hashing and pinning belong to [`aontu hash`](#aontu-hash) and the package tooling, [`aontu pkg`](#aontu-pkg).

**The bundled vocabularies.** `@"aontu:system"` ([the system vocabulary](https://aontu.dev/docs/reference-language#the-aontusystem-vocabulary)) and `@"aontu:view"` (the schema for a [view document’s](#aontu-view) declarations) are served from the engine rather than from disk, so they need neither the filesystem nor package resolution and resolve under every include capability except `'none'`. They appear in the manifest with capability `std`. A host that wants a different vocabulary supplies its own source under its own name; the bundled one is engine-owned.

**Relation checks.** `relationCheck(src)` in TypeScript and `Aontu.RelationCheck(src)` in Go run the [declared-relation](https://aontu.dev/docs/reference-language#declared-relations) checks over the derived edge set, returning the `{verdict, findings}` record that [`aontu relations`](#aontu-relations) prints: that section has the verdicts, the finding fields, and the exit codes.

**The derived graph.** After a unification, an evaluated document’s link structure is observable too: `result.graph` in TypeScript (also available as the pure function `graphOf(val)`) and `Aontu.Graph` in Go. It is the edge set:

```ts
{
  edges: [ { from: '$.services.billing', key: 'dependsOn',
             to: '$.services.auth',
             at: '$.services.billing.dependsOn.0' }, … ],
}
```

There is no entity index, because there is no second namespace to index: a node’s address is its path. One entry per checked [link](https://aontu.dev/docs/reference-language#checked-links-refert):

-   **`from`** is the node the link starts at: the link’s own position with the relation key and any list indices stripped, so a link inside a list is an edge from the node that holds the list. `$` when the link sits at the top of the document.
-   **`key`** is the relation. A `rel()`\-minted link carries its predicate declared; a bare `refer()` has it inferred as the first real key above the link.
-   **`to`** is the **resolved** address, always absolute. A relative address means a different node from each position it is written at, so an edge set whose far ends were spellings could not be traversed: the link’s own _value_ is still what the author wrote.
-   **`at`** is where the link is written.

It is **deterministic**: edges are sorted by construction, and both runners re-derive the graph on a fresh engine and require the same bytes (`test/spec/graph.tsv`). Impact analysis, reachability and node slices are traversals over it; their exposure as verbs and projections is the machine-access layer’s.

### `AontuContext`

A context threads variables, error state, and resolver configuration through a run. Create one with `aontu.ctx()`.

-   `ctx.vars: Record<string, Val>`: values for `$name` variables.
-   `ctx.err: any[]`: collected errors (when `collect`).
-   `ctx.find(path: string[]): Val | undefined`: look a value up by path.

Pass the context as the third argument: `aontu.generate(src, undefined, ctx)`.

### `Val` (TypeScript)

The unified value. Useful members:

| Member | Description |
| --- | --- |
| `canon: string` | Reparseable canonical form (see [language reference](https://aontu.dev/docs/reference-language#canonical-form)). |
| `gen(ctx): any` | Emit the native value (used by `generate`). |
| `err: any[]` | Errors attached to this value (`NilVal`s). |
| `isVal: boolean` and `isMap`/`isList`/`isScalar`/`isNil`/… | Type discriminators. |
| `path: string[]` | Path from the root. |

`Val` is an abstract base; concrete subclasses (`MapVal`, `ListVal`, `IntegerVal`, `NumberVal`, `BigIntegerVal`, `BigDecimalVal`, `StringVal`, `BooleanVal`, `NullVal`, `ScalarKindVal`, `ConjunctVal`, `DisjunctVal`, `PrefVal`, `RefVal`, `VarVal`, the `*FuncVal`s, …) are exported from their modules under `dist/val/`.

The four numeric subclasses are the four numeric leaves: `IntegerVal` is `integer`, `NumberVal` is `float` (the class name is historical: `number` is the pure supertype, not a leaf), and `BigIntegerVal` / `BigDecimalVal` are the exact leaves `biginteger` / `bigdecimal`.

### Exact numbers and `exactJSON`

`generate()` returns **native** values, and a document that opts into the `0d` exact literals returns two of them that `JSON.stringify` cannot write:

| aontu kind | Source | `generate()` returns |
| --- | --- | --- |
| `integer` | `x:5` | `number`, or `bigint` past `Number.MAX_SAFE_INTEGER` (see below) |
| `float` | `x:1.5` | `number` |
| `biginteger` | `x:0d5` | `bigint` |
| `bigdecimal` | `x:0d0.1` | `Decimal` |

**Why an `integer` can be a `bigint`.** The `integer` leaf is an int64 window, and JavaScript stores it in a double. Below `Number.MAX_SAFE_INTEGER` that is faithful: the integers are contiguous there, so the `number` renders its own exact digits. Above it they are not (`JSON.stringify(2**60)` is `1152921504606847000`, a _different_ integer that merely rounds to the same double) so `generate()` returns a `bigint`, which `exactJSON` writes exactly. A `float` stays a `number` at any magnitude, because a double’s shortest form already names it exactly (`1e21` serialises as `1e+21`, in this port and in Go).

```ts
typeof gen('x:9007199254740991').x     // 'number'  (2^53-1)
typeof gen('x:9007199254740992').x     // 'bigint'  (2^53)
typeof gen('x:1e21').x                 // 'number'  (float kind)
exactJSON(gen('x:1152921504606846976'))  // '{"x":1152921504606846976}'
```

An integer-kind `bigint` is still not a `biginteger`: the leaves stay disjoint and only canon tells them apart (`1152921504606846976` versus `0d1152921504606846976`). Go needs none of this: its `integer` leaf is an `int64`, exact across the whole window, so `Generate` returns an `int64` at every magnitude. The serialised JSON is identical in both ports.

A `0d`\-free document generates exactly what it always did: the exact leaves are reached only by writing `0d` (see the [language reference](https://aontu.dev/docs/reference-language#the-four-numeric-leaves)). Both leaves survive nesting: `generate('x:{y:0d7} z:[0d1,0d0.5]')` puts a `bigint` at `x.y` and a `Decimal` at `z[1]`. Note that an _integral_ bigdecimal is still a `Decimal` and never a `bigint`: `0d1e3` is a bigdecimal by source form, and the leaves are disjoint.

`JSON.stringify` **throws** on a `bigint` (`TypeError: Do not know how to serialize a BigInt`), and a `replacer` cannot rescue it: a replacer may only return another _value_, and anything it returns that is not already a JSON primitive gets quoted, so the exact digits could come back only as a JSON _string_, which is a different document. JSON itself was never the obstacle: a JSON number is arbitrary-precision decimal text, and `{"x":9007199254740993}` is a legal document. Only JavaScript’s serialiser stands in the way, so the package ships its own.

```ts
exactJSON(value: any, indent?: number | string): string
```

Serialises a `generate()` result as JSON text, preserving exact numbers. **Use it instead of `JSON.stringify` on generated output.**

```ts
import { Aontu, exactJSON } from 'aontu'

const out = new Aontu().generate('x:0d9007199254740993')
typeof out.x        // 'bigint'
exactJSON(out)      // '{"x":9007199254740993}'
exactJSON(out, 2)   // '{\n  "x": 9007199254740993\n}'
JSON.stringify(out) // TypeError: Do not know how to serialize a BigInt
```

-   **`indent`** has `JSON.stringify`’s `space` semantics: a number of spaces (clamped to `0`–`10`) or a literal string (truncated to 10 characters). Omitted or `0` gives **compact** output: no spaces, no newlines.
-   A `bigint` writes its digits. A `Decimal` writes its plain digit form (`1000.0`, `0.1`, `-1.5`): no `0d` marker, since that belongs to canon and is not JSON, but an integral bigdecimal keeps its `.0` so the JSON still shows a decimal.
-   Object keys are emitted in **lexicographic order** (by UTF-16 code unit), matching Go’s `encoding/json`, which sorts map keys. This is done at emit time and not by `generate()`, because a JavaScript object _cannot_ hold the required order: ECMAScript lists canonical array-index keys first, ascending numerically, so an object can never present `"10"` before `"9"`. It applies to any object passed in, not only `generate()` output, since this is a general emitter, and it is the one place the result deliberately differs from `JSON.stringify`.
-   Ordinary values are otherwise written exactly as `JSON.stringify` writes them: the same string escaping, `null` for `NaN` and `Infinity`, and `undefined`/function/symbol dropped from an object but written as `null` inside an array. An object with a `toJSON` method is asked for its replacement (`Decimal` is handled as a number before that check).
-   U+2028 and U+2029 are escaped, which `JSON.stringify` does not do: that is the one place JavaScript and Go disagree by default, and escaping is both legal JSON and safe to embed in JavaScript source.
-   It always returns a string: a top-level `undefined` becomes `null`.
-   It throws `AontuError` if the value contains a reference cycle. A _shared_ subtree (which unification produces routinely) is fine; only a true cycle is refused, as in `JSON.stringify`.

The output is byte-identical to the Go port’s `encoding/json` with `SetEscapeHTML(false)` for the same document; that equivalence is pinned by the test suite both implementations run. The `aontu` CLI calls this same export with `indent` of `2`, so there is exactly one implementation for the pretty and compact forms to stay in step with.

`Decimal` is exported from `aontu` alongside it: the type a bigdecimal generates as. It is an immutable exact base-10 value (`unscaled: bigint` plus `scale: number`) in normal form, so numerically equal decimals have equal fields:

| Member | Description |
| --- | --- |
| `new Decimal(unscaled: bigint, scale: number)` | Construct and normalise. |
| `Decimal.fromString(src: string)` | Parse `[+-]?digits[.digits][e[+-]digits]`, with or without a `0d` marker. |
| `toString(): string` | Plain digit form: what `exactJSON` writes. |
| `canon(): string` | Canonical form, with the `0d` marker. |
| `equals` / `compare` / `add` / `negate` / `ceil` / `floor` / `isZero` | Exact operations: no rounding anywhere. |

`Decimal.fromString` refuses input beyond the exactness budget (at most 4096 coefficient digits and an absolute scale of at most 4096: see the [language reference](https://aontu.dev/docs/reference-language#the-exactness-budget)), the same refusal a literal gets.

### Variables

`$name` references are filled from `ctx.vars`. Build value objects with the exported `Val` constructors:

```ts
import { Aontu } from 'aontu'
import { IntegerVal } from 'aontu/dist/val/IntegerVal'
import { StringVal }  from 'aontu/dist/val/StringVal'
import { MapVal }     from 'aontu/dist/val/MapVal'

const aontu = new Aontu()
const ctx = aontu.ctx()
ctx.vars.foo = new IntegerVal({ peg: 11 })
ctx.vars.bar = new StringVal({ peg: 'hello' })
ctx.vars.obj = new MapVal({ peg: { x: new IntegerVal({ peg: 1 }) } })

aontu.generate('a:$foo b:$bar c:$obj', undefined, ctx)
// { a: 11, b: 'hello', c: { x: 1 } }
```

**Exact-input constructors.** The two exact leaves take a `bigint`, a `Decimal`, or the digits as **text**: never a JS `number`, which binary64 has already rounded before this library could inspect it, so an exact value above 2^53 could not arrive that way intact:

```ts
import { Decimal }       from 'aontu'
import { BigIntegerVal } from 'aontu/dist/val/BigIntegerVal'
import { BigDecimalVal } from 'aontu/dist/val/BigDecimalVal'

new BigIntegerVal({ peg: 5n })                     // 0d5
new BigIntegerVal({ peg: '9007199254740993' })     // 0d9007199254740993
new BigIntegerVal({ peg: 5 })                      // throws: not-biginteger

new BigDecimalVal({ peg: new Decimal(15n, 1) })    // 0d1.5
new BigDecimalVal({ peg: '0.10' })                 // 0d0.1  (normalised)
new BigDecimalVal({ peg: 1.5 })                    // throws: not-bigdecimal
```

Both reject malformed text (`'5.5'` is not a biginteger). `BigDecimalVal` additionally refuses input over the exactness budget, exactly as a `0d` literal does; a biginteger has no bound and is as wide as its digits. Because the constructor picks the leaf where a literal’s source text would, `new BigDecimalVal({ peg: '5' })` is a _bigdecimal_ and canons `0d5.0`.

### Exports

From `aontu`:

```ts
Aontu          // class (also default export)
AontuOptions   // type
AontuContext   // class
AontuError     // error class (thrown by generate)
Val            // base value type
Lang           // the parser
VERSION        // the package version string
runparse, util // parsing helpers
formatExplain  // pretty-print an `explain` trace
exactJSON      // exact JSON emitter — use instead of JSON.stringify
Decimal        // the type a bigdecimal generates as
vet            // the validation verb (see `aontu vet` above)
sarifReport    // a vet report as SARIF 2.1.0
subsume        // the subsumption query (docs/reference-language.md,
               // "Subsumption"): subsume(general, specific, {profile?, at?})
               // -> {verdict, findings}; Go: aontu.Subsume
trimCheck      // the redundancy reporter (see `aontu trim` above):
               // trimCheck(src, {path?}) -> {verdict, redundant};
               // Go: aontu.New().TrimCheck(src)
hcanon         // the HASH FORM of an evaluated Val (see `aontu hash`
               // above): canon plus the close()/type()/hide()
               // wrappers; Go: aontu.Hcanon
canonHash      // the canon-hash pin over that form,
               // "aon1-"+base64url(SHA-256(...)); Go: aontu.CanonHash
get            // the query surface (see `aontu model get` above):
               // get(src, path, {view?, depth?, path?, trust?}) ->
               // {ok, out, findings}; Go: aontu.New().Get(src, path, opts)
why            // provenance (see `aontu model why` above):
               // why(src, path, {path?, trust?}) -> {ok, record, findings},
               // record = {path, value, conjuncts}; Go: (*Aontu).Why
patch          // the overlay patch (see `aontu model set` above):
               // patch(entry, overlay, ["$.a.b=1"], opts?) ->
               // {overlay, appended, verdict, findings}; Go: aontu.Patch
diff           // what changed at which paths between two documents:
               // diff(left, right, {at?, trust?}) -> {changes, same, findings};
               // Go: aontu.Diff
agentsMd       // the generated AGENTS.md stanza (see `aontu agentsmd`
               // above): agentsMd(src, {name?}) -> {stanza, ok};
               // Go: (*Aontu).AgentsMd
format         // the source formatter (see `aontu fmt` above):
               // format(src, {path?, lint?, template?}) -> {verdict,
               // text, changed, findings} or {verdict, errors};
               // `template` is the marker of a generator, formatted as
               // the document it carries; Go: aontu.New().Format(src),
               // and FormatWith(src, FormatOptions{Lint, Template})
unifiedDiff    // unifiedDiff(name, before, after): the diff `aontu fmt
               // --diff` prints; Go: aontu.UnifiedDiff
loadProfile    // a profile document -> {profile} or {errors}:
               // evaluated under the caller's include options, vetted
               // against aontu:profile as a settled value and met with
               // it, so the defaults are filled; what --profile <file>
               // hands to `template`, `fmt` and `trace`
desugarTemplate // a generator in the target's own syntax -> the
               // canonical aontu it means (see `aontu template`):
               // desugarTemplate(src, marker?) -> string;
               // Go: aontu.DesugarTemplate
resugarTemplate // the other direction, a fixpoint against the first:
               // resugarTemplate(src, marker?) -> string;
               // Go: aontu.ResugarTemplate
markerFor      // the marker a file's extension names, `//-` by
               // default: markerFor(path) -> string;
               // Go: aontu.MarkerFor
```

#### Evaluating a document you did not write

`vet`, `get`, `why` and `diff` each take a **`trust`** option, the same profile [`AontuOptions.trust`](#aontuoptions) takes, and it means the same thing: what the document being evaluated may reach.

These four verbs exist to be pointed at source from somewhere else (a candidate an agent emitted, a live system dump, the other side of a diff) and without a profile they resolve `@"…"` through the default chain, which reaches anything on the filesystem the process can read. **Opening an untrusted source is reading your disk**, so pass a profile whenever the source is not yours. Reading, never running: an include’s extension decides what the file is (`.aontu` as aontu source, `.json`, `.jsonld`, `.jsonc`, `.json5`, `.jsonic`, `.jsc`, `.toml`, `.yaml`, `.yml` and `.ini` as configuration data, and `.txt` as text (the bytes, as one string)) and every other extension is refused. `textExt` widens the text set; it cannot re-read a named format, and cannot reach `.js`.

```ts
vet(schemaSrc, candidateSrc, { trust: { include: 'none' } })
```

The [MCP server](#the-mcp-server) supplies its profile (`{ include: 'none' }`, or `{ include: { root } }` when started with `--root <dir>`) to every tool from a single place, rather than each tool applying it for itself: a tool that must remember to confine itself is one that eventually forgets, and the forgetting is silent. The engines that take no `trust` option (`subsume`, `trimCheck`, `relationCheck`, `patch`) are confined there by a pre-parse under the same profile: includes resolve at parse, so a document whose confined parse is clean gives the engine nothing it could reach further with.

---

## Go API

Module `github.com/aontu-lang/aontu/go`, package `aontu`.

```go
import aontu "github.com/aontu-lang/aontu/go"
```

### type `Aontu`

```go
func New() *Aontu                 // relative @"file" loads resolve from the cwd
func NewWithBase(base string) *Aontu  // …resolve from base (a directory)
```

Use `NewWithBase` when a source’s relative `@"file"` loads should resolve from somewhere other than the process working directory: typically the directory of an entry file:

```go
abs, _ := filepath.Abs(file)
a := aontu.NewWithBase(filepath.Dir(abs))
```

Absolute `@"file"` paths are unaffected by the base. (The `aontu` CLI does exactly this for a file argument.)

| Method | Signature | Notes |
| --- | --- | --- |
| `Parse` | `Parse(src string) (Val, error)` | AST, not unified. |
| `Unify` | `Unify(src string) (Val, error)` | Parse + fixpoint unify. |
| `UnifyVars` | `UnifyVars(src string, vars map[string]Val) (Val, error)` | `Unify` with `$name` variables. |
| `Generate` | `Generate(src string) (any, error)` | Parse → unify → native Go value. |
| `GenerateVars` | `GenerateVars(src string, vars map[string]Val) (any, error)` | `Generate` with variables. |
| `Trace` | `Trace(src string, opts *TraceOptions) TraceReport` | What wrote each line of a component tree (see [`aontu trace`](#aontu-trace)): `Trace` is one `TraceEntry` per stamped piece (`At`, `File`, `Node` and `Rule`), or `Errors`. `aontu.TraceTree(root Val) []TraceEntry` is the walk alone, over a value already unified. |
| `LoadProfile` | `LoadProfile(src string) (map[string]any, []VetFinding)` | A profile document, evaluated under this instance’s include options, vetted against `aontu:profile` as a settled value and met with it so the defaults are filled: the profile `--profile <file>` hands to `template`, `fmt` and `trace`, or the findings that refused it. |
| `Format` | `Format(src string) FormatReport` | The source formatter (see [`aontu fmt`](#aontu-fmt)): the agreed form, or the findings that say why there is none. `FormatWith(src string, opts FormatOptions) FormatReport` is the same with the options: `Lint` fills the report’s `Findings`, the style findings of `--lint`, and `Template` is the marker of a generator, formatted as the document it carries. `aontu.UnifiedDiff(name, before, after string) string` is the diff `--diff` prints. |

```go
a := aontu.New()
v, err := a.Unify("a:1 a:number")   // v.Canon() == `{"a":1}`
out, err := a.Generate("a:1 b:$.a") // out == map[string]any{"a":1,"b":1}
```

All methods return an `error` (never panic for ordinary conflicts); `Generate` returns `(nil, err)` on any unresolved or conflicting value. Generated output uses Go’s natural types (`map[string]any`, `[]any`, `int64`/`float64`, `string`, `bool`, `nil`), plus `*big.Int` and `*Decimal` for the exact leaves: see [Exact numbers in Go](#exact-numbers-in-go).

### `Val` (Go)

The lattice element interface:

```go
type Val interface {
    Canon() string              // canonical source-like form
    Gen(ctx *Ctx) (any, error)  // native value (error if not generable)
    Unify(peer Val, ctx *Ctx) Val
    Dc() int                    // done-counter; DONE (-1) == fully resolved
    Nil() bool                  // true for a unification failure (bottom)
    // …plus unexported lattice-ordering methods
}
```

Concrete exported types: `TopVal`, `NilVal`, `ScalarVal`, `ScalarKindVal`, `MapVal`, `ListVal`, `ConjunctVal`, `DisjunctVal`, `PrefVal`, `RefVal`, `VarVal`, `FuncVal`, `PlusOpVal`. Every scalar leaf (including the two exact ones) is a `ScalarVal`; it holds its kind internally, so from outside the package a leaf is told apart by the concrete type `Gen` returns, or by `Canon`.

### Exact numbers in Go

`Generate` returns Go’s natural types, and the two exact leaves come out as the two types that can hold them exactly:

| aontu kind | Source | `Generate` returns |
| --- | --- | --- |
| `integer` | `x:5` | `int64` |
| `float` | `x:1.5` | `float64` |
| `biginteger` | `x:0d5` | `*big.Int` |
| `bigdecimal` | `x:0d0.1` | `*Decimal` |

A `0d`\-free document generates exactly what it always did. An _integral_ bigdecimal is still a `*Decimal` and never a `*big.Int`: `0d1e3` is a bigdecimal by source form, and the leaves are disjoint.

Both types implement `json.Marshaler` and emit **exact digits as a raw JSON number**, so `encoding/json` needs no help:

```go
out, _ := aontu.New().Generate("a:0d9007199254740993 b:0d1e3 c:0d0.1")
b, _ := json.Marshal(out)
// {"a":9007199254740993,"b":1000.0,"c":0.1}
```

The pointer is part of the contract. A non-pointer `big.Int` inside an `any` has no `MarshalJSON` in its method set, so `encoding/json` falls back to the struct encoder and writes `{}`: an exact number silently replaced by an empty object, which is the class of failure the exact leaves exist to eliminate.

A generated `*big.Int` is a **copy**, so a caller may mutate it without disturbing the value it came from.

`Decimal` is an exact base-10 value (coefficient plus scale), immutable and always in normal form. Its exported surface is what a consumer of generated output needs:

| Method | Description |
| --- | --- |
| `String() string` | Plain digit form (`1000.0`, `0.1`, `-1.5`). |
| `MarshalJSON() ([]byte, error)` | The same digits, as a raw JSON number. |
| `Canon() string` | Canonical form, with the `0d` marker. |

`json.Marshal` output matches the TypeScript port’s [`exactJSON`](#exact-numbers-and-exactjson) byte for byte once HTML escaping is off (`json.Encoder` + `SetEscapeHTML(false)`); that equivalence is pinned by the test suite both implementations run.

### `Ctx` and errors

-   `Ctx` carries the root, variables, and collected errors through a run; you normally let `Unify`/`Generate` create it.
-   `AontuError{ Msg string }` implements `error` and is returned (wrapped) for conflicts; its message matches the TypeScript phrasing (for example `Cannot unify value: 2 with value: 1`).

### Variables in Go

`UnifyVars`/`GenerateVars` accept a `map[string]Val`. Build the values with the exported constructors:

| Constructor | Returns |
| --- | --- |
| `NewString(s string) Val` | string scalar |
| `NewInteger(i int64) Val` | `integer` scalar: **refuses** an `int64` binary64 cannot carry exactly (see below) |
| `NewNumber(f float64) Val` | `float` scalar (the name is kept for API compatibility; the kind it builds is `KindFloat`) |
| `NewBigInteger(n *big.Int) Val` | `biginteger` scalar: the exact unbounded integer leaf |
| `NewBigDecimal(s string) (Val, error)` | `bigdecimal` scalar: the exact base-10 leaf |
| `NewBoolean(b bool) Val` | boolean scalar |
| `NewNull() Val` | null scalar |
| `NewScalarKind(k Kind) Val` | type constraint (`KindString`, `KindBoolean`, `KindNull`, and the numeric lattice `KindNumber` with its leaves `KindInteger`, `KindFloat`, `KindBigInteger`, `KindBigDecimal`) |
| `NewMap(map[string]Val) Val` | map (keys inserted in sorted order) |
| `NewList([]Val) Val` | list |

```go
vars := map[string]aontu.Val{
    "port": aontu.NewInteger(8080),
    "host": aontu.NewString("localhost"),
    "obj":  aontu.NewMap(map[string]aontu.Val{"x": aontu.NewInteger(1)}),
}
out, err := aontu.New().GenerateVars(
    "server: { host: $host, port: $port }", vars)
// out == map[string]any{"server": map[string]any{"host":"localhost","port":8080}}
```

Pass `nil` vars when a model uses no `$name` variables. An undefined `$name` is a `Cannot resolve` error.

**`NewInteger` obeys the same storage contract as a literal.** An `int64` that binary64 cannot carry exactly is refused rather than stored, exactly as the equivalent literal is refused: otherwise the API would be a hole straight through that rule, since Go’s `integer` leaf is an `int64` and the canonical TypeScript port’s is a double. The refusal is a **nil value**, not a panic and not a second return: aontu errors are values, so it flows through unification and surfaces at `Generate` with the same “not exactly representable” message and the same `0d` escape a lossy literal gets.

The rule is **exactness, not magnitude**: every power of two in the window is fine however large, `math.MinInt64` included.

```go
aontu.NewInteger(1152921504606846976)   // 2^60 — fine
aontu.NewInteger(math.MinInt64)         // -2^63, a power of two — fine
aontu.NewInteger(9007199254740993)      // 2^53+1 — nil value
aontu.NewInteger(math.MaxInt64)         // 2^63-1, rounds up — nil value
aontu.NewBigInteger(big.NewInt(9007199254740993))  // the exact escape
```

**Exact-input constructors.** `NewBigInteger` **copies** its argument and never mutates the copy, so a caller may keep using (and mutating) the `*big.Int` it passed in; a `nil` argument is zero. `NewBigDecimal` takes a **string**: an optional sign, an optional `0d` marker, digits, an optional fraction and an optional exponent, and no `_` separators (those are literal syntax, not part of a number’s text). A `float64` is deliberately not accepted: it has already rounded before the library can inspect it.

```go
n, _ := new(big.Int).SetString("123456789012345678901234567890", 10)
aontu.NewBigInteger(n)          // 0d123456789012345678901234567890
aontu.NewBigDecimal("0.10")     // 0d0.1   (normalised)
aontu.NewBigDecimal("0d1e3")    // 0d1000.0
aontu.NewBigDecimal("1_000")    // error: Not an exact decimal
```

`NewBigDecimal` returns an error for malformed text and for input over the exactness budget (at most 4096 coefficient digits and an absolute scale of at most 4096: see the [language reference](https://aontu.dev/docs/reference-language#the-exactness-budget)), the same refusal a literal gets. A biginteger has no bound and is as wide as its digits. Because the constructor picks the leaf where a literal’s source text would, `NewBigDecimal("5")` is a _bigdecimal_ and canons `0d5.0`.

---

## Behavioural parity

Both implementations are validated against the same [`test/spec/*.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/) cases and agree on: scalars and scalar kinds (including the numeric tower’s four leaves (`integer`, `float`, `biginteger`, `bigdecimal`) under the pure supertype `number`, their `0d` exact literals, and exact arithmetic) maps (nesting, merge, spreads `&:`, optional keys, `close`/`open`), lists (incl. spreads), conjunction `&`, disjunction `|`, preference `*`, references (`$.a.b`, `.x.a`), `$name` variables, the `+` operator, all twelve functions, `type`/`hide` marks, and `@"…"` source loading: plus `parse` / `unify` / `generate` and the canonical form.

Generated **bytes** are in parity too: `exactJSON` in TypeScript and `encoding/json` in Go produce the same JSON text for the same document, which the shared suite’s byte-exact `gens` rows pin. What byte equality cannot see (a `bigint` where a `number` was due, since both serialise as `5`) is pinned by per-port API tests instead.

**Validation reports** are in parity as well: `aontu vet` produces the same report from both commands, text and JSON, with the same exit code: pinned by the shared suite’s [`vet.tsv`](https://github.com/aontu-lang/aontu/blob/main/test/spec/vet.tsv) rows for everything but each finding’s `message`, which is prose. One thing still differs by construction: the wording of a “cannot read ” failure, which is the host’s. The `aontu.version` field agreed from 0.70.0, when the two version series merged into one.

The shared parser stack is identical: TypeScript uses `@tabnas/jsonic` + `@tabnas/{expr,path,multisource,directive,debug}`; Go uses the ports `github.com/tabnas/{jsonic,expr,path,multisource,directive}/go`. See the [Explanation](https://aontu.dev/docs/explanation#two-implementations-one-behaviour) for how parity is maintained.
