- tests/conformance/local_column.rs: the first dual-tier column, run against the kv = mem + large = local factory (ADR-012 §6.3) — fresh temp-root store per open, per-instance tracked roots, idempotent teardown safe against abandoned/panicking rows (structural tests included); never skips (mem-gated — the dual-tier constructor rides feature mem until wave 3 provides an ungated kv engine) - the reference INV row set (rows.rs) mirrored against the dual-tier factory at a column-declared tuned threshold (1024), so the shared matrix and race fixtures exercise both tiers' bodies (INV-7) - dispatch rows grown: pure-function routing across put paths and re-puts with an on-disk placement oracle (tier residency is facade-invisible; with kv = mem the medium is the oracle), threshold boundary legs at the tuned and shipped-default thresholds with the constructor-tuned re-route leg, and fall-through per op with both-tier miss shapes (ADR-003/ADR-012 §6.1/§6.3/§6.5) - local-specific rows: on-disk stage-hygiene exact counts over both put paths (the real stage form, INV-3), pread windows over committed files with slice digests verified independently (ADR-006/ADR-010), virgin-store no-ops on a fresh root, and exact sweep counts over a both-tiers pool (INV-4/INV-5's both-tiers form) - rows.rs shared fixtures made pub(crate); README records the column's row inventory (§Columns beyond the reference set), the declared- threshold posture, and the new current state - stat-as-stat rides the mirrored INV-6 row's over-threshold legs; the Drop-cancellation hygiene form stays per-engine (recorded) Verification: cargo test (default) green; cargo test --features mem green (97 lib + 37 conformance + 4 git-interop); cargo test --all-features green; clippy --all-targets -D warnings green in both feature shapes (default + all-features); cargo fmt --check green; cargo doc --no-deps shows only the pre-existing warning class
The conformance suite — home and conventions
The in-crate conformance harness for alkblobs' engines. Decided
point (docs/plans/implementation.md, 2026-10-11): engines are
features of one crate, so the suite lives in-crate, factory-
parameterized over engine constructors — the alkstore
ADR-017/022 contract-suite pattern adapted (there a sibling suite
crate over Box<dyn Store>; here one test target over factories
whose open vends the future Store facade, whose concrete type
wave 2 binds).
This document is the convention's normative home on the test side
(the in-code module docs in main.rs/factory.rs/stamp.rs restate
their slices). Waves 2–6 grow columns against it: one column per
engine, each column the same rows run against that engine's factory.
Row anatomy
A row is one named, independently-runnable property — an async
function taking a StoreFactory (not a store), asserting by panicking
on violation (a failing row is a test failure naming the property and
expectation; the family-wide no-panic rule is relaxed here by the
suite's test-side purpose).
Row naming
snake_case, and the name states the property, not the engine (e.g.round_trip_known_length,dedup_is_one_pool_entry). The engine dimension is the column (which factory the row runs against), never the row name — that is what makes rows engine-uniform.- Dimensions a property spans within one run — the content matrix, both put paths, both tiers where the invariant ranges over them — loop inside the row's body. A genuinely different property (e.g. sweep safety's fleet form vs its single-node form) is a separate row, not a flag inside the owner.
- Planned row families are registered in the invariant backlog (table below); a row that does not trace to a backlog entry is not a conformance row.
Contract stamps (the ADR-017 §4.2 convention adapted)
Every row is version-stamped with a doc-comment list item beginning
Contract stamp:, citing the spec § and/or ADR § that pins the
behavior tested:
/// <one-line statement of the property>
///
/// Contract stamp: store-api.md §Invariants 2; ADR-003 (whole-value
/// put is CAS first-writer-wins); ADR-005 (re-put refcounts).
pub async fn dedup_is_one_pool_entry<F: StoreFactory>(factory: &F) { ... }
Rules:
- The stamp cites the ADR § / spec § that pins the property — never a bare "wave 2" or a date. Several stamps are allowed when one row tests several changes' behavior together.
- A row added to pin a new contract addition is stamped with that addition's ADR; a row whose property predates the suite is stamped with the ADR/spec section that pinned it.
- A later change that amends a row's property appends to its stamp list; rows accumulate stamps, the ADRs' own texts record the history.
Contract stamp:is greppable —grep -rn "Contract stamp:" tests/conformance/is the stamp inventory.- Rows are also free of engine-specific code by construction: a row
may not branch on
factory.name().
One normative owner per property
Each property has exactly one normative owner: the suite row.
Engine-specific unit tests inside src/ may duplicate coverage for
debugging convenience, but the suite row is the normative assertion —
an engine's conformance claim rests on its column being green, not on
its in-crate tests. When the suite itself needs per-engine divergence
(the fleet-form sweep safety against postgres, INV-4), that is its own
row with its own owner stamp, still one per property.
The factory contract
factory.rs pins the [StoreFactory] shape. The contract:
open()vends a fresh, isolated store instance. No state is shared between rows or between columns: a fresh backing store per call — a fresh temp directory (local), a fresh pool (mem), a fresh schema or file (sqlite/postgres/pg-lo) — so a property's rows never observe another property's entries. Isolation is the implementer's owned guarantee.teardown()is idempotent and safe to run against an abandoned store: a failing assertion panics the row (early exit, no teardown on that path), so teardown must also make sense over a dropped / half-open instance. The factory owns what teardown means — deleting the temp dir, dropping the schema, closing the pool — and owns the isolated-ness guarantee above.- Factories are
Send + Sync: shareable across row runs. - The wave-2 binding — made and recorded. The
Storefacade type landed with wave 2's store-construction task as a concrete type (alkblobs::Store, engine-blind; src/store.rs holds the engine- representation design point). The binding made the anticipated shape correction: the skeleton'sStoreassociated type collapsed into the concrete facade —open()vendsalkblobs::Storedirectly (factory.rs records the ruling; the wave-1 mock probe retired with it, replaced by the mem-gated binding demo factories inmain.rs). A new engine column binds by implementing the trait; there is no longer a vendible type to choose. - The row body owns its lifecycle. A row calls
open()first — a fresh, isolated store per call — drives its assertions, and callsteardown()before returning; a failing assertion panics the row before teardown runs (the early exit above), which is whyteardown()must make sense over the abandoned instance — dropped or still alive at unwind. The mem column's structural tests (factory_teardown_is_safe_against_a_panicking_row) exercise that path; isolation itself (fresh state per open, nothing shared) is exercised alongside (factory_open_vends_fresh_isolated_stores).
The SKIP posture
SQL-attached factories (postgres, pg-lo) ride a server (dockerized
postgres) whose connection settings come from the environment. When no
server is reachable, the column skips cleanly: a skip is not a
failure, and the harness reports it as skipped rather than green or
red. Two sides to keep honest:
- Engine waves' acceptance criteria require green against the server (the alkstore harness convention) — a skip never substitutes for acceptance.
- Server-less engines (
mem,sqlite,local, and any future dependency-free engine) never skip: their columns must always be green in every environment.
The invariant backlog (rows-to-grow)
The seven invariants of store-api.md §Invariants, registered as the table every wave's rows grow against. Traceability: each row family names its store-api.md invariant and the ADRs pinning it. A suite row asserting something outside this table (or outside a wave's specced additions) needs a backlog entry and its stamp first.
| # | Invariant (store-api.md §Invariants) | Row family | Pinning text | First column |
|---|---|---|---|---|
| INV-1 | Whole-blob put/get round-trips byte-identical under the canonical derivation — both tiers, both put paths, every shipped engine | one round-trip family; content matrix and put paths loop in-body; per-engine columns | store-api.md §Invariants 1; ADR-002 (derivation); ADR-003 (put paths, dispatch); ADR-007/008/009/011 (engines) | wave 2 — mem (kv tier), then local (large tier) |
| INV-2 | Dedup: identical content twice (same or different path) is one pool entry | one dedup row, same-path and different-path legs | store-api.md §Invariants 2; ADR-003 (CAS first-writer-wins); ADR-005 (re-put refcounts) | wave 2 — mem |
| INV-3 | Stage hygiene: failure mid-put leaves zero stage files / no entry; success leaves exactly one committed entry | one hygiene family over both put paths and both failure modes (discard, failed commit) | store-api.md §Invariants 3; ADR-010 §1 (staged-put contract); ADR-003 | wave 2 — mem (degenerate stage) and local |
| INV-4 | Sweep safety: exact counts with correct liveness; aborting sources delete nothing. Engine-gated fleet form (DB-backed pins + sweep lease vs postgres) | one single-node family; the fleet form is its own engine-gated row family (runs under --all-features) |
store-api.md §Invariants 4; ADR-005 (windows, sweeps); ADR-008 (fleet form); ADR-012 §1/§4/§6.7; ADR-013 §2 | wave 2 — mem (in-process protocol); fleet form waits for wave 6 |
| INV-5 | list() correctness is observable only through GC |
list-correctness asserted through sweep outcomes (exact counts), never by comparing lists directly | store-api.md §Invariants 5; ADR-003 (complete + well-formed list()); ADR-005 |
wave 2 — mem |
| INV-6 | stat/size returns entry length without fetching content |
one stat-discipline family per engine column | store-api.md §Invariants 6; ADR-008 (size probe); ADR-009 (pg-lo companion-row probe) |
wave 2 — mem |
| INV-7 | Engine-conformance uniformity: every engine's bodies pass the same contract suite mem passes | the meta-gate — a column's green run is the row; no separate test | store-api.md §Invariants 7; ADR-010 §1; ADR-011 §2 (mem's reference role) | wave 2 — mem defines it |
The reference row set (INV-7's basis)
The mem column's rows (rows.rs) are the reference row set — the
required set every other engine's rows mirror (ADR-011 §2, mem's
contract-reference role; store-api.md §Invariants 7). A column mirrors
this inventory row-for-row against its own factory; an addition to the
set propagates to all columns (INV-7). The INV-row families are the
engine mirror targets — the round-trip family covers the tier the
column's engine serves (mem: the kv tier; local: the large tier). The
two constructor-mode rows mirror wherever that configuration is
constructible: a column with a kv-only factory runs the kv-only row
against it — the row discovers the mode's threshold by probing, since
known-length rejections read nothing, and requires the threshold
materializable for its at-threshold legs (≤ 8 MiB,
MAX_BOUNDARY_MATERIALIZATION).
Row (rows.rs) |
Traces to |
|---|---|
round_trip_puts_and_gets_are_byte_identical |
INV-1 (content matrix, both put paths, in-body) |
dedup_identical_content_is_one_pool_entry |
INV-2 (same-path and cross-path legs) |
stage_hygiene_zero_residue_on_failure_exactly_one_entry_on_success |
INV-3 (both paths, both failure modes) |
sweep_counts_are_exact_across_the_full_protection_set |
INV-4 (happy shape: full protection set) |
sweep_without_liveness_sources_aborts_and_deletes_nothing |
INV-4 (abort shape: NoLivenessSources) |
sweep_with_a_failed_protect_callback_aborts_and_deletes_nothing |
INV-4 (abort shape: ProtectFailed) |
sweep_is_single_flight_the_contended_second_aborts_as_data |
INV-4 (abort shape: SweeperLock lease) |
sweep_racing_a_pinned_put_never_deletes_the_protected_entry |
INV-4 (the sweep-vs-put race) |
list_correctness_is_observable_through_sweep_outcomes |
INV-5 (exact counts, never list comparison) |
stat_reports_len_and_algorithm_without_fetching_content |
INV-6 (the stat-discipline family) |
mem_only_mode_accepts_every_length_on_both_put_paths |
constructor mode (ADR-011 §4) |
kv_only_mode_rejects_over_threshold_puts_at_put_time |
constructor mode (ADR-012 §6.2) |
Stamps live on the rows themselves — grep -rn "Contract stamp:" tests/conformance/ is the inventory. The INV-4 rows beyond the
families' first shapes (the SweeperLock abort) complete the three
GcAbortCause variants within the single-node exact-count family
(ADR-013 §2's lease, its in-process ADR-012 §5 form).
Columns beyond the reference set (INV-7's mirror + extension)
The constructor-mode rows of the reference set run wherever the mode
they name is constructible (README §The reference row set). A
multi-tier column mirrors the set and grows its own rows for what
its configuration carries that the reference set's single-tier host
did not: the dispatch-coverage shape (ADR-012 §6.3 — both tiers
load-bearing) and the engine's tier-specific properties. Those rows
are still conformance rows — ADR-stamped, named for the property,
traced — but they take the column's concrete factory instead of
the StoreFactory trait: they observe the backing medium, which an
engine-uniform row (unable to branch on any engine, and unable to see
any tier's residency through the facade) cannot. Tier placement is
facade-invisible by design; with kv = mem, the medium itself is the
oracle — the mem kv tier never touches the factory's disk, so a
committed file outside the staging area under the column's temp root
is exactly one large-tier entry, and its absence beside a served read
is exactly a kv-tier entry.
The local column's own rows (tests/conformance/local_column.rs, the dispatch-coverage and local-specific families):
| Row | Traces to |
|---|---|
routing_is_a_pure_function_of_length_across_put_paths_and_re_puts |
ADR-003 §Dispatch policy (purity/no-migration); ADR-012 §6.3; INV-7 facade-contract row |
threshold_boundary_lengths_route_deterministically |
ADR-003 §Dispatch policy (at/below/above the threshold; constructor-tuned re-route); ADR-012 §6.3 |
kv_tier_misses_fall_through_to_the_large_tier_per_op |
ADR-012 §6.1 (fall-through per op); ADR-012 §6.5 ruling 5 (miss shapes); ADR-006 |
stage_hygiene_exact_counts_on_disk_over_both_put_paths |
INV-3's stage form (exact counts on disk); ADR-010 §1; ADR-003 |
pread_windows_read_committed_files_mid_slices_and_eof_clamped |
store-api.md §Read path; ADR-010 §1 (streaming cursor); ADR-006 |
virgin_store_reads_are_no_ops_on_a_fresh_root |
store-api.md §Error model (virgin-store semantics); ADR-012 §6.5 |
sweep_counts_stay_exact_over_both_tiers |
INV-4/INV-5's both-tiers form (exact counts + list correctness through outcomes); ADR-005; ADR-012 §1/§6.3/§6.7 |
The dual-tier threshold is declared configuration, not a
behavioral probe. The kv-only reference row discovers its factory's
threshold by probing (rejections read nothing — free). A dual-tier
store never rejects a length, and tier residency is facade-invisible,
so there is nothing to probe: the dispatch rows take the threshold
the column's factory declares (the same value its constructor took —
configuration, like the mem column's kv-only threshold). The mem-only
and kv-only mode rows do not mirror onto the dual-tier factories (the
precondition mode() shapes they assert are not this column's mode);
the dispatch rows are the dual-tier mode's facade-contract coverage.
Column plan
mem is the contract-reference engine (ADR-011 §2): wave 2 lands
its column first (the first column binds the Store facade type), and
every other engine's rows mirror mem's column — an addition to mem's
column propagates as a requirement to all other columns (INV-7). The
pg columns (postgres, pg-lo) additionally run the SKIP posture above;
the fleet-form rows of INV-4 are engine-gated to the postgres column
and first run in wave 6.
Current state
The harness is green on two columns (wave 2). The mem column
is the contract-reference column: the factory binding is the concrete
alkblobs::Store facade (see §The factory contract — the binding
demonstrator remains in binding.rs), the reference row set is landed
(§The reference row set — rows.rs, every row ADR-stamped and traced
to the backlog), the column's factories (mem-only, the reference;
kv-only(mem) at a column-declared threshold) vend fresh isolated
stores with idempotent teardown (mem_column.rs, the structural
factory-contract tests included), and mem never skips.
The local column (the suite's first dual-tier column, the
dispatch-coverage shape ADR-012 §6.3 names) mirrors the reference INV
rows against its kv = mem + large = local factory at a
column-declared tuned threshold — the shared matrix and race fixtures
cross it, so both tiers' bodies are exercised — and grows the
dispatch-coverage rows (pure-function routing with an on-disk
placement oracle, fall-through per op, threshold boundaries at both
the tuned and the shipped-default threshold) plus the local-specific
rows (on-disk stage hygiene exact counts, pread windows, virgin-store
no-ops, the both-tiers sweep shape) — §Columns beyond the reference
set is the recorded inventory. Its factory instances each track and
remove their own temp roots (idempotent teardown, panicking-row-safe;
each runner owns its instance so concurrent rows never share roots).
mem-only/kv-only modes and the local column never skip, but the local
column is feature-gated on mem: the dual_tier_mem_local
constructor rides feature mem because mem is the only constructible
kv engine until wave 3 (the gate is the kv pairing's, not the local
engine's, which is ungated in src/) — the column runs under cargo test --features mem, with the reference row set compiling ungated so
it stays type-checked in default builds.