--- status: draft last_updated: 2026-10-01 --- # Store API ## What this is The store core's public surface: the typed facade above the backends that consumers (alkgit, alkfs, the ops module) program against. It owns typed keys and hashing (ADR-002), size-threshold dispatch (ADR-003), the put-pinning seam, and the liveness/sweep seam (ADR-005) — and nothing else: no paths, no manifests, no wire (ADR-004, ADR-001). ## Public surface ### Write path - **`put(len: Option, stream)` → `(Key, Pin)`** — the streaming put seam (POC #3 finding A1). Two internal paths behind one signature: - *Known length* (encouraged, documented as such on the API): the preamble hashes before content flows; one pass, no staging needed for hashing. Covers git objects, pre-staged files (`stat`-derived length), network offers, `Content-Length`-bearing uploads. - *Unknown length*: content buffers in memory up to the dispatch threshold; on commit the buffer is the whole stored artifact (kv tier) — or, on mid-stream threshold overflow, the buffer flushes into a stage file and the stream continues into it (fs tier — the streaming pass cannot include the preamble, so the derivation pass restarts over preamble + staged bytes; forced by the preamble being inside the hashed input, not a design choice). Verification runs over the eventual stored artifact as a whole; a stream error on either path leaves no entry and no stage file. Eliminates the A6 dispatch asymmetry by construction: routing is a pure function of content length (ADR-003). - **Stage-file hygiene invariant** (POC #3 finding A5, codified): every failure path converges on stage-discard; every success path converges on commit-rename. No early returns that bypass cleanup; this is an asserted invariant with dedicated exact-count tests, not review hygiene. - **`Pin` / `Batch`** — RAII guard on the put path (ADR-005): the entry is liveness-protected until the guard drops or the caller converts it into a consumer-side reference (registered liveness, a root tag). **`Batch`** groups multi-put writes under one batch-scoped pin (manifest writes are exactly this); a batch's pins drop together on batch commit or drop (ADR-005 §Decision). ### Read path - **`get(key) → Option<(len, stream)>`** — small tier returns a bounded read; large tier streams a file handle. Dispatch fall-through: a small-tier miss queries the large tier (POC #1 finding 6). - **`stat(key) → Option`** — cheap length/type probe (phase-0 OQ-BL-04 addendum; gix's header-only read is git's cheapest primitive and the packfile-serving surface rides this). `EntryMeta` carries `len: u64` and the key's algorithm (the "type" from the store's perspective is the hash algorithm — there is no other type at this layer). - **`read_range(key, range) → (slice, slice_digest)`** — slice plus an out-of-band slice digest: SHA-256 over the slice bytes, fixed by convention so callers and ops handlers agree without negotiation (per-range verification against the canonical digest is impossible — ADR-006). Range reads fall through tiers exactly as `get` does (small-tier miss slices the large tier's value). Local range serving (packfiles) is the consumer of this. - **`get_stream` / fanout** — the ops module's fetch handler is a broadcast above the store: one reader, store arm + subscriber arms (POC #3 finding A3); late joiners degrade to normal verified `get` after commit. The store exposes the seam; the fanout policy lives in the ops layer (ADR-001). ### Lifecycle - **`has(key)`, `delete(key)`** — deletion participates in ADR-005's delete windows; direct deletes are permitted but refuse protected keys — pinned or re-observed live via registered sources (the same per-key arbitration sweeps apply, typed error) — and are unusual by posture; most deletion flows through sweeps. - **`list() → stream of keys`** — whole-pool enumeration; complete by contract (see backends doc for why list correctness is load-bearing). - **`register_liveness_source(...)`** — the live-shared GC seam (ADR-005, §Decision; the POC's "install implies copy" failure (finding 7) is why the verb/name is pinned here). - **`sweep() → SweepReport`** — explicit mark-and-sweep; embedders own the cadence (ADR-005, no ambient timers). A sweep with no registered liveness sources aborts without deleting — the safe default. ## Error model `thiserror`; no panics in library code; no `unwrap`/`expect` outside tests (AGENTS convention 2). Distinguished failure families: - `Missing` — key absent (get/stat miss) - `Verification` — put/get hash-check failure (content ≠ key) - `Io(String)` — backend media failure (stringly because `std::io::Error` is not stable across versions) - `GcAborted` — a protection source failed; nothing deleted (ADR-005) - `KeyInvalid` — malformed key bytes at the boundary (hashing doc) Virgin-store semantics: read paths on a fresh store see absent/empty, never "table does not exist" errors (POC #1 finding 2 — the redb lesson generalizes to any kv engine). ## Concurrency posture - Async I/O throughout; `tokio::sync` for lifecycle correlation; `parking_lot` for short-held internal locks (AGENTS convention 3); poisoned locks degrade via `unwrap_or_else(|e| e.into_inner())`. - Blocking file work lives in `spawn_blocking` inside backend impls (the alkgit trait-execution pattern); the store never blocks the executor. - The sweep-vs-put window is an architectural mechanism — delete windows — specified in ADR-005, not an implementation note. ## Invariants (the test gate) 1. Whole-blob put/get round-trips byte-identical under the canonical derivation, both tiers, both put paths (interop with real git remains the source of truth, per POC #1's lesson about hardcoded vectors). 2. Dedup: putting identical content twice (same or different path) is one pool entry. 3. Stage hygiene: any failure mid-put leaves zero stage files; any success leaves exactly one committed entry. 4. Sweep safety: with correct liveness registered, sweep counts are exact; with aborting sources, sweep deletes nothing. 5. `list()` correctness is observable only through GC — list-related tests assert through sweep outcomes (POC #1 finding 2's lesson codified). ## Design Decisions | ADR | Decision | Summary | |---|---|---| | [002](decisions/002-canonical-hash-and-key-encoding.md) | Hashing & keys | derivation + encoding this surface is built on | | [003](decisions/003-backend-contract-and-dispatch.md) | Dispatch | routing is a pure function of length; pre-threshold buffering | | [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pools & GC | pins, liveness seams, delete windows, sweep semantics | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | whole-blob checks; slice digests out-of-band | ## Open Questions None owned by this document beyond the cross-references above. ## References - `docs/research/poc-largeblob-findings.md` findings A1/A5/A6, benchmark table - `docs/research/poc-trait-dispatch-findings.md` findings 1–3, 7 - ADR-003 (put-path buffering), ADR-005 (pin/Batch/Pin lifecycle), ADR-006 (range-read semantics), ADR-002 (keys) - [ops-surface.md](ops-surface.md) — the fanout/fetch consumer of the read path - [gc-and-namespaces.md](gc-and-namespaces.md) — the lifecycle seam details