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

242 lines
12 KiB
Markdown

# 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