Skip to content
VERASPEC
Repository
ADR-0001Accepted

Two-plane versioning

Status

Accepted — 23 August 2026. Implemented in `VERSIONING.md`, the 1.0.1 schema $id, and the standards/ver/<release>/ layout.

Context

VER 1.0 shipped on 21 August 2026 as four artifacts — a specification document, a JSON Schema, a golden example, and a wire-profile lexicon — and two version fields inside every Record: ver_version and cpnp_version.

Two days later a standards audit produced 189 findings. Most needed nothing but corrected prose. Three needed a schema constraint the prose had always required but the schema never enforced. Four needed the golden example's self-referential digests to be true. None needed a new field.

That set of corrections has to be published somehow, and the publication mechanism forces a versioning question with a wrong answer that looks obvious. The wrong answer is to bump ver_version to "1.0.1", because that is what "we published a corrected version" feels like. It would break every deployed consumer — ver_version is a const in the schema — in exchange for communicating something no consumer needs to know.

Three concrete pressures made the question urgent:

  • The 1.0.0 schema $id is …/ver-record-1.0.json, with no patch component (finding SCH-19). A corrective schema either reuses that identifier — silently changing what conformance means for every cached consumer — or takes a new one.
  • The audit's most-cited defect class is prose ambiguity in CPNP-1. Any change to CPNP produces different Canonical Buffers for some inputs, and therefore a different pixel_hash, with no signal to a consumer comparing hashes across the change.
  • The specification is a .docx. There is no mechanism for editing a published document in place that does not also destroy the ability to say what was published.

Options

(a) One version plane: bump ver_version for every publication. Simple to explain, and every published artifact set is identified by exactly one number. It also breaks every consumer on every typo fix, because ver_version is what a consumer dispatches on. Records would declare "1.0.1" while being byte-compatible with "1.0" parsers that now reject them.

(b) One version plane: never bump anything; publish corrections in place. No consumer breaks, because nothing appears to change. Also nobody can say which bytes they validated against, a published $id serves different content over time, and the frozen artifact set is a fiction. This is the failure mode the $id policy exists to prevent.

(c) Two planes: a wire version that describes record shape, and an artifact release version that describes the published file set. More to explain — the release version and the record's own version differ, which looks like a bug until it is documented. In exchange, corrective releases become possible without touching a single deployed consumer, and every published byte stays addressable.

Decision

Option (c). VER carries two independent version planes.

The wire plane is what a Record declares about itself:

  • ver_version identifies the record format. It moves only when the format gains or changes fields. It stays "1.0" for artifact releases 1.0.0 and 1.0.1; 1.1 Records will carry "1.1".
  • cpnp_version identifies the pixel_hash universe. It stays "1.0" for the entire VER 1.x line. Any CPNP change is CPNP-2, and CPNP-2 is VER 2.0 with a dual-publish migration window.

The artifact plane is semver over the published file set: 1.0.0 (frozen), 1.0.1 (corrective), 1.1.0-draft (additive, unratified). Each release lives in its own directory under standards/ver/, carries a RELEASE.md recording the SHA-256 of every file in it, and is never modified after publication.

Two supporting rules follow directly and are not separable from the decision:

  1. Schema $ids are never reused. 1.0.1 takes …/ver-record-1.0.1.json; the 1.0.0 identifier keeps serving the 1.0.0 bytes forever.
  2. The specification document is never edited. Corrections ship as a normative annex over the published text. Where annex and published text disagree, the annex governs.

Consequences

Good.

  • A consumer written against 1.0.0 parses every 1.0.1 Record without a code change. This is not a hope; it is a property of ver_version not moving.
  • Corrections become cheap. There is no reason to defer a prose fix, so prose fixes stop accumulating into a version nobody wants to publish.
  • Every published artifact stays addressable and verifiable. "Which bytes did you validate against?" has an answer.
  • The CPNP invariant is stated once, in the only place that can enforce it, and every CPNP-touching errata item is forced to be a disambiguation rather than an amendment. That constraint materially improved the 1.0.1 annex: each CPNP item had to side with observed producer behaviour or be rejected.

Bad.

  • The two planes will be confused. A release labelled 1.0.1 publishes Records that declare "1.0", which reads as an error to anyone who has not read this document. Mitigated by stating it in VERSIONING.md §1, in the annex's own §1, and in the migration guide's first paragraph — three places, deliberately.
  • The artifact release version is not visible in a Record. Given a Record alone, you cannot tell whether its producer had read the 1.0.1 annex. This is accepted: the alternative is a field that changes what consumers dispatch on, and the annex changes no Record shape, so the information has no consumer.
  • standards/ver/ duplicates spec/. The freeze copies rather than moves, because one load-bearing code path resolves the schema by relative path (FREEZE-SCHEMA-PATH) and published prose cites spec/. Two locations for the same bytes is a hazard, mitigated by recorded digests in RELEASE.md and by a test that asserts the copies still match.

Neutral.

  • The wire profile (VER-F) versions independently again, on a third clock. Its lack of any versioning mechanism (WIRE-15) is a real defect, but fixing it is a wire-profile major change and has no Record-plane impact.