feat(store): the Store facade — constructor modes, size-threshold dispatch, the factory binding

The wave-2 facade skeleton and routing (tasks/store-construction.md):
the public surface consumers program against, with the ops methods
still to land against it in store-read-paths/gc-protocol/store-put-paths.

- src/store.rs — the public `Store` type (engine-blind, ADR-007/008/
  010) over crate-internal tier enums (`KvEngine`{mem}, `LargeEngine`{
  local}): the engine-representation design point decided as concrete
  + enum dispatch, generic rejected (pub(crate) engines, no dyn);
  the three constructor modes as named constructors — `mem_only`,
  `kv_only_mem`, `dual_tier_mem_local` (the ADR-012 §6.3
  dispatch-coverage shape) — with `StoreMode` introspection; the
  write-side routing seam (`tier_for`), the read-side fall-through
  order (`kv()` then `large()`, ADR-012 §6.1), and the `GcHome`
  GC-state field; the ADR-012 §3 fleet declaration ruled deferred to
  the engine waves (nothing fleet-valid is constructible yet)
- src/dispatch.rs — the pure routing function (`route`, at-or-below
  → kv), `DEFAULT_DISPATCH_THRESHOLD` (128 KiB) exported at the
  crate root, boundary-value unit tests (exactly-at-threshold,
  degenerate tunings, purity)
- src/gc.rs — `GcHome`: the store-instance GC-state home the
  gc-protocol task fills (in-process; fleet state stays on the
  engine-state seam per ADR-010 §2/ADR-012 §4)
- tests/conformance — the wave-2 binding made as anticipated: the
  factory's `Store` associated type collapsed into the concrete
  `alkblobs::Store` (`open` vends it directly), recorded in
  factory.rs + README; the wave-1 mock probe retired, replaced by
  mem-gated binding demo factories (binding.rs) over both
  constructible shapes

Verification: `cargo test` (default), `cargo test --features mem`,
`cargo test --all-features` green (41/53/53 unit + conformance
targets green in all three shapes); `cargo clippy --all-targets
-- -D warnings` green in all three feature shapes; `cargo fmt
--check` green; `cargo doc --no-deps` adds no warnings beyond the
pre-existing private-intra-doc-links class.
This commit is contained in:
glm-5.3-flash committed 2026-10-11 22:18:32 +00:00
1 parent 856d436f81
commit 532adfa707
9 files changed
+840 -84

No files matched your search

+118
View File
@@ -1,2 +1,120 @@
//! Size-threshold dispatch between the kv and large tiers (ADR-003):
//! routing as a pure function of content length.
//!
//! **The routing seam the facade programs through.** [`route`] is the
//! pure function — `route(len, threshold) → Tier`, **at-or-below the
//! threshold stays on the kv tier** (the boundary's pinned reading:
//! store-put-paths' "≤ threshold → kv; > threshold → large") — and
//! [`DEFAULT_DISPATCH_THRESHOLD`] is the shipped default, 128 KiB, the
//! conservative edge of the measured ~128–256 KiB crossover zone (POC #3
//! A4). Purity is the ADR-003 invariant: same content ⇒ same length ⇒
//! same tier, re-puts are deterministic, no content ever migrates.
//!
//! The threshold is **constructor-tunable** (a per-deployment media
//! re-tuning, never an API change): the facade's mode constructors take
//! it explicitly and default their docs to this constant. Read-side
//! consult order — kv first, then large — is the facade's fall-through
//! general rule (ADR-012 §6.1), carried on the `Store` facade type
//! ([`crate::store`]); mode-specific behavior (kv-only's put-time
//! rejection, mem-only's no-threshold posture) lives on the facade's
//! mode machinery, not here: this module routes lengths, it does not
//! own modes.
//!
//! The `#![allow(dead_code)]` below marks the seam's scope: `route` is
//! driven by the facade's write path (store-put-paths) and consulted by
//! the facade's read paths; the allow shrinks as those land.
#![allow(dead_code)]
/// The default dispatch threshold in bytes: 128 KiB — the conservative
/// edge of the measured ~128–256 KiB crossover zone (ADR-003; POC #3
/// finding A4's first-party benchmark). Constructor-tunable per
/// deployment media; retuning is a constructor parameter, never an API
/// change.
pub const DEFAULT_DISPATCH_THRESHOLD: u64 = 128 * 1024;
/// The two tiers a content length routes between. `pub(crate)`: the
/// enum is the routing *answer*, not the engine — tier names the
/// routing class (ADR-011 §1), and which engine backs the tier is the
/// constructor's choice, never this type's business.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Tier {
/// The kv tier — at-or-below-threshold content.
Kv,
/// The large tier — over-threshold content.
Large,
}
/// The pure routing function: over the threshold goes to the large
/// tier; **at or below** (exactly-at-threshold included) stays on the
/// kv tier. The boundary reading is pinned here and boundary-tested
/// below — `store-put-paths` and the read paths consume this exact
/// reading through the facade's seam.
///
/// Total purity discipline (ADR-003): a pure function of `len` — no
/// engine input, no namespace, no state. Same length is the same tier
/// forever, across mixed put paths, re-puts, and restarts.
pub(crate) fn route(len: u64, threshold: u64) -> Tier {
if len > threshold {
Tier::Large
} else {
Tier::Kv
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The shipped default is 128 KiB — pinned numerically (131072).
#[test]
fn default_threshold_is_128_kib() {
assert_eq!(DEFAULT_DISPATCH_THRESHOLD, 131_072);
assert_eq!(DEFAULT_DISPATCH_THRESHOLD, 128 * 1024);
}
/// The pinned boundary reading: exactly-at-threshold routes to the
/// kv tier (≤ threshold → kv); one over routes large.
#[test]
fn exactly_at_threshold_routes_kv_one_over_routes_large() {
assert_eq!(route(4096, 4096), Tier::Kv);
assert_eq!(route(4095, 4096), Tier::Kv);
assert_eq!(route(4097, 4096), Tier::Large);
}
/// Boundary values across the domain: zero length, the default
/// threshold, and the degenerate tunings a constructor may pass —
/// all route deterministically.
#[test]
fn boundary_values_route_deterministically() {
assert_eq!(route(0, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
assert_eq!(route(131_071, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
assert_eq!(route(131_072, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
assert_eq!(route(131_073, DEFAULT_DISPATCH_THRESHOLD), Tier::Large);
assert_eq!(route(0, 0), Tier::Kv, "zero-length content always kv");
assert_eq!(
route(1, 0),
Tier::Large,
"threshold 0: everything else large"
);
assert_eq!(route(0, u64::MAX), Tier::Kv, "max threshold: nothing large");
assert_eq!(
route(u64::MAX, u64::MAX),
Tier::Kv,
"at-threshold is kv even at MAX"
);
}
/// Purity (the ADR-003 invariant): the same length is the same tier
/// on every call — routing derives from inputs alone.
#[test]
fn routing_is_a_pure_function_of_length() {
let len = 103_487;
let first = route(len, DEFAULT_DISPATCH_THRESHOLD);
for _ in 0..32 {
assert_eq!(route(len, DEFAULT_DISPATCH_THRESHOLD), first);
}
}
}
+13
View File
@@ -36,6 +36,19 @@ use crate::backend::Backend;
use crate::facade::PinState;
use crate::Error;
/// The store-instance GC state home — held as a field of the public
/// facade type ([`Store`](crate::store::Store)). ADR-011 §1's vocabulary
/// and ADR-010 §2's ownership rule together: the store instance owns
/// all GC state, so in-process GC state carries on the facade
/// instance itself. The wave-2 gc-protocol task fills it — the pin
/// registry (per-key refcounts under the arbitration lock), registered
/// liveness sources, the protect callback, and the in-process delete
/// window (ADR-005's protocol inline, ADR-012 §5's non-SQL staging) —
/// and drives sweep/direct-delete through the facade. Fleet state never
/// lands here: it rides the engine-state companion seam below
/// (ADR-010 §2), hosted on the fleet's kv engine (ADR-012 §4).
pub(crate) struct GcHome;
/// A delete-window candidate staged into the durable window: the key and
/// the time it becomes visible for deletion — exactly the
/// `gc_window (key, visible_after)` row pair (ADR-005's delete-window
+3
View File
@@ -6,11 +6,14 @@ pub mod gc;
pub mod hashing;
pub mod keys;
pub mod local;
pub mod store;
pub use dispatch::DEFAULT_DISPATCH_THRESHOLD;
pub use error::Error;
pub use facade::{Batch, EntryMeta, GcAbortCause, Pin, PinState, SweepReport};
pub use hashing::{Algorithm, Digest};
pub use keys::Key;
pub use store::{Store, StoreMode};
#[cfg(feature = "mem")]
pub mod mem;
+419
View File
@@ -0,0 +1,419 @@
//! The `Store` facade type (store-api.md §What this is): the public
//! surface consumers program against — constructor modes and the
//! size-threshold dispatch seam. **Skeleton and routing only**: the ops
//! methods land against this type in `store-read-paths` (get/stat/
//! read_range/has/list with the fall-through general rule),
//! `gc-protocol` (sweep, direct delete, the pin/liveness machinery),
//! and `store-put-paths` (put, the kv-only rejection behavior). This
//! module owns the engine representation and the constructor-mode
//! machinery; the pure routing function lives in [`crate::dispatch`].
//!
//! **Named design point — engine representation: concrete `Store`,
//! crate-internal enum dispatch.** A `Store<Kv: Backend, Large: Backend>`
//! generic was the alternative: rejected. The engines are statically
//! known and in-crate (no `dyn` anywhere — wave-1 design point 3), and
//! they are `pub(crate)` behind the crate-internal [`Backend`][crate::backend::Backend]
//! trait — a generic facade parameterized over `Backend` implementers
//! would leak engine types through the public surface, force consumers
//! to name engines they must never see (the facade is **engine-blind**,
//! ADR-007/008/010), or push `dyn` onto the surface. The concrete type
//! holds two crate-internal engine-holding enums ([`KvEngine`],
//! [`LargeEngine`]) dispatching over the closed five-engine set; the
//! conformance factory's binding note anticipated exactly this collapse
//! (tests/conformance/factory.rs — "most plausibly the associated type
//! collapsing to the concrete facade type across all factories"). The
//! public surface is engine-blind: consumers name modes and engines
//! only through named constructors, feature-gated with their engine
//! (`mem_only` needs feature `mem`; the sqlite/postgres/pg-lo
//! constructors arrive with waves 3–5 and slot into the same enums and
//! constructor pattern without reshaping — adding an enum variant and
//! a named constructor is slotting in).
//!
//! **Constructor modes** (ADR-011 §4; ADR-012 §6.3):
//!
//! - **dual-tier** (the default): kv + large, size-threshold dispatch
//! via [`crate::dispatch::route`]. The wave-2 constructible shape is
//! `kv = mem` + `large = local` — the dispatch-coverage test shape
//! ADR-012 §6.3 names (both tiers load-bearing). Over-threshold
//! content routes to the large tier; at-or-below to kv.
//! - **kv-only**: no large tier; over-threshold puts reject **at put
//! time** (ADR-012 §6.2 — known-length immediately; unknown-length at
//! mid-stream threshold overflow). The rejection behavior itself
//! lands with the put seam; the threshold this mode carries is the
//! rejection point's value.
//! - **mem-only**: the mem engine alone, **no dispatch threshold in
//! effect** (everything is local and small) — a testing/embedder-
//! ephemeral posture, never production. Structurally distinct from
//! kv-only precisely by that threshold absence.
//!
//! Modes are validated structurally — unrepresentable states are
//! unrepresentable by construction: dual-tier's signature constructs
//! the large engine itself, mem-only's constructor exists only with
//! feature `mem` and constructs the mem engine — and every instance
//! carries its mode for introspection ([`Store::mode`]).
//!
//! **The fleet declaration defers to the engine waves.** ADR-012 §3
//! rules fleet mode a constructor declaration (`fleet: true`), never
//! inferred — but it is a constructor *error* for every non-fleet-valid
//! configuration, and no fleet-valid configuration is constructible yet
//! (mem is never fleet-valid; `local` needs the shared-media
//! declaration of ADR-008 §3; the fleet-valid engines — postgres, pg-lo
//! — are waves 4–5). Adding the declaration then is additive; carrying
//! a flag that could only ever be rejected today would be dead
//! machinery.
//!
//! **The dispatch seam the ops tasks program through:** tier selection
//! for a given length ([`Store::tier_for`] composing
//! [`crate::dispatch::route`] per mode), the fall-through consult
//! order — **kv first, then large** — that every key-addressed op
//! consumes (ADR-012 §6.1's general rule; [`Store::kv`] then
//! [`Store::large`]), and the GC state home ([`crate::gc::GcHome`])
//! the gc-protocol task fills.
//!
//! The `#![allow(dead_code)]` below marks the skeleton's scope: the
//! accessors and engine enums exist for the ops tasks' drivers, and the
//! kv engine set is empty in default builds (sqlite is wave 3). The
//! allow shrinks as the ops tasks land.
#![allow(dead_code)]
use crate::dispatch::{route, Tier};
use crate::gc::GcHome;
use crate::local::LocalBackend;
#[cfg(feature = "mem")]
use crate::mem::MemBackend;
#[cfg(feature = "mem")]
use crate::Error;
#[cfg(feature = "mem")]
use std::path::PathBuf;
use self::StoreMode::{DualTier, KvOnly, MemOnly};
/// The store instance — the public facade above the tier engines
/// (ADR-011 §1's vocabulary: *one running instantiation of the store
/// facade — one engine set per the constructor mode, plus dispatch and
/// GC state*; **the GC arbitration domain** for single-instance
/// topologies).
///
/// Engine-blind by construction: the engines are held in crate-internal
/// enums dispatched statically (see the module's engine-representation
/// design point); consumers name modes only. The GC state home
/// ([`crate::gc::GcHome`]) rides as a field — the store instance owns
/// all GC state (ADR-010 §2), which the gc-protocol task fills.
pub struct Store {
/// The kv-tier engine — the first consult in every fall-through.
kv: KvEngine,
/// The large-tier engine — `None` in single-tier modes.
large: Option<LargeEngine>,
/// The dispatch threshold in effect where the mode has one; mem-only
/// carries the shipped default but never consults it — "no dispatch
/// threshold in effect" is the mode's ruling (ADR-011 §4), not a
/// value absence.
threshold: u64,
/// The constructor mode; the dispatch/put/read/gc paths branch on it.
mode: StoreMode,
/// The GC state home (ADR-010 §2: the store core owns GC state)
/// — the gc-protocol task's machinery lands here.
gc: GcHome,
}
/// The constructor modes (ADR-011 §4's pinned set; ADR-012 §6.3's
/// dual-tier ruling). Three variants, ADR-pinned — a new mode is a new
/// ADR.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreMode {
/// kv + large, size-threshold dispatch between them (the default).
DualTier,
/// kv alone; over-threshold puts reject at put time (ADR-012 §6.2).
KvOnly,
/// The mem engine alone, no dispatch threshold in effect — never
/// production (ADR-011 §4).
MemOnly,
}
/// The kv-tier engine set — the closed per-tier engine enum of the
/// engine-representation design point. Feature-gated variants slot in
/// as their engine waves land: sqlite (wave 3), postgres (wave 4).
pub(crate) enum KvEngine {
/// The contract-reference ephemeral engine (ADR-011 §2).
#[cfg(feature = "mem")]
Mem(MemBackend),
}
/// The large-tier engine set. `local` is ungated (dependency-free,
/// always present, ADR-013 §4); pg-lo slots in at wave 5.
pub(crate) enum LargeEngine {
/// The sharded-filesystem engine (ADR-003/008).
Local(LocalBackend),
}
impl Store {
/// **mem-only mode** (ADR-011 §4): the mem engine alone, no
/// dispatch threshold in effect — a testing/embedder-ephemeral
/// posture, never production. Every length routes to the (sole)
/// mem engine; there is no large tier and nothing to reject.
#[cfg(feature = "mem")]
pub fn mem_only() -> Store {
Store {
kv: KvEngine::Mem(MemBackend::default()),
large: None,
threshold: crate::dispatch::DEFAULT_DISPATCH_THRESHOLD,
mode: MemOnly,
gc: GcHome,
}
}
/// **kv-only mode** (ADR-011 §4; ADR-012 §6.2) with the mem kv
/// engine: no large tier; `threshold` is the put-time rejection
/// point — known-length puts over it reject immediately,
/// unknown-length puts at mid-stream threshold overflow (the put
/// seam's behavior; zero residue on either rejection). Routing in
/// this mode is kv for every length.
#[cfg(feature = "mem")]
pub fn kv_only_mem(threshold: u64) -> Store {
Store {
kv: KvEngine::Mem(MemBackend::default()),
large: None,
threshold,
mode: KvOnly,
gc: GcHome,
}
}
/// **dual-tier mode** (the default, ADR-011 §4) in the wave-2
/// constructible shape — `kv = mem` + `large = local`, the
/// dispatch-coverage test shape ADR-012 §6.3 names: both tiers
/// load-bearing, routed by size-threshold dispatch at `threshold`
/// (constructor-tunable; [`crate::dispatch::DEFAULT_DISPATCH_THRESHOLD`]
/// is the shipped default).
///
/// Constructing the large engine creates the root directory —
/// fallible I/O, hence a fallible constructor. The mem kv engine
/// cannot fail.
///
/// Not fleet-anything: mem is never fleet-valid (ADR-011 §4).
#[cfg(feature = "mem")]
pub async fn dual_tier_mem_local(
root: impl Into<PathBuf>,
threshold: u64,
) -> Result<Store, Error> {
Ok(Store {
kv: KvEngine::Mem(MemBackend::default()),
large: Some(LargeEngine::Local(LocalBackend::new(root).await?)),
threshold,
mode: DualTier,
gc: GcHome,
})
}
/// The constructor mode (ADR-011 §4's vocabulary) — introspection
/// for embedders and tests; the dispatch/put/read paths branch on
/// it crate-internally.
pub fn mode(&self) -> StoreMode {
self.mode
}
/// The active dispatch threshold — `Some` in dual-tier (the routing
/// boundary) and kv-only (the put-time rejection point); `None` in
/// mem-only mode ("no dispatch threshold in effect", ADR-011 §4).
pub(crate) fn threshold(&self) -> Option<u64> {
match self.mode {
DualTier | KvOnly => Some(self.threshold),
MemOnly => None,
}
}
/// **Write-side tier selection for a given content length** — the
/// dispatch seam's routing half (the put seam programs through
/// this; read paths do not consult it). Dual-tier:
/// [`crate::dispatch::route`]'s pure function at the instance's
/// threshold (exactly-at-threshold stays kv — the pinned boundary
/// reading). kv-only: kv for every length — the over-threshold
/// *rejection* is the put seam's behavior keyed off
/// [`Store::threshold`] (ADR-012 §6.2), not a routing answer.
/// Mem-only: kv — there is no other tier and no threshold in
/// effect.
pub(crate) fn tier_for(&self, len: u64) -> Tier {
match self.mode {
DualTier => route(len, self.threshold),
KvOnly | MemOnly => Tier::Kv,
}
}
/// **The fall-through consult order — kv first** (ADR-012 §6.1's
/// general rule): every key-addressed op consults the kv tier
/// first; large comes after ([`Store::large`]). A miss on both
/// tiers resolves per op's shape (`None` on get/stat,
/// `Err(Missing)` on read_range/delete).
pub(crate) fn kv(&self) -> &KvEngine {
&self.kv
}
/// **…then large** — the second consult; `None` in single-tier
/// modes (mem-only, kv-only), where large-tier content cannot
/// exist.
pub(crate) fn large(&self) -> Option<&LargeEngine> {
self.large.as_ref()
}
/// The GC state home (ADR-010 §2: the store core owns all GC
/// state) — the gc-protocol task's pin registry, liveness sources,
/// protect callback, and in-process delete window land here and
/// drive the sweep and direct delete from this facade.
pub(crate) fn gc(&self) -> &GcHome {
&self.gc
}
}
#[cfg(all(test, feature = "mem"))]
mod tests {
use super::*;
use crate::dispatch::DEFAULT_DISPATCH_THRESHOLD;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
/// A fresh temp root per test (mirrors `local.rs`'s helper); Drop
/// removes the whole tree idempotently.
struct TempRoot(PathBuf);
fn temp_root(tag: &'static str) -> TempRoot {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"alkblobs-store-{tag}-{}-{:x}-{seq}",
std::process::id(),
nanos
));
fs::create_dir_all(&dir).expect("temp root created");
TempRoot(dir)
}
impl TempRoot {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
/// mem-only constructs and validates: the ephemeral mode's shape —
/// no large tier, no threshold in effect, kv routing for every
/// length (ADR-011 §4).
#[cfg(feature = "mem")]
#[test]
fn mem_only_constructs_and_validates() {
let store = Store::mem_only();
assert_eq!(store.mode(), MemOnly);
assert_eq!(store.threshold(), None, "no dispatch threshold in effect");
assert!(store.large().is_none(), "mem-only has no large tier");
for len in [0u64, 1, DEFAULT_DISPATCH_THRESHOLD + 1, u64::MAX] {
assert_eq!(
store.tier_for(len),
Tier::Kv,
"mem-only routes every length to its sole tier"
);
}
}
/// kv-only constructs and validates: no large tier; the threshold
/// is the *rejection point's* carrier (ADR-012 §6.2), not a routing
/// answer — every length routes kv there, and the put seam rejects
/// known-length over-threshold puts immediately (its behavior).
#[cfg(feature = "mem")]
#[test]
fn kv_only_mem_constructs_and_validates() {
let store = Store::kv_only_mem(1024);
assert_eq!(store.mode(), KvOnly);
assert_eq!(store.threshold(), Some(1024));
assert!(store.large().is_none(), "kv-only has no large tier");
for len in [0u64, 1023, 1024, 1025, u64::MAX] {
assert_eq!(
store.tier_for(len),
Tier::Kv,
"kv-only routes every length to its sole tier"
);
}
}
/// The ADR-012 §6.3 dispatch-coverage shape: `kv = mem` +
/// `large = local` dual-tier construction works, both tiers
/// load-bearing, dispatch threshold active — boundary values route
/// deterministically (at/at-below threshold kv, above large), and
/// tunability is proven against a non-default threshold.
#[cfg(feature = "mem")]
#[tokio::test]
async fn dual_tier_mem_local_constructs_and_routes() {
let root = temp_root("dual-tier");
let store = Store::dual_tier_mem_local(root.path(), 1024)
.await
.expect("dual-tier mem+local constructs");
assert_eq!(store.mode(), DualTier);
assert_eq!(store.threshold(), Some(1024));
assert!(store.large().is_some(), "both tiers present");
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"length {len} routes deterministically"
);
assert_eq!(
store.tier_for(len),
tier,
"same length ⇒ same tier (the purity invariant, re-checked)"
);
}
}
/// The shipped default threshold is the constructor-tunable
/// baseline: dual-tier at `DEFAULT_DISPATCH_THRESHOLD` routes at
/// the 128 KiB boundary exactly as the pure function does.
#[cfg(feature = "mem")]
#[tokio::test]
async fn dual_tier_routes_at_the_default_threshold() {
let root = temp_root("dual-tier-default");
let store = Store::dual_tier_mem_local(root.path(), DEFAULT_DISPATCH_THRESHOLD)
.await
.expect("dual-tier constructs at the default threshold");
for (len, tier) in [
(131_071, Tier::Kv),
(131_072, Tier::Kv),
(131_073, Tier::Large),
] {
assert_eq!(store.tier_for(len), tier, "length {len} at 128 KiB");
}
}
/// The GC state home rides as a field of the store instance
/// (ADR-010 §2 — the store instance owns GC state) and is
/// reachable crate-internally: the gc-protocol task's machinery
/// lands against this handle.
#[cfg(feature = "mem")]
#[test]
fn gc_state_home_rides_on_the_foundation() {
let store = Store::mem_only();
let _ = store.gc();
}
}