diff --git a/docs/plans/implementation.md b/docs/plans/implementation.md index 58be7f2..db12cc0 100644 --- a/docs/plans/implementation.md +++ b/docs/plans/implementation.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-09 (pg-fix-tx-wake landed — the second HIGH fix, retiring F-1's engine arm; wave 5's remaining gate is the fix batch's review gate) +last_updated: 2026-10-10 (wave-4 fix batch landed and gate-verified — review-wave-4-fixes passed; wave 5 decomposed) --- # alkstore — Implementation plan @@ -36,7 +36,7 @@ parallel). | [2](#wave-2--sqlite-substrate-fork) | honker-core fork into `alkstore-sqlite/src/substrate/`: port, deltas, provenance, test floor | wave 1 (workspace scaffold only) | implemented + reviewed (2026-10-08) | | [3](#wave-3--sqlite-engine) | SQLite engine: connection architecture, re-derived queue ops on contract v1, scheduler/outbox, tx seam, SQLite backlog column | waves 1 + 2 | implemented + reviewed (2026-10-08) | | 4 | [Postgres engine](#wave-4--postgres-engine): schema bootstrap, pool/open, listener/forwarder, all mechanisms, tx seam, pg backlog column | wave 1 | implemented + reviewed (2026-10-08) | -| 5 | Contract suite: the cross-engine equivalence properties (core-contract.md §Verification backlog), version-stamped per ADR-017 | waves 3 + 4 | not yet decomposed | +| 5 | [Contract suite](#wave-5--contract-suite): the cross-engine equivalence properties (core-contract.md §Verification backlog), version-stamped per ADR-017 | waves 3 + 4 | decomposed (2026-10-10) | | 6 | Release readiness: crate docs, deployment matrix final pass, README (written last, honestly), publish prep; mem-engine and fuzzing decisions | wave 5 | not yet decomposed | ## Wave 1 — Foundations @@ -181,6 +181,69 @@ natural realization — the mechanism name *is* the wake channel streams), legal by construction under the shared namespace — with the reserved reconnect-wake channel staying the only reserved string. +## Wave 5 — Contract suite + +Filling the scaffolded suite (`alkstore-contract-suite`) with the +remaining verification-backlog rows and running the compatibility +gate. Waves 3 and 4 each landed a nine/ten-row backlog column; the +wave-5 decomposition started from an audit of what those columns +already discharge versus what the backlog still lacks. + +**Already discharged by the engines' columns** (the wave-5 review +gate verifies each claim by reading the pinning row, not by trusting +the audit): name validation (the exemplar, incl. `schedule()`'s +queue-argument validation), the numeric-domain rows (extents + +durations), `encode_payload` typed failure + round-trip, the +`PayloadTooLarge` occurrence asymmetry (both engine-scoped arms), +drop-=-rollback no-ghosts, in-tx read-your-own-writes (which also +discharges the `save_offset_tx` exactly-once backlog row across +itself and the drop-rollback row), receiver close/save arms + monotone +composition (both columns), and — pinned engine-side rather than as +suite rows, per the sqlite-engine-integration task's scoping call — +plain-path SQLite open and the watcher-cadence knob +(`open_tests.rs`; SQLite-scoped backlog rows). + +**Wave 5's task set** (the gaps): + +- Queue depth: the job-handle validity predicate, the ADR-010 depth + properties (reclaim-eats-attempt, dead-letter moves, the + no-stranded-rows sweep), and M-1's retention-failure pin + (`suite-queue-depth-rows`). +- Scheduler: boundary fires, bounded catch-up, leadership discipline — + with a documented extension of the suite's determinism posture to + admit runner-driving rows (`suite-scheduler-rows`). +- Tx seam: `outbox_enqueue_tx` and `publish_with_key_tx` + commit-atomicity, plus N-5's `with_tx` panic-disposition probe — + the panic path pinned against real engines for the first time + (`suite-tx-commit-atomicity-rows`). +- Streams: the cross-engine ordering-equivalence row and the full + `trim_to` semantics row (`suite-stream-rows`). +- Locks: TTL/expiry re-acquisition and the concurrent-`try_lock` + loser posture — the SQLite busy-path is the open question the row + exists to answer (`suite-lock-rows`). +- Wakes: the pinned `WakeReceiver` shapes, tolerance-bounded + (`suite-wake-rows`). +- Arithmetic equivalence: the backoff-curve row and the two + wall-clock-adjacent opts-resolution legs the existing row deferred + to this wave (`suite-opts-backoff-rows`). +- Engine-side hardening riding the window: the wave-3 review's + deferred SQLite commit-error-arm coverage (`sqlite-commit-error-arm`) + and F-1's defense-in-depth candidates — explicitly not load-bearing + since `pg-fix-tx-wake` retired the flake's root cause + (`pg-suite-infra-hardening`). +- The review gate (`review-wave-5`): the backlog discharge audit, + stamp verification, green-on-both-engines, the parked-disposition + audit — and the gate that flips `engine-sqlite.md` / + `engine-postgres.md` to `stable`. + +Judgment calls the decomposition parked in task Notes (the review +gate audits each): where the M-1 retention-failure pin, the +beyond-64-cap skip-forward leg, and the backoff 1-hour-cap leg land +when the public surface cannot express them (engine-side is the +expected answer, recorded per case); whether the scheduler rows +demand the pg fire-wake parity the wave-4 fix-batch gate recorded; +and what the SQLite lock busy-path actually returns under contention. + ## Decided points - **Contract-suite layout — option (a)**: a small internal @@ -261,6 +324,12 @@ decomposes. Specific gates: `tasks/review-wave-4.md`) and making `tx_publishes_compose_with_the_handle` deterministic (20 consecutive solo runs green); the remaining fixes ride the wave-4 fix-batch decomposition below. +- **Wave 5 decomposition** (2026-10-10) — audit-first: the engines' + backlog columns already discharge most of the §Verification backlog + (map recorded in `tasks/review-wave-5.md`'s appendix for the gate to + verify); the wave fills the gaps with seven suite-row tasks grouped + by mechanism, two engine-side hardening tasks, and the gate. See the + Wave 5 section. - **Wave 4 decomposition** (2026-10-08) — shaped as wave 3's structural twin; wave-3 outcomes absorbed (constructors exist; arithmetic re-owned per engine; per-engine `@every` parser; diff --git a/tasks/pg-suite-infra-hardening.md b/tasks/pg-suite-infra-hardening.md new file mode 100644 index 0000000..fc901b3 --- /dev/null +++ b/tasks/pg-suite-infra-hardening.md @@ -0,0 +1,63 @@ +--- +id: pg-suite-infra-hardening +name: Postgres test-infra hardening — F-1's defense-in-depth candidates +status: pending +depends_on: [] +scope: single +risk: low +impact: isolated +level: implementation +tags: [wave-5, postgres-engine, tests] +--- + +## Description + +Land the defense-in-depth test-infra hardening the wave-4 review +recorded for F-1 (`tasks/review-wave-4.md` Notes) — **explicitly not +load-bearing since `pg-fix-tx-wake` retired F-1's engine-side root +cause** (the compose test is deterministic, 20/20 solo runs green at +the fix-batch gate). This task exists because wave 5 was the named +home ("wave 5's suite hardening, or a wave-5-adjacent test-infra +task") and the candidates are cheap; if the wave-5 window gets tight, +cancelling this task loses nothing but insurance. + +Work (the review's candidate dispositions, cheapest first): + +- **(a) Harden `must_recv_event`'s deadline** — replace the 20 ms + polling loop with a parked `recv().await` under a timeout wrapper; + the deadline assert still bounds the property, the poll-loop's + wake-subscription interaction surface goes away. +- **(b) A `LOG_LAGGED`-style diagnostic counter on the bridge** — + discriminates "wake never arrived" from "wake arrived, re-drain + missed" if a wake-shaped flake ever recurs. Land only if it is a + small, honest addition (a test-observation counter, `#[cfg(test)]`- + gated or eprintln-postured per the house test style); skip and + record if it wants more machinery than that. + +Candidate (c) (leave the skip-posture as-is) is the do-nothing arm — +adopted by default for anything not landed. + +## Acceptance Criteria + +- [ ] `must_recv_event` parks on `recv().await` under a timeout + wrapper (no polling loop); the pg stream tests stay green + against the harness server (a full stream-module run) +- [ ] Candidate (b) either landed small or recorded as skipped with + the reason +- [ ] `cargo test -p alkstore-postgres` green (server-less skips + clean); clippy `-D warnings`; fmt clean + +## References + +- tasks/review-wave-4.md (F-1's Notes — the candidate dispositions) +- tasks/review-wave-4-fixes.md (F-1 retired; defense-in-depth posture) +- tasks/pg-fix-tx-wake.md (the engine-side fix that made this + non-load-bearing) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/review-wave-5.md b/tasks/review-wave-5.md new file mode 100644 index 0000000..4168b62 --- /dev/null +++ b/tasks/review-wave-5.md @@ -0,0 +1,127 @@ +--- +id: review-wave-5 +name: Wave 5 review gate — the suite as compatibility instrument +status: pending +depends_on: [suite-queue-depth-rows, suite-scheduler-rows, suite-tx-commit-atomicity-rows, suite-stream-rows, suite-lock-rows, suite-wake-rows, suite-opts-backoff-rows, sqlite-commit-error-arm, pg-suite-infra-hardening] +scope: broad +risk: low +impact: project +level: review +tags: [wave-5, review-gate] +--- + +## Description + +The wave-5 review gate (docs/plans/implementation.md §Review gates): +"the suite as compatibility instrument — every backlog row present, +version-stamped, green on both engines; this is the gate that flips +the engine specs to `stable`." + +Primary lenses: + +1. **Backlog audit — every row present.** Walk + core-contract.md §Verification backlog item by item against the + suite (and the engine-side pins for the SQLite-scoped rows) and + record the discharge map. The decomposition's audit (below) is the + starting point — verify each "discharged" claim by reading the + pinning row/test, not by trusting the map. +2. **Version stamps.** Every row's `Contract stamp:` items cite ADR § + per `version_stamp.rs`'s convention; stamps accumulate on + amendment (the extended `enqueue_opts_resolution` row is the + amendment case to check); greppable via `STAMP_MARKER`. Cross-check + against ADR-017 §2's no-silent-change discipline: every stamp's + cited ADR § actually pins the behavior tested. +3. **Green on both engines.** The SQLite column green server-less; + the pg column green against the harness server (twice, the + wave-4 gate's posture). Workspace gates green + (build/test/clippy/fmt). +4. **Dispositions audited.** The decomposition parked several + judgment calls in task Notes — verify each was made and recorded: + the M-1 retention-failure pin's location (suite row vs engine-side), + the beyond-64 skip-forward leg's location, the backoff cap leg's + location, the scheduler fire-wake parity call, the lock-row + divergence call (if any), the SQLite reconnect-success test + taken/not-taken. +5. **Conformance spot-checks.** The rows assert what the ADR text + pins (no weaker, no stronger — a row pinning beyond the ADR text + is a suite defect: it would fail a correct engine); the + determinism posture holds (tolerance-bounded state assertions + only, no timing-value asserts, no two-task races) including the + scheduler rows' documented posture extension; no duplication + between rows' legs. +6. **Flip the engine specs to `stable`** — `engine-sqlite.md` and + `engine-postgres.md` frontmatter `status: draft` → `status: + stable`, with dated `last_updated` annotations citing this gate. + Update `docs/plans/implementation.md`: the wave table's wave-5 + status, a review-rounds entry for this gate. + +## Acceptance Criteria + +- [ ] Backlog discharge map recorded (every §Verification backlog item + → its pinning row/test or an explicit gap with a disposition) +- [ ] Every row version-stamped per convention; amendment stamps + accumulated; stamps verified against the cited ADR text +- [ ] Suite green on both engines (SQLite server-less; pg vs harness, + two consecutive full runs); workspace gates green +- [ ] All parked dispositions from the wave-5 tasks audited and + recorded +- [ ] Engine specs flipped to `stable` with dated annotations; + implementation.md updated (wave table + review rounds) +- [ ] Findings recorded; wave 6 decomposition may proceed + +## References + +- docs/plans/implementation.md §Review gates (wave 5), §The waves +- docs/architecture/core-contract.md §Verification backlog +- docs/architecture/decisions/017-contract-versioning.md §4.2 +- docs/architecture/decisions/022-contract-suite-layout.md +- alkstore-contract-suite/src/version_stamp.rs (the stamp convention) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion + +## Appendix — the decomposition's backlog audit (verify, don't trust) + +Discharged by existing rows (waves 1/3/4 — verify each): + +| Backlog item | Discharged by | +|---|---| +| Name validation at every entry point (ADR-008 §4) | `name_validation_rejects_empty_and_reserved` (exemplar) | +| `schedule()` queue-argument validation (ADR-021 §3) | same exemplar row | +| Numeric-domain semantics (ADR-023 §2) | `extent_clamp_semantics` + `duration_refusal_on_non_positive_ttl` | +| `encode_payload` typed failure + round-trip (ADR-023 §1, ADR-020 §4) | `payload_round_trip_stores_exact_encoding` | +| `PayloadTooLarge` occurrence asymmetry (ADR-016 §5) | `payload_too_large_never_produced_on_sqlite` + `payload_too_large_produced_on_pg` | +| Drop = rollback no-ghosts (ADR-021 §4) | `drop_rollback_leaves_no_ghosts` | +| In-tx read-your-own-writes (ADR-021 §1) | `in_tx_reads_see_own_writes` | +| `save_offset_tx` exactly-once-within-a-business-tx shape | discharged across `in_tx_reads_see_own_writes` (own-save visibility) + `drop_rollback_leaves_no_ghosts` (the save never lands on rollback) — verify the combination genuinely covers the backlog row's claim | +| Receiver error/close arms + save-offset monotone composition (ADR-021 §5, ADR-019 §6) | `receiver_close_and_save_arms` (both columns; the pg reconnect-stays-open leg pins engine-side — verify that pin exists) | +| Plain-path SQLite open (ADR-023 §3) | engine-side: `uri_shaped_open_path_is_a_literal_filename` (`open_tests.rs`) — SQLite-scoped backlog row per the sqlite-engine-integration task | +| Watcher cadence default + knob (ADR-023 §4) | engine-side: `poll_interval_flows_to_the_watcher_config` (`open_tests.rs`) — same scoping | + +Added by wave-5 tasks (verify presence + stamps): + +| Backlog item | Wave-5 task | +|---|---| +| Job-handle validity predicate (ADR-010 §2) | `suite-queue-depth-rows` | +| ADR-010 depth: reclaim-eats-attempt, dead-letter, no-stranded-rows sweep + M-1 retention pin | `suite-queue-depth-rows` | +| Scheduler boundary/catch-up/leadership (ADR-009) | `suite-scheduler-rows` | +| `outbox_enqueue_tx` commit-atomicity (ADR-014) | `suite-tx-commit-atomicity-rows` | +| `publish_with_key_tx` commit-atomicity (ADR-015 §2) | `suite-tx-commit-atomicity-rows` | +| `with_tx` panic disposition (N-5) | `suite-tx-commit-atomicity-rows` | +| Cross-engine stream ordering equivalence (ADR-015 §3/§4) | `suite-stream-rows` | +| `trim_to` full semantics (ADR-015 §5) | `suite-stream-rows` | +| Lock TTL/expiry re-acquisition + concurrent loser (ADR-008 §7) | `suite-lock-rows` | +| Wake semantics under `WakeReceiver` shapes (ADR-008 §3) | `suite-wake-rows` | +| Backoff-curve equivalence (ADR-010 §3) + deferred opts clock legs (ADR-020) | `suite-opts-backoff-rows` | + +Engine-side hardening riding the wave-5 window: + +| Item | Task | +|---|---| +| SQLite commit-error-arm coverage (wave-3 review deferral) | `sqlite-commit-error-arm` | +| F-1 defense-in-depth (pg test-infra; not load-bearing) | `pg-suite-infra-hardening` | \ No newline at end of file diff --git a/tasks/sqlite-commit-error-arm.md b/tasks/sqlite-commit-error-arm.md new file mode 100644 index 0000000..1737a75 --- /dev/null +++ b/tasks/sqlite-commit-error-arm.md @@ -0,0 +1,68 @@ +--- +id: sqlite-commit-error-arm +name: SQLite engine — commit-error-arm coverage (wave-3 review's deferred test) +status: pending +depends_on: [] +scope: narrow +risk: medium +impact: isolated +level: implementation +tags: [wave-5, sqlite-engine, tests] +--- + +## Description + +Land the wave-3 review gate's deferred coverage item +(`tasks/review-wave-3.md` Notes: "Coverage of the commit-error arm — +no test induces a failing `COMMIT` ... natural home is wave 5's suite +hardening"): a test that drives a failing `COMMIT` through the SQLite +engine's `commit` path and pins the fix the review verified by +code-read — the writer slot is replenished via the `reopen` closure on +the failed `COMMIT` (no stranding), the error surfaces as the opaque +`Database`, and the store remains fully usable afterward (subsequent +`begin_tx`/auto-commit ops work). + +The injection mechanism is this task's to find — candidates: a +`PRAGMA max_page_count` squeeze forcing `SQLITE_FULL` on a +space-consuming commit, or a `cfg(test)` fault seam in the seam layer +if the PRAGMA route cannot hit the COMMIT arm deterministically. The +wave-4 fix batch's `cfg(test)` config-seam precedent +(`pg-fix-forwarder-reconnect`) is the house pattern if a seam is +needed. Prefer the least-invasive mechanism that deterministically +reaches the arm; document the choice. + +If the same session has cheap room for it, the waves-1–2 review's +optional add — the watcher reconnect-*success* path test (upstream's +W-1 test drives only the failure path; "a reconnect success test would +require a file appearing mid-run") — rides here **only if** the +engine kept the current watcher shape and the test is genuinely cheap; +otherwise leave it recorded as not-taken in Notes (it was a +suggestion, not an order). + +## Acceptance Criteria + +- [ ] A test induces a failing `COMMIT` on the SQLite engine and pins: + error surfaces (`Database`), writer slot replenished (a + subsequent `begin_tx` succeeds), no partial-commit residue +- [ ] The injection mechanism documented in Notes (PRAGMA vs seam, and + why) +- [ ] The watcher reconnect-success test either landed or explicitly + recorded as not-taken with the reason +- [ ] `cargo test -p alkstore-sqlite` green server-less; clippy + `-D warnings`; fmt clean + +## References + +- tasks/review-wave-3.md (Notes: the deferred commit-error-arm coverage) +- tasks/sqlite-engine-seam-tx.md (the `commit` replenish fix the test pins) +- tasks/pg-fix-forwarder-reconnect.md (the `cfg(test)` seam precedent) +- docs/reviews/001-waves-1-2-general-review.md §2 (the watcher + reconnect-success suggestion) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-lock-rows.md b/tasks/suite-lock-rows.md new file mode 100644 index 0000000..57350cd --- /dev/null +++ b/tasks/suite-lock-rows.md @@ -0,0 +1,78 @@ +--- +id: suite-lock-rows +name: Contract-suite rows — lock TTL/expiry re-acquisition + concurrent try_lock loser +status: pending +depends_on: [] +scope: narrow +risk: medium +impact: phase +level: implementation +tags: [wave-5, contract-suite, locks] +--- + +## Description + +Add the named-locks contract rows to +`alkstore-contract-suite/src/properties.rs` and wire them into both +engines' suite targets, discharging two backlog rows +(core-contract.md §Verification backlog): "Named-lock TTL/expiry +re-acquisition on SQLite" (pinned on pg by the POC's lock probe; on +SQLite it rests on the forked substrate's lock machinery — the suite +pins both engines to identical behavior) and "Concurrent `try_lock` +loser/error behavior on SQLite" (the substrate's busy-path under lock +contention is the thing to pin — the pg side returns cleanly). + +Rows to add: + +- **`lock_ttl_expiry_and_reacquisition`** — the ADR-008 §7 guarantee + row: mutual exclusion bounded by TTL + renewal. Pin: a held lock + excludes a second acquirer (`None`); after TTL expiry exclusion + lapses *silently* (no revocation event, no error — a second owner + acquires); the original holder's post-expiry `renew` is refused + (`false` — the deadline-lapse refusal, ADR-010 §2's uniform + predicate shape on the lock handle); `release` on the second owner + frees the name for a third acquirer. Short TTL (1 s) + tolerance + sleeps — sequential single-store, fits the determinism posture. +- **`concurrent_try_lock_loser_is_a_value`** — contention posture: + with a lock held, contender `try_lock` calls return the clean + no-work value (`None`) on **both** engines — never a `Database` + error, never a busy-throw surfacing (the SQLite substrate's + busy-path is the risk site; the pg side is the reference behavior). + Drive several contenders sequentially against one held lock, and a + release-then-contend cycle proving the loser path leaves no state + that blocks a later acquire. + +Risk note: what the SQLite busy-path *actually* returns under +contention is the open question this row exists to answer — if it +surfaces `Database` where pg returns `None`, that is a real +conformance finding: stop, record it in Notes, and raise it (an +engine fix or an ADR-level asymmetry call) rather than pinning the +divergence silently. + +## Acceptance Criteria + +- [ ] Two rows exist, version-stamped (ADR-008 §7, ADR-019 §1, + ADR-023 §2 as applicable) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] Any engine divergence found is recorded in Notes with its + disposition (fix or documented asymmetry) — not pinned silently +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (lock + TTL/expiry re-acquisition; concurrent try_lock loser) +- docs/architecture/decisions/008-contract-v1-pinning.md §7 +- docs/architecture/decisions/019-mechanism-handle-surfaces.md §1 +- docs/architecture/engine-sqlite.md (the substrate's lock machinery) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-opts-backoff-rows.md b/tasks/suite-opts-backoff-rows.md new file mode 100644 index 0000000..9d3ac24 --- /dev/null +++ b/tasks/suite-opts-backoff-rows.md @@ -0,0 +1,81 @@ +--- +id: suite-opts-backoff-rows +name: Contract-suite rows — backoff-curve equivalence + the deferred enqueue-opts clock legs +status: pending +depends_on: [] +scope: narrow +risk: low +impact: phase +level: implementation +tags: [wave-5, contract-suite, queues] +--- + +## Description + +Complete the arithmetic-equivalence backlog rows +(core-contract.md §Verification backlog): "Backoff-curve and +stamp-resolution equivalence across engines" and the two +wall-clock-adjacent legs the existing `enqueue_opts_resolution` row +explicitly deferred ("The `run_at`-alone-literal and +neither-field-resolves-to-now legs ... pin with wave 5's equivalence +row"). + +Work: + +- **Extend `enqueue_opts_resolution` in place** (one owner per + property — the row's own doc comment names this wave-5 completion; + stamps accumulate per the convention): the `run_at`-alone literal + leg (a future `run_at` is the row's ready time verbatim; a past + `run_at` resolves to ready-now — the boundary-total rule, ADR-023 + §2) and the neither-field leg (ready now — tolerance-bounded + proximity, never a tight timing assert). +- **Add `backoff_curve_equivalence`** — the equal-jitter exponential + curve (ADR-010 §3): `delay ∈ [base·2^(a−1)/2, base·2^(a−1)]` per + attempt, uniform jitter over the lower half — *the range, not a + jitter label, is the definition*. Drive claim → `retry(err, None)` + cycles on a queue with a small `backoff_base_s` stamp (2 s keeps + the integer-second row resolution honest: ranges [1,2], [2,4], + [4,8] across the first attempts; the inter-attempt waits stay + tolerance-bounded and sequential), reading the computed delay back + via `get_job`'s `run_at` minus now, asserting range membership per + attempt on both engines — identical bounds is the equivalence pin + (ADR-012 §2: engine-side arithmetic, suite-pinned identical + outputs). Include the explicit-delay override leg + (`retry(err, Some(d))` honored — a `Some(0)` resolves ready-now per + the boundary-total rule). **The 1-hour cap leg is not + suite-pinnable** (reaching capped attempts requires waiting out + minute-scale delays); it stays pinned engine-side (both engines' + unit tests already pin the curve against the ADR text — the + wave-3/4 reviews verified it) — record that disposition in Notes + for the review gate. + +## Acceptance Criteria + +- [ ] `enqueue_opts_resolution` extended with both clock legs; its + doc comment's deferral note updated (stamps accumulate) +- [ ] `backoff_curve_equivalence` exists, version-stamped (ADR-010 §3, + ADR-020 §1–§3, ADR-012 §2, ADR-023 §2) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] The cap-leg disposition recorded in Notes +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (backoff-curve + and stamp-resolution equivalence; enqueue-opts resolution equivalence) +- docs/architecture/decisions/010-queue-semantics-depth.md §3 +- docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md §1–§3 +- docs/architecture/decisions/012-forked-substrate-design.md §2 +- alkstore-contract-suite/src/properties.rs (`enqueue_opts_resolution`'s + deferral note) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-queue-depth-rows.md b/tasks/suite-queue-depth-rows.md new file mode 100644 index 0000000..7ab8abf --- /dev/null +++ b/tasks/suite-queue-depth-rows.md @@ -0,0 +1,94 @@ +--- +id: suite-queue-depth-rows +name: Contract-suite rows — job-handle validity predicate + ADR-010 queue depth +status: pending +depends_on: [] +scope: moderate +risk: medium +impact: phase +level: implementation +tags: [wave-5, contract-suite, queues] +--- + +## Description + +Add the queue-depth contract rows to +`alkstore-contract-suite/src/properties.rs` and wire them into both +engines' suite targets (`alkstore-sqlite/tests/contract_suite.rs` +directly; `alkstore-postgres/tests/contract_suite.rs` via the +`harness_row!` macro), discharging the backlog rows "Job-handle +validity predicate on both engines" and the ADR-010 depth properties +(core-contract.md §Verification backlog). One owner per property +(ADR-012 §2 mirror): the rows are engine-independent, run against both +factories, version-stamped per the crate's convention. + +Rows to add: + +- **`job_handle_validity_predicate`** — the uniform predicate + (ADR-010 §2): an op succeeds only while the row is `processing` and + the claim deadline is unexpired. Pin: the late-heartbeat boundary + (heartbeat refused exactly when the deadline has lapsed — a short + `visibility_timeout_s` stamp plus a tolerance sleep past it; the + sleep-then-assert-state shape is sequential single-store and fits + the suite's determinism posture), the post-lapse ack refusal + (at-least-once: the worker's ack does not land after lapse), + `ack_batch` applying the predicate per id (lapsed or non-claimed ids + silently not counted — the returned count reflects only live + acks), `retry`'s refusal being `false` with the row untouched, and + the engine-default strings landing identically (`fail(None)` → + `"failed"`, exhaustion → `"max attempts exceeded"`). +- **`queue_depth_reclaim_and_dead_letter`** — the ADR-010 depth + properties: a visibility reclaim consumes an attempt (expired claim + → reclaim → `attempts` incremented, `claimed_at` refreshed), + exhaustion moves the row to dead storage (`get_job` sees the dead + row with `last_error`/`died_at`), and `cancel` is an unconditional + delete (not an interrupt). +- **`sweep_no_stranded_rows`** — `sweep_expired()` moves *every* + past-expiry row (any state — pending-expired, processing-lapsed, + dead-past-retention) and enforces dead-letter retention; the + no-stranded-rows property (ADR-010 §5). Include the M-1 follow-up + pin here (the waves-1–2 general review's suggested wave-5 add): the + sweep's retention half rolling back with the sweep on failure — + **with an explicit disposition**: if a retention-DELETE failure is + not injectable through the contract surface (it likely is not — + the failure is storage-level), pin that leg engine-side + (SQLite substrate test seam) and record the disposition in this + task's Notes for the review gate to audit. The plan ordered "M-1's + retention-failure test → wave 5's contract-suite rows"; where the + pin actually lands is the implementer's honest call, documented. + +Timing posture: short stamps (1 s visibility) + tolerance sleeps +(bounded well above the transition's resolution), asserting state +outcomes never timing values. No two-task races. + +## Acceptance Criteria + +- [ ] Three rows exist in `properties.rs`, `pub async fn(&dyn + StoreFactory)`, version-stamped (ADR-010 §2/§5, ADR-019 §3, + ADR-008 §5 as applicable; stamps cite ADR § per the + `version_stamp.rs` convention) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] The M-1 retention-failure pin's location decided and recorded in + Notes (suite row or engine-side, with the reason) +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (job-handle + validity predicate; ADR-010 depth properties) +- docs/architecture/decisions/010-queue-semantics-depth.md §2/§5 +- docs/architecture/decisions/019-mechanism-handle-surfaces.md §3 +- docs/reviews/001-waves-1-2-general-review.md (M-1, §4's suggested adds) +- tasks/sqlite-engine-integration.md (the wave-5 lesson: rows are + mock-blind until a real single-writer engine runs them) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-scheduler-rows.md b/tasks/suite-scheduler-rows.md new file mode 100644 index 0000000..89088ca --- /dev/null +++ b/tasks/suite-scheduler-rows.md @@ -0,0 +1,93 @@ +--- +id: suite-scheduler-rows +name: Contract-suite rows — scheduler boundary/catch-up/leadership (+ determinism-posture extension) +status: pending +depends_on: [] +scope: moderate +risk: medium +impact: phase +level: implementation +tags: [wave-5, contract-suite, scheduler] +--- + +## Description + +Add the scheduler contract rows to +`alkstore-contract-suite/src/properties.rs` and wire them into both +engines' suite targets, discharging the backlog row "Scheduler + depth +properties on Postgres" (core-contract.md §Verification backlog): the +tick machinery — boundary advance, bounded catch-up, leadership-loss +discipline — pinned engine-uniform (SQLite inherits the substrate's +test-pinned implementation; pg re-derived; the suite pins identical +*behavior*). + +**Posture extension first** (test-side, ADR-017 §2 class 4): the +suite's determinism statement (`properties.rs` module doc) currently +says runner spawning stays engine-test-side. The backlog explicitly +demands these rows in the suite, so amend the module doc to admit +**runner-driving rows**: a spawned `run_schedules(stop)` task with a +`StopToken`, assertions on state outcomes (jobs fired, leadership +returned) with tolerances bounded well above the boundary resolution — +never timing-value assertions, never two-task race windows. Document +the extension where the posture is stated. + +Rows to add: + +- **`scheduler_boundary_fires`** — register `@every 1s`, run the + runner briefly, stop: boundary fires land as ordinary claimable jobs + stamped with the `ScheduleOpts`/derived-default resolution + (ADR-009 §3, ADR-020 §3); no double-fire per boundary while one + leader runs (fire + boundary-advance commit atomically). +- **`scheduler_bounded_catchup`** — register, delay before starting + the runner (several elapsed boundaries), then run: missed boundaries + replay boundary-by-boundary (at-least-once per elapsed boundary), + the fired count bounded by the 64-cap (a multi-second downtime stays + far below it — the ≤64 catch-up leg is the suite-pinnable one). + **The beyond-cap skip-forward leg needs >64 elapsed boundaries + (~65+ s) and row-backdating the public surface does not express — + pin it engine-side where the schedule row's `next_fire_at` is + directly manipulable (both engines' scheduler tests), and record + that disposition in Notes for the review gate.** +- **`scheduler_leadership_discipline`** — two concurrent runners: + exactly one leader (fires not duplicated), the loser surfaces + `LeadershipLost` (ADR-009 §6) rather than silently co-ticking; + a stopped runner's clean-stop return shape per ADR-019 §4. + +**Disposition note from the wave-4 fix-batch gate** (recorded, not a +defect): the pg tick's in-tx enqueue fires issue no `pg_notify` wake +(SQLite's watcher wakes on those commits) — a cross-engine +wake-latency-parity gap the re-poll safety net covers for correctness. +If this row's shape demands the parity, the one-call `wake_tx` inside +the tick's tx rides **this** task; otherwise record the row's +latency-posture in Notes and leave the engines as they are. + +## Acceptance Criteria + +- [ ] The determinism-posture extension documented in + `properties.rs`'s module doc (runner-driving rows admitted, + tolerance-bounded state assertions only) +- [ ] Three rows exist, version-stamped (ADR-009 §3/§4/§6, ADR-019 §4, + ADR-020 §3 as applicable) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] The skip-forward-leg disposition and the fire-wake-parity + disposition recorded in Notes +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (scheduler + + depth properties on Postgres) +- docs/architecture/decisions/009-scheduler-collapse.md §3/§4/§6 +- docs/architecture/decisions/019-mechanism-handle-surfaces.md §4 +- tasks/review-wave-4-fixes.md (the fire-wake-parity recorded-for-later note) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-stream-rows.md b/tasks/suite-stream-rows.md new file mode 100644 index 0000000..4c0f9c0 --- /dev/null +++ b/tasks/suite-stream-rows.md @@ -0,0 +1,80 @@ +--- +id: suite-stream-rows +name: Contract-suite rows — cross-engine stream ordering equivalence + trim_to semantics +status: pending +depends_on: [] +scope: moderate +risk: low +impact: phase +level: implementation +tags: [wave-5, contract-suite, streams] +--- + +## Description + +Add the streams contract rows to +`alkstore-contract-suite/src/properties.rs` and wire them into both +engines' suite targets, discharging two backlog rows +(core-contract.md §Verification backlog): "Cross-engine stream +equivalence" (ADR-015 §3/§4) and "`trim_to` semantics on both +engines" (ADR-015 §5 — the legs the existing `extent_clamp_semantics` +row does not carry). + +Rows to add: + +- **`stream_ordering_equivalence`** — the ordering guarantee row: + a publish sequence with keyed/unkeyed interleavings yields `offset + ASC` global FIFO per stream — same publish order → same read order + on `read_since`, `read_from_consumer`, and a subscriber's attach + drain alike; offsets are strictly increasing per stream (per-stream + relative order — absolute offset values are explicitly *not* + cross-pinned: pg bigserial vs SQLite AUTOINCREMENT); `key` + round-trips exactly (`None` stays `None`, `Some` stays `Some`, on + every read form); `stream` carries the stream name and `created_at` + is unix-seconds-at-publish (informational — tolerance-bounded + proximity to now, never an ordering assertion). The single-event + key/payload round-trip already pins in + `payload_round_trip_stores_exact_encoding` — this row owns the + *sequence/ordering* property. +- **`trim_to_semantics`** — the full ADR-015 §5 row: exact-boundary + trim (`offset <= horizon` — the horizon's own row deletes, + horizon+1 survives), surviving rows keep their offsets (gaps legal, + never renumbered — the negative-horizon/immutability legs already + pin in `extent_clamp_semantics`; this row owns the exact-boundary + and resume legs), a read from a trimmed-away region resumes at the + trim horizon's first remaining row, a saved offset below the horizon + stays a valid position marker (`get_offset` returns it; + `read_from_consumer` resumes at the horizon), trim emits no + dedicated wake and no notify (a pre-attached listener idles across + the trim — tolerance-bounded absence), and a subscriber with a + saved checkpoint never loses its place across a trim (its next read + continues from the horizon, not from a renumbered past). + +## Acceptance Criteria + +- [ ] Two rows exist, version-stamped (ADR-015 §3/§4/§5, ADR-019 §6 + as applicable) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] No duplication with the existing rows' legs (cross-reference in + each row's doc comment which row owns which leg) +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (cross-engine + stream equivalence; trim_to semantics) +- docs/architecture/decisions/015-streams-depth.md §3/§4/§5 +- docs/architecture/decisions/019-mechanism-handle-surfaces.md §6 +- alkstore-contract-suite/src/properties.rs (the existing rows whose + legs this task complements) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-tx-commit-atomicity-rows.md b/tasks/suite-tx-commit-atomicity-rows.md new file mode 100644 index 0000000..ffb8b74 --- /dev/null +++ b/tasks/suite-tx-commit-atomicity-rows.md @@ -0,0 +1,83 @@ +--- +id: suite-tx-commit-atomicity-rows +name: Contract-suite rows — outbox/keyed-publish tx commit-atomicity + with_tx panic probe +status: pending +depends_on: [] +scope: moderate +risk: low +impact: phase +level: implementation +tags: [wave-5, contract-suite, tx-seam] +--- + +## Description + +Add the tx-seam commit-atomicity rows to +`alkstore-contract-suite/src/properties.rs` and wire them into both +engines' suite targets, discharging three backlog rows +(core-contract.md §Verification backlog): "`outbox_enqueue_tx` +commit-atomicity on both engines" (ADR-014), "`publish_with_key_tx` +commit-atomicity on both engines" (ADR-015 §2), and the N-5 +`with_tx` panic-disposition probe (the waves-1–2 general review's +ordered wave-5 add — the panic path "can only be fully pinned once a +real engine's handle exists"). + +Rows to add: + +- **`outbox_enqueue_tx_commit_atomicity`** — rollback drops the + backing-queue job row together with the business write (no ghost + job; `run_once` afterwards claims nothing); commit makes the job + claimable by `run_once` exactly when the business write commits + (drive a real delivery through the `Delivery` closure); the stamped + opts are the outbox's derived 60/5/5 set, `get_job`-visible and + identical on both engines (ADR-014 §1, ADR-010 §3a). The + reserved/empty outbox-name validation legs already pin in the + exemplar row — do not duplicate them. +- **`publish_with_key_tx_commit_atomicity`** — rollback drops the + keyed event row with the business write (no ghost event); commit + makes it visible to `read_since` (and a subscriber attach); the key + round-trips on the committed event. The empty-`Some`-key + `InvalidName` leg already pins in the exemplar — do not duplicate. +- **`with_tx_panicking_closure_rolls_back`** — a closure that panics + mid-flight ⇒ rollback with no residue (the uniform no-ghosts list: + job/event/notify/offset — ADR-021 §4's drop = rollback through the + panic path), and the store remains usable afterward (the SQLite + writer slot replenished — the wave-3 stranding fix's property, now + pinned at contract level). Mechanics hint: the panic crosses an + await inside `with_tx`'s own future, so drive it via + `tokio::spawn` + `JoinError::is_panic()` (a `catch_unwind` around an + async closure does not see the panic point); assert the panic + surfaced *and* the post-panic state. This is N-5's probe against + real engines for the first time — the SQLite handle-on-lease Drop + impl and the pg pooled-object discard arm must both survive it. + +## Acceptance Criteria + +- [ ] Three rows exist, version-stamped (ADR-014 §1, ADR-015 §2, + ADR-021 §4, ADR-007 as applicable) +- [ ] Rows wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] The panic probe passes on both engines (no writer-slot stranding, + no pooled-object leak) — if an engine fails here, that is a real + defect: stop, record, fix engine-side before completing the task +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (outbox tx + commit-atomicity; keyed-publish tx commit-atomicity) +- docs/architecture/decisions/014-outbox-tx-enqueue.md §1 +- docs/architecture/decisions/015-streams-depth.md §2 +- docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md §4 +- docs/reviews/001-waves-1-2-general-review.md (N-5) +- tasks/sqlite-engine-seam-tx.md (the Drop impl N-5's probe exercises) + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/suite-wake-rows.md b/tasks/suite-wake-rows.md new file mode 100644 index 0000000..a6696f0 --- /dev/null +++ b/tasks/suite-wake-rows.md @@ -0,0 +1,73 @@ +--- +id: suite-wake-rows +name: Contract-suite row — wake semantics under the pinned WakeReceiver shapes +status: pending +depends_on: [] +scope: narrow +risk: low +impact: phase +level: implementation +tags: [wave-5, contract-suite, notify] +--- + +## Description + +Add the wake-semantics contract row to +`alkstore-contract-suite/src/properties.rs` and wire it into both +engines' suite targets, discharging the backlog row "Wake semantics +under `WakeReceiver` shapes" (core-contract.md §Verification +backlog): the POCs verified wake delivery/coalescing through their own +probe types; the pinned `Wake { channel }` / recv forms (ADR-008 §3) +need the suite's own property on both engines. + +Row to add: + +- **`wake_receiver_shapes`** — the wake contract's pinnable core, + tolerance-bounded (wakes are best-effort hints — the row asserts + state outcomes, never delivery counts or latencies): + - a pre-attached listener receives a wake after a committed + `notify` on its channel (`recv`/`try_recv`/`recv_timeout` forms + all exercised; the wake's `channel` field matches the listened + channel — the one piece of semantic content a wake carries); + - a burst of notifies yields *at least one* wake — never an exact + per-notify count (coalescing is the documented engine asymmetry: + possibly coalesced on SQLite, per-notify on pg; the row pins the + floor, not the shape); + - a listener attached *after* a commit never sees that commit's + notify (no-replay — `recv_timeout` idles; the pg no-replay hole + and SQLite's burst coalescing are both legal under this pin); + - wakes from a *different* channel do not arrive on this listener + (channel-scoped delivery — the invalidation-key property caching + subscribers rely on). + +The failure-surface arms (watcher death → `recv() -> None` on SQLite; +the synthetic reconnect-wake on pg) are the engine-differing close +arms — already pinned engine-side and in the receiver-arms row's +disposal leg; this row does not re-pin them (cross-reference in the +doc comment). + +## Acceptance Criteria + +- [ ] The row exists, version-stamped (ADR-006, ADR-008 §3) +- [ ] Wired into both engines' `contract_suite.rs` targets +- [ ] SQLite column green server-less; pg column green against the + harness server +- [ ] The row's doc comment cross-references the close-arm pins it + deliberately does not duplicate +- [ ] `cargo test -p alkstore-sqlite -p alkstore-postgres` green + (pg rows skip cleanly server-less); clippy `-D warnings`; fmt clean + +## References + +- docs/architecture/core-contract.md §Verification backlog (wake + semantics under WakeReceiver shapes); §notify/listen +- docs/architecture/decisions/006-wake-and-delivery-contract.md +- docs/architecture/decisions/008-contract-v1-pinning.md §3 + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file