Skip to content

Schema compatibility — spec versions, release tags, and what refuses what

OpenRegs carries two independent version axes, and keeping them independent is a design rule rather than an accident:

axisversions bywhat it sayswhere it is written
spec/semver MAJOR.MINOR.PATCHthe shape of the data: JSON Schemas, taxonomies, the fields a release manifest carriesspec/VERSION
a content releasedate tag YYYY.MMwhen the law was read — fixreg@2025.04 is the canon as of 2025-04-01the release’s directory name and its release-meta.yaml tag

Neither axis may drag the other. Bumping spec/ must not invalidate a release that was already published and signed; cutting a new release must not require a spec bump. A consumer therefore never asks “is this release new enough?” — it asks “is this release shaped like something I can read?”, and the two questions have different answers.

Every release declares what it was built under. release-meta.yaml carries two fields, written by openregs release build and never by hand:

schema_version: 1.0.0 # the spec version at the commit this was built from
spec_schema_range:
min: 1.0.0 # the oldest spec version that can read it
max: 2.0.0 # exclusive: the first that cannot

schema_version is read out of spec/VERSION at the commit being built, not from the checkout doing the building. That is what decouples the axes: rebuilding fixreg@2025.04 from its own commit on a machine whose spec/ has moved on since reproduces the bytes that release shipped, and a spec bump never restamps — and so never invalidates the digests and signatures of — a published release.

spec_schema_range is derived from that version by openregs.compat.range_for(): the floor is the first version of the major, not the version it was built under, because minors are additive. A release built under 1.1.0 is readable by a consumer implementing 1.0.0; writing min: 1.1.0 would be a release claiming a break that did not happen.

Every consumer declares what it can read. SUPPORTED_SCHEMA_MAJORS in tooling/openregs/compat.py is the CLI’s and the server’s own statement — today (1,). There is deliberately no minor-level or patch-level constraint anywhere in that module: within one major, a consumer accepts every release, which is what makes “a minor bump cannot break a consumer” true by construction rather than by promise.

openregs.compat.check_document() is the one gate, and it refuses on either of two independent statements:

  1. the consumer’s — the release’s declared major is not in SUPPORTED_SCHEMA_MAJORS;
  2. the release’s — this build’s own spec version falls outside the spec_schema_range the release declares.

The refusal names both versions and points back at this document. It is a non-zero exit, never a warning and never a partial read:

$ openregs release verify fixtures/releases/future-schema
release verify: release fixreg@2099.01 at fixtures/releases/future-schema/release-meta.yaml
declares spec schema_version 2.0.0, which this build of openregs cannot read: it
implements spec 1.2.0 and reads releases in 1.0.0 <= schema_version < 2.0.0. ...
see docs/schema-compatibility.md
$ echo $?
2

The gate runs first, before digests, before signatures, before a row of the bundle is read — including under openregs serve --insecure-skip-verify, because that flag waives trust and no amount of trust makes an unreadable shape readable. Nothing is weakened by that ordering: a release that passes the gate goes on to exactly the verification it would have had.

Where the gate sits, plane by plane:

consumerentry point
openregs release verify_run_release_verify
openregs diff, openregs impact, openregs bump, the feedopenregs.release.published_meta()
openregs serve, openregs serve --mcp, openregs evalopenregs.serve.startup.verify_for_serving() and ServedRelease.from_directory()
openregs pullopenregs.release.pull.pull_release(), before a byte is staged
openregs compileopenregs.codegen._checked()
openregs index verifyopenregs.combinations, against the combination’s own spec_schema_range
the release builderopenregs.release.build._spec_schema() — it refuses to build a canon whose spec major it does not implement, which is the same rule pointed the other way

A release refuses to load and names a major above yours. Your build is older than the release. Upgrade openregs to a build whose SUPPORTED_SCHEMA_MAJORS includes that major; there is no flag that reads it anyway, because a major bump means the shapes changed meaning and a half-understood obligation is worse than none. Until you upgrade, the previous release of that regime is still readable — releases are never retracted by a spec bump.

A release refuses to load and names a major below yours. Your build dropped support for a shape that release still uses. Read it with the build it was published for, or re-cut the release from its canon commit with the current tooling.

You are bumping spec/ yourself.

  • Minor or patch — additive only: new optional fields, new schema files, new taxonomy entries. Edit spec/VERSION, make the change, and stop. No consumer changes, no release is rebuilt, no already-published release moves. Releases cut after the bump declare the new version and old consumers keep loading them.
  • Major — a field changes meaning, is removed, or becomes required. Bump spec/VERSION’s major, then add the new major to SUPPORTED_SCHEMA_MAJORS in the same change, and say in this document what moved. Published releases keep declaring the major they were built under; a build that supports both majors is what makes the transition survivable.
spec versionwhat changed
1.0.0the initial published shapes: atoms, unit metadata, index combinations, controls, sources, trust, deprecations, embeddings, entity types
1.1.0additivespec/VERSION written down, and spec/schemas/release-meta.schema.json added so the release manifest’s own shape is spec’d rather than implicit. No existing document becomes invalid; consumers built against 1.0.0 read 1.1.0 releases unchanged
1.2.0additive — the optional licenses block in the release manifest: {code: Apache-2.0, data: CC-BY-4.0}, the licensing split stated where a downstream packager can read it. Not required, so a release cut before it declares nothing and stays valid; the three standing releases were rebuilt from their own commits to carry it, which changed only release-meta.yaml

Rendered from openregs/openregs@cbe9615:docs/schema-compatibility.md