Schema versioning policy
Cofferdam exposes three schemas to projects and plugins. All three will evolve. Without an explicit versioning discipline, every engine release risks silently re-interpreting a long-lived spec or rule file in a way the project did not consent to. This document is the contract that prevents that.
The three schemas
| Schema | Surface | Owner |
|---|---|---|
cofferdam.invariants.toml | The project architecture spec — layers, boundaries, invariants, public API | cd-9hp.12 (this doc) |
| Canonical graph | Emitted by adapters, consumed by rules | cd-T1 / cd-9hp.9 |
| Predicate DSL | Surface syntax for scripted rules inside cofferdam.invariants.toml | cd-9hp.1 |
Each is versioned independently. They share the same policy, defined below.
Version format
MAJOR.MINOR. Semver-flavoured but only two components — there is no PATCH because schemas don't carry implementation bugs.
Accepted on the wire as either:
- an integer (
schema_version = 1) — treated asMAJOR.0 - a string (
schema_version = "1.0",schema_version = "1.2")
The canonical form on serialisation (e.g. via cofferdam advise or any future cofferdam invariants normalize) is the string form.
When to bump
- MINOR — additive, backward-compatible changes. New optional fields. New node/edge types in the canonical graph. New DSL predicates. A spec written for an older MINOR continues to load unchanged.
- MAJOR — breaking changes. Removed fields. Changed semantics for an existing field. Renamed predicates. Default-value changes that would silently re-interpret existing specs.
A MAJOR bump must also:
- Extend
MIN_SUPPORTED_SCHEMA_VERSIONonly after the previous MAJOR has been deprecated for at least 2 MAJOR releases (see below). - Ship a documented migration recipe — manual instructions at minimum,
cofferdam invariants migrateif the change can be mechanically applied. - Add a CHANGELOG entry under
### Schema changesdescribing the break and pointing to the migration recipe.
Deprecation window
A version v is supported when MIN_SUPPORTED ≤ v ≤ CURRENT.
Within that window:
v == CURRENT— no action.MIN_SUPPORTED ≤ v < CURRENT— accepted, with a one-time deprecation hint emitted by the engine. The user's rules apply unchanged.
Outside the window:
v > CURRENT— rejected with: "schema_version X.Y exceeds this build's maximum supported version (CURRENT); upgrade cofferdam or pin the spec to a version your build understands."v < MIN_SUPPORTED— rejected with: "schema_version X.Y is no longer supported by this build (minimum supported is MIN_SUPPORTED); runcofferdam invariants migrateagainst an older cofferdam release or update the spec to a supported version."
Recommended window sizes:
- MINOR bumps — no deprecation (additive changes are backward-compatible by construction). All MINORs of the current MAJOR are supported.
- MAJOR bumps — keep the previous MAJOR in the support window for at least 2 MAJOR releases past it. That is, when
vN+1.0ships,vNstays accepted (with a hint) untilvN+3.0ships, at which pointvNfalls out of the window andMIN_SUPPORTEDbecomesvN+1.0.
The window can be longer, never shorter. Tightening below 2 MAJORs breaks the contract.
Missing fields
A spec without an explicit schema_version field is treated as CURRENT_SCHEMA_VERSION for backwards compatibility with the v0 surface that predates this policy. The engine emits a one-time hint encouraging the user to declare the field explicitly. From the first MAJOR bump onward, projects should treat the explicit declaration as required hygiene — relying on the implicit default leaves them exposed to a silent re-interpretation at the next MAJOR.
Today's policy
pub const CURRENT_SCHEMA_VERSION = SchemaVersion { major: 1, minor: 0 };
pub const MIN_SUPPORTED_SCHEMA_VERSION = SchemaVersion { major: 1, minor: 0 };For cofferdam.invariants.toml today: only 1.0 is accepted; any other declared value is rejected. The canonical graph and predicate DSL inherit the same policy once they ship (cd-T1, cd-9hp.1).
What is versioned and what isn't
Versioned:
- The TOML field set on
cofferdam.invariants.toml. - The shape of nodes and edges in the canonical graph.
- The surface syntax of the predicate DSL.
Not versioned (intentionally):
cofferdam.tomlper-check options. Each option's stability is governed by the owning check'sCheckMeta; the option layer never re-interprets a long-lived value.- The
Issue/FindingJSON output schema. This is additive-only and documented underdocs/public/checks.json's ownschema_versionfield — see the gen-docs pipeline. - Internal Rust APIs. Not a user-facing surface.
Implementation reference
The reference implementation lives in crates/cofferdam-core/src/invariants.rs:
SchemaVersion— theMAJOR.MINORtuple, withparse_strandDisplay.CURRENT_SCHEMA_VERSION,MIN_SUPPORTED_SCHEMA_VERSION— constants bumped on each release.validate_version(declared, current, min_supported) -> VersionCheck— pure function exercised across the full matrix in unit tests so the deprecation-window logic is testable today, before MAJOR=2 of any schema exists.InvariantsError::{FutureSchemaVersion, UnsupportedSchemaVersion, MalformedSchemaVersion}— actionable error variants withis_fatal()returningtrue, so the engine fails loudly rather than silently ignoring the spec.
The canonical-graph and DSL halves of this policy ship with their own beads (cd-T1 / cd-9hp.9 for the graph; cd-9hp.1 for the DSL). Both should reuse SchemaVersion, validate_version, and the same deprecation policy described here — same trio of error variants, same loudness contract.
Release process
When a schema-touching change lands:
- If MINOR — bump
CURRENT_SCHEMA_VERSION.minor. - If MAJOR — bump
CURRENT_SCHEMA_VERSION.major, reset.minor = 0, write the migration recipe, and (only if the previous MAJOR has been deprecated for at least 2 MAJORs) bumpMIN_SUPPORTED_SCHEMA_VERSION. - Update every fixture in
crates/cofferdam-engine/tests/spec_contract/to declare the new version. - Add a
### Schema changesblock to the CHANGELOG entry for the release; link the migration recipe.
A CI gate that fails the release when a schema-touching commit forgets to update the relevant version constant is tracked separately (see the TODO at the bottom of this file).
TODO
- CI gate: fail the release workflow if any of (a) the TomlDoc struct, (b) the canonical-graph schema module, (c) the DSL parser change without a matching version constant bump and CHANGELOG entry. Tracked alongside cd-9hp.12; not yet implemented.
cofferdam invariants migrate <input>— one-shot migration tool. Stub until the first MAJOR bump makes it earn its keep.