Files
alkstore/docs/architecture/decisions/017-contract-versioning.md
T

18 KiB
Raw Blame History

ADR-017: Contract versioning discipline — the core crate's semver is the contract's semver

Status

Accepted (2026-10-06, Phase 1 — OQ-10's resolution; governs the versioning of the surface core-contract.md carries; discharges the version-coordination duty ADR-001 records as its negative consequence)

Context

The crate split makes the core crate's trait surface a contract the engine crates must track — ADR-001's one structural negative consequence ("version bumps in core must be adopted by engines in lockstep when the contract changes"). OQ-10 framed three candidate mechanisms for how: semver-bump-only-when-contract-changes, engines pinning core ranges, and a contract-compatibility test suite the engines run against the core's trait definitions — plus the direct question: what happens to a released engine crate when core makes a contract breaking change?

Much has been decided since the OQ was framed, and it narrows and feeds the answer:

  • The contract's content is no longer just v1. ADR-009 and ADR-010 added the first post-v1 extensions (the scheduler collapse surface; the QueueOpts depth) — ADR-009's negative consequence explicitly deferred their versioning discipline to OQ-10, and ADR-010's depth rode the same extension path (ADR-008's framing). ADR-014 and ADR-015 created the other precedent class: amend-in-place, pre-implementation — both framed "no versioning event, OQ-10 untouched." The discipline must codify both frames and draw the line between them.
  • The fold (ADR-013) removed a would-be versioning surface. The forked substrate is a module subtree of alkstore-sqlite — unpublished, path-invisible, with no crate identity of its own. It has no version number to track anything with; cherry-picks ride the engine crate's own semver and OQ-11's provenance procedure. The pairing the discipline must track is the engine/core pairing, full stop.
  • The capability surface is gone (ADR-016): no descriptor struct that contracts would have to keep honest across versions — one less surface class for this discipline to govern.
  • No artifact is released. No crate exists; the pinned text (ADR-008 through ADR-016, and the specs it carries) is entirely pre-implementation. The discipline is therefore forward policy — every claim it makes about released engines is governable from day one, and nothing needs retrofitting.
  • One structural fact shapes the whole answer: the core crate is a contract artifact and nothing else (alkstore — "the unified trait surface, types, error model, and the contract documentation. No driver dependencies," ADR-001 §1 as annotated by ADR-016). A core release cannot be motivated by anything but a contract change: the crate has no other behavior to ship.

Decision

1. The core crate's semver is the contract version — no parallel versioning surface

There is no separate contract-version register, no version descriptor API, no compatibility-matrix document: the core crate's version number is the contract version, because the crate is nothing but contract. This is the one-normative-owner rule (ADR-012 §2) applied to the version question — a second ledger would be a second normative home for "which contract is this," exactly the drift surface ADR-016 rejected in the runtime-descriptor case. A dependency edge states the contract pairing; it cannot drift from the truth it states.

Concretely:

  • The core crate's changelog is the version ledger: every contract change entry cites its ADR (the "no silent contract change" rule, §2 below makes the citation mandatory).
  • core-contract.md is the content of record for the current contract text; the ADR table there is the change history. Neither duplicates the version number as a normative field.
  • The coupling binds fully at the core crate's 1.0 (contract major ⇔ crate major). The initial release is 1.0.0 — contract v1's content (ADR-008 through ADR-017's text, the class-1 additions included) is crate major 1, so the identity holds at the moment it binds rather than being reached by accumulation. Pre-1.0 (the contingency of interim dev releases, not the planned path) follows Cargo's 0.x semantics: a contract breaking change bumps 0.y; additions bump 0.y.z — at 1.0 the count re-bases: contract v1's breaking history is exactly the majors 1.x accumulates from. The pinned text's name for the initial surface — contract v1 (ADR-008) — is a content designation, not yet a semver claim.

2. The event taxonomy: four classes of contract change

Every change to the contract text is exactly one of these, and classes 1–3 name their ADR in the core crate's changelog entry (no silent contract changes — a contract change without an ADR and a changelog citation is a process defect, the guard this discipline most depends on because semantic breaks are not compiler-visible, §5; class 4's doc clarifications carry no ADR duty — that is what makes them non-events).

  1. Pre-implementation amend-in-place (the ADR-014/015 precedent) — completing a pinned-but-under-pinned surface, before the core crate first publishes. No versioning event, because there is no versioned artifact for the change to be an event on. This class terminates permanently at the core crate's first release: once any consumer can pin the contract text by a version number, "the contract changed under the same number" stops being available. ADR-009/ADR-010, though framed as post-v1 extensions, fall in this class in fact — nothing was released when they were made, so their additions ship inside the initial contract v1 text (the initial release carries ADR-008 through ADR-017 whole).
  2. Additive extension (the ADR-009/010 shape, applied post-release) — a new trait method, a new type, a new error variant, a new field on a consumer-read value type (§3's #[non_exhaustive] class), a new guarantee row, or a new contract-suite row pinning new behavior. Core bumps its minor (pre-1.0: patch). Existing correct consumers are untouched; §5 carries the engine-side consequences. (Opts-struct field additions do not ride this class — §3 exempts those structs, pricing their field additions class 3.)
  3. Breaking change — any signature change, removal, or rename (cutting a pinned v1 method name was already ruled a contract- breaking event, ADR-015 §1's option-(c) reasoning), and any semantics change a correct consumer's behavior could depend on: weakening a guarantee row, changing a pinned contract constant (the 64-cap, the 1-hour backoff cap, the 8000-byte pg payload limit), changing a variant's meaning or production conditions. Compile-compatible is not the test — the act-differently measure is (ADR-008 §5's rule, generalized exactly as ADR-016 §2 generalized it to surface): if consumer code that was correct before the change behaves differently after, the change is breaking regardless of whether it compiles clean. Core bumps its major (pre-1.0: its 0.y).
  4. Non-events — engine-internal changes (engine opts, derived names, substrate cherry-picks per OQ-11's procedure — ADR-018, resolved: contract-blind by ADR-012 §2, so they cannot be contract events by construction); and doc clarifications only where a correct implementation could not have behaved differently before them (the conservative inverse of the act-differently measure: if it could have, it is class 3, not a clarification).

Extensions and breaking changes additionally obey the row-first discipline both ADR-009 §2 and ADR-015 §1 applied: a consumer- inventory row naming the need precedes the extension (breaking changes have their ADR-008 §5-rule justification instead — the change exists because the pinned text was wrong, which no row names).

3. #[non_exhaustive] on the consumer-read contract types — opts structs exempt

Class 2 is only mechanically additive if Rust's rules agree, and Rust's rules split the value types by who constructs them. Pinned: #[non_exhaustive] on the types consumers read but never construct — the top-level Error, StreamEvent, Job, Schedule, and Wake — so field and variant additions there are semver-minor under Cargo's rules, not major. On Error this also makes the taxonomy's fallback posture contractual: consumers matching Error cannot match exhaustively — the opaque Database-carrying catch-all the act-differently rule designed is the required shape, mechanically enforced. (Job's dead-only last_error/died_at fields — the ADR-010 §1 addition — are this class's demonstrated growth surface, now made mechanically minor.)

The opts structs (EnqueueOpts, QueueOpts, ScheduleOpts) are deliberately not #[non_exhaustive]: consumers construct them (the pinned v1 field sets, ADR-008 §1, carried whole), and #[non_exhaustive] bans struct-literal construction of the type from outside the defining crate — functional-update syntax included (E0639). Pinning it would silently remove the pinned construction posture for every enqueue/queue/schedule call — itself a semantics change by this ADR's own class-3 measure. The honest cost, stated: an opts field addition post-release is a class 3 breaking change (major) — heavier than a minor for "one more field," and priced that way deliberately; the row-first gate keeps such additions rare (the v1 field sets are complete, and ADR-010's depth is in them; a consumer row must name the need, and the field must earn it).

Trait methods remain the additive class Rust's rules cannot paper over (adding a method breaks every impl; #[non_exhaustive] does not cover trait items). That cost is carried openly, not hidden: the lockstep duty, §5.

4. The pairing: how an engine crate declares what it implements

The engine/core contract pairing is tracked by three carriers of one fact (the ADR-016 pattern — statements at the right altitude, not a runtime descriptor):

  1. The manifest pin: each engine crate declares alkstore = "1.y" (caret-within-major; pre-1.0, 0.y) — the dependency edge stating the contract version the engine's code implements. Cargo's resolution makes cross-major mixing a compile error rather than a runtime surprise.
  2. The contract suite: the verification backlog (core-contract.md §Verification backlog) matures into the contract suite — the compatibility instrument (the OQ's option (c)), not a dev convenience. An engine release claiming contract vN runs the suite's vN rows; each row is version-stamped with the contract change that added it — the same no-silent-change discipline (§2's ADR-citation duty is what the stamp records), which is what makes the suite auditable against the changelog. The suite's layout — shared crate, workspace test target, or per-engine module — is a test-side convenience decided at implementation (ADR-001 §4's posture for test artifacts). (Layout resolved 2026-10-07: ADR-022 — the shared internal alkstore-contract-suite crate; engines take it as a dev-dependency and drive the rows against their own stores via a StoreFactory the suite defines.)
  3. Engine-crate docs: each engine's docs state the contract version(s) it implements — the same standing-statement altitude as its deployment posture (ADR-016 §3's carrier 2).

The engine crate's own semver stays independent — it versions the engine's own surface (constructors, opts). A contract adoption release is a normal engine release; nothing in the engine's version number must encode the contract version (its manifest pin does, §4.1).

5. Lockstep duties — and what a breaking change does to released engines

  • Contract minor (class 2): both engine crates adopt by release before-or-with the core minor's announcement — mandatory for new trait methods (their impls must exist or downstream builds of the engine against the new core fail), and the suite's new rows do not go green without the adoption either way. A consumer feels class 2 as available engine updates; the consumer adopts when it wants the new surface. An engine not yet adopting stays a coherent crate against its last-verified core minor, but note the mechanics: a consumer's unpinned caret requirement resolves to the newest compatible core minor — against which a method-method-addition non-adopter fails to compile. That compile failure is the adoption duty's enforcement, not a consumption guarantee; the grace window holds for consumers whose graphs deliberately resolve the previous minor (a pinned requirement or a lockfile), and for additions that are not trait methods (new opts never ride trait additions; new suite rows test what the engine already shipped).
  • Contract major (class 3): nothing is forced on released engines. A released engine crate keeps its pinned core major range; consumers keep working; the engine's docs record the contract majors it serves. The new major gets new engine majors (implementing it, per §4's carriers), and consumer migration is a deliberate two-line dependency bump — the single-driver design (ADR-001) makes the whole move visible in one place. This is the OQ's direct question, answered: the released engine serves its pinned contract majors until archived; "what happens" is that the ecosystem gains a new parallel line, and no released artifact has anything pulled out from under it.
  • The lockstep duty's edge is stated, not smoothed (ADR-001's "no trait-default drift" line), on both sides the mechanics actually allow: core must not ship class-2 additions as default-bodied trait methods (a failing default would make the addition optional for engines to implement and silently non-additive for consumers — the exact drift the lockstep duty exists to prevent; additions are required methods), and engines must not stub required surface with error-returning impls to fake adoption.

Consequences

Positive

  • The versioning machinery is nothing new: no descriptor, no register, no compatibility API — the discipline rides the artifact graph (crate version, manifest edge, suite, changelog), the same compile-time-honesty posture as ADR-016. A dependency edge is the pairing statement.
  • The two precedent classes are codified with their boundary: amend-in-place has a termination line (first release); the extension shape has a mandatory-adoption rule. ADR-009/010 are correctly understood as pre-implementation events; ADR-014/015's "OQ-10 untouched" framing is vindicated and bounded.
  • #[non_exhaustive] makes the read-type additive-minor rule mechanically true and mechanically enforces the catch-all matching posture the taxonomy already mandated in prose; opts-field additions are honestly priced as breaking (class 3) rather than smuggled in as minor.
  • The suite graduates from backlog to compatibility instrument with a version discipline — the thing that makes "engines still implement the contract" an auditable claim rather than an intention.

Negative

  • Contract minor bumps carry a real (if small) maintenance cost: both engines must publish adoption releases — mandatory for trait methods (non-adopter engines fail to compile against the new core), and suite-row readiness effectively demands it for the rest. This is the ADR-001 negative consequence made concrete, not removed.
  • Semantic breaks ride semver only through this discipline's honesty rule — the compiler cannot see a weakened guarantee row. The guards are review, the no-silent-contract-change rule, and the suite's version-stamped rows; a dishonestly-classified change is a process defect, not a caught one.
  • Pre-1.0, the coupling is looser (0.y bumps for breaks) — the usual pre-release latitude, bounded at 1.0.

References

  • OQ-10 (docs/architecture/open-questions.md) — this ADR's resolution; the three mechanisms it framed compose here (semver discipline → §1/§2, range pins → §4.1, the suite → §4.2), the house pattern of options-are-one-posture (ADR-016 §1).
  • ADR-001 — the crate split and its version-coordination negative consequence this ADR discharges; §1's core-crate contents list (contract artifact, and nothing else); §4's test-artifact posture the suite's layout defers by.
  • ADR-008 — contract v1 (the content designation §1 keeps); §5's act-differently rule, generalized twice (surface by ADR-016 §2, change classification by this ADR's class 3).
  • ADR-009 — the first extension shape (its negative consequence named this discipline "still open"); §2's row-first extension path.
  • ADR-010 — the QueueOpts-depth extension; its stamped-fields pattern is the opts addition §3's #[non_exhaustive] rule anticipates.
  • ADR-012 — §2's one-normative- owner rule (§1) and the contract-blind boundary that makes substrate changes structurally non-events (class 4).
  • ADR-013 — the fold that removed the substrate's versioning surface entirely.
  • ADR-014 §4 / ADR-015 §2 — the amend-in-place precedents ("no versioning event, pre-implementation") this ADR codifies and terminates at first release.
  • ADR-016 — the capability-surface rejection that narrowed this discipline's scope (no descriptor to version); its three-carriers pattern (§4).
  • OQ-11 — the fork-scaffold residue whose cherry-pick procedure governs the substrate-side non-events (class 4) — resolved by ADR-018 (2026-10-06).
  • docs/research/consumer-inventory.md — the row-first gate on extensions (class 2/§2's closing rule).