--- id: core-trait-surface name: Core trait surface (Store, TxHandle, mechanism handles, receivers) status: completed depends_on: [core-value-types] scope: moderate risk: medium impact: project level: implementation tags: [wave-1, core] --- ## Description Implement the core crate's full trait surface — contract v1 in code. Every shape is pinned by ADR text; this task is faithful transcription, and it is the surface both engines implement in waves 3–4, so shape errors here are the expensive kind. The wave-1 review gate checks this task line-by-line against the ADRs. **`Store` trait** (ADR-008 §1/§6, ADR-019 §1, ADR-009 §1): ```text begin_tx() -> Box notify(channel, payload) listen(channel) -> Box stream(name) -> Box queue(name, opts) -> Box outbox(name) -> Box try_lock(name, owner, ttl) -> Option> schedule(name, spec, queue, payload, opts) -> Result unschedule(name) -> bool run_schedules(stop: StopToken) -> Result<()> ``` Constructors/options live in engine crates (ADR-008 §6) — core defines only the trait. Name-bearing methods validate at the entry point (shared helper from `core-errors-and-validation`); the trait's doc contract states that obligation (engines must reject before any round trip, on auto-commit and tx paths alike). **`TxHandle` trait** (ADR-008 §2, ADR-014, ADR-015 §2, ADR-021 §1/§4): `enqueue_tx`, `publish_tx`, `publish_with_key_tx`, `notify_tx`, `save_offset_tx`, `get_job_tx`, `get_offset_tx`, `read_since_tx`, `read_from_consumer_tx`, `outbox_enqueue_tx`, `commit(self: Box) -> Result<()>`. Drop = rollback is an engine obligation stated in the trait docs. No generic methods (object safety); payloads cross as `serde_json::Value`; async methods use the boxed-future form (family-standard async trait posture). **Mechanism handles** (ADR-019 §1, exact method sets): `Queue { name, enqueue, claim_one, claim_batch, ack_batch, cancel, get_job, sweep_expired }`; `StreamHandle { name, publish, publish_with_key, read_since, read_from_consumer, save_offset, get_offset, trim_to, subscribe }`; `Outbox { name, enqueue, run_once }`; `Lock { name, renew, release(self: Box) }`; `JobHandle { job(&self) -> &Job, ack(self), retry(self, err, delay) -> bool, fail(self, err) -> bool, heartbeat(self, extend) -> bool }` — the one-shot/repeatable split is deliberate (heartbeat takes `&self`). **Receivers** (ADR-008 §3/§8): `WakeReceiver { recv() -> Option, try_recv() -> Result>, recv_timeout(d) -> Result> }` (timeout expiry = `Ok(None)`; `Err` carries `Database`/`Closed` only); `EventReceiver { recv() -> Option>, try_recv() -> Result>, read_since(offset, limit) -> Result>, save_offset(&mut self) -> Result<()>, offset() -> i64 }` — no `recv_timeout` on `EventReceiver` (deliberate absence, ADR-008 §8 third-round annotation). **`run_once` delivery callable** (ADR-019 §5): consumer-supplied async callable taking `Box`, returning the boxed future of `Result<()>`; object-safe encoding (named one-method trait object or `&mut dyn FnMut` form — implementer's choice under the pinned semantics). `with_tx` (ADR-007, signature pinned 2026-10-07): `with_tx(f) -> Result<()>` with `f: FnOnce(&mut dyn TxHandle) -> BoxedFuture>` — `Ok` ⇒ commit, `Err`/drop/panic ⇒ rollback; non-generic, values return through captured state, no handle escapes the closure. This lives on the `Store` trait as a provided method (default body over `begin_tx`). All traits re-exported from `src/lib.rs`. Doc comments carry the pinned semantics (guarantee-row references, validity predicate, drop=rollback, no-claim_tx absence rationale). ## Acceptance Criteria - [x] Every trait above with the exact pinned method sets and signatures; compiles as object-safe (`Box` everywhere the contract says) - [x] `with_tx` provided method present with the pinned signature - [x] A compile-probe test constructs `Box`, `Box`, etc. from a trivial local impl, proving object safety of the whole surface - [x] Trait docs state: entry-point validation obligation, drop = rollback, the uniform handle-op validity predicate reference, heartbeat's absolute-reset semantics, no `claim_tx` rationale - [x] `cargo test -p alkstore`, clippy `-D warnings`, fmt clean ## References - docs/architecture/core-contract.md (the seam, mechanism contracts) - docs/architecture/decisions/008-contract-v1-pinning.md §1–§3, §6, §8 - docs/architecture/decisions/007-transactional-seam.md - docs/architecture/decisions/014-outbox-tx-enqueue.md - docs/architecture/decisions/019-mechanism-handle-surfaces.md §1–§5 - docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md ## Notes - **Async posture**: the "desugared boxed-future form" (ADR-008 §2) is realized as a named alias, `BoxedFuture<'a, T> = Pin + Send + 'a>>` in `src/future.rs` — no `async_trait` macro, no core dependency beyond the pinned posture (Send bounds the futures: `begin_tx` returns `Box`, so its futures must be equally portable). Sync methods (`try_recv`/`save_offset`/`offset` on receivers, `job(&self)`, `name(&self)`) stay sync as pinned. - **`with_tx` dyn encoding**: the pinned signature is non-generic — a `Store` trait object must stay object-safe — so the generic-`F` form does not compile on the trait; the pinned `FnOnce(&mut dyn TxHandle) -> BoxedFuture>` shape crosses dyn-encoded as `Box`, aliased `WithTxClosure<'a>` (re-exported) to keep the complex type out of signatures. Values return through captured state; the provided default body commits on `Ok`, drops (rolls back) on `Err`. - **Delivery callable encoding** (ADR-019 §5's implementer's choice): the named one-method trait object — `trait Delivery { fn deliver(&mut self, Box) -> BoxedFuture>; }` — with `run_once(worker_id, delivery: &mut dyn Delivery)`. `&mut dyn FnMut` cannot return boxed futures of borrowed data under object safety, so the named-trait form is the workable answer; the engine acks on the closure's `Ok` and retries on its `Err`. - **Fallibility**: the ADR sketches elide `Result<>` for brevity (ADR-008 §2's note: every fallible op returns the §5 taxonomy); entry points validating names must err, so constructors (`stream`/`queue`/`outbox`/`listen`) are `Result`-returning as is `notify` (PayloadTooLarge); `begin_tx` returns `Result` (SQLite writer-slot acquisition can fail); no-claim and no-lock outcomes stay values (`Option`), handle-op outcomes stay `bool`s per ADR-008 §5's act-differently rule. - **Scheduler error variants** (`InvalidSpec { spec }`, `LeadershipLost` — ADR-009 §6) joined `Error` here per `core-errors-and-validation`'s note (exactly-six list was that task's scope; their signatures pin in this task). Unit-tested. - **`tokio` added as a *dev*-dependency only** (test to drive the boxed-future probes) — runtime deps remain exactly serde/serde_json/thiserror (ADR-020 §4, ADR-001: no driver deps in core). - `Wake`'s `#[non_exhaustive]` prevented the probe's struct-literal construction — the probe re-exports or constructs nothing of the read types beyond the existing value-type tests; trait-object construction is the probe's whole point. - Every trait doc carries the pinned semantics text: entry-point validation obligations (Store; TxHandle's own mirror — name-bearing `*_tx` methods validate like their auto-commit counterparts), drop=rollback (TxHandle), no-`claim_tx` rationale (Queue), the uniform validity predicate + absolute-reset heartbeat (JobHandle), monotone saves (StreamHandle/EventReceiver), terminal-close arms and the deliberate no-`recv_timeout` absence (receivers). ## Summary The core trait surface — contract v1 in code — is implemented and re-exported from `alkstore/src/lib.rs`: `BoxedFuture` + `TxHandle` (the eleven pinned methods incl. ADR-021 §1's four tx reads, `commit(self: Box)`, drop=rollback docs); `Store` (the pinned method set + the `with_tx` provided method in the pinned dyn-encoded signature, with commit/rollback dispositions — unit-tested); mechanism handles `Queue`/`JobHandle`/`StreamHandle`/ `Outbox` (+ `Delivery`)/`Lock` with the exact ADR-019 §1/§3/§5 method sets; receivers `WakeReceiver` (timeout expiry = `Ok(None)`; `Err` = Database/Closed only) and `EventReceiver` (no `recv_timeout`, deliberate); `Error::InvalidSpec`/`LeadershipLost` added (ADR-009 §6). Object-safety compile probes construct every trait object from trivial local impls (`trait_objects_construct`), and the `with_tx` dispositions run against a probe store (`with_tx_commit_and_ rollback_dispositions`) — 23 tests green; `cargo test -p alkstore`, workspace `cargo build`, clippy `-D warnings` (workspace), and `cargo fmt --check` all clean.