16 KiB
ADR-016: Deployment honesty — no runtime capability surface; the boundary is compile-time identity + the documented matrix
Status
Accepted (2026-10-06, Phase 1 — OQ-08's resolution; decides the capability-surface question ADR-008 §1/§6 deliberately left open; the contract surface is unchanged by this ADR except one verification-backlog row and the annotations this resolution hangs on ADR-001/ADR-008)
Context
The two engines' host postures are facts, decided since Phase 0:
- Postgres is natively multi-host — POC #2 verified no
single-host assumption anywhere (
poc-pg-posture-findings.md"OQ-ST-08": the property tests ran all-through-network over the docker bridge; the listener/wake machinery is per-process, connection-based — ADR-004). - SQLite is single-machine by nature — file-backed; NFS
two-writers unsupported (honker's honesty posture —
/workspace/honker@ f4e53c6,README.md"Honker is single-machine and file-backed... two servers writing the same.dbover NFS is not a Honker deployment strategy"), inherited by the substrate at the fork (ADR-003, ADR-011); cross-process on one host is verified ground (data_versionis cross-process by nature).
deployment.md carries these facts as a matrix (host semantics, connection budgets, durability knobs, toolchain notes). OQ-08 owns the trait-surface half: the unified surface must not pretend SQLite is multi-host — where does that honesty live? Three options were framed there:
- Compile-time only — the consumer picks an engine crate at dependency time; the engine's docs carry its deployment facts. Smallest contract; nothing runtime to match on.
Store::capabilities()— a runtime description (payload limits, wake-cadence knobs, host semantics). Lets a consumer adapt, but adds a contract surface all engines must keep honest.- Deployment matrix only — no API surface; the document holds the facts.
The constraint set is fixed by decisions already made:
- The engine choice is a dependency-graph fact. Single-driver binaries (ADR-001): a consumer depends on core plus exactly one engine crate; constructors and their options live in engine crates (ADR-008 §6) — "the engine choice is a dependency-graph fact, not a runtime branch."
- Consumer code never branches on engine type (guiding principle 4, phase-0 §Vision; ADR-006's positive consequence: consumer code branches on mechanism choice, never on engine type).
- Contract v1's guarantees are engine-uniform. The delivery table (ADR-006 §2) and its extensions (locks — ADR-008 §7; scheduler — ADR-009 §4; streams ordering — ADR-015 §4) pin identical contract text for both engines. The engines' genuine behavioral differences (wake coalescing vs per-notify, the pg no-replay hole and its reconnect-wake recovery, the SQLite writer-slot parking) are documented engine notes, not guarantee deltas — even the scheduler's leadership is engine-uniform (the leadership lock is the efficiency layer that runs on both engines; ADR-009 §4).
- No consumer row names a runtime-adapt need. The consumer
inventory (
docs/research/consumer-inventory.md) names coordination needs — commit-atomic notify, offset replay, at-least-once work, TTL locks — not introspection needs. And the starting artifact has nothing to rename either: honker carries its honesty in prose, not an API (no capability surface exists anywhere in the honker-rs surface at the reference revision; no capability-surface row exists in ADR-008 §8's rename-table pattern to inherit or rename). - No spec-side placeholder remains. ADR-008 §1/§6 pinned "no capability surface in v1" and deferred the whether-ever question to this OQ; core-contract.md's capability-surface section and deployment.md's options frame held the question open. Both engines are POC-verified (so nothing here gates implementation), the contract's shape is fully pinned (so the trait constrains where a capability difference could surface), and OQ-13/OQ-12's resolutions completed the surface without needing one.
Decision
1. No runtime capability surface — options 1 and 3 are the resolution, and they compose
Store carries no capabilities accessor — in v1 and by default
ever. The honest single-host/multi-host boundary lives in the two
places it is already true:
- Compile-time identity (option 1). The engine crate a binary
depends on is the deployment statement:
alkstore-sqlite's identity says single-machine (its docs carry the NFS two-writers boundary and the cross-process-on-one-host ground);alkstore-postgres's says multi-host native (its docs carry the listener budget line,max_size + 1, and the co-tenancy posture). A dependency edge cannot drift out of sync with the truth it states; a runtime struct can. - The documented matrix (option 3). deployment.md is the ops-facing document of record for the facts a deployer needs — host semantics, connection budgets, durability knobs, toolchain floors — the matrix's tables are unchanged; this ADR only resolves its open frame.
The three options were framed as rivals, but options 1 and 3 are one posture at two altitudes (engine-crate docs serve the consumer choosing the dependency; the deployment matrix serves the operator choosing the topology) — the fork in the road was only ever option 2 vs both of them. Decided: no runtime surface.
2. Why capability flags are rejected
A capabilities struct earns each field only if a caller can act differently on it — ADR-008 §5's act-differently rule (pinned for error variants) generalized to surface. Field by field:
- Host semantics (
single_host/multi_host): no in-process action exists. Knowing the boundary cannot make SQLite multi-host; the topology is decided in the same act that chooses the engine crate, before anyStoreexists. A flag would invite the exact branch principle 4 bans —match caps.host { MultiHost => …, SingleHost => … }in generic code is engine-type branching with a contract-sanctioned hook, and unlike the sanctioned branching (notify vs streams vs queues — the consumer's mechanism choice), it has no mechanism decision behind it to branch for. The mechanism handles are the only per-engine difference surface the pinned trait retains, and ADR-008 pins even those uniform. - Payload limits (
notify_payload_limit): the one asymmetry a caller can genuinely act on — and its runtime surface is already pinned:PayloadTooLargeis a universal taxonomy variant carrying the limit, produced pg-side, contract-wide matchable (ADR-008 §5). The error is the honest runtime descriptor for this capability; acapabilities()field would duplicate it as a second normative home — the one-owner discipline (ADR-012 §2's formula rule) applies to it identically. - Wake cadence knobs: engine options at open (ADR-008 §6) — engine-crate configuration, not contract description; restating them in a returned struct would add a second normative home to the config split §6 pinned.
- Connection budgets / durability knobs / toolchain floors: deployer-time and build-time facts (deployment.md's tables) — they describe things decided before the process runs; nothing a running caller branches on.
Under all of it: no consumer row names the need. Every inventory row's need is already served by engine-uniform contract text; real runtime machinery — a descriptor all engines must keep honest forever and every future engine addition must grow — for an unnamed need is exactly the scope discipline ADR-002 exists to enforce. The cost asymmetry seals it: a capabilities surface is a promise (every field honest on every engine, forever, plus a versioning surface for OQ-10 to govern) against a need named by no row; the compile-time + docs posture has zero incremental surface and zero drift risk.
3. What "must not pretend" then means, concretely
The honesty obligation OQ-08 posed resolves into three standing statements rather than one API:
- Contract text: no guarantee row distinguishes hosts; where the
engines genuinely differ in a caller-observable way, the error
taxonomy carries it (
PayloadTooLarge— universal variant, pg-occurrence documented) rather than a flags descriptor. This is the contract suite's business too — the backlog gains one row pinning the occurrence asymmetry (§5 below). - Engine-crate docs: the engine's identity prose carries its
posture (SQLite: single-machine, NFS two-writers unsupported,
one-host cross-process supported; Postgres: multi-host native,
the listener budget, co-tenancy expected).
Store::open's documented signature is the interface a deployer meets before anyStoreexists (ADR-008 §6). - The deployment matrix: the ops-facing document of record, unchanged — this ADR only resolves its open frame.
The misconfiguration case is handled by the family's established
posture, not by detection: a SQLite database file operated as a
multi-writer network share is a deployment violation the crate
cannot honestly observe — the same shape as alkblobs'
deployment-verified invariants ("the constructor cannot prove
cross-node truth, the deployment asserts it, and the detection
symptom is documented" — /workspace/@alkdev/alkblobs @ 7b9d904,
docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md
§3, fleet mode as a constructor declaration). The crate's duty is honesty where the deployer reads:
document the boundary in the engine-crate docs and the matrix, and
never fabricate runtime machinery that pretends to detect it. What
the constraint rules out is the contract claiming a posture —
that claim would have been the pretense. A documented boundary,
backed by a compile-time engine choice that cannot silently disagree
with the deployment, is the honesty.
4. Explicit rejections and the re-entry gate
- No
Store::capabilities()— §2. - No
engine_name()/ debug accessor — the dependency name is the compile-time fact; a runtime string restating it adds surface with no consumer row behind it, and engine-agnostic code — the only code the contract governs — is defined not to care. - No
#[cfg]-style capability features — the engine-crate split (ADR-001) is the compile-time mechanism, already working; a feature gate parallel to it would be a second engine-selection axis to keep honest. - Re-entry gate: a consumer-inventory row naming a runtime-adapt need — a generic, engine-agnostic consumer that must act differently per engine at runtime, with the action named. Until then, capability introspection is out of the contract's future as well as its v1: OQ-10's versioning discipline has one less surface class to govern, and a future engine stays purely additive without a descriptor to grow.
5. Verification backlog addition
PayloadTooLargeoccurrence asymmetry pinned in the contract suite — with no capabilities API, the error variant is the only runtime carriage of an engine asymmetry, so the suite pins it: pg rejects an oversizednotify/notify_txpayload client-side before any round trip (POC #2's payload boundary); SQLite never produces the variant at any size. The property is the variant's matchability: engine-agnostic code writes the same match on both engines and the non-occurring arm simply never fires.
No other new rows — the boundary decision adds nothing surface to test (there is none); the engines' cross-engine uniformity is the existing backlog's business, unchanged.
Consequences
Positive
- The trait surface stays exactly as pinned — zero methods, zero types added; ADR-008 §1/§6's "no capability surface" line graduates from deferred question to pinned answer with no code weight either way.
- Principle 4 keeps its only sanctioned branching: mechanism choice. Engine-agnostic consumer code has no hook inviting an engine branch, and a future engine stays purely additive — one more crate implementing the traits, no descriptor to grow (the ADR-001/ADR-008 positive consequence, unweakened).
- OQ-10's versioning discipline has no capability struct to track; the contract's governable surface is the smaller for this.
- One normative home per class of engine-difference fact: the compile-time identity (deployment statement), engine-crate docs (the engine's posture), deployment.md's matrix (ops facts) — each at one altitude, none duplicated in a runtime struct.
Negative
- A consumer who wants runtime introspection (a generic store-level
broker adapting to payload limits without reading the typed
error) has no in-contract means — they rely on
PayloadTooLargematchability or learn the engine at build time. No named consumer carries this cost today; the re-entry gate is the relief valve. - The misconfiguration case (SQLite treated as shared network storage) relies on documentation, not detection — the honest posture, but a deployer who ignores the docs gets the failure symptom the matrix documents, not an API error.
References
- OQ-08 (
docs/architecture/open-questions.md) — this ADR's resolution; the option set framed in deployment.md. - deployment.md — the facts (host semantics, budgets, knobs) and the options list this decision resolves.
- ADR-001 — single-driver binaries; the dependency-graph fact the boundary's compile-time half rides (item 1 annotated: capability flags dropped from the core crate's contents list).
- ADR-008 — §1/§6 (the partition that
parked the question; the config split; "the engine choice is a
dependency-graph fact, not a runtime branch"), §5 (the
act-differently rule generalized in §2;
PayloadTooLarge, the one runtime-visible asymmetry). - ADR-006 — engine-uniform guarantee rows; the never-branch-on-engine-type statement and the note that the trait's shape constrains where capability differences can surface (resolved: nowhere).
- ADR-009 §4, ADR-015 §4 — the later guarantee-row extensions checked for engine-uniformity (scheduler leadership is uniform; the ordering row is uniform).
- ADR-012 — §2's one-normative-owner rule (§2's field reasoning); the honesty posture the substrate inherits.
- POC findings:
docs/research/poc-pg-posture-findings.md("OQ-ST-08: the pg engine is natively multi-host");docs/research/poc-sqlite-posture-findings.md(single-machine ground). - Honker's prose honesty (
/workspace/honker@ f4e53c6,README.md:112) — the reference posture; no capability API exists in the starting artifact to inherit or rename. - alkblobs' deployment-asserts-truth precedent
(
/workspace/@alkdev/alkblobs/docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md§fleet) — the documented-invariant-not-detected posture §3 adopts. - OQ-04 (the pinning that parked this question), OQ-10 (the versioning duty this resolution narrows), OQ-11.