aontu

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 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:

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:

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

Now ask whether the proposal breaks anybody:

$ 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:

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

$ 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:

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

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

- 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.

The live version is 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 so consumers can tell it moved.