From 8c8fec5cb8fa0928be77ed3b3080355248d08946 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Tue, 6 Oct 2026 06:29:42 +0000 Subject: [PATCH] =?UTF-8?q?ADR-016:=20deployment=20honesty=20=E2=80=94=20n?= =?UTF-8?q?o=20runtime=20capability=20surface;=20compile-time=20identity?= =?UTF-8?q?=20+=20matrix=20(OQ-08=20resolved)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/README.md | 14 +- docs/architecture/core-contract.md | 46 ++- .../architecture/decisions/001-crate-split.md | 8 +- .../006-wake-and-delivery-contract.md | 3 + .../decisions/008-contract-v1-pinning.md | 22 +- .../decisions/016-deployment-honesty.md | 302 ++++++++++++++++++ docs/architecture/deployment.md | 54 +++- docs/architecture/engine-postgres.md | 17 +- docs/architecture/engine-sqlite.md | 14 +- docs/architecture/open-questions.md | 73 +++-- docs/architecture/overview.md | 10 +- docs/research/phase-0.md | 3 +- 12 files changed, 494 insertions(+), 72 deletions(-) create mode 100644 docs/architecture/decisions/016-deployment-honesty.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a8e365d..0700efb 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # alkstore — Architecture @@ -24,11 +24,11 @@ pending architecture review and OQ resolution. | Doc | Status | Purpose | Key OQs | |---|---|---|---| | [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — | -| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10 | +| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-10 | | [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12 (resolved), OQ-13 (resolved) | -| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-08, OQ-12 (resolved), OQ-13 (resolved) | +| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-08 (resolved), OQ-12 (resolved), OQ-13 (resolved) | | [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) | -| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 | +| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 (resolved) | | [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — | ## Architecture Decision Records @@ -50,6 +50,7 @@ pending architecture review and OQ resolution. | [013](decisions/013-fold-substrate-into-sqlite.md) | Fold the forked substrate into `alkstore-sqlite` — no fourth crate | Accepted | | [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue — `outbox_enqueue_tx` on the `TxHandle` trait | Accepted | | [015](decisions/015-streams-depth.md) | Streams depth — carried-metadata keys, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` | Accepted | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty — no runtime capability surface; compile-time engine identity + documented matrix | Accepted | ## Open Questions @@ -58,7 +59,6 @@ Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors OQ-ST-NN — with new Phase 1 questions appended after). Open, in suggested resolution order: -- **OQ-08** (medium): capability-surface shape. - **OQ-10** (medium): contract versioning across engine crates. - **OQ-11** (medium): forked-substrate follow-through (provenance register format, cherry-pick procedure; item (1) dissolved by @@ -72,7 +72,9 @@ Resolved (kept with resolutions): OQ-01 (feature scope), OQ-02 scheduler guarantee row pinned), ~~OQ-05~~ (queue semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), ~~OQ-06~~ (honker-core quality read — fork fired, -[ADR-011](decisions/011-sqlite-substrate-fork.md)), ~~OQ-13~~ +[ADR-011](decisions/011-sqlite-substrate-fork.md)), ~~OQ-08~~ +(capability-surface shape — none, by default ever; +[ADR-016](decisions/016-deployment-honesty.md)), ~~OQ-13~~ (transactional outbox enqueue shape — [ADR-014](decisions/014-outbox-tx-enqueue.md)), ~~OQ-12~~ (streams depth — [ADR-015](decisions/015-streams-depth.md)). diff --git a/docs/architecture/core-contract.md b/docs/architecture/core-contract.md index df7f1a1..a55561c 100644 --- a/docs/architecture/core-contract.md +++ b/docs/architecture/core-contract.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # Core contract @@ -20,7 +20,12 @@ joins the `TxHandle` trait, and by semantics, `StreamEvent` shape, the ordering row, `trim_to`, and `publish_with_key_tx`); ADR-009/ADR-010 add the first *post-v1 contract extensions* (scheduler collapse surface, `QueueOpts` depth — -versioning discipline for such extensions is OQ-10's); the +versioning discipline for such extensions is OQ-10's); +[ADR-016](decisions/016-deployment-honesty.md) decides the parked +capability-surface question: **none, by default ever** — engine +differences surface at compile time (engine-crate identity) and in +the [deployment matrix](deployment.md), never as a runtime +descriptor; the *obligations* are this document. ## Concepts @@ -122,8 +127,11 @@ never sees it. trip, verified by POC #2); no limit on SQLite. The error variant is contract-wide (callers match it identically on both engines), the *occurrence* is the documented engine asymmetry - ([ADR-008](decisions/008-contract-v1-pinning.md) §5) — see also - OQ-08. + ([ADR-008](decisions/008-contract-v1-pinning.md) §5) — with no + capability surface + ([ADR-016](decisions/016-deployment-honesty.md)), this variant is + the one runtime carriage of an engine asymmetry; its occurrence + asymmetry is pinned in the verification backlog below. - `listen(channel) -> Box` — starts from "now"; delivers opaque `Wake { channel }` signals ([ADR-006](decisions/006-wake-and-delivery-contract.md)), never @@ -371,10 +379,16 @@ state (`get_job`), not an error. ### Capability surface -None in contract v1. Whether the `Store` exposes engine capabilities -at all — and if so, which (payload limits, host semantics, -wake-cadence knobs) — is OQ-08's decision -([deployment.md](deployment.md)). +None — decided by +[ADR-016](decisions/016-deployment-honesty.md) (2026-10-06): `Store` +carries no capabilities accessor, in v1 and by default ever. The +honest single-host/multi-host boundary lives at compile time (the +engine-crate dependency *is* the deployment statement — +[ADR-001](decisions/001-crate-split.md)'s single-driver binaries) and +in [deployment.md](deployment.md)'s matrix. Runtime carriage of the +one caller-actionable engine asymmetry is the contract-wide +matchable `PayloadTooLarge` variant (Errors above); re-entry requires +a consumer-inventory row naming a runtime-adapt need. ### Naming / reserved namespace @@ -471,6 +485,14 @@ before the engine specs are called `stable`: keyed event row with the business write (no ghost event); commit makes it visible to `read_since`/`subscribe`; empty-`Some`-key `InvalidName` on both engines' tx paths. +- **`PayloadTooLarge` occurrence asymmetry** ([ADR-016](decisions/016-deployment-honesty.md) + §5) — with no capability surface, `PayloadTooLarge` is the one + runtime carriage of an engine asymmetry, so its matchability pins + in the suite: an oversized `notify`/`notify_tx` payload is rejected + client-side by the Postgres engine before any round trip (the + limit in the variant), and the SQLite engine never produces the + variant at any size — engine-agnostic code writes the same match + on both engines and the non-occurring arm simply never fires. - **`trim_to` semantics on both engines** ([ADR-015](decisions/015-streams-depth.md) §5) — exact-boundary trim (`<=`), surviving rows keep their offsets (gaps legal, never renumbered), reads resume at the trim horizon's @@ -493,6 +515,7 @@ before the engine specs are called `stable`: | [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; engine-side formula arithmetic pinned equivalent by the contract suite | | [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue (amends 008) | `outbox_enqueue_tx` on `TxHandle`; outbox-name validation; derived backing queue reached only through the outbox surface | | [015](decisions/015-streams-depth.md) | Streams depth (amends 006/008) | key = carried metadata, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty (decides 008's parked question) | no runtime capability surface — compile-time engine identity + documented matrix; `PayloadTooLarge` occurrence asymmetry pinned | ## Open Questions @@ -502,11 +525,12 @@ questions affecting this document: - **OQ-10**: contract versioning discipline across engine crates ([open](open-questions.md)) -- **OQ-08**: capability-surface shape ([open](open-questions.md)) Resolved on this document's surface: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), **OQ-13** (transactional outbox enqueue shape — -[ADR-014](decisions/014-outbox-tx-enqueue.md)), and **OQ-12** (streams -depth — [ADR-015](decisions/015-streams-depth.md)), 2026-10-05. \ No newline at end of file +[ADR-014](decisions/014-outbox-tx-enqueue.md)), **OQ-12** (streams +depth — [ADR-015](decisions/015-streams-depth.md)), and **OQ-08** +(capability-surface shape — none, by default ever; +[ADR-016](decisions/016-deployment-honesty.md)), 2026-10-05/06. \ No newline at end of file diff --git a/docs/architecture/decisions/001-crate-split.md b/docs/architecture/decisions/001-crate-split.md index 18fa5b6..9fa5b2a 100644 --- a/docs/architecture/decisions/001-crate-split.md +++ b/docs/architecture/decisions/001-crate-split.md @@ -2,7 +2,8 @@ ## Status -Accepted +Accepted *(capability-flags content annotated 2026-10-06 by +[ADR-016](016-deployment-honesty.md))* ## Context @@ -37,6 +38,11 @@ The project ships as a **family of crates**: surface, types, error model, capability flags, and the contract documentation. No driver dependencies. Compile-lean by construction: the base crate has no engine machinery to keep out. + *(Annotated 2026-10-06: capability flags are not part of this + contents list as decided — [ADR-016](016-deployment-honesty.md) + resolves there is no runtime capability surface, by default ever; + the engine boundary lives at compile time and in the deployment + matrix.)* 2. **`alkstore-sqlite`** — the SQLite engine implementing the core surface. Single driver: rusqlite + the forked honker-core lineage, carried in-tree as the engine crate's substrate module diff --git a/docs/architecture/decisions/006-wake-and-delivery-contract.md b/docs/architecture/decisions/006-wake-and-delivery-contract.md index de7b64e..5363b71 100644 --- a/docs/architecture/decisions/006-wake-and-delivery-contract.md +++ b/docs/architecture/decisions/006-wake-and-delivery-contract.md @@ -106,6 +106,9 @@ consumer-inventory-row-gated extension — not assumed now. queues), never on engine type (guiding principle 4; the honest single/multi-host line is [deployment.md]'s and OQ-08's, not this contract's). + *(OQ-08 resolved 2026-10-06 by + [ADR-016](016-deployment-honesty.md): nowhere at + runtime — compile-time engine identity + the documented matrix.)* **Negative** diff --git a/docs/architecture/decisions/008-contract-v1-pinning.md b/docs/architecture/decisions/008-contract-v1-pinning.md index 22bbbd4..b1283bc 100644 --- a/docs/architecture/decisions/008-contract-v1-pinning.md +++ b/docs/architecture/decisions/008-contract-v1-pinning.md @@ -94,7 +94,10 @@ implement identically): `schedule()`) — OQ-09 decides; the scheduler surface (if any) is not part of contract v1. - **Capability flags** — OQ-08 decides whether `Store` exposes them at - all; v1 has no capability surface. + all; v1 has no capability surface. *(Resolved 2026-10-06 by + [ADR-016](016-deployment-honesty.md): no capability surface — by + default ever; the boundary is compile-time engine identity + + deployment.md's matrix.)* - **`claim_waker`** — dropped from the contract surface; wake-driven claim is the engine's consumption posture ([engine-postgres.md](../engine-postgres.md)'s LISTEN-driven claim), @@ -311,6 +314,10 @@ elsewhere, cancelled); `try_lock` returning `Option`; lock constructor's own obligation — a `Store` handed out by an engine crate honors the full v1 surface. - No capability surface in v1 (OQ-08 owns whether one ever exists). + *(Resolved 2026-10-06 by [ADR-016](016-deployment-honesty.md): by + default never — the engine choice stays a dependency-graph fact; + `PayloadTooLarge` is the one runtime carriage of an engine + asymmetry.)* - Consumer code stays engine-agnostic by depending on core and being constructed by exactly one engine crate (ADR-001's single-driver binaries) — the engine choice is a dependency-graph fact, not a @@ -410,9 +417,12 @@ prefix, §4). reused after commit (callers re-`begin_tx`) — matches the caller-owned lifetime the POCs verified, but is stricter than honker's re-usable `&Transaction` style. -- OQ-05/OQ-09/OQ-08 remain open: this ADR deliberately does not pin - queue depth, the scheduler shape, or capability flags; the v1 - additions to those surfaces will be later contract extensions. +- OQ-05/OQ-09/OQ-08 remained open here: this ADR deliberately did not + pin queue depth, the scheduler shape, or capability flags; the v1 + additions to those surfaces were later contract decisions. + *(OQ-05/OQ-09 resolved by [ADR-010](010-queue-semantics-depth.md)/ + [ADR-009](009-scheduler-collapse.md); OQ-08 resolved 2026-10-06 by + [ADR-016](016-deployment-honesty.md) — none, by default ever.)* ## References @@ -432,5 +442,7 @@ prefix, §4). [ADR-006](006-wake-and-delivery-contract.md) (wake contract, namespace existence, guarantee table), [ADR-007](007-transactional-seam.md) (tx seam mechanics). -- OQ-09 (scheduler row transfer), OQ-08 (capability surface), OQ-10 +- OQ-09 (scheduler row transfer), OQ-08 (capability surface — + resolved by [ADR-016](016-deployment-honesty.md), no runtime + surface), OQ-10 (versioning discipline for future contract extensions). \ No newline at end of file diff --git a/docs/architecture/decisions/016-deployment-honesty.md b/docs/architecture/decisions/016-deployment-honesty.md new file mode 100644 index 0000000..90f4163 --- /dev/null +++ b/docs/architecture/decisions/016-deployment-honesty.md @@ -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. \ No newline at end of file diff --git a/docs/architecture/deployment.md b/docs/architecture/deployment.md index 306ff55..cf2f4a7 100644 --- a/docs/architecture/deployment.md +++ b/docs/architecture/deployment.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # Deployment @@ -8,8 +8,11 @@ last_updated: 2026-10-05 What a deployer must know to size, run, and reason about alkstore engines: host semantics, connection budgets, durability knobs, and where engine differences may honestly surface in the contract. The -capability-surface *decision* (how much of this the trait exposes) is -OQ-08's; this document holds the facts and the decision's frame. +capability-surface *decision* is resolved +([ADR-016](decisions/016-deployment-honesty.md), 2026-10-06): no +runtime capability surface — this document's matrix is (with the +engine crates' own docs) where the honest boundary lives; this +document holds the facts. ## Host semantics @@ -18,24 +21,41 @@ OQ-08's; this document holds the facts and the decision's frame. | SQLite | **single-machine**, file-backed | NFS two-writers unsupported (honker's honesty posture, inherited, [ADR-003](decisions/003-sqlite-driver.md)). Cross-process *on one host* is verified POC ground (`data_version` is cross-process by nature). | | Postgres | **multi-host native** | Nothing assumes a shared host; POC #2 ran all-through-network (docker bridge) with the same properties ([ADR-004](decisions/004-postgres-driver.md)). | -The unified trait must not pretend SQLite is multi-host — but whether -that honesty lives as runtime capability flags, compile-time engine -knowledge, or a documented matrix only is OQ-08 -([ADR-006](decisions/006-wake-and-delivery-contract.md) note: the -trait's shape constrains where capability differences can surface). +The unified trait must not pretend SQLite is multi-host — and it does +not: [ADR-016](decisions/016-deployment-honesty.md) resolves that +honesty to compile-time engine identity (the engine crate a binary +depends on *is* the deployment statement) plus this documented matrix. +There is no `Store::capabilities()` — the trait's shape constrains +where capability differences can surface +([ADR-006](decisions/006-wake-and-delivery-contract.md)), and the +resolution is: nowhere at runtime. -Options for OQ-08, with their shape: +Options for OQ-08, with their outcome +([ADR-016](decisions/016-deployment-honesty.md)): 1. **Compile-time only** — a consumer chooses an engine crate at dependency time; the engine's docs carry its deployment facts. - Smallest contract; nothing runtime to match on. + Smallest contract; nothing runtime to match on. **Adopted** — + together with (3); the two compose (engine docs serve the consumer + choosing the dependency, the matrix serves the operator choosing + the topology). 2. **`Store::capabilities()`** — a runtime description (payload limits, wake cadence knobs, host semantics). Lets a consumer adapt (e.g., chunk large notify payloads) but adds a - contract surface all engines must keep honest. + contract surface all engines must keep honest. **Rejected** — + field-by-field under ADR-008 §5's act-differently rule, and no + consumer-inventory row names a runtime-adapt need + ([ADR-016](decisions/016-deployment-honesty.md) §2). 3. **Deployment matrix only** (this document) — no API surface. The honest-middle choice; matches the ecosystem's doc-first posture - but provides no programmatic guard. + but provides no programmatic guard. **Adopted** (with (1)) — the + "programmatic guard" gap is closed where it can honestly be: + the engine-crate dependency edge cannot drift out of sync with + the truth it states; the misconfiguration case (SQLite as shared + network storage) follows the family's + deployment-asserts-truth posture — documented detection symptom, + no fabricated runtime machinery + ([ADR-016](decisions/016-deployment-honesty.md) §3). ## Connection budgets @@ -107,7 +127,8 @@ From both POCs (single-box, relative shapes are the deliverable — | [003](decisions/003-sqlite-driver.md) | SQLite driver | bundling, toolchain floor | | [004](decisions/004-postgres-driver.md) | Postgres driver | listener budget line, forwarder posture | | [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | where capability differences may surface | -| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | constructor/options in engine crates; no capability surface in v1 (OQ-08) | +| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | constructor/options in engine crates; no capability surface in v1 (OQ-08; resolved by [ADR-016](decisions/016-deployment-honesty.md)) | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface — compile-time identity + this matrix; `PayloadTooLarge` is the one runtime asymmetry carriage | ## Open Questions @@ -115,5 +136,8 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this document: -- **OQ-08**: capability-surface shape — compile-time vs runtime flags - vs matrix-only (open) \ No newline at end of file +- **OQ-08**: capability-surface shape — **resolved** + (2026-10-06, [ADR-016](decisions/016-deployment-honesty.md)): + no runtime capability surface; compile-time engine identity + + this matrix; re-entry via a consumer-inventory row naming a + runtime-adapt need. \ No newline at end of file diff --git a/docs/architecture/engine-postgres.md b/docs/architecture/engine-postgres.md index 6990481..0709b3e 100644 --- a/docs/architecture/engine-postgres.md +++ b/docs/architecture/engine-postgres.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # Postgres engine @@ -111,6 +111,7 @@ the record in [ADR-003](decisions/003-sqlite-driver.md). | [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate (SQLite side); pg engine owns its own curve/stamps arithmetic, equivalence pinned by the contract suite | | [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side inside the caller's tx | | [015](decisions/015-streams-depth.md) | Streams depth | nullable key column (carried metadata); bigserial offsets, global-FIFO reads; keyed tx publish; `trim_to` as a pool-connection delete | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface — this engine's multi-host posture is stated by its crate identity and docs; `PayloadTooLarge` occurrence pinned contract-suite (this engine produces it, client-side pre-round-trip) | ## Open Questions @@ -118,8 +119,12 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this document: -- **OQ-08**: capability surface (shared with - [deployment.md](deployment.md)) (open) +- **OQ-08**: capability surface — **resolved** + (2026-10-06, [ADR-016](decisions/016-deployment-honesty.md)): + no runtime capability surface; this engine's multi-host posture + lives in its crate identity + docs and the deployment matrix; + the engine's one runtime-visible asymmetry (`PayloadTooLarge`) + was already contract-pinned. - **OQ-12**: streams depth — **resolved** (2026-10-05, [ADR-015](decisions/015-streams-depth.md)): key = carried metadata; global-FIFO-by-offset ordering row @@ -130,6 +135,8 @@ questions affecting this document: Resolved: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue -semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), and +semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), **OQ-12** (streams depth — [ADR-015](decisions/015-streams-depth.md)), -2026-10-05. \ No newline at end of file +and **OQ-08** (capability surface — none; +[ADR-016](decisions/016-deployment-honesty.md)), +2026-10-05/06. \ No newline at end of file diff --git a/docs/architecture/engine-sqlite.md b/docs/architecture/engine-sqlite.md index e55b975..f20ab44 100644 --- a/docs/architecture/engine-sqlite.md +++ b/docs/architecture/engine-sqlite.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # SQLite engine @@ -33,7 +33,10 @@ per-contract obligations live in the core spec and are not restated here. Every engine call runs in `spawn_blocking`. - Single-host by nature: file-backed, one machine, NFS-two-writers unsupported (honker's honesty posture, inherited). See - [deployment.md](deployment.md). + [deployment.md](deployment.md); the boundary's surface location is + decided ([ADR-016](decisions/016-deployment-honesty.md)) — this + crate's identity and docs state the posture; no runtime descriptor + exists. ## Connection architecture @@ -127,6 +130,7 @@ family is `__alkstore_*` (ADR-010 §8's naming authorization). | [013](decisions/013-fold-substrate-into-sqlite.md) | Substrate packaging | folded into `alkstore-sqlite` (`src/substrate/`); no fourth crate | | [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side through the writer-slot lease | | [015](decisions/015-streams-depth.md) | Streams depth | key = carried metadata (the inherited nullable column); global-FIFO ordering; `trim_to` as a writer-lease delete; event shape already the substrate's | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface; single-host posture stated by crate identity + docs; this engine never produces `PayloadTooLarge` (pinned in the contract suite) | ## Open Questions @@ -153,6 +157,8 @@ questions affecting this document: Resolved: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue -semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), and +semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)), **OQ-12** (streams depth — [ADR-015](decisions/015-streams-depth.md)), -2026-10-05. \ No newline at end of file +and **OQ-08** (capability surface — none, by default ever; +[ADR-016](decisions/016-deployment-honesty.md)), +2026-10-05/06. \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 8792c6d..5df9dbb 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # alkstore — Open Questions @@ -27,13 +27,17 @@ is OQ-11); **the fork packaging folded** (2026-10-05, **OQ-12 resolved** (2026-10-05, [ADR-015](decisions/015-streams-depth.md) — streams depth pinned: carried-metadata keys, global-FIFO ordering row, `StreamEvent` shape, -`publish_with_key_tx`, `trim_to`). -Next: **OQ-08** (rides the now-pinned trait -shape and the ADR-011 substrate fork), OQ-10 (versioning discipline +`publish_with_key_tx`, `trim_to`); **OQ-08 resolved** (2026-10-06, +[ADR-016](decisions/016-deployment-honesty.md) — no runtime +capability surface; the honest boundary lives in compile-time engine +identity + the documented deployment matrix). +Next: **OQ-10** (versioning discipline for contract extensions; note ADR-011 changes its substrate-side facts for SQLite — the forked machinery lives in-tree inside the engine crate per [ADR-013](decisions/013-fold-substrate-into-sqlite.md), so -the engine/core contract pairing is what the discipline must track), +the engine/core contract pairing is what the discipline must track; +OQ-08's resolution also narrowed its surface — no capability struct +to govern), OQ-11 (fork follow-through items — substrate-side, non-consumer-facing). Resolved questions stay listed with their resolution; they are not @@ -288,23 +292,52 @@ narrowed to the pinning work its own record already scoped.)* ## Theme: Deployment and capabilities -### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* +### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* — **RESOLVED** - **Origin**: [deployment.md](deployment.md) -- **Status**: open +- **Status**: resolved (2026-10-06, Phase 1 — + [ADR-016](decisions/016-deployment-honesty.md)) - **Priority**: medium -- **Resolution**: open. The pg engine is natively multi-host (POC #2 - verified — no single-host assumption to remove); SQLite is - single-machine by nature (file-backed, NFS-two-writers unsupported — - honker's honesty posture, inherited by the forked substrate). The - unified surface must not pretend SQLite is multi-host. Options: - per-engine capability flags (`Store::capabilities()`), a documented - deployment matrix only ([deployment.md](deployment.md) carries the - facts), or compile-time knowledge only (a consumer choosing the - SQLite engine knows). Rides the now-pinned contract shape - ([ADR-008](decisions/008-contract-v1-pinning.md)): the trait - constrains where capability differences can surface. -- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md), [ADR-012](decisions/012-forked-substrate-design.md) (the substrate inherits the honesty posture). +- **Resolution**: Pinned by + [ADR-016](decisions/016-deployment-honesty.md): **no runtime + capability surface — in v1 and by default ever**. The honest + single-host/multi-host boundary lives in the two places it is + already true, which compose rather than rival: (1) + **compile-time engine identity** — the engine crate a binary + depends on *is* the deployment statement (single-driver binaries, + [ADR-001](decisions/001-crate-split.md); constructors in engine + crates, [ADR-008](decisions/008-contract-v1-pinning.md) §6 — + "the engine choice is a dependency-graph fact, not a runtime + branch"); (2) **deployment.md's documented matrix** — the + ops-facing facts of record, unchanged. Option 2 + (`Store::capabilities()`) rejected field-by-field under ADR-008 + §5's act-differently rule generalized to surface: host semantics + admit no in-process action (a flag would invite the engine-type + branch principle 4 bans); payload limits already have their runtime + carriage — the universal, contract-wide matchable + `PayloadTooLarge` variant (a `capabilities()` field would be a + second normative home); wake cadence and knobs are engine-crate + config (§6's split); and no consumer-inventory row names any + runtime-adapt need. No `engine_name()`, no `#[cfg]` capability + axes. The "must not pretend SQLite is multi-host" obligation + resolves into three standing statements (contract text carries + asymmetries via the taxonomy, engine-crate docs carry posture, + deployment.md carries ops facts); the misconfiguration case + (SQLite as shared network storage) follows the family's + deployment-asserts-truth posture (alkblobs precedent) — documented + boundary, no fabricated detection. Contract-suite row added: + `PayloadTooLarge` occurrence asymmetry (pg client-side pre-round- + trip, SQLite never) — with no capabilities API, the variant is the + one runtime carriage of an engine asymmetry, so its matchability + is pinned. Re-entry gate: a consumer-inventory row naming a + runtime-adapt need. +- **Cross-references**: OQ-04 (the pinning that parked this), + OQ-10 (narrowed by this — no capability struct to govern), OQ-11, + [ADR-001](decisions/001-crate-split.md), + [ADR-006](decisions/006-wake-and-delivery-contract.md), + [ADR-008](decisions/008-contract-v1-pinning.md) §1/§5/§6, + [ADR-012](decisions/012-forked-substrate-design.md), + [deployment.md](deployment.md). @@ -416,6 +449,6 @@ narrowed to the pinning work its own record already scoped.)* ## Deferred / Blocked None currently. Every open OQ above is actionable Phase 1 work -(capability-surface shape, versioning discipline, fork-scaffold +(versioning discipline, fork-scaffold follow-through) with its evidence base complete — no external arrivals are being waited on. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 6fa8aac..a4d5acc 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-05 +last_updated: 2026-10-06 --- # alkstore — Overview @@ -28,7 +28,7 @@ Per [ADR-001](decisions/001-crate-split.md): | Crate | Contents | Driver dependencies | |---|---|---| -| `alkstore` (core) | trait surface, types, error model | none (capability flags, if ever, are OQ-08's to add) | +| `alkstore` (core) | trait surface, types, error model | none (no capability surface — [ADR-016](decisions/016-deployment-honesty.md): engine differences are compile-time identity + the deployment matrix, never a runtime descriptor) | | `alkstore-sqlite` | SQLite engine ([ADR-003](decisions/003-sqlite-driver.md)) | rusqlite; the forked honker-core lineage rides in-tree as the engine crate's substrate module subtree ([ADR-011](decisions/011-sqlite-substrate-fork.md), designed in [ADR-012](decisions/012-forked-substrate-design.md), folded per [ADR-013](decisions/013-fold-substrate-into-sqlite.md)) | | `alkstore-postgres` | Postgres engine ([ADR-004](decisions/004-postgres-driver.md)) | tokio-postgres, deadpool-postgres | | (mem engine, optional) | test convenience, decided at implementation ([ADR-001](decisions/001-crate-split.md)) | none | @@ -56,7 +56,7 @@ Per [ADR-002](decisions/002-feature-scope.md): | [engine-sqlite.md](engine-sqlite.md) | SQLite engine: mapping the contract onto the forked substrate module/rusqlite | | [engine-postgres.md](engine-postgres.md) | Postgres engine: mapping the contract onto tokio-postgres/LISTEN | | [queues.md](queues.md) | Queue/scheduler/outbox semantics depth (ADR-009/ADR-010 resolved) | -| [deployment.md](deployment.md) | Host capabilities, connection budgets, deployment matrix (OQ-08) | +| [deployment.md](deployment.md) | Host capabilities, connection budgets, deployment matrix (OQ-08 resolved) | | [open-questions.md](open-questions.md) | OQ-01..NN tracker | | [decisions/](decisions/) | ADRs | @@ -79,6 +79,7 @@ Per [ADR-002](decisions/002-feature-scope.md): | [013](decisions/013-fold-substrate-into-sqlite.md) | Fold the forked substrate into `alkstore-sqlite` (no fourth crate) | Accepted | | [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue (`outbox_enqueue_tx` on `TxHandle`) | Accepted | | [015](decisions/015-streams-depth.md) | Streams depth (carried-metadata keys, global-FIFO ordering, `StreamEvent`, `trim_to`) | Accepted | +| [016](decisions/016-deployment-honesty.md) | Deployment honesty (no runtime capability surface; compile-time identity + matrix) | Accepted | ## Non-goals @@ -90,7 +91,8 @@ Per [ADR-002](decisions/002-feature-scope.md): alkcall and is a separate future decision (store layer stays substrate-free, the alkblobs store-layer precedent). - Not multi-machine on SQLite: single-host honesty is inherited - ([deployment.md](deployment.md)). + ([deployment.md](deployment.md); the boundary's location in the + surface is decided — [ADR-016](decisions/016-deployment-honesty.md)). ## Evidence base diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index 1e90c84..276858c 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -701,7 +701,8 @@ unless a consumer appears; nothing remains to extract or decide here. ### OQ-ST-08: Multi-host / deployment posture -**Status: open.** +**Status: resolved (2026-10-06, Phase 1 — promoted as OQ-08; +[ADR-016](../architecture/decisions/016-deployment-honesty.md).)** Honker is explicitly single-machine (file-backed SQLite). Postgres is natively multi-host — POC #2 verified the pg engine side has no