ADR-017: contract versioning — core crate's semver is the contract version (OQ-10 resolved)
This commit is contained in:
1 parent
8c8fec5cb8
commit
eba77909a7
9 files changed
+473
-51
No files matched your search
@@ -0,0 +1,338 @@
|
||||
# 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 —
|
||||
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).
|
||||
- `docs/research/consumer-inventory.md` — the row-first gate on
|
||||
extensions (class 2/§2's closing rule).
|
||||
Reference in new issue
Block a user