# ADR-013: Family composition — the reactive boundary, the sweep lease, and the feature-graph resolution ## Status Accepted (2026-10-11) ## Context While this crate paused mid-pre-decomposition (ADR-012 was its last round), the sibling **alkstore** shipped v0.1.0: one reactive coordination contract (`Store` — notify/listen, named TTL locks, durable queues, streams with per-consumer offsets, outbox, leader-elected scheduler) behind engine crates (`-mem`, `-sqlite`, `-postgres`), contract-pinned by a factory-driven suite. Its consumer-inventory — built from the paused consumers' written documents under this crate's own deferral discipline — grades three needs of *this crate's specced machinery*: - the fleet sweeper lock → named locks, **pinned**; - maintenance cadence (sweep scheduling, pin-renewal workers) → scheduler/queues, **documented** — against ADR-005's "embedder owns cadence"; - fleet invalidation ("sweep/manifest changed" among fleet instances) → notify, **documented**. The first consumers (alkgit, alkfs) will deploy both crates together; composition facts are therefore inter-crate decisions that must be decided on evidence in hand, not parked on deployments that cannot pre-exist this crate (the Schrödinger's-code rule; the OQ register's OQ-09/OQ-10 precedents). The audit round that produced this ADR also found one live circular hedge in the register: OQ-11 parked the pg-only kv feature graph on "a named deployment requirement arriving from outside this crate" — a deciding fact *this tree already contains* (REQ-2's consolidation posture, which ADR-009 shipped as "a fleet node can consolidate on postgres-only"), the same shape-shifter OQ-09/OQ-10 were rejected for, and the last one in the register. Additionally, the alkcall failure mode ("published doc surface promising unimplemented behavior, surviving versions") motivates stating this crate's publish discipline before anything ships. ## Decision ### 1. The reactive boundary: no sibling dependency in-core; reactivity is deployment-layer composition **The core crate gains no alkstore dependency and no reactive machinery.** No in-crate notify, no in-crate scheduler, no queues, no invalidation hooks on any facade method. ADR-001's substrate posture extends by the same reasoning to the reactive substrate: the store layer ships bytes-and-addresses, and everything that makes bytes *reactive* is composition above the facade. - **ADR-005's "embedder owns cadence" stands, with the substrate named.** An embedder wanting interval sweeps, pin-renewal workers (the TTL/3 cadence, ADR-008), or maintenance dispatch composes them above the facade — typically with the sibling substrate's scheduler/queues. The punt was always a placement decision, not a missing mechanism; the sibling's existence changes nothing here. - **Sweep outcomes are data (ADR-012 §1) — with a pinned consumer.** The report is what the embedder's reactive wiring consumes: `SweepReport` → sibling-notify/stream events (downstream cache invalidation), report cadence → the embedder's scheduler. No in-crate hook exists for any of it; this crate never references the sibling's types, channels, or names. - The engine-driver dependency posture is likewise untouched: no alkstore engine crate enters this crate's feature graph (see §4's graph, which names only storage engines). ### 2. The sweep lease: semantics pinned, provider shipped The fleet sweep single-flighter (ADR-008 §2's advisory lock, whose state home is the engine-state seam's sweep-lock clause — ADR-010 §2, ADR-012 §5) is hereby pinned as a **named exclusive sweep lease**: - at most one sweep in flight per pool; - acquisition is an atomic test-and-set at the fleet host; - holdership ends at explicit release, owner-process death, or the underlying engine session's loss; - contention is the `GcAbortCause::SweeperLock` report abort with nothing deleted (ADR-012 §1 — report data, never an error). These are exactly pg advisory-lock semantics — the shipped provider (ADR-008's mechanism, unchanged; a lease whose life is the sweep's own) — and exactly what a named TTL lock provides when its lease spans the sweep run. Pinning the *semantics* is what makes the provider substitutable without a contract change: if the family ever wants the in-crate provider swapped (e.g. onto the sibling's lock), that is the ADR-007-shaped substitution door — a new ADR with its own evidence, not an open question. A deployment may *additionally* serialize its maintenance above this crate: an embedder holding a named lease on the sibling substrate before invoking `sweep()` composes one discipline over its own scheduled tasks (sweeps, expiry reaping, renewal workers). This is composition above the facade — it changes no contract and is not required for fleet correctness, because the in-crate advisory clause is this crate's own guarantee regardless. ### 3. Co-tenancy and the no-cross-store-atomicity ruling (the downstream-facing composition contract) A fleet pg instance hosts, as co-tenants: this crate's CAS rows and GC state (its pool; `gc_pins` / `gc_window` — ADR-012 §4); the sibling substrate's reactive machinery; consumer business tables (refs, manifests, path trees — outside this crate per ADR-004); and the consumer liveness tables this crate's fleet arbitration consults as named table references (ADR-012 §6.6). **No crate owns another's tables; co-tenancy is a named deployment statement** — the posture decisions/007 and ADR-008 §2 already assumed, now citable so downstream ADRs (alkgit's refs, alkfs's tree) reference it rather than re-derive it. - **Cross-store atomicity is unachievable by construction** — each crate owns its own transaction scope, and no seam bridges them. The family posture downstream is the outbox/compensation pattern (alkstore's `outbox`/`notify_tx` are its substrate), and this crate's load-bearing properties for that pattern are already guaranteed and stay so: **puts are idempotent under CAS** (delete-then-recover is a byte-identical re-put, ADR-005), **deletes are windowed and re-arbitrated before execution** (ADR-005; ADR-012 §6.7), and **sweep outcomes are data** (ADR-012 §1). Nothing downstream may rely on cross-store-atomic anything; nothing here relies on it either. - **Fleet liveness registration's two forms are the downstream-facing seam:** the fleet form — a named liveness table co-tenant on the fleet host (ADR-012 §6.6) — and the single-node form — the in-process callback (ADR-005). Every downstream crate plans against both. The requirement to keep a consumer's liveness protection expressible on the fleet host is a *composition constraint* on downstream schema placement: a consumer whose liveness lives on a non-co-tenant engine participates in fleet GC via pins (put-path) only, and its sweep-time liveness is expressed by materializing rows per ADR-008 §2's materialization rule. ### 4. OQ-11 resolved: engine features pull driver clusters independently — including sqlite **Features gate engines; tiers are constructor composition, not features.** The ADR-010 §1 discipline ("engine features pull the driver cluster independently" — pg-lo does not imply postgres) extends to every engine: | Engine | Feature | Default | Pulls | |---|---|---|---| | kv `sqlite` | `sqlite` | **on** | `rusqlite` | | kv `postgres` | `postgres` | off | `tokio-postgres` + `deadpool-postgres` | | kv `mem` | `mem` | off | nothing | | large `local` | *(ungated)* | always present | nothing | | large `pg-lo` | `pg-lo` | off | the shared pg driver cluster | - The base `kv` feature — which existed because sqlite was the kv tier's only shippable driver carrier and so pulled `rusqlite` under the tier's name — is retired, as is the `large` tier feature (nothing was ever behind it; `local` is ungated because it pulls nothing). ADR-011 §3's `feature fs` → `feature large` rename line is superseded by this table; ADR-003's "tiers default-on" wording now reads as constructor-presence, not feature claims. - **A pg-pure build (`sqlite` off — kv=postgres, and `large`=pg-lo if wanted) is a supported configuration this crate does not default to.** This resolves OQ-11 on evidence in hand: REQ-2 (operator authority, 2026-10-03) is the deployment requirement the parked row awaited — its consolidation posture (ADR-009's postgres-only fleet node) is the pg-pure node, and parking the feature graph on it was circular (a deployment requirement whose name the tree already carries is not an absent fact). Mechanism unchanged from the precedent: engine features pull their own drivers, so sqlite's absence is one feature toggle, never a graph surgery. - **Default builds are unchanged**: `sqlite` default-on keeps the batteries-included default (the lean-base principle constrains default builds, which still pull only tokio/thiserror/parking_lot plus the default engine's driver). ### 5. Publish guard (stated pre-code, on the alkcall lesson) The crate publishes only after its facade exists and passes the invariants' test gate. From first publish, its semver is the contract version — every published surface is implemented, every contract change cites its ADR — the discipline the sibling substrate pinned in its contract-versioning ADR. No published doc surface ever carries a "planned/not-implemented" promise. ## Consequences **Positive** - The composition contract exists as a named ADR: alkgit/alkfs cite §3 for schema co-tenancy, the two-form liveness seam, and the no-cross-store-atomicity posture, instead of re-deriving or guessing. - The register's one live circular hedge (OQ-11) is resolved on in-tree evidence, pre-consumer, where the fix is cheapest — the alkcall-shaped outcome this round exists to avoid. - A new fleet node's dependency tree is honest: pg-pure nodes do not compile rusqlite; nothing reactive is dead code in the core. - The sweep lease's semantics and provider are single-valued; a provider swap later is a mechanism ADR, not a contract break. **Negative** - One more ADR's worth of surface for every reader of the fleet GC story (§2 changes no signatures; it pins names for semantics the mechanisms already had). - The feature graph must be implemented as specced in §4's table — an implementation-facing edit arriving with the engine tasks rather than inside one of them. **Neutral** - No tier, engine, constructor mode, facade signature, error variant, or ops payload changes. `fleet: true` validation (ADR-012 §3) and the composite fleet-validity predicate are untouched. - The sibling substrate relationship is documentary here — requirements-level facts and boundary rules — with the eventual dependency edge (if any, e.g. an ops-layer convenience) requiring its own ADR through the substitution door. ## References - ADR-001 (the substrate posture §1 extends to the reactive substrate), ADR-003/007 (the substitution door §2 applies), ADR-005 (cadence ownership; delete-then-recover as the idempotency backstop), ADR-006 (the deferred-cost pattern this ADR's OQ-11 resolution follows), ADR-008 (the fleet sweep lock §2 pins and the liveness materialization rule §3 reuses), ADR-010/012 (the engine-state seam, GC-state host, outcomes-as-data, the composite predicate), ADR-011 (vocabulary; the constructor tables §4 supersedes in feature shape) - [requirements.md](../requirements.md) — REQ-2 (the fact OQ-11 parked on), REQ-5 (the composition requirement §3 serves) - [overview.md](../overview.md) (dependency posture, amended §4; non-goals, extended §1); [backends-and-dispatch.md](../backends-and-dispatch.md) (engine/features table, amended §4); [store-api.md](../store-api.md) (sweep semantics, §2's lease wording); [gc-and-namespaces.md](../gc-and-namespaces.md) (sweeper locking); [open-questions.md](../open-questions.md) (OQ-11 resolved) - alkstore — `/workspace/@alkdev/alkstore`: its consumer-inventory (`docs/research/consumer-inventory.md`, the graded fleet rows), ADR-016 (deployment honesty), ADR-017 (contract versioning — §5's cited discipline); the sibling's payload model (JSON-only) is its own non-goal boundary, matching this crate's object-storage boundary here - The alkcall hedge precedent (published-doc-surface drift) — the failure mode §5 guards, on the operator's record