Files
alkstore/docs/reviews/001-waves-1-2-general-review.md
T
glm-5.3-flash 060f262e62 ADR-024 — the mem engine adopted as the family's third engine (wave 6; release readiness renumbered to wave 7)
Row-first gate: consumer-inventory.md gains an engine-tier section — the mem-engine need recorded operator-authority, dated 2026-10-10 (wasm32 sandboxing across the alk protocol family + downstream ffi/napi/python-adapter economics; rusqlite/tokio-postgres structurally out on wasm, so mem is the only triad member the sandbox can carry).

ADR-024 (decisions/024-mem-engine.md) pins the decision set: (1) separate `alkstore-mem` crate — discharges and amends ADR-001 §4 (superseded in part: full contract-v1 engine, not an "implementation convenience"; annotation on ADR-001 §4 + Status, class-1 window still open); (2) honest-ephemeral posture — no durability, single-process, ephemeral by design, never fleet-valid (ADR-010 §3's shared-pool predicate fails structurally), riding ADR-016's carriers unchanged (identity + docs + matrix); (3) full contract v1 tier with the asymmetry classification — `PayloadTooLarge` never produced (SQLite arm), no reconnection concept (reconnect-wake/watcher rows absent from mem's per-engine column), receiver close at engine drop, wake coalescing N/A; (4) `MemStore::new()` engine-native constructor — the one documented exception to the open-prose, core-contract.md Store concept amended; shared-instance (two opens → one engine) rejected with the ADR-016 §4-style re-entry gate (consumer-inventory row), register stays closed; (5) wasm32 — compile-clean for wasm32-unknown-unknown plus the suite's mem column green on host under a current-thread runtime flavor are wave-6 acceptance items; on-target suite execution scoped out with a named collapse condition (wasm-bindgen-test adapter crate) — a scope decision, not a pending-client hedge; (6) mem is not a core dev-dependency — core's doc examples stay mock-based, ADR-001 §4's doctest posture declines.

engine-mem.md (draft): the in-process mapping — guarded per-mechanism state (no pool, no schema bootstrap, no forwarder, no spawn_blocking seam), the tx overlay (staged *_tx effects merged atomically at commit; read-your-own-writes easy, drop-=-rollback trivial; wake-at-commit structural — the seam pg needed pg-fix-tx-wake for, mem pins from birth via the existing tx-commit-atomicity rows), ADR-012 §2 third owner of the curve/opts/@every arithmetic (three-way equivalence, ADR-022's StoreFactory reused verbatim — born pinned).

deployment.md: host-semantics row (single-process, ephemeral, never fleet-valid, wasm32-clean), mem connection-budget subsection (none — nothing to budget), durability-knobs row (the honest row is the absence), toolchain row (wasm32 subset + current-thread-clean, acceptance-cited). README/overview: engine-mem.md row, ADR-024 row, family table entry (alkstore-mem, no driver deps), current-state updated to the implementation phase with engine specs' stable statuses reflected.

implementation.md: wave table gains wave 6 (mem engine, depends waves 1 + 5) and renumbers release readiness to wave 7 (fuzzing decision — the one remaining decided-at-implementation deferral — rides wave 7); wave-6 section records the born-pinned posture, acceptance items, the one suite-harness pre-task (past_stamp_sleep blocking sleep → async sleep, review 003's properties.rs:1683 note), and the window riders (review 003 Finding 1: the pg transient-fault seam, MEDIUM, rides wave 6; Findings 2–3 follow their park into wave 7's pre-release); wave-6 review gate defined; Decided points updated (mem discharged, fuzzing renumbered); stale wave-6 references swept across review docs and the landed tasks' notes (release-readiness items renumbered or annotated).

Docs-only change; verified by cross-reference audit (all ADR/OQ/anchor references resolve, residual wave-6 mentions are correct under the new numbering or carry dated annotations).
2026-10-10 09:42:40 +00:00

293 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# General review — waves 1 and 2
> **Post-review note (2026-10-08):** the four decision-needing
> findings below are resolved as
> [ADR-023](../architecture/decisions/023-fourth-review-round.md) —
> N-2 (`encode_payload` typed, `Codec`), M-2 (numeric-argument domains
> pinned contract-side, extents clamp empty — decided broader than
> this report's engine-layer recommendation: the same dialect hazard
> exists on the stream reads, and domains must be engine-uniform),
> N-4 (URI flag dropped, a registered fork delta), N-6 (1 ms default
> stands, cadence carried onto `SqliteOpts`). The task-carried items
> (lint removal, wave-5 suite adds, release-readiness N-1 review —
> wave 7 since ADR-024's renumbering) stand as
> ordered in §7.
- **Reviewer**: opencode (glm-5.3-flash)
- **Scope**: general review of everything landed by waves 1–2 (11 tasks, commits
`34e0b97..af5b59e`) — security posture, code smells, contract adherence,
and coverage (cargo-llvm-cov). Distinct from the two wave review gates, which
were type/shape-scoped.
- **Input**: full read of `alkstore/src` (all 23 files), the substrate subtree
(`schema.rs`, `watcher.rs`, `ops.rs`, `queue_ops.rs`, `PROVENANCE.md`), the
contract suite, the trait doc text vs `core-contract.md` and the ADRs, and
spot-checks against the upstream checkout `/workspace/honker` @ `f4e53c6`.
- **Gates at review time**: build/test/clippy/fmt all green. Coverage 93.1%
lines / 94.5% functions workspace-wide before this review's inline fixes.
## Verdict
**Solid.** The contract-blind boundary (ADR-012 §2) holds under line-by-line
inspection; the re-derived queue ops are consistently built (uniform validity
predicate everywhere, savepoint-guarded dead-letter moves, no stranded-rows
testing under induced failures); the provenance register is unusually rigorous;
core's doc text matches `core-contract.md` nearly verbatim. Two findings are
real (M-1, M-2); the rest are minor or noted-for-later. M-1/M-2 are substrate-
layer, reachable only through wave-3's engine wiring — no consumer surface
exists yet (engine crates are stubs), so none of this blocks wave 3 from
starting; it just needs to fix or consciously absorb these first.
---
## 1. Security review
Nothing alarming. Checked specifically for:
- **SQL injection** — every SQL statement is a static string; all user-supplied
values (names, queue names, specs, payloads) ride `rusqlite::params![]`
binds. The one `format!`-built DDL path (`SAVEPOINT {name}` /
`RELEASE SAVEPOINT {name}` in `in_savepoint`, ops.rs:71,79) carries only
compile-time constant savepoint names ("alkstore_*"), never user data —
safe by construction. `column_present`/`add_column_if_absent` interpolate
table/column names built from the same constant list.
- **Path/URI injection** — `open_conn` enables `SQLITE_OPEN_URI` (inherited
from upstream); the path reaches `Connection::open_with_flags` from the
engine layer's constructor, so a wave-3 engine must not pass consumer-
influenced strings there without review. Noting it now so the wiring task
doesn't inherit it silently (N-4).
- **Payload handling** — payloads cross as `serde_json::Value`, serialize
once (`encode_payload`), and decode via `payload_as` with `Error::Codec`
mapping; no re-parsing of stored strings beyond serde_json. `encode_payload`
maps serialization failure to empty bytes (see N-2) — the engine layer
should validate before insert when wave 3 lands.
- **`unsafe`** — exactly one block per substrate module pair:
`ctx.get_connection()` in the scalar-function wrappers (the only rusqlite-
sanctioned way to reach the connection from a UDF) and its use is immediate
and scoped. `WatcherDeathGuard`/watcher threads contain no unsafe.
- **DoS-shaped surface** — `scheduler_tick` bounds catch-up fires
(`SCHEDULER_MAX_CATCHUP_FIRES = 64`); `claim_batch` honors its limit only
via SQLite's `LIMIT ?2` semantics (see M-2); `NOTIFY_ATTACH_MAX_ROWS` caps
notification growth at attach (D-16); `arg_i64`/`real_to_i64` reject
fractional/out-of-range REALs cleanly rather than truncating (tested).
- **Secrets** — none; nothing logged beyond watcher diagnostics.
## 2. Findings
### M-1 · `sweep_expired`'s retention half runs outside the savepoint guard — the no-stranded-rows property has a hole (FIXED INLINE)
**Where**: `queue_ops.rs` — `sweep_expired` (was lines 236–246).
**What**: the savepoint frame covered `sweep_expired_live` (expiring live
rows → dead) only; `sweep_dead_retention` (DELETE of aged-out dead rows) ran
after the frame. The module's own doc text
("savepoint-guarded dead-letter moves (the #133 defect class, not inherited)")
and the register's D-12 entry claim both halves are the guarded, atomic shape.
Before the fix, a DB failure mid-retention-DELETE (e.g. `SQLITE_FULL`) after a
successful live→dead sweep did NOT strand rows (both halves commit
independently), but it made `sweep_expired` a two-atomicity operation whose
error left the sweep's *effects* partially applied with `Err` returned —
exactly the state the savepoint guard exists to preclude, and a property the
module claims (and only half-tests: `sweep_rolls_back_no_stranded_rows_*`
induce the failure in the live half).
**Fixed by**: moving `sweep_dead_retention` inside the `in_savepoint` frame
(`Ok(live_moved + retained)` out of the closure). Semantics unchanged when
nothing fails — retention deletion is idempotent, the moved+retained count is
identical — and the error path now actually rolls back what the property says
it does. All 84 substrate tests (retention, no-stranded-rows, decode-failure
rollbacks) pass untouched.
**Follow-up**: the coverage hole is now the only remaining gap — no test
induces retention-DELETE failure. The natural test (trigger raising ABORT on
`__alkstore_dead` DELETE) is sketched in §4 below; left for the wave-5
contract suite or a wave-3 substrate touch-up rather than added now, since
M-1's fix itself is behavior-preserving and fully covered by existing tests.
### M-2 · `claim_batch` ignores a non-positive `n` (upstream-inherited shape, needs an engine-layer guard)
**Where**: `queue_ops.rs` — `claim_batch`, `LIMIT ?2` in the claim CTE.
**What**: in SQLite, `LIMIT -1` = no limit and `LIMIT 0` = empty. So
`claim_batch(conn, q, w, -1, …)` claims the **entire queue**, and
`claim_batch(conn, q, w, 0, …)` is a silent no-op return. Contract text
(core-contract.md queue section, and the `Queue::claim_batch` doc in
`alkstore/src/queue.rs`) pins "Claim up to `n` rows" — no non-positive rule.
The substrate honors its contract-blind discipline by passing `n` through,
which is *correct* for the fork; but wave 3's engine layer will inherit this
behavior unless the trait-impl entry point clamps/rejects `n` first.
**Recommended**: wave 3's engine wiring validates `n >= 1` (or clamps to 0 =
no-work value) at the trait-impl boundary, mirroring how `lock_renew` guards
`ttl_s <= 0` substrate-side. The engine-layer resolution point is the right
place per the ADR-012 boundary — this note exists so the decision is made
there deliberately, not inherited from SQLite's dialect. Adding a test to the
contract suite row (`claim_batch(n=0)` behavior) is cheap and pins whatever
the engine layer decides.
### N-1 · Unbounded growth paths in the engine layer, deferred by design (record, not action)
Three table families grow without internal hygiene caps: `__alkstore_dead`
with `dead_letter_retention_s = NULL` (the default — the doc text is honest
that retention is opt-in), `__alkstore_notifications` rows pruned only at
attach (D-16's 10k cap triggers only next attach — an idle-but-listening
process never prunes), and `__alkstore_stream` (growth is the contract's
durable-log posture; `trim_to` is consumer-invoked per ADR-015). None of
these is wrong for v1; all three are documented engine-internal. **Recorded
so wave 6's release-readiness review consciously accepts them**; the notify
one in particular may want an "attach is not the only prune site" note in the
sqlite engine's docs (cadence recipes) when wave 3 wires the engine.
### N-2 · `encode_payload`'s empty-Vec failure arm is unreachable-but-wrong-looking
`serde_json::to_vec` on a `Value` cannot fail in practice (all its variants
represent valid JSON); the `unwrap_or_else(|_| Vec::new())` arm maps failure
to empty bytes, which would silently store a decode-to-`Null` payload. Two
better shapes: `Result<Vec<u8>, Error::Codec>` (a signature change — core is
pre-release, cheap now) or debug_assert-unreachable. ADR-020 §4 pins "engines
serialize with serde_json and store exactly those bytes" without pinning the
helper's signature, so neither shape violates the ADR. Core-side, minor, cheap
to fix before wave 3 starts consuming `encode_payload` (the sqlite engine is
where the payload bridge first calls it); flagged as a **wave-3 pre-work
candidate** rather than changed inline — signature choice is a call the engine
implementers should weigh in on since both engines consume this helper.
### N-3 · Validation asymmetry: `validate_name`'s whitespace-both-kinds rule is duplicated as prose in `Doc` but the whitespace set is narrow
`validate_name` rejects names whose `trim().is_empty()` — tab, space, newline
(and their mixes). Notably this does NOT reject zero-width, `\r`-only, or
other unicode-blank-only names (e.g. "\u{200B}", "\r"). Harmless (they round-
trip stringly everywhere and never collide with the reserved prefix; the
engines' SQL never interprets them), and `name.trim()` uses
char::is_whitespace only — so "\r" alone (is_whitespace = true) IS rejected.
Zero-width space "\u{200B}" is not whitespace by Rust's definition, passes,
and is arguably a legal (if ugly) name on every engine. **Not a bug**; noting
so future review rounds don't re-derive the question. No action.
### N-4 · `SQLITE_OPEN_URI` is inherited from upstream and reaches the wave-3 engine constructor
`open_conn` passes `OpenFlags::SQLITE_OPEN_URI` (honker uses it for `file:`),
and the watcher's `open_watcher_conn` does not. URI mode means the *path*
string is URI-interpretable (`?` params like `?nolock=1` mutate locking). No
consumer path reaches it yet (engine crates are stubs); wave 3's `open`
signature should either (a) drop the flag (breaking nothing today), or (b)
document that the path argument is URI-interpreted. Cheap to decide now;
surfaces here because it's the substrate's inherited attack surface, not the
engine's.
### N-5 · `with_tx`'s panic-disposition is documented but half-tested
The doc text (store.rs:123) pins "`Err`, drop, or panic across the closure ⇒
rollback (ADR-021 §4's drop = rollback)". `trait_probe_tests.rs` exercises
Ok→commit and Err→rollback; no test drives a panicking closure through
`with_tx`. The panic path matters because the closure's future is pinned
inside `with_tx`'s own future — the rollback depends on the *engine's*
TxHandle Drop impl being panic-safe, which is wave-3 territory (the sqlite
engine's handle-on-lease Drop). Adding a panics-closure probe to core is
cheap; flagged for the wave-5 contract suite rather than inline (panics-in-
closure semantics can only be fully pinned once a real engine's handle exists
— the Probe store would only test the probe).
### N-6 · Watcher poll cadence inherited at 1ms
`DEFAULT_WATCHER_POLL_INTERVAL = 1ms` with a `PRAGMA data_version` query per
tick — the lineage's default, kept verbatim per D-20. This is ~1000 pragma
queries/sec/instance on an idle store. Honker ships the same number, and the
config surface (`with_poll_interval`) allows raising it; but the wave-3
engine's `open` should decide whether 1ms is the shipping default or a dev
default (SQLite's `data_version` read is cheap-ish, but it's a wake-signal
cadence a busy system could tune). Not a defect; a tuning decision for wave
3's config surface.
---
## 3. Code smells / smells-adjacent
**None blocking.** Noted, not acted on:
- `mod.rs` sets crate-wide `#![allow(dead_code)]` `#![allow(unused_imports)]`
(documented, wave-2 posture — the substrate isn't wired yet). **This lint
suppression must be removed when wave 3 wires the engine layer** — it will
otherwise mask genuinely dead substrate code that should be ported-cut.
Filed here so wave 3 doesn't miss the removal.
- `row_to_dead_job` clones `row_to_live_job`'s decode then overwrites four
fields — slightly clever, but the DEAD_SELECT's shape (constant list,
matching `LIVE_COLUMNS` order + 2 extra) makes it readable; leave as-is.
- `queue_next_claim_at`'s `claim_expires_at + 1` in the UNION second arm is
correct (a processing row's wake is the first second past its deadline) but
under-documented in SQL; the doc text above it (line 1219's `>= unixepoch()`
note) exists — fine.
- `queue_ops.rs` line-formatting churn between `enqueue(...)` callsites is
rustfmt-normal; no smells.
## 4. Coverage analysis (cargo-llvm-cov)
Workspace 93.10% lines / 94.52% functions before this review's inline fixes;
~85% after on previously-missed substrate regions (see deltas below).
Per-file notes (only the interesting ones):
| File | Cover | Notes |
|---|---|---|
| `stop_token.rs` | 63.6% | **Fixed inline**: `Debug` impl lines 51–55 were the only miss; new `stop_token_debug_shows_cancelled_state` test covers them. Now 100%. |
| `ops.rs` | 87.9% | **Partly fixed inline**: `arg_opt_i64` (28–33) was fully dead — new `optional_arg_helper_keeps_null_and_converts_whole_reals` test covers it. Remaining misses are error-path bodies (real_to_i64's message arms except the fractional one, savepoint-undo arms, `UnwindUndo` drop log path, stream/lock fn wrappers' err paths) — all error arms or wrapper glue; the machinery itself is covered. Two notable uncovered lines: 103–108 (`UnwindUndo::drop` eprintln on post-panic rollback failure) is genuinely unreachable-in-test territory; 171/177-184/190-197 are the scalar-attachment sites' success bodies, covered only through the pressure suite's stream/lock paths (which do run). |
| `queue_ops.rs` | 96.0% | Very strong. Missed lines are error-path tails (105 = claim CTE's RETURNING tail under a claim-race arm; 122/137/158/188/214/316/324/335/397 = `.execute`-error tails; 442/472/493 = dead_letter_exhausted's error path tail; 525/562 = parse/spec error arms partially covered; 1215 = asserted-dead line in an assert-driven test). None is a semantic hole; the no-stranded-rows, validity-predicate, and catch-up-cap suites are extensive. |
| `schema.rs` | 89.6% | Misses are `add_column_if_absent`'s migration arms (244–305) — the pre-existing-schema migration test exercises the happy ALTER path but each absent-column arm's *err* branch is uncovered; `Write`/`Readers` close paths partially covered through the lock tests. Wave 3 (real engine `open` integration) will naturally widen this. |
| `watcher.rs` | 93.6% | Missing: 65–71 (HighRes/LowRes `file_id` variants — Windows-only, fine to leave), 99–110 (`is_transient_lock_error` matches — driven only indirectly), 187–202 (reconnect-loop success body — the W-1 test drives the *failure* path; a reconnect *success* test would require a file appearing mid-run), 280/331/386/428/437 (drop guards' success bodies), 473/586-591 (death-signal test's arms). The reconnect-success case (187–202) is the one worth a cheap test if wave 3 touches the watcher at all. |
| `payload.rs` | 92.3% | One missed line = `encode_payload`'s failure arm (see N-2 — if the helper's signature becomes fallible, coverage and correctness both improve). |
| contract-suite `properties.rs` | 94.9% | Exemplar row only so far; wave 5 fills rows (by design). |
**Suggested wave-5 contract-suite adds** (from the coverage + finding analysis):
sweep-retention-failure rollback pin (M-1's follow-up), `claim_batch` non-
positive-`n` decision pin (M-2's follow-up), `with_tx` panic-disposition probe
(N-5's follow-up), and the reconnect-success watcher path if the engine keeps
the current watcher shape.
## 5. Contract-adherence spot-checks
Checked (and passing) specifically:
- `#[non_exhaustive]` placement matches ADR-017 §3 exactly: `Error`,
`Job`, `JobState`, `Schedule`, `StreamEvent`, `Wake` — on; `EnqueueOpts`,
`QueueOpts`, `ScheduleOpts` — off (opts are consumer-constructed).
- Core deps: `serde`, `serde_json`, `thiserror` only — no driver deps
(ADR-001/ADR-020 §4).
- Core has no `unwrap`/`expect`/`panic!` outside `#[cfg(test)]` modules and
one documented-in-ADR test-relaxation (contract-suite rows).
- Trait surface matches `core-contract.md`'s pinned list method-for-method
(`Store`, `TxHandle`, `Queue`, `StreamHandle`, `Outbox`, `Lock`,
`JobHandle`, `EventReceiver`, `WakeReceiver`, `StopToken`, `WithTxClosure`).
- `with_tx`'s provided-body shape matches ADR-021 §4's pinned signature and
Ok⇒commit / Err⇒rollback dispositions.
- Reserved namespace: `RESERVED_PREFIX`/`RESERVED_LISTENER_RECONNECTED`
exact-match core-contract.md; substrate-derived outbox queues will ride
`__alkstore_outbox:{name}` (engine-side, wave 3).
- Substrate is contract-blind: no `alkstore` types, no core imports under
`alkstore-sqlite/src/substrate/` (verified grep: zero `alkstore::` in the
subtree).
- Provenance register: fork point, D-01..D-28 all present, categorized,
lineage-tagged; D-27/D-28 (wave-2 gate fixes) present. `LICENSE` bundle
(MIT OR Apache-2.0) in-tree under `src/substrate/`.
- Substrate code carries no panics, no unwrap/expect outside tests.
## 6. Inline fixes made by this review
1. `stop_token.rs` Debug coverage — test added (`value_type_tests.rs`).
2. `arg_opt_i64` dead-code coverage — test added (`ops.rs` real_arg_tests).
3. **M-1 fix**: `sweep_expired` retention DELETE moved inside the
`in_savepoint` frame (behavior-preserving; the whole operation now
actually satisfies the no-stranded-rows property under failure).
All gates re-run green after the fixes: build, workspace tests
(24 + 85 + 3), clippy `-D warnings`, fmt --check.
## 7. Recommended ordering
- **Before wave 3 starts**: decide N-2's `encode_payload` signature (tiny,
core-side, both engines consume it) and N-4's URI-open posture (engine
constructor). Both are 10-minute decisions, both get cheaper if made now.
- **Wave 3 wiring tasks**: carry M-2's `n` guard decision, remove the two
`allow` lints in `substrate/mod.rs`, and adjudicate N-6's poll-cadence
default.
- **Wave 5 contract suite**: the §4 test adds.
- **Wave 6 release review**: consciously accept N-1's growth postures or
add engine-side hygiene.