--- id: store-construction name: Store facade type — constructor modes and size-threshold dispatch status: completed depends_on: [engine-mem, engine-local] scope: moderate risk: medium impact: project level: implementation tags: [wave-2, facade, dispatch] --- ## Description Introduce the `Store` facade type (store-api.md §What this is) — the public surface consumers program against — with its constructor modes and the size-threshold dispatch. This task builds the *skeleton and routing*: the ops methods land in `store-read-paths`, `gc-protocol`, and `store-put-paths` against this type. - **The `Store` type** (public, `src/facade.rs` or a new module — implementer's call, one module per file): holds the kv-tier engine and the large-tier engine (or none, per mode) plus the GC state home the `gc-protocol` task fills. **Named design point — engine representation**: a concrete `Store` with a crate-internal enum dispatching over the closed five-engine set, vs a generic `Store`. The conformance factory's binding note (tests/conformance/factory.rs) anticipates the concrete type ("most plausibly the associated type collapsing to the concrete facade type across all factories"); engines are statically known and in-crate (no `dyn` anywhere, wave-1 design point 3). Decide, record, and keep the public surface engine-blind (ADR-007/008/010: which engines a node runs changes nothing here). - **Constructor modes** (ADR-011 §4; ADR-012 §6.3): - **dual-tier** (the default): kv + large, size-threshold dispatch. Wave-2 constructible shape: `kv = mem` + `large = local` — the dispatch-coverage test shape ADR-012 §6.3 names (both tiers load-bearing). - **kv-only**: no large tier; over-threshold puts reject at put time (ADR-012 §6.2 — the rejection behavior itself lands in `store-put-paths`; this task carries the mode and its validation). - **mem-only**: the mem engine alone, no dispatch threshold in effect; never production. - Mode machinery is built so the wave-3–5 engine constructors slot in without reshaping (sqlite/postgres/pg-lo constructors are later waves; only feature-gated constructors exist when their feature is on). - **Size-threshold dispatch** (ADR-003): routing as a **pure function of content length** — same content ⇒ same length ⇒ same tier, no content ever migrates. Default threshold **128 KiB** (constructor-tunable; the conservative edge of the measured ~128–256 KiB crossover zone, POC #3 A4). The dispatch function is pure and unit-tested (boundary values: exactly-at-threshold routing). - **The dispatch seam** the ops tasks program through: tier selection for a given length, and the fall-through order (kv first, then large) the read paths and delete consume (ADR-012 §6.1's general rule). - **Conformance factory binding**: bind `StoreFactory::Store` to the real facade type (the skeleton's anticipated collapse to the concrete type); if binding reveals a shape correction, make it and record it in factory.rs + tests/conformance/README.md per the skeleton's note. ## Acceptance Criteria - [ ] Public `Store` type with the three constructor modes; engine representation decided and recorded (enum-dispatch vs generic) with the public surface engine-blind - [ ] Size-threshold dispatch: pure function of length, default 128 KiB, constructor-tunable; boundary-value unit tests (at/below/above threshold route deterministically) - [ ] `kv = mem` + `large = local` dual-tier construction works (the ADR-012 §6.3 dispatch-coverage shape); kv-only and mem-only modes construct and validate - [ ] `StoreFactory::Store` bound to the real facade; any shape correction recorded in factory.rs + README - [ ] `cargo test` green (default build: local ungated); `cargo test --features mem` and `cargo test --all-features` green; clippy/fmt gates green in all three feature shapes ## References - docs/architecture/store-api.md (§What this is; §Public surface) - docs/architecture/backends-and-dispatch.md (§Size-threshold dispatch; §Constructor modes) - docs/architecture/decisions/003-backend-contract-and-dispatch.md (§Dispatch policy) - docs/architecture/decisions/011-vocabulary-tiers-engines-and-fleet-state.md §4 - docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §6.2/§6.3 - tests/conformance/factory.rs (the binding note) ## Notes - **Engine representation decided**: concrete `Store` holding two crate-internal engine enums (`KvEngine`: mem [feature-gated]; `LargeEngine`: local [ungated]), not the generic `Store` — the generic would leak `pub(crate)` engine types through the public surface or force `dyn` (wave-1 design point 3); the facade stays engine-blind (ADR-007/008/010) and the conformance factory's binding note anticipated exactly this collapse. Waves 3–5 slot in by adding an enum variant + a named (feature-gated) constructor — no reshaping. Recorded in `src/store.rs` module docs. - **Constructor modes** land as named constructors, engines named by the constructor (public surface can't take `pub(crate)` engine handles): `mem_only()` / `kv_only_mem(threshold)` / `dual_tier_mem_local(root, threshold)` (the ADR-012 §6.3 dispatch-coverage shape; fallible because the local root's creation is I/O). Modes validate structurally (unrepresentable states unrepresentable — dual-tier constructs the large engine itself) and every instance carries its mode (`Store::mode() → StoreMode`, public). The **fleet declaration (`fleet: true`, ADR-012 §3) defers to the engine waves**: it is a constructor *error* for every non-fleet-valid configuration, and none is constructible yet — the ruling is recorded in store.rs docs rather than carried as a dead-machinery flag. - **Threshold shape**: `None`-vs-value is the mode's ruling (mem-only = "no dispatch threshold in effect", ADR-011 §4), so the field is always a value and mem-only's `threshold()` returns `None` by mode branch — no unrepresentable `Option` state to `expect` away (AGENTS convention 2). The pure function is `dispatch::route(len, threshold)` with the pinned boundary reading **at-or-below → kv**; `DEFAULT_DISPATCH_THRESHOLD` (128 KiB) is exported at the crate root and constructors take the threshold explicitly (tunability proven by tests at a non-default threshold). - **Dispatch seam handed to the ops tasks**: write-side `Store::tier_for(len)` (dual-tier pure routing; kv-only routes kv for every length — the over-threshold *rejection* is the put seam's behavior keyed off `threshold()`, ADR-012 §6.2; mem-only kv), and read-side fall-through order as `kv()`-then-`large()` accessors documented as ADR-012 §6.1's general rule. The engine enum's `Backend` dispatch methods and the cursor/staging enum wrappers are deliberately not built here — they are store-read-paths and store-put-paths named design points (the public stream shape, the declared-len mismatch case) and adding methods to the enums is slotting in. - **GC state home**: `pub(crate) struct GcHome` in `src/gc.rs`, held as a `Store` field with a crate-internal `gc()` accessor — the gc-protocol task fills it (pin registry, liveness sources, protect callback, in-process window); fleet state explicitly never lands there (ADR-010 §2 / ADR-012 §4 host rule). - **Factory binding made and recorded**: the skeleton's `Store` associated type is gone — `StoreFactory::open` vends the concrete `alkblobs::Store` directly (`tests/conformance/factory.rs` + README §The factory contract record the ruling); the wave-1 mock probe retired, replaced by mem-gated binding demo factories (`tests/conformance/binding.rs`) covering mem-only and the dispatch-coverage dual-tier shape with isolation + idempotent teardown. - **Default-build note**: the kv engine set is empty until wave 3 (sqlite), so `Store` exists in default builds but is unconstructible — constructors and construction tests are `#[cfg(feature = "mem")]`; the `#![allow(dead_code)]` scope notes on `dispatch.rs`/`store.rs` follow the house pattern and shrink as the ops tasks land. ## Summary Implemented the `Store` facade skeleton in `src/store.rs`: the public engine-blind facade type holding the two crate-internal tier enums (`KvEngine` {mem}, `LargeEngine` {local}) per the recorded engine-representation ruling (concrete + enum dispatch, generic rejected), the three constructor modes as named constructors — `mem_only()`, `kv_only_mem(threshold)`, `dual_tier_mem_local(root, threshold)` (the ADR-012 §6.3 shape) — with `StoreMode` introspection, the write-side routing seam (`tier_for`) composing the pure `dispatch::route` (default 128 KiB, constructor-tunable, at-threshold → kv), the read-side fall-through order (`kv()` then `large()`, ADR-012 §6.1), and the `GcHome` GC-state field the gc-protocol task fills (added to `src/gc.rs`). `dispatch.rs` grew the pure routing function plus `DEFAULT_DISPATCH_THRESHOLD` with boundary-value unit tests (exactly-at-threshold, degenerate tunings, purity). The conformance factory binding collapsed the `Store` associated type into the concrete facade (recorded in `factory.rs` + the suite README); the wave-1 probe became mem-gated binding demo factories. Tests: mem-only/kv-only/ dual-tier construction + validation, threshold tunability at a non-default value, dispatch boundary values, factory open/teardown lifecycle in both constructible shapes. Verification: `cargo test` (default), `cargo test --features mem`, `cargo test --all-features` all green; `cargo clippy --all-targets -- -D warnings` green in all three feature shapes; `cargo fmt --check` green; `cargo doc --no-deps` clean of new warnings beyond the pre-existing private-intra-doc-links class.