# 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](008-contract-v1-pinning.md) §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](004-postgres-driver.md)). - **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 `.db` over NFS is not a Honker deployment strategy"), inherited by the substrate at the fork ([ADR-003](003-sqlite-driver.md), [ADR-011](011-sqlite-substrate-fork.md)); cross-process *on one host* is verified ground (`data_version` is cross-process by nature). [deployment.md](../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: 1. **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. 2. **`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. 3. **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](001-crate-split.md)): a consumer depends on core plus exactly one engine crate; constructors and their options live in engine crates ([ADR-008](008-contract-v1-pinning.md) §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](006-wake-and-delivery-contract.md)'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](006-wake-and-delivery-contract.md) §2) and its extensions (locks — ADR-008 §7; scheduler — [ADR-009](009-scheduler-collapse.md) §4; streams ordering — [ADR-015](015-streams-depth.md) §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](../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](008-contract-v1-pinning.md) §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 any `Store` exists. 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*: `PayloadTooLarge` is 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; a `capabilities()` field would duplicate it as a second normative home — the one-owner discipline ([ADR-012](012-forked-substrate-design.md) §2's formula rule) applies to it identically. - **Wake cadence knobs**: engine options at open ([ADR-008](008-contract-v1-pinning.md) §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](002-feature-scope.md) 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: 1. **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). 2. **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 any `Store` exists ([ADR-008](008-contract-v1-pinning.md) §6). 3. **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](001-crate-split.md)) *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 - **`PayloadTooLarge` occurrence 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 oversized `notify`/`notify_tx` payload 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 `PayloadTooLarge` matchability 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](../deployment.md) — the facts (host semantics, budgets, knobs) and the options list this decision resolves. - [ADR-001](001-crate-split.md) — 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](008-contract-v1-pinning.md) — §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](006-wake-and-delivery-contract.md) — 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](009-scheduler-collapse.md) §4, [ADR-015](015-streams-depth.md) §4 — the later guarantee-row extensions checked for engine-uniformity (scheduler leadership is uniform; the ordering row is uniform). - [ADR-012](012-forked-substrate-design.md) — §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.