Files
alkstore/docs/architecture/decisions/016-deployment-honesty.md
T

302 lines
16 KiB
Markdown

# 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.