docs: open Phase 1 — architecture spec set over the Phase 0 evidence

docs/architecture/ now exists: README index, overview, five component
specs (core-contract, engine-sqlite, engine-postgres, queues,
deployment), ADR-001..007 carrying the Phase 0 resolved decisions
(crate split, feature scope, per-engine drivers, dependency
ownership, wake contract, tx seam), and the centralized
open-questions tracker promotion: OQ-ST-01..08 mirror to OQ-01..08
one-to-one with statuses/resolutions carried; new Phase 1 questions
append (OQ-09 scheduler collapse, OQ-10 contract versioning).
Open Phase 1 work: OQ-04 contract pinning (high), OQ-05 queue
semantics depth (high), OQ-06 honker-core quality read (high;
fork-trigger gate), OQ-08 capability surface, OQ-09, OQ-10.

Erratum fixed in phase-0 OQ-ST-04 (thread-affinity friction is
SQLite-side, previously garbled as pg-side) and a stale scheduler-
boundary pointer corrected in consumer-inventory.md. Two review
passes run (findings: OQ-promotion numbering faithfulness, ADR
back-reference sync) — all critical/warning findings resolved.
This commit is contained in:
glm-5.3-flash committed 2026-10-04 18:13:10 +00:00
1 parent db73678090
commit 4391f6e879
17 files changed
+1812 -14

No files matched your search

@@ -0,0 +1,128 @@
# ADR-006: Wake contract — opaque wake + re-read, and the notify-vs-streams delivery split
## Status
Accepted (the contract skeleton the evidence supports); remaining
surface pinning is OQ-04's contract work against this ADR
## Context
The two engines' wake mechanisms are structurally different — SQLite
wakes by a watcher polling `PRAGMA data_version` (deliver-on-commit,
no server push), Postgres by LISTEN/NOTIFY (server push,
connection-bound, no replay). A naive unification either collapses to
polling behavior on the strong side or promises semantics only one
engine has. Both POCs (2026-10-04) measured the pieces and — the
load-bearing finding — verified that **the two engines can share the
same wake contract unchanged**: wake is opaque, delivery is not
guaranteed to be precise, and consumers re-read state.
Honker's own processing-guarantees table is the cautionary prior art:
per-binding auto-checkpoint-vs-manual-save ambiguity produces
*different* guarantees under one function name. A single-crate version
must pick one answer per mechanism, not inherit the table.
## Decision
The contract fixes three things:
### 1. The unified wake contract: opaque wake + re-read state
`listen()` (and stream/queue subscription wake) delivers an **opaque
wake signal: "something changed; re-read the state you care about."**
The contract never carries semantic content, ordering promises, or
change descriptions. Consumers that need the actual data re-read it
(the hot-path pattern the ecosystem already uses: a subscriber holding
a cache invalidates on wake and re-reads, instead of re-polling).
- SQLite: `data_version` watcher fires on commit; wakes **coalesce**
under bursts (overtriggering on purpose — "one indexed SELECT is
cheap; a missed wake is a correctness bug").
- Postgres: LISTEN delivers per-notification (no coalescing), and the
forwarder's synthetic reconnect-wake covers connection gaps.
- Both: consumer code must be correct if wakes repeat, coalesce, or
arrive in any order, with at-least-once wake delivery. Exactly-once
*processing* semantics belong to queues/streams (below), never to
the wake layer.
Measured ground: wake latency p50 ≈ 1.1–2.2 ms on both engines at
default cadence; per-payload burst delivery verified (30/30 on pg,
coalesced-but-complete re-read on SQLite).
### 2. Delivery guarantees, pinned per mechanism (no per-binding table)
| Mechanism | Durability | Replay | Atomicity | Guarantee |
|---|---|---|---|---|
| `notify` / listen | none | never | commit-atomic (delivers at commit; rollback drops) | fire-and-forget, at-most-once per listener session |
| streams | durable table row | yes, per-consumer offset, replay-on-attach default | publish is commit-atomic | every committed event readable by every consumer that hasn't passed its offset |
| queues | durable row | claim/ack model | enqueue is commit-atomic | at-least-once *work* with visibility timeouts |
- The trait **does not promise replay under `listen()`** — on either
engine. Durability+replay needs are what streams are for; this split
is the contract's load-bearing line.
- Listener sessions start "from now" (SQLite: the watcher's current
state; Postgres: `MAX(id)`-equivalent at attach). History belongs to
streams.
- Wake *delivery* failures surface, not silence: watcher death closes
subscriber channels (SQLite, honker-core's
`WatcherDeathGuard` behavior); Postgres connection gaps surface as
the synthetic reconnect-wake on a reserved channel
([ADR-004], [engine-postgres.md]).
### 3. Reserved names are a contract surface
The Postgres forwarder's synthetic reconnect-wake needs a reserved
channel namespace no consumer channel may collide with. The naming
convention for reserved/meta channels (and any internal queue/stream
names) is pinned in the core contract — exact strings are OQ-04
contract work; the *existence of a reserved namespace* is decided
here.
### The caching-subscriber pattern rides the same contract
A subscriber that also holds a cache gets invalidation from the
opaque wake + re-read: the contract deliberately does not carry
key-level payloads, so cache clients key their invalidation on their
own read-set, informed by channel names (the channel name is the one
piece of semantic content `listen()` carries). Key-payload design,
if any consumer needs richer invalidation, is a
consumer-inventory-row-gated extension — not assumed now.
## Consequences
**Positive**
- One wake contract, verified identically on both engines — the
"how does a change become visible" question alkstore exists to
answer, answered once.
- The delivery-guarantee table is pinned per mechanism, killing the
per-binding ambiguity class honker's docs exhibit.
- Consumer code branches on *mechanism choice* (notify vs streams vs
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).
**Negative**
- The opaque wake is deliberately less informative than
key-payload notify systems; consumers needing fine-grained
invalidation pay a re-read or need streams.
- Wake overtriggering on SQLite means consumers must be idempotent on
wake (a documented consumer obligation, part of the contract text).
## References
- `docs/research/poc-sqlite-posture-findings.md` §Probe 2,
§"What feeds where"; `docs/research/poc-pg-posture-findings.md`
§Sub-module W.
- OQ-ST-04 (`docs/research/phase-0.md`) — de-risked by these
measurements; the contract remainder is OQ-04 +
`core-contract.md`.
- [ADR-002](002-feature-scope.md) — notify/listen and streams are
first-class; the guarantee split is why both exist.
- [ADR-004](004-postgres-driver.md) — the forwarder owning the
Postgres side of this contract.
- [ADR-007](007-transactional-seam.md) — commit-atomicity's mechanism
(the `*_tx` seam).
- [core-contract.md](../core-contract.md) — the spec carrying this
contract's full surface.