Files
alkblobs/docs/architecture/decisions/013-family-composition-reactive-boundary-and-feature-graph.md
T
glm-5.3-flash bee86b0dbd docs(architecture): ADR-013 — family composition, reactive boundary, sweep lease; OQ-11 resolved
- No sibling (alkstore) dependency in-core; reactivity is
  deployment-layer composition above the facade — cadence stays
  embedder-owned (ADR-005), with alkstore the named substrate for it;
  in-crate notify/scheduler/queues/invalidation hooks are non-goals.
- Fleet sweeper lock pinned as 'named exclusive sweep lease'
  semantics; shipped provider unchanged (pg advisory clause);
  provider swaps are ADR-007-shaped substitution doors.
- Composition contract for downstream ADRs: co-tenancy of CAS/GC
  state + reactive machinery + consumer/liveness tables on one fleet
  host; no-cross-store-atomicity ruled; compensation rests on
  idempotent re-puts, windowed deletes, sweeps-as-report-data.
- OQ-11 resolved (the register's one live circular hedge): engine
  features pull their own driver clusters — sqlite becomes feature
  'sqlite' (default-on, carries rusqlite); 'kv'/'large' tier features
  retired; 'local' ungated; pg-pure builds are supported,
  non-default. Deciding fact already in-tree (REQ-2/ADR-009).
- requirements.md: UC-4/REQ-5 records the composition facts
  (operator record 2026-10-11, corroborated by alkstore's
  consumer-inventory).
- Publish guard stated pre-code: facade ships before publish;
  semver-is-contract from first publish (alkstore ADR-017 discipline).
- Amended accepted ADRs' feature wording (ADR-010 §1, ADR-011 §3/§4,
  ADR-003) with supersession notes; README/overview/open-questions/
  backends/store-api/gc aligned; parked index now empty.

Verification: docs-only round (no Rust surface touched); full-tree
single-value check on the feature graph and sweep-lease wording.
2026-10-11 16:47:22 +00:00

12 KiB

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 — REQ-2 (the fact OQ-11 parked on), REQ-5 (the composition requirement §3 serves)
  • overview.md (dependency posture, amended §4; non-goals, extended §1); backends-and-dispatch.md (engine/features table, amended §4); store-api.md (sweep semantics, §2's lease wording); gc-and-namespaces.md (sweeper locking); 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