diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 0700efb..5126b79 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-06 +last_updated: 2026-10-06 (ADR-017; OQ-10 resolved) --- # alkstore — Architecture @@ -24,7 +24,7 @@ pending architecture review and OQ resolution. | Doc | Status | Purpose | Key OQs | |---|---|---|---| | [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — | -| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-10 | +| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-10 (resolved) | | [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12 (resolved), OQ-13 (resolved) | | [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-08 (resolved), OQ-12 (resolved), OQ-13 (resolved) | | [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) | @@ -51,6 +51,7 @@ pending architecture review and OQ resolution. | [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue — `outbox_enqueue_tx` on the `TxHandle` trait | Accepted | | [015](decisions/015-streams-depth.md) | Streams depth — carried-metadata keys, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` | Accepted | | [016](decisions/016-deployment-honesty.md) | Deployment honesty — no runtime capability surface; compile-time engine identity + documented matrix | Accepted | +| [017](decisions/017-contract-versioning.md) | Contract versioning — core crate's semver is the contract version; change classes, pairing carriers, lockstep duties | Accepted | ## Open Questions @@ -59,7 +60,6 @@ Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors OQ-ST-NN — with new Phase 1 questions appended after). Open, in suggested resolution order: -- **OQ-10** (medium): contract versioning across engine crates. - **OQ-11** (medium): forked-substrate follow-through (provenance register format, cherry-pick procedure; item (1) dissolved by [ADR-013](decisions/013-fold-substrate-into-sqlite.md)). @@ -77,7 +77,9 @@ scheduler guarantee row pinned), ~~OQ-05~~ (queue semantics depth — [ADR-016](decisions/016-deployment-honesty.md)), ~~OQ-13~~ (transactional outbox enqueue shape — [ADR-014](decisions/014-outbox-tx-enqueue.md)), ~~OQ-12~~ (streams -depth — [ADR-015](decisions/015-streams-depth.md)). +depth — [ADR-015](decisions/015-streams-depth.md)), ~~OQ-10~~ +(contract versioning — +[ADR-017](decisions/017-contract-versioning.md)). No deferred OQs: all open questions are actionable Phase 1 work with complete evidence bases. diff --git a/docs/architecture/core-contract.md b/docs/architecture/core-contract.md index a55561c..3846a89 100644 --- a/docs/architecture/core-contract.md +++ b/docs/architecture/core-contract.md @@ -18,9 +18,13 @@ taxonomy, config split — amended in place, pre-implementation, by joins the `TxHandle` trait, and by [ADR-015](decisions/015-streams-depth.md): streams depth — key semantics, `StreamEvent` shape, the ordering row, `trim_to`, -and `publish_with_key_tx`); ADR-009/ADR-010 add the first *post-v1 -contract extensions* (scheduler collapse surface, `QueueOpts` depth — -versioning discipline for such extensions is OQ-10's); +and `publish_with_key_tx`); ADR-009/ADR-010 add the first extensions +beyond ADR-008's original v1 partition (scheduler collapse surface, +`QueueOpts` depth — factually pre-release additions per ADR-017's +class 1, shipped inside the initial contract v1 text; versioning +discipline for the surface's changes is +[ADR-017](decisions/017-contract-versioning.md)'s, resolved: the +core crate's semver is the contract version); [ADR-016](decisions/016-deployment-honesty.md) decides the parked capability-surface question: **none, by default ever** — engine differences surface at compile time (engine-crate identity) and in @@ -509,13 +513,14 @@ before the engine specs are called `stable`: | [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | opaque wake + re-read; notify-vs-streams guarantee split | | [007](decisions/007-transactional-seam.md) | Tx seam | caller-held handle, `*_tx` methods, native commit-atomicity | | [008](decisions/008-contract-v1-pinning.md) | Contract v1 pinning | surface partition, `TxHandle` trait shape, `Wake` type, reserved strings, error taxonomy, config split, locks guarantee row | -| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse (post-v1 extension) | queues + `schedule()`/`run_schedules`, `@every`-only v1, boundary guarantee row | -| [010](decisions/010-queue-semantics-depth.md) | Queue depth (post-v1 extension) | visibility/renewal, opts stamping, backoff curve, dead-letter, sweep, layout | +| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse (pre-release extension) | queues + `schedule()`/`run_schedules`, `@every`-only v1, boundary guarantee row | +| [010](decisions/010-queue-semantics-depth.md) | Queue depth (pre-release extension) | visibility/renewal, opts stamping, backoff curve, dead-letter, sweep, layout | | [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | SQLite substrate owned (`__alkstore_*` naming); queue ops re-derived on contract v1 | | [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; engine-side formula arithmetic pinned equivalent by the contract suite | | [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue (amends 008) | `outbox_enqueue_tx` on `TxHandle`; outbox-name validation; derived backing queue reached only through the outbox surface | | [015](decisions/015-streams-depth.md) | Streams depth (amends 006/008) | key = carried metadata, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` | | [016](decisions/016-deployment-honesty.md) | Deployment honesty (decides 008's parked question) | no runtime capability surface — compile-time engine identity + documented matrix; `PayloadTooLarge` occurrence asymmetry pinned | +| [017](decisions/017-contract-versioning.md) | Contract versioning (governs this surface's changes) | core crate's semver *is* the contract version; four change classes; amend-in-place ends at first release; pairing = manifest pin + version-stamped contract suite + docs | ## Open Questions @@ -523,14 +528,19 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this document: -- **OQ-10**: contract versioning discipline across engine crates - ([open](open-questions.md)) +- **OQ-10**: contract versioning discipline across engine crates — + **resolved** ([ADR-017](decisions/017-contract-versioning.md)): + the core crate's semver is the contract version; change classes + pinned; the verification backlog below matures into the + version-stamped contract suite (the pairing instrument). Resolved on this document's surface: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), **OQ-13** (transactional outbox enqueue shape — [ADR-014](decisions/014-outbox-tx-enqueue.md)), **OQ-12** (streams -depth — [ADR-015](decisions/015-streams-depth.md)), and **OQ-08** +depth — [ADR-015](decisions/015-streams-depth.md)), **OQ-08** (capability-surface shape — none, by default ever; -[ADR-016](decisions/016-deployment-honesty.md)), 2026-10-05/06. \ No newline at end of file +[ADR-016](decisions/016-deployment-honesty.md)), and **OQ-10** +(the versioning discipline this document's surface is governed by — +[ADR-017](decisions/017-contract-versioning.md)), 2026-10-05/06. diff --git a/docs/architecture/decisions/001-crate-split.md b/docs/architecture/decisions/001-crate-split.md index 9fa5b2a..842ce0f 100644 --- a/docs/architecture/decisions/001-crate-split.md +++ b/docs/architecture/decisions/001-crate-split.md @@ -81,7 +81,13 @@ crate. A consumer binary links at most one driver. - A version-coordination duty: the core's trait surface is a contract the engine crates must track. Version bumps in core must be adopted by engines in lockstep when the contract changes (minor/major - discipline; no trait-default drift). + discipline; no trait-default drift). *(Discharged 2026-10-06: the + duty's concrete shape is the versioning discipline — + [ADR-017](017-contract-versioning.md), OQ-10's resolution: the + core crate's semver is the contract version, change classes + + lockstep duties pinned, the pairing tracked by manifest pin + + version-stamped contract suite + docs; released engines on a + breaking change keep their pinned majors — nothing forced.)* - Slightly more crate plumbing; naming/publishing overhead. - Consumers who want *both* engines in one binary (rare, unsupported-by design) cannot get a both-features build today. @@ -96,6 +102,8 @@ crate. A consumer binary links at most one driver. link-collision constraint motivates this split. - [ADR-004](004-postgres-driver.md) — the Postgres driver choice. - OQ-10 (`docs/architecture/open-questions.md`) — trait-versioning - duties created here. + duties created here; resolved by + [ADR-017](017-contract-versioning.md); OQ-11 — the fork-follow-through + residue (per [ADR-013](013-fold-substrate-into-sqlite.md) §4). [ADR-003]: 003-sqlite-driver.md [ADR-004]: 004-postgres-driver.md diff --git a/docs/architecture/decisions/008-contract-v1-pinning.md b/docs/architecture/decisions/008-contract-v1-pinning.md index b1283bc..df21397 100644 --- a/docs/architecture/decisions/008-contract-v1-pinning.md +++ b/docs/architecture/decisions/008-contract-v1-pinning.md @@ -89,10 +89,17 @@ implement identically): - **Queue semantics depth** (retry/backoff curve, dead-letter move-vs-flag, visibility-renewal mechanics, sweep cadence, `QueueOpts` fields beyond the v1 skeleton) — OQ-05's design surface. v1 pins the - skeleton above; depth additions extend the contract later. + skeleton above; depth additions extend the contract later. *(Resolved + 2026-10-05 by [ADR-010](010-queue-semantics-depth.md); and re-framed + 2026-10-06 by [ADR-017](017-contract-versioning.md): nothing had been + released when the depth landed, so it factually ships inside the + initial contract v1 text.)* - **Scheduler surface shape** (first-class mechanism vs queues + `schedule()`) — OQ-09 decides; the scheduler surface (if any) is not - part of contract v1. + part of contract v1. *(Resolved 2026-10-05 by + [ADR-009](009-scheduler-collapse.md); re-framed 2026-10-06 by + [ADR-017](017-contract-versioning.md) on the same basis — the + collapse surface ships in the initial contract v1 text.)* - **Capability flags** — OQ-08 decides whether `Store` exposes them at all; v1 has no capability surface. *(Resolved 2026-10-06 by [ADR-016](016-deployment-honesty.md): no capability surface — by @@ -445,4 +452,5 @@ prefix, §4). - OQ-09 (scheduler row transfer), OQ-08 (capability surface — resolved by [ADR-016](016-deployment-honesty.md), no runtime surface), OQ-10 - (versioning discipline for future contract extensions). \ No newline at end of file + (versioning discipline for future contract extensions) — + resolved by [ADR-017](017-contract-versioning.md), 2026-10-06). diff --git a/docs/architecture/decisions/009-scheduler-collapse.md b/docs/architecture/decisions/009-scheduler-collapse.md index fcc68a4..9153628 100644 --- a/docs/architecture/decisions/009-scheduler-collapse.md +++ b/docs/architecture/decisions/009-scheduler-collapse.md @@ -221,7 +221,12 @@ respawning; distinct from clean stop, §1). **post-v1 contract extensions** — the first exercises of the extension path [ADR-008](008-contract-v1-pinning.md) leaves open; their versioning discipline (how engine crates track the additions) - is OQ-10's, still open. + is OQ-10's, still open. *(Resolved 2026-10-06 by + [ADR-017](017-contract-versioning.md): nothing was released when + these extensions were made, so they factually fall in ADR-017 + class 1 — pre-implementation amend-in-place — and ship inside the + initial contract v1 text; the post-release extension *shape* this + ADR exercised is ADR-017's class 2.)* ## References diff --git a/docs/architecture/decisions/014-outbox-tx-enqueue.md b/docs/architecture/decisions/014-outbox-tx-enqueue.md index b018682..565b243 100644 --- a/docs/architecture/decisions/014-outbox-tx-enqueue.md +++ b/docs/architecture/decisions/014-outbox-tx-enqueue.md @@ -134,7 +134,11 @@ smallest surface that restores the property. the v1 surface partition's tx-seam bullet and [core-contract.md](../core-contract.md)'s seam. ADR-008's outbox bullet carries a pointer annotation, mirroring how ADR-010 §8's - SQLite half recorded its supersession. + SQLite half recorded its supersession. *(Codified 2026-10-06 as + versioning class 1 by + [ADR-017](017-contract-versioning.md) — and bounded by it: this + amend-in-place frame terminates at the core crate's first + release.)* - **Contract-suite row**: `outbox_enqueue_tx` commit-atomicity on both engines — rollback drops the backing-queue job row with the business write (no ghost job); commit makes it claimable by `run_once`; diff --git a/docs/architecture/decisions/015-streams-depth.md b/docs/architecture/decisions/015-streams-depth.md index 9836142..ed3b3f0 100644 --- a/docs/architecture/decisions/015-streams-depth.md +++ b/docs/architecture/decisions/015-streams-depth.md @@ -127,7 +127,11 @@ the key the same way the non-tx form does (`None` or non-empty — an empty `Some` key is `InvalidName`) with no new error variants ([ADR-008](008-contract-v1-pinning.md) §5's rule). Framed as completing contract v1 in place, pre-implementation, like ADR-014 — -no versioning event, OQ-10 untouched. A plain-`publish_tx` is +no versioning event, OQ-10 untouched. *(Codified 2026-10-06 as +versioning class 1 by +[ADR-017](017-contract-versioning.md) — and bounded by it: this +amend-in-place frame terminates at the core crate's first +release.)* A plain-`publish_tx` is `publish_with_key_tx(stream, None, payload)`, not a separate engine path. *(Naming pinned here too: the publish family returns the **assigned offset** — honker's `publish` already returns it — and the diff --git a/docs/architecture/decisions/017-contract-versioning.md b/docs/architecture/decisions/017-contract-versioning.md new file mode 100644 index 0000000..cbb5f84 --- /dev/null +++ b/docs/architecture/decisions/017-contract-versioning.md @@ -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). diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 5df9dbb..b9ae0e7 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-06 +last_updated: 2026-10-06 (OQ-10 resolved) --- # alkstore — Open Questions @@ -30,15 +30,13 @@ carried-metadata keys, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to`); **OQ-08 resolved** (2026-10-06, [ADR-016](decisions/016-deployment-honesty.md) — no runtime capability surface; the honest boundary lives in compile-time engine -identity + the documented deployment matrix). -Next: **OQ-10** (versioning discipline -for contract extensions; note ADR-011 changes its substrate-side facts -for SQLite — the forked machinery lives in-tree inside the engine -crate per [ADR-013](decisions/013-fold-substrate-into-sqlite.md), so -the engine/core contract pairing is what the discipline must track; -OQ-08's resolution also narrowed its surface — no capability struct -to govern), -OQ-11 (fork follow-through items — substrate-side, non-consumer-facing). +identity + the documented deployment matrix); **OQ-10 resolved** +(2026-10-06, [ADR-017](decisions/017-contract-versioning.md) — the +core crate's semver is the contract version; four change classes +with amend-in-place terminating at first release; the pairing +tracked by manifest pin + version-stamped contract suite + docs). +Next: **OQ-11** (fork follow-through items — substrate-side, +non-consumer-facing). Resolved questions stay listed with their resolution; they are not deleted. @@ -71,7 +69,8 @@ deleted. effectively requires). Decision recorded in [ADR-001](decisions/001-crate-split.md) — including the reasoning that superseded the inventory's single-crate lean. -- **Cross-references**: OQ-03, OQ-10. +- **Cross-references**: OQ-03, OQ-10 (resolved — + [ADR-017](decisions/017-contract-versioning.md)). ### OQ-03: Driver story — sqlx, tokio-postgres, or per-engine drivers? *(== OQ-ST-03)* @@ -135,7 +134,9 @@ narrowed to the pinning work its own record already scoped.)* expiry); the scheduler row is explicitly transferred to OQ-09's resolution. A verification backlog (core-contract.md §Verification backlog) tracks the one-engine-pinned properties. -- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08; OQ-13 (the one +- **Cross-references**: OQ-01, OQ-10 (resolved — + [ADR-017](decisions/017-contract-versioning.md), the discipline + these v1 extensions now fall under), OQ-09, OQ-08; OQ-13 (the one gap the v1 pinning's own Phase 1 review found in it), OQ-12 (the sibling depth gap — resolved, [ADR-015](decisions/015-streams-depth.md)). @@ -270,25 +271,68 @@ narrowed to the pinning work its own record already scoped.)* fork-scaffold task (its opening section), not before it and not as a gate on writing it. - **Cross-references**: OQ-08 (the contract surface the engine maps - the substrate under), OQ-10 (the engine's versioning discipline now - carries the fold's provenance duties in-tree), ADR-011, ADR-012, - ADR-013. + the substrate under), OQ-10 (the engine's versioning discipline + now carries the fold's provenance duties in-tree — and its + class-4 non-events rule is what keeps substrate cherry-picks from + being contract events; + [ADR-017](decisions/017-contract-versioning.md)), ADR-011, + ADR-012, ADR-013. -### OQ-10: How do engine crates track core-contract version changes? +### OQ-10: How do engine crates track core-contract version changes? — **RESOLVED** - **Origin**: [overview.md](overview.md), [ADR-001](decisions/001-crate-split.md) -- **Status**: open +- **Status**: resolved (2026-10-06, Phase 1 — + [ADR-017](decisions/017-contract-versioning.md)) - **Priority**: medium -- **Resolution**: open. The core crate's trait surface is a contract - the engine crates must track ([ADR-001](decisions/001-crate-split.md) negative consequence). What - is the versioning/sync discipline — semver-bump-only-when- - contract-changes, engines pin core ranges, a contract-compatibility - test suite the engines run against the core's trait definitions? - What happens to a released engine crate when core makes a contract - breaking change? -- **Cross-references**: OQ-04 (the contract being versioned — now - including its first post-v1 extensions, ADR-009/ADR-010), OQ-02. +- **Resolution**: Pinned by [ADR-017](decisions/017-contract-versioning.md): + **the core crate's semver is the contract version — no parallel + versioning surface** (the OQ's three candidate mechanisms compose + rather than rival: semver discipline → the change-class rule, + engine range pins → the manifest carrier, the contract suite → + the compatibility instrument). (1) One-normative-owner rule + applied to the version question: the crate is a contract artifact + and nothing else, so its version ledger *is* the contract ledger, + and its changelog cites the ADR of every contract change (no + silent contract changes — the semantic-break guard, since + guarantee-row breaks are not compiler-visible). (2) **Four change + classes**: amend-in-place (the ADR-014/015 precedent — terminates + *permanently* at the core crate's first release; ADR-009/010, + though framed "post-v1," factually fall in this class and ship in + the initial contract v1 text); additive extension (the + ADR-009/010 *shape*, post-release — core minor, row-first + discipline); breaking change (signatures *and* semantic breaks — + the act-differently measure generalized to change + classification; core major); non-events (engine-internal change — + structurally guaranteed by ADR-012 §2's contract-blind boundary: + substrate cherry-picks cannot be contract events). (3) + `#[non_exhaustive]` on the consumer-read value types (`Error`, + `StreamEvent`, `Job`, `Schedule`, `Wake`) — mechanically making + additions there minor and enforcing the catch-all matching + posture; the consumer-constructed opts structs are exempt + (non_exhaustive bans struct-literal construction), so their + field additions are breaking (class 3), priced deliberately; + trait methods stay the honest lockstep cost. (4) The pairing + tracked by three carriers of one + fact: the engine's manifest pin (`alkstore = "1.y"`), the + version-stamped contract suite (the backlog matures into the + OQ's option-(c) compatibility instrument), and engine-crate docs. + (5) **Released engines on a breaking change: nothing forced** — + the released engine serves its pinned contract majors until + archived; the new major gains its own engine line, migration is + the consumer's deliberate two-line bump; and no + default-trait-method shims to dodge adoption (the "no trait- + default drift" line, stated). Engine semver stays independent + (it versions the engine's own surface). +- **Cross-references**: OQ-04 (the contract being versioned — its + first post-v1 extensions, ADR-009/010, resolved pre-release into + contract v1's initial text), OQ-02, OQ-08 (the narrowing — no + capability struct to govern), + [ADR-011](decisions/011-sqlite-substrate-fork.md)/ + [ADR-012](decisions/012-forked-substrate-design.md)/ + [ADR-013](decisions/013-fold-substrate-into-sqlite.md) (the fold + that removed the substrate's versioning surface), OQ-11 (the + cherry-pick procedure governing the substrate-side non-events). ## Theme: Deployment and capabilities @@ -448,7 +492,6 @@ narrowed to the pinning work its own record already scoped.)* ## Deferred / Blocked -None currently. Every open OQ above is actionable Phase 1 work -(versioning discipline, fork-scaffold -follow-through) with its evidence base complete — no -external arrivals are being waited on. +None currently. The one remaining open OQ above (OQ-11) is +actionable Phase 1 work (fork-scaffold follow-through) with its +evidence base complete — no external arrivals are being waited on.