Audience: engine / storage / release maintainers Status: living document
Omnigraph has four independent version axes. They have different compatibility contracts because they fail in different ways and at different costs. Conflating them (for example, treating a storage-format change like a wire change) is how you either ship an unsafe silent-misread or carry migration code you do not need.
| Axis | Policy | Mechanism |
|---|---|---|
| Release (semver) | All published crates move in lockstep. | Maintenance-contract rule 4 in AGENTS.md: a release bump updates every crate manifest, Cargo.lock, openapi.json, and the surveyed version line together. |
| CLI ↔ server wire | Additive and rolling-safe; no version gate. New fields are optional; old clients ignore unknown fields and omit new ones. | Additive JSON DTOs in omnigraph-api-types; the OpenAPI-drift test (crates/omnigraph-server/tests/openapi.rs) catches an unintended wire change. |
| Storage (internal manifest schema) | Strict single version; upgrade is a cutover via export/import, never an in-place migration. | A stamp (omnigraph:internal_schema_version) in __manifest's schema metadata + refuse_if_stamp_unsupported, with MIN_SUPPORTED == CURRENT. |
| Lance on-disk format | Pinned to one Lance version; bumped deliberately with the engine. | data_storage_version: V2_2 at every write site + the surface guards in lance.md, re-run on every Lance bump. |
The internal-schema stamp gates the on-disk shape of __manifest. The contract is:
this binary reads exactly one internal-schema version. Omnigraph::open (both
read-write and read-only) reads main's stamp before any data and refuses anything
it cannot serve:
- a stamp below CURRENT → refused with a rebuild-via-export/import message (see the upgrade guide);
- a stamp above CURRENT → refused with "upgrade omnigraph", so an old binary cannot silently misread a newer format.
There is no in-place migration dispatcher. The single source file
db/manifest/migrations.rs holds only the version constant, the stamp read/write,
and refuse_if_stamp_unsupported.
This is a liability decision, not a limitation we have not gotten around to. In-place
migration code is permanent surface: every future format change has to write,
test, and keep working a vN → vN+1 step, plus the legacy readers and crash-recovery
paths each step needs, for a storage format that is still pre-release and changing.
The strand model trades that ongoing cost for a one-time operator action (export +
import) when a format changes. Per "engineering is programming integrated over time"
(see AGENTS.md), the lower-liability option is to not carry
the machinery until a concrete graph demands it.
The stamp + refuse_if_stamp_unsupported floor is exactly the seam a future in-place
migration would re-introduce: re-add a dispatcher and lower MIN_SUPPORTED below
CURRENT for the versions it can actually walk forward. Until then that machinery is
deliberately absent.
The stamp is validated at the graph (main) level: Omnigraph::open checks main
once, and branch reads trust it. The stamp is a graph-wide storage-format property
(the upgrade path is a whole-graph export/import), so with one binary version every
branch is always CURRENT — init stamps main, create_branch forks the stamp, and the
publisher writes rows without re-stamping. A branch stamped out of range while main
stays in range is only reachable with concurrent multi-version writers, an
unsupported topology; the residual is recorded as a known gap in
invariants.md.
The CLI↔server boundary is the opposite case: clients and servers are deployed independently and a hard gate there would force lockstep redeploys for every field addition. So that axis is additive — old and new coexist — and the OpenAPI-drift test is the guard that a change stayed additive rather than breaking the shape.
- Storage format: bump
INTERNAL_MANIFEST_SCHEMA_VERSION, keepMIN_SUPPORTED == CURRENT(unless you are re-introducing migration), update the stamp history on the constant's doc-comment, and add a release note pointing at the upgrade guide. The change is breaking by construction — pre-bump graphs are refused. - Wire: keep it additive; regenerate
openapi.json(OMNIGRAPH_UPDATE_OPENAPI=1); do not add a version gate. - Lance: follow the Lance-bump checklist in lance.md — re-run the
surface guards first, then
cargo test --workspace(a clean build is not a clean alignment). - Release: lockstep per the maintenance contract.