ADR-016: deployment honesty — no runtime capability surface; compile-time identity + matrix (OQ-08 resolved)
This commit is contained in:
1 parent
8c4ec48f92
commit
8c8fec5cb8
12 files changed
+494
-72
No files matched your search
@@ -0,0 +1,302 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user