diff --git a/alkstore-mem/src/fuzz_surface_tests.rs b/alkstore-mem/src/fuzz_surface_tests.rs new file mode 100644 index 0000000..438ebe7 --- /dev/null +++ b/alkstore-mem/src/fuzz_surface_tests.rs @@ -0,0 +1,5 @@ +#[test] +fn doc_hidden_reexports_reach_the_crate_root() { + assert_eq!(crate::parse_every_interval("@every 5s").unwrap(), 5); + assert_eq!(crate::skip_forward(0, 20, 5), 25); +} diff --git a/alkstore-mem/src/lib.rs b/alkstore-mem/src/lib.rs index 63841ec..93d5ab9 100644 --- a/alkstore-mem/src/lib.rs +++ b/alkstore-mem/src/lib.rs @@ -51,4 +51,18 @@ mod tx; mod validation; mod wakes; +#[cfg(test)] +mod fuzz_surface_tests; + pub use store::MemStore; + +/// Fuzz/test-only surface: the mem engine's `@every` parser — not +/// contract surface, not consumer API, no stability guarantee. +#[doc(hidden)] +pub use scheduler::parse_every_interval; + +/// Fuzz/test-only surface: the mem engine's schedule skip-forward +/// arithmetic — not contract surface, not consumer API, no stability +/// guarantee. +#[doc(hidden)] +pub use scheduler::skip_forward; diff --git a/alkstore-mem/src/scheduler.rs b/alkstore-mem/src/scheduler.rs index 7c53a65..c74ffa9 100644 --- a/alkstore-mem/src/scheduler.rs +++ b/alkstore-mem/src/scheduler.rs @@ -87,7 +87,7 @@ fn leader_owner() -> String { /// parser (third owner per ADR-012 §2; the grammar is small and /// pinned). Everything else — 5-field cron, seconds-field cron — /// rejects as `InvalidSpec`. -pub(crate) fn parse_every_interval(spec: &str) -> Result { +pub fn parse_every_interval(spec: &str) -> Result { let Some(body) = spec.strip_prefix("@every ") else { return Err(invalid_spec(spec)); }; @@ -251,7 +251,7 @@ fn fire_due(sched: &mut ScheduleState, queues: &mut QueueState, state: &State, n /// The bounded catch-up's skip-forward: land the next boundary strictly /// in the future, grid-aligned (the schedule row's advance makes the /// gap inspectable). -fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { +pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { let gap = (now - next_fire_at) as i128; let interval = interval as i128; let next = (next_fire_at as i128) + (gap / interval + 1) * interval; diff --git a/alkstore-postgres/src/fuzz_surface_tests.rs b/alkstore-postgres/src/fuzz_surface_tests.rs new file mode 100644 index 0000000..438ebe7 --- /dev/null +++ b/alkstore-postgres/src/fuzz_surface_tests.rs @@ -0,0 +1,5 @@ +#[test] +fn doc_hidden_reexports_reach_the_crate_root() { + assert_eq!(crate::parse_every_interval("@every 5s").unwrap(), 5); + assert_eq!(crate::skip_forward(0, 20, 5), 25); +} diff --git a/alkstore-postgres/src/lib.rs b/alkstore-postgres/src/lib.rs index 4166395..4048b89 100644 --- a/alkstore-postgres/src/lib.rs +++ b/alkstore-postgres/src/lib.rs @@ -48,6 +48,9 @@ mod store; mod stream; mod tx; +#[cfg(test)] +mod fuzz_surface_tests; + pub use opts::{DEFAULT_MAX_SIZE, PgOpts}; pub use schema::{ BootstrapError, BootstrapResult, DEFAULT_SCHEMA, QualifiedTable, bootstrap, quote_identifier, @@ -55,3 +58,14 @@ pub use schema::{ }; pub use store::{PgStore, open}; pub use tx::PgTxHandle; + +/// Fuzz/test-only surface: the pg engine's `@every` parser — not +/// contract surface, not consumer API, no stability guarantee. +#[doc(hidden)] +pub use scheduler::parse_every_interval; + +/// Fuzz/test-only surface: the pg engine's schedule skip-forward +/// arithmetic — not contract surface, not consumer API, no stability +/// guarantee. +#[doc(hidden)] +pub use scheduler::skip_forward; diff --git a/alkstore-postgres/src/scheduler.rs b/alkstore-postgres/src/scheduler.rs index 153d36d..382375f 100644 --- a/alkstore-postgres/src/scheduler.rs +++ b/alkstore-postgres/src/scheduler.rs @@ -221,7 +221,7 @@ fn invalid_spec(spec: &str) -> Error { /// with `n` a positive integer and unit in `s|m|h|d`. Everything else /// (cron strings, bare `every 5s`, floats, unknown units) rejects. /// Returns the interval in seconds. -pub(crate) fn parse_every_interval(spec: &str) -> Result { +pub fn parse_every_interval(spec: &str) -> Result { let reject = || invalid_spec(spec); let body = spec.strip_prefix("@every ").ok_or_else(reject)?; if body.is_empty() { @@ -261,7 +261,7 @@ fn next_every_boundary(spec: &str, after: i64) -> Result { /// `next + (gap / interval + 1) * interval` — strictly beyond `now` /// (the skipped boundaries never enqueue). 128-bit intermediates keep /// the arithmetic saturation-safe; the result clamps at `i64::MAX`. -fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { +pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { let gap = (now - next_fire_at) as i128; let interval = interval.max(1) as i128; let next = (next_fire_at as i128) + (gap / interval + 1) * interval; diff --git a/alkstore-sqlite/src/fuzz_surface_tests.rs b/alkstore-sqlite/src/fuzz_surface_tests.rs new file mode 100644 index 0000000..438ebe7 --- /dev/null +++ b/alkstore-sqlite/src/fuzz_surface_tests.rs @@ -0,0 +1,5 @@ +#[test] +fn doc_hidden_reexports_reach_the_crate_root() { + assert_eq!(crate::parse_every_interval("@every 5s").unwrap(), 5); + assert_eq!(crate::skip_forward(0, 20, 5), 25); +} diff --git a/alkstore-sqlite/src/lib.rs b/alkstore-sqlite/src/lib.rs index f288ef4..2d26cd4 100644 --- a/alkstore-sqlite/src/lib.rs +++ b/alkstore-sqlite/src/lib.rs @@ -36,6 +36,20 @@ mod stream; mod substrate; mod tx; +#[cfg(test)] +mod fuzz_surface_tests; + pub use opts::{DEFAULT_MAX_READERS, SqliteOpts}; pub use store::{SqliteStore, open}; pub use tx::SqliteTxHandle; + +/// Fuzz/test-only surface: the substrate's `@every` parser — not +/// contract surface, not consumer API, no stability guarantee. +#[doc(hidden)] +pub use substrate::parse_every_interval; + +/// Fuzz/test-only surface: the substrate's schedule skip-forward +/// arithmetic — not contract surface, not consumer API, no stability +/// guarantee. +#[doc(hidden)] +pub use substrate::skip_forward; diff --git a/alkstore-sqlite/src/substrate/PROVENANCE.md b/alkstore-sqlite/src/substrate/PROVENANCE.md index 4d76f34..c1db409 100644 --- a/alkstore-sqlite/src/substrate/PROVENANCE.md +++ b/alkstore-sqlite/src/substrate/PROVENANCE.md @@ -129,6 +129,7 @@ Cherry-picks: empty at scaffold, append-only forever. | D-34 | drop | `queue_next_claim_at` cut (the queue's earliest-wake deadline query): upstream exposed it as a `honker_queue_next_claim_at` scalar registration for external poll-loop drivers and its leader loop used it to compute the sleep horizon; this fork's leader loop computes the boundary out of `scheduler_tick` + `scheduler_soonest` inside the tick transaction (the engine's scheduler.rs) and exposes no substrate-side next-wake function — the v1 scheduler surface has no consumer-inventory row for one | `queue_ops.rs` (function + its substrate test `queue_next_claim_at_reports_the_earliest_wake`) | ADR-009 (the v1 scheduler collapse: register/tick/soonest/unregister only — D-21's surface rule; no wake-time query on the v1 surface); ADR-012 §2 (the substrate exposes what the engine calls, and the engine never calls this); wave-3 lint-removal obligation — cut, not suppressed | ours | | D-35 | drop | `Writer::try_acquire` cut (the non-blocking writer-slot try): upstream's embedding harness used it for try-shaped entry points its surface exposed; this engine's seam always blocks (`acquire`) — the writer-slot lease is a *parks-the-writer* lease by design (ADR-007's honest model), so a try-shaped acquire would contradict the posture rather than extend it. The probe tests driving it re-express their properties through `acquire`/`release`/`close` (the engine-reachable surface); the engine-level lease tests (`tx_tests`' lease/parking suite) pin the observable posture end-to-end | `schema.rs` (`Writer::try_acquire` + the three `writer_reader_tests` call sites) | ADR-007 (the caller-held-lease seam: `begin_tx` parks a concurrent writer — no try-shaped begin exists on the contract surface); ADR-012 §2 (nothing on the engine side reaches a try-acquire); wave-3 lint-removal obligation — cut, not suppressed | ours | | D-36 | drop | `UpdateWatcher::spawn` and `SharedUpdateWatcher::new` cut (the default-cadence convenience constructors): upstream's embedding harnesses opened watchers without explicit config; this engine's single open path always constructs a `WatcherConfig` — from `SqliteOpts` (ADR-023 §4's `poll_interval` knob, `None` = the 1 ms default) — and passes it through `*_with_config`; a config-less twin would re-create two open postures (the mixed-flag inconsistency D-29 resolved at the URI layer). `WatcherConfig::default()` remains the default's single owner; the tests driving the cut constructors now build the config explicitly, pinning the same behavior | `watcher.rs` (both constructors + five watcher-test call sites) | ADR-023 §4 (the cadence knob rides the engine's opts — one open posture carries config); ADR-012 §2 (one construction path the engine reviews); wave-3 lint-removal obligation — cut, not suppressed | ours | +| D-37 | re-derivation | visibility-only widening on the re-derived `@every` surface: `parse_every_interval` (`pub(crate)` → `pub`) and `skip_forward` (module-private → `pub`) in `queue_ops.rs`, plus this subtree's facade re-export (`mod.rs`: `pub use queue_ops::{parse_every_interval, skip_forward}`), so the engine crate root can `#[doc(hidden)] pub use` both as the wave-7 fuzzing differential target's drive surface (`fuzz-parser-exposure`, target `every_parse`). Zero logic change, zero signature change, zero name change; `mod substrate` stays private, so containment holds — nothing leaves the crate except the doc-hidden root re-exports. A `pub use` re-export mechanically requires item-level `pub` (E0364/E0603), which is why the facade line is in `mod.rs`; the module doc's "nothing re-exported" boundary sentence was amended in the same commit to name this sole exception | `queue_ops.rs` (two visibility keywords); `mod.rs` (facade re-export line + boundary-doc exception sentence) | ADR-012 §3 (fidelity governs the ported honker body — `watcher.rs`/`schema.rs`/`ops.rs` untouched; `queue_ops.rs` is the re-derived half, owned contract-v1 code, so a visibility-only delta is legitimate); docs/research/fuzzing.md §3.2 (option (b): the `#[doc(hidden)]` narrow exposure); ADR-017 §3 (doc-hidden test surface, no stability guarantee — the `Job::from_row` precedent) | ours | ### Cherry-picks diff --git a/alkstore-sqlite/src/substrate/mod.rs b/alkstore-sqlite/src/substrate/mod.rs index f4dbb8b..cb8226c 100644 --- a/alkstore-sqlite/src/substrate/mod.rs +++ b/alkstore-sqlite/src/substrate/mod.rs @@ -16,9 +16,12 @@ //! returns primitives; the engine layer above resolves contract //! semantics into primitive arguments and maps substrate outcomes into //! the contract taxonomy. Nothing here is re-exported through the -//! engine crate's public API. The fork point, every deliberate -//! divergence, and any future cherry-pick are recorded in the -//! provenance register (`PROVENANCE.md`, this directory; ADR-018). +//! engine crate's public API — the sole exception (register D-37): the +//! re-derived half's `@every` parser pair (`parse_every_interval`, +//! `skip_forward`) is re-exported to the engine crate root as a +//! `#[doc(hidden)]` fuzz/test-only surface. The fork point, every +//! deliberate divergence, and any future cherry-pick are recorded in +//! the provenance register (`PROVENANCE.md`, this directory; ADR-018). //! //! Fidelity posture (ADR-012 §3): where scope *keeps* upstream //! machinery, upstream's module structure and internal function names @@ -59,9 +62,13 @@ pub(crate) use ops::{ }; pub(crate) use queue_ops::{ FireOpts, Stamps, ack, ack_batch, cancel, claim_batch, enqueue, fail, get_job, heartbeat, - parse_every_interval, retry, scheduler_register, scheduler_soonest, scheduler_tick, - scheduler_unregister, sweep_expired, + retry, scheduler_register, scheduler_soonest, scheduler_tick, scheduler_unregister, + sweep_expired, }; +/// The doc-hidden fuzz/test re-exports' substrate-side facade (the +/// engine's `lib.rs` republishes these under `#[doc(hidden)]`; register +/// D-37). +pub use queue_ops::{parse_every_interval, skip_forward}; pub(crate) use schema::Error as SchemaError; pub(crate) use schema::open_conn_bootstrapped; pub(crate) use schema::{Readers, Writer}; diff --git a/alkstore-sqlite/src/substrate/queue_ops.rs b/alkstore-sqlite/src/substrate/queue_ops.rs index e249784..687c733 100644 --- a/alkstore-sqlite/src/substrate/queue_ops.rs +++ b/alkstore-sqlite/src/substrate/queue_ops.rs @@ -487,7 +487,7 @@ fn now_unix(conn: &Connection) -> rusqlite::Result { conn.query_row("SELECT unixepoch()", [], |r| r.get(0)) } -pub(crate) fn parse_every_interval(spec: &str) -> Result { +pub fn parse_every_interval(spec: &str) -> Result { let body = spec .strip_prefix("@every ") .ok_or_else(|| format!("unsupported spec {spec:?}; @every only"))?; @@ -524,7 +524,7 @@ fn next_every_boundary(spec: &str, after: i64) -> Result { .ok_or_else(|| format!("next boundary overflows for {spec:?}")) } -fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { +pub fn skip_forward(next_fire_at: i64, now: i64, interval: i64) -> i64 { let gap = (now - next_fire_at) as i128; let interval = interval as i128; let next = (next_fire_at as i128) + (gap / interval + 1) * interval; diff --git a/tasks/fuzz-parser-exposure.md b/tasks/fuzz-parser-exposure.md index 5954732..83abe2d 100644 --- a/tasks/fuzz-parser-exposure.md +++ b/tasks/fuzz-parser-exposure.md @@ -1,7 +1,7 @@ --- id: fuzz-parser-exposure name: Parser exposure — `#[doc(hidden)]` narrow surface for the three `@every` parsers + boundary arithmetic -status: pending +status: completed depends_on: [] scope: narrow risk: low @@ -73,23 +73,26 @@ differential runs on the exposed functions. ## Acceptance Criteria -- [ ] All three engines re-export their `parse_every_interval` and +- [x] All three engines re-export their `parse_every_interval` and `skip_forward` from the crate root, `#[doc(hidden)]`, with the fuzz/test-only doc note -- [ ] `alkstore-sqlite/src/substrate/**` diff is **visibility-only**: +- [x] `alkstore-sqlite/src/substrate/**` diff is **visibility-only**: exactly the two keywords in `queue_ops.rs` (`parse_every_interval` → `pub`, `skip_forward` → `pub`); the lineage body - (`watcher.rs`, `connection.rs`, `ops.rs`, `schema.rs`, `mod.rs`) - untouched; a PROVENANCE.md delta entry records the change -- [ ] pg/mem `scheduler.rs` diffs are visibility-only (two keywords + (`watcher.rs`, `connection.rs`, `ops.rs`, `schema.rs`) untouched; + a PROVENANCE.md delta entry records the change + *(amended at implementation, 2026-10-10 — `mod.rs` was in this + list, but a second mechanical constraint forced one facade line + there: see Notes. No lineage-body file was touched.)* +- [x] pg/mem `scheduler.rs` diffs are visibility-only (two keywords each: parser → `pub`, `skip_forward` → `pub`) plus nothing else -- [ ] The re-exports are reachable from an external crate (pinned by a +- [x] The re-exports are reachable from an external crate (pinned by a compile-level test or the `fuzz-target-every-parse` build itself; record which) -- [ ] No behavior change: workspace gates green (`cargo build`, +- [x] No behavior change: workspace gates green (`cargo build`, `cargo test`, clippy `-D warnings`, fmt); the engines' own scheduler/parser test suites untouched and green -- [ ] `cargo check --target wasm32-unknown-unknown -p alkstore-mem` +- [x] `cargo check --target wasm32-unknown-unknown -p alkstore-mem` still clean (visibility widening is wasm-neutral, pinned anyway) ## References @@ -112,8 +115,61 @@ differential runs on the exposed functions. ## Notes -> To be filled by implementation agent +Decisions of record the implementation made that the description didn't +pin: + +- **Second mechanical amendment — `mod.rs` could not stay untouched.** + Widening `queue_ops.rs`'s two functions to `pub` is necessary but not + sufficient: `lib.rs` cannot name the path `substrate::queue_ops::…` + at all — `mod substrate` is private *to `substrate`'s subtree* for + `queue_ops` purposes, so a through-the-path re-export fails E0603, + and `skip_forward` had **no `mod.rs` export** whatsoever (it was not + in the `pub(crate) use` list). The only route is the facade: `mod.rs` + gained `pub use queue_ops::{parse_every_interval, skip_forward};` + (one line; `parse_every_interval` moved out of the `pub(crate)` list + into it — a duplicate binding would be E0252), compile-probed clean. + This remains visibility-only — zero logic, zero signatures, zero + names; lineage body (`watcher.rs`/`connection.rs`/`ops.rs`/ + `schema.rs`) untouched; containment holds (`mod substrate` private). +- **`mod.rs`'s boundary-doc sentence amended.** "Nothing here is + re-exported through the engine crate's public API" would have become + stale the moment the facade landed; the sentence now names this sole + exception inline, and the full story is registered as **PROVENANCE.md + D-37** (category `re-derivation`, per this task's amendment). +- **Reachability pin (criterion 4): compile-level test.** Each engine + gained `src/fuzz_surface_tests.rs` (`#[cfg(test)]`, declared in + `lib.rs`) — `doc_hidden_reexports_reach_the_crate_root` drives both + re-exports through the crate-root path (one accept case + one + `skip_forward` assertion each). Compile of the `pub use` itself is + the E0364-class pin; the true external-crate consumption arrives with + `fuzz-target-every-parse`'s build (its `shared` crate path-depends on + all three engines). Neither engine's existing suite files were + touched — the pin is a separate module. +- **Semver note (recorded per the description):** doc-hidden `pub` + items are still public API — removal is technically breaking; the + same commitment core's four `#[doc(hidden)]` constructors made, + bounded to a fuzz/test purpose. ## Summary -> To be filled on completion \ No newline at end of file +Landed (2026-10-10): the `#[doc(hidden)]` narrow fuzz/test surface for +the three engines — `parse_every_interval` + `skip_forward` re-exported +from each crate root with the one-line fuzz/test-only doc note +(`Job::from_row` doc-posture style). + +- **alkstore-sqlite**: `substrate/queue_ops.rs` two keywords widened to + `pub`; `substrate/mod.rs` gained the facade `pub use` line + the + boundary-doc exception sentence; `substrate/PROVENANCE.md` D-37 + registered; `lib.rs` `#[doc(hidden)] pub use substrate::…` ×2. +- **alkstore-postgres / alkstore-mem**: `scheduler.rs` two keywords + each widened to `pub`; `lib.rs` `#[doc(hidden)] pub use scheduler::…` + ×2. No provenance concern (engine-owned files). +- **Reachability pins**: `src/fuzz_surface_tests.rs` ×3 — one test per + engine driving both crate-root re-exports. + +Verified: `cargo build` clean; `cargo test` green (all suites; the +three new pins pass; no engine suite touched); `cargo clippy +--all-targets -- -D warnings` clean; `cargo fmt --check` clean; +`cargo check --target wasm32-unknown-unknown -p alkstore-mem` clean. +Diff audited against the (twice-amended) criteria: engine +`scheduler.rs`/`queue_ops.rs` diffs are visibility keywords only. \ No newline at end of file