---
title: "Gate schema changes"
description: "Gate schema edits with aontu breaking, so a change that would refuse previously valid documents fails the review."
source: "https://aontu.dev/how-to/gate-schema-changes/"
---

# Gate schema changes

Gate schema edits with aontu breaking, so a change that would refuse previously valid documents fails the review.

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

Consumers validated their documents against the schema you published. Edit it, and every one of those documents is re-judged. `vet` answers “does this data hold?”; `breaking` answers the question a schema edit raises: do documents that were valid against the old version still hold under the new one?

Point it at the earlier version. Here is a released `profile-v1.aon`:

```aontu
profile: close({
  id: string & re("^C[0-9]{7}$")
  email: string
  tier: "standard"|"premium"|"enterprise"
})
```

and a proposed `profile-v2.aon` that adds one optional key:

```aontu
profile: close({
  id: string & re("^C[0-9]{7}$")
  email: string
  tier: "standard"|"premium"|"enterprise"
  locale?: string
})
```

Now ask whether the proposal breaks anybody:

```sh
$ aontu breaking --against profile-v1.aon profile-v2.aon
verdict: compatible
```

Additive and optional, so every v1-valid document is still admitted. A proposal that _requires_ a new key is a different story: write it as `require-owner.aon`:

```aontu
profile: close({
  id: string & re("^C[0-9]{7}$")
  email: string
  tier: "standard"|"premium"|"enterprise"
  locale?: string
  owner: string
})
```

```sh
$ aontu breaking --against profile-v2.aon require-owner.aon
verdict: breaking

$.profile.owner: compat_required_added [compat]
  the general value requires this key; the specific value admits instances without it
  expected: string
  actual:   {"email":string,"id":re("^C[0-9]{7}$"),"locale"?:string,"tier":"standard"|"premium"|"enterprise"}
  general: require-owner.aon:6:10 (string)
  specific: profile-v2.aon:1:10 ({"email":string,"id":re("^C[0-9]{7}$"),"locale"?:string,"tier":"standard"|"premium"|"enterprise"})
$ echo $?
1
```

Every old document that omitted `owner` is now refused, and the finding names the key and both versions’ sites. Narrowing an existing field breaks the same way (`compat_narrowed`): tightening `email` to a pattern rejects any v1 document with a plain string there.

## The query underneath

`breaking` is [subsumption](https://aontu.dev/docs/reference-language#subsumption) pointed at history: the new version must **subsume** the old: admit every instance the old admitted. The query is a verb of its own when you want to compare two arbitrary documents rather than versions:

```sh
$ aontu subsume profile-v2.aon profile-v1.aon
verdict: subsumes
```

The general document goes first, the specific second. The same query is a library export in both ports (`subsume` / `aontu.Subsume`) for programmatic gates.

## In CI

`--against` also takes `git#<rev>`, so the gate needs no copies of old versions lying around: it materialises the revision’s tree and evaluates the old document from there, includes and all:

```sh
$ aontu breaking --against git#HEAD profile.aon
```

One line then gates every pull request against the branch it merges into:

```yaml
- run: aontu breaking --against git#origin/main profile.aon
```

## What to watch for

Exit `1` means a previously valid document is now refused. Exit `3` means the query could not decide (always with a `sub_*` reason naming why), and it **fails the gate** unless you pass `--allow-undecided`; a gate that shrugs is not a gate. Two more flags earn their keep in practice: `--at <path>` anchors the comparison at the contract, so a version string at the top level stops self-breaking the gate on every release, and `--allow-deprecated-removal` admits the removal of a field the old version already marked `deprecate()`: the supported rename path. All of it is specified under [`aontu breaking`](https://aontu.dev/docs/reference-api#aontu-breaking).

The live version is [use-cases/04-schema-evolution](https://aontu.dev/use-cases/04-schema-evolution): three released versions of a customer profile, every verdict above asserted by its `check.sh`, the `git#HEAD` gate included. When a change lands, [pin the new meaning](https://aontu.dev/how-to/pin-a-document-hash) so consumers can tell it moved.
