341 lines
18 KiB
Markdown
341 lines
18 KiB
Markdown
# 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](../core-contract.md)
|
||
carries; discharges the version-coordination duty
|
||
[ADR-001](001-crate-split.md) 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](008-contract-v1-pinning.md)'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](013-fold-substrate-into-sqlite.md)) 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](016-deployment-honesty.md)): 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](001-crate-split.md) §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](012-forked-substrate-design.md) §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](../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](008-contract-v1-pinning.md)) — 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](015-streams-depth.md) §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](018-provenance-register-and-cherry-picks.md), 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](../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](001-crate-split.md) §4's posture for test artifacts).
|
||
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](001-crate-split.md) — 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](008-contract-v1-pinning.md) — 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](009-scheduler-collapse.md) — the first extension shape
|
||
(its negative consequence named this discipline "still open");
|
||
§2's row-first extension path.
|
||
- [ADR-010](010-queue-semantics-depth.md) — the `QueueOpts`-depth
|
||
extension; its stamped-fields pattern is the opts addition §3's
|
||
`#[non_exhaustive]` rule anticipates.
|
||
- [ADR-012](012-forked-substrate-design.md) — §2's one-normative-
|
||
owner rule (§1) and the contract-blind boundary that makes
|
||
substrate changes structurally non-events (class 4).
|
||
- [ADR-013](013-fold-substrate-into-sqlite.md) — the fold that
|
||
removed the substrate's versioning surface entirely.
|
||
- [ADR-014](014-outbox-tx-enqueue.md) §4 /
|
||
[ADR-015](015-streams-depth.md) §2 — the amend-in-place
|
||
precedents ("no versioning event, pre-implementation") this ADR
|
||
codifies and terminates at first release.
|
||
- [ADR-016](016-deployment-honesty.md) — 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](018-provenance-register-and-cherry-picks.md) (2026-10-06).
|
||
- `docs/research/consumer-inventory.md` — the row-first gate on
|
||
extensions (class 2/§2's closing rule).
|