Files

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 .db over 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_version is 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:

  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): 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 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 §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:

  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 §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) 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 — 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.