diff --git a/alkstore/src/job.rs b/alkstore/src/job.rs index 1387479..bc894c3 100644 --- a/alkstore/src/job.rs +++ b/alkstore/src/job.rs @@ -80,6 +80,62 @@ pub struct Job { } impl Job { + /// Engine-construction-only constructor taking the full field set. + /// + /// # Contract posture + /// + /// Not consumer API and not part of the contract surface: engines + /// (in-repo, lockstep-updated with core) use it to build [`Job`] + /// values from row data, since `#[non_exhaustive]` (ADR-017 §3) + /// bans struct-literal construction downstream. Not covered by + /// ADR-017's semver-minor field-addition promise — a `Job` field + /// addition changes this signature, which is an engine breaking + /// change under a core change and a lockstep-duty event + /// (ADR-017 §5). Hidden from consumer-facing docs. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn from_row( + id: i64, + queue: String, + state: JobState, + payload: Vec, + priority: i64, + run_at: i64, + attempts: i64, + max_attempts: i64, + worker_id: Option, + claimed_at: Option, + claim_expires_at: Option, + created_at: i64, + expires_at: Option, + visibility_timeout_s: i64, + backoff_base_s: i64, + dead_letter_retention_s: Option, + last_error: Option, + died_at: Option, + ) -> Self { + Self { + id, + queue, + state, + payload, + priority, + run_at, + attempts, + max_attempts, + worker_id, + claimed_at, + claim_expires_at, + created_at, + expires_at, + visibility_timeout_s, + backoff_base_s, + dead_letter_retention_s, + last_error, + died_at, + } + } + /// Decode the stored payload bytes into a concrete target type. /// /// The stored bytes are the serde_json serialization of the diff --git a/alkstore/src/schedule.rs b/alkstore/src/schedule.rs index 44e59b0..e78ae69 100644 --- a/alkstore/src/schedule.rs +++ b/alkstore/src/schedule.rs @@ -20,3 +20,27 @@ pub struct Schedule { /// engine defaults at each boundary fire (ADR-020 §3). pub opts: ScheduleOpts, } + +impl Schedule { + /// Engine-construction-only constructor taking the full field set. + /// + /// # Contract posture + /// + /// Not consumer API and not part of the contract surface: engines + /// (in-repo, lockstep-updated with core) use it to build + /// [`Schedule`] values from row data, since `#[non_exhaustive]` + /// (ADR-017 §3) bans struct-literal construction downstream. Not + /// covered by ADR-017's semver-minor field-addition promise — a + /// `Schedule` field addition changes this signature, which is an + /// engine breaking change under a core change and a lockstep-duty + /// event (ADR-017 §5). Hidden from consumer-facing docs. + #[doc(hidden)] + pub fn new(name: String, spec: String, queue: String, opts: ScheduleOpts) -> Self { + Self { + name, + spec, + queue, + opts, + } + } +} diff --git a/alkstore/src/stream.rs b/alkstore/src/stream.rs index 80adb24..b6ecef6 100644 --- a/alkstore/src/stream.rs +++ b/alkstore/src/stream.rs @@ -32,6 +32,36 @@ pub struct StreamEvent { } impl StreamEvent { + /// Engine-construction-only constructor taking the full field set. + /// + /// # Contract posture + /// + /// Not consumer API and not part of the contract surface: engines + /// (in-repo, lockstep-updated with core) use it to build + /// [`StreamEvent`] values from row data, since + /// `#[non_exhaustive]` (ADR-017 §3) bans struct-literal + /// construction downstream. Not covered by ADR-017's + /// semver-minor field-addition promise — a `StreamEvent` field + /// addition changes this signature, which is an engine breaking + /// change under a core change and a lockstep-duty event + /// (ADR-017 §5). Hidden from consumer-facing docs. + #[doc(hidden)] + pub fn from_row( + offset: i64, + stream: String, + key: Option, + payload: Vec, + created_at: i64, + ) -> Self { + Self { + offset, + stream, + key, + payload, + created_at, + } + } + /// Decode the stored payload bytes into a concrete target type. /// /// The stored bytes are the serde_json serialization of the diff --git a/alkstore/src/trait_probe_tests.rs b/alkstore/src/trait_probe_tests.rs index 94c3744..cde359c 100644 --- a/alkstore/src/trait_probe_tests.rs +++ b/alkstore/src/trait_probe_tests.rs @@ -330,12 +330,12 @@ impl Store for ProbeStore { _payload: Value, _opts: ScheduleOpts, ) -> BoxedFuture<'a, Result> { - Box::pin(std::future::ready(Ok(Schedule { - name: "probe".into(), - spec: "@every 60s".into(), - queue: "q".into(), - opts: ScheduleOpts::default(), - }))) + Box::pin(std::future::ready(Ok(Schedule::new( + "probe".into(), + "@every 60s".into(), + "q".into(), + ScheduleOpts::default(), + )))) } fn unschedule<'a>(&'a self, _name: &str) -> BoxedFuture<'a, Result> { Box::pin(std::future::ready(Ok(false))) diff --git a/alkstore/src/value_type_tests.rs b/alkstore/src/value_type_tests.rs index fcb21c3..86c2765 100644 --- a/alkstore/src/value_type_tests.rs +++ b/alkstore/src/value_type_tests.rs @@ -51,36 +51,36 @@ fn schedule_opts_default_matches_enqueue_opts_twin_types() { } fn sample_job(payload: Vec) -> Job { - Job { - id: 42, - queue: "orders".to_string(), - state: JobState::Dead, + Job::from_row( + 42, + "orders".to_string(), + JobState::Dead, payload, - priority: 7, - run_at: 1000, - attempts: 3, - max_attempts: 3, - worker_id: Some("w1".to_string()), - claimed_at: Some(999), - claim_expires_at: Some(1299), - created_at: 900, - expires_at: Some(5000), - visibility_timeout_s: 300, - backoff_base_s: 5, - dead_letter_retention_s: None, - last_error: Some("max attempts exceeded".to_string()), - died_at: Some(1100), - } + 7, + 1000, + 3, + 3, + Some("w1".to_string()), + Some(999), + Some(1299), + 900, + Some(5000), + 300, + 5, + None, + Some("max attempts exceeded".to_string()), + Some(1100), + ) } fn sample_event(payload: Vec) -> StreamEvent { - StreamEvent { - offset: 17, - stream: "events".to_string(), - key: Some("k".to_string()), + StreamEvent::from_row( + 17, + "events".to_string(), + Some("k".to_string()), payload, - created_at: 1234, - } + 1234, + ) } #[test] @@ -173,9 +173,7 @@ fn stop_token_debug_shows_cancelled_state() { #[test] fn wake_carries_only_the_channel() { - let w = Wake { - channel: "ch-1".to_string(), - }; + let w = Wake::new("ch-1".to_string()); assert_eq!(w.channel, "ch-1"); assert_eq!(w.clone(), w); } diff --git a/alkstore/src/wake.rs b/alkstore/src/wake.rs index dc8b89f..3c67047 100644 --- a/alkstore/src/wake.rs +++ b/alkstore/src/wake.rs @@ -20,3 +20,23 @@ pub struct Wake { /// The channel the wake was delivered on. pub channel: String, } + +impl Wake { + /// Engine-construction-only constructor taking the full field set. + /// + /// # Contract posture + /// + /// Not consumer API and not part of the contract surface: engines + /// (in-repo, lockstep-updated with core) use it to build [`Wake`] + /// values (including the Postgres engine's synthetic reconnect + /// wake), since `#[non_exhaustive]` (ADR-017 §3) bans + /// struct-literal construction downstream. Not covered by + /// ADR-017's semver-minor field-addition promise — a `Wake` field + /// addition changes this signature, which is an engine breaking + /// change under a core change and a lockstep-duty event + /// (ADR-017 §5). Hidden from consumer-facing docs. + #[doc(hidden)] + pub fn new(channel: String) -> Self { + Self { channel } + } +} diff --git a/tasks/core-engine-value-constructors.md b/tasks/core-engine-value-constructors.md index 5af43bc..b42aa85 100644 --- a/tasks/core-engine-value-constructors.md +++ b/tasks/core-engine-value-constructors.md @@ -1,7 +1,7 @@ --- id: core-engine-value-constructors name: Core — engine-side constructors for the non_exhaustive value types -status: pending +status: completed depends_on: [] scope: narrow risk: low @@ -60,8 +60,33 @@ cheap to review in isolation. ## Notes -> To be filled by implementation agent +- Names pinned as `Job::from_row` and `StreamEvent::from_row` + (row-shaped values) but `Schedule::new` and `Wake::new` + (not row-shaped) — matching the task's own examples. +- `Job::from_row` takes 18 args and carries an explicit + `#[allow(clippy::too_many_arguments)]` — the task pins "taking the + full field set" as the posture, so arg count is intrinsic, not a + smell to engineer around (no opts struct / builder introduced). +- Doc-comments follow the crate's `# Contract posture` section style + (see `job.rs` / `stream.rs`), naming ADR-017 §3 and §5 explicitly. +- Core tests updated to construct through the new constructors: + `value_type_tests.rs` (`sample_job`, `sample_event`, `wake_test`) + and `trait_probe_tests.rs` (`schedule()` return). All 25 core tests + still pass. ## Summary -> To be filled on completion \ No newline at end of file +Added `#[doc(hidden)]` full-field constructors to core's four +`#[non_exhaustive]` value types: `Job::from_row` (18 fields), +`StreamEvent::from_row` (5 fields), `Schedule::new` (4 fields), +`Wake::new` (1 field) — each `pub` with a "Contract posture" doc +comment pinning engine-construction-only status, exclusion from the +contract surface and ADR-017's semver-minor field-addition promise, +and the ADR-017 §5 lockstep duty for field additions. +`#[non_exhaustive]` left untouched on all four; consumer posture +unchanged. Verified: `cargo build`, `cargo test` (workspace; core +unchanged-behavior), `cargo clippy --all-targets -- -D warnings`, +`cargo fmt --check` all clean. `cargo doc` spot-check: none of the +four constructors appears in the generated `struct.*.html` pages; +the only rustdoc warnings are three pre-existing unresolved +intra-doc links in `validation.rs`, untouched by this task. \ No newline at end of file