diff --git a/Cargo.toml b/Cargo.toml index 6807a64..2720751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 476 +# version: 477 [workspace] resolver = "3" -members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] +members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"] [workspace.package] -version = "0.3.9-pre.1.fix.2" +version = "0.3.9-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-worker-api/Cargo.toml b/crates/ksp-worker-api/Cargo.toml new file mode 100644 index 0000000..e145f04 --- /dev/null +++ b/crates/ksp-worker-api/Cargo.toml @@ -0,0 +1,14 @@ +# file: crates/ksp-worker-api/Cargo.toml +# version: 1 + +[package] +name = "ksp-worker-api" +version.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +ksp-core-lib = { path = "../ksp-core-lib" } + +[lints] +workspace = true diff --git a/crates/ksp-worker-api/src/error.rs b/crates/ksp-worker-api/src/error.rs new file mode 100644 index 0000000..b987a18 --- /dev/null +++ b/crates/ksp-worker-api/src/error.rs @@ -0,0 +1,12 @@ +// file: crates/ksp-worker-api/src/error.rs +// version: 1 + +/// Error code used when a Worker identifier violates its bounded safe-code contract. +pub const ERROR_CODE_WORKER_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_id_invalid"); +/// Error code used when a Worker kind code violates its bounded safe-code contract. +pub const ERROR_CODE_WORKER_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_kind_invalid"); +/// Error code used when a Worker snapshot sequence cannot advance without wrapping. +pub const ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED: ksp_core_lib::ErrorCode = + ksp_core_lib::ErrorCode::new("worker_api", "worker_snapshot_sequence_exhausted"); +/// Error code used when a requested Worker lifecycle transition is not allowed. +pub const ERROR_CODE_WORKER_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_transition_invalid"); diff --git a/crates/ksp-worker-api/src/identity.rs b/crates/ksp-worker-api/src/identity.rs new file mode 100644 index 0000000..a6f19ea --- /dev/null +++ b/crates/ksp-worker-api/src/identity.rs @@ -0,0 +1,76 @@ +// file: crates/ksp-worker-api/src/identity.rs +// version: 1 + +/// Maximum UTF-8 byte length admitted for one Worker identifier. +pub const MAX_WORKER_ID_BYTES: usize = 128; +/// Maximum UTF-8 byte length admitted for one Worker kind code. +pub const MAX_WORKER_KIND_CODE_BYTES: usize = 128; + +/// Bounded caller-supplied identity of one logical Worker instance. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WorkerId(std::string::String); + +impl WorkerId { + /// Creates one non-empty Worker identifier using the KSP safe-code alphabet. + pub fn new(value: impl std::convert::Into) -> crate::Result { + let value = value.into(); + if !valid_worker_code(value.as_str(), crate::MAX_WORKER_ID_BYTES) { + return std::result::Result::Err(identity_error(crate::ERROR_CODE_WORKER_ID_INVALID, "worker_id")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the validated Worker identifier. + #[must_use] + pub fn as_str(&self) -> &str { + return self.0.as_str(); + } +} + +impl std::fmt::Debug for WorkerId { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("WorkerId(..)"); + } +} + +/// Bounded stable code identifying one concrete family of Workers. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WorkerKindCode(std::string::String); + +impl WorkerKindCode { + /// Creates one non-empty Worker kind using the KSP safe-code alphabet. + pub fn new(value: impl std::convert::Into) -> crate::Result { + let value = value.into(); + if !valid_worker_code(value.as_str(), crate::MAX_WORKER_KIND_CODE_BYTES) { + return std::result::Result::Err(identity_error(crate::ERROR_CODE_WORKER_KIND_INVALID, "worker_kind")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the validated stable Worker kind code. + #[must_use] + pub fn as_str(&self) -> &str { + return self.0.as_str(); + } +} + +impl std::fmt::Debug for WorkerKindCode { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_tuple("WorkerKindCode").field(&self.0).finish(); + } +} + +fn identity_error(code: crate::ErrorCode, field: &'static str) -> crate::Error { + return crate::Error::new(code, "invalid bounded Worker identity").with_context("field", field); +} + +fn valid_worker_code(value: &str, maximum_len: usize) -> bool { + if value.is_empty() || value.len() > maximum_len { + return false; + } + return value.bytes().all(|byte| return byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':')); +} + +#[cfg(test)] +#[path = "../unit_tests/identity.rs"] +mod tests; diff --git a/crates/ksp-worker-api/src/lib.rs b/crates/ksp-worker-api/src/lib.rs new file mode 100644 index 0000000..a869f01 --- /dev/null +++ b/crates/ksp-worker-api/src/lib.rs @@ -0,0 +1,62 @@ +// file: crates/ksp-worker-api/src/lib.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Passive runtime-neutral lifecycle contracts for continuous KSP Workers. +//! +//! This foundation owns bounded Worker identity, explicit continuous lifecycle +//! transitions, cooperative stop intent and a fixed latest-value observation +//! contract. Concrete runtimes, restart policy, Transport, Store, Config, Jobs +//! and domain-specific Worker behavior remain outside this crate. + +mod error; +mod identity; +mod lifecycle; +mod snapshot; +mod stop; + +/// Error code used when a Worker identifier violates its bounded safe-code contract. +pub use self::error::ERROR_CODE_WORKER_ID_INVALID; +/// Error code used when a Worker kind code violates its bounded safe-code contract. +pub use self::error::ERROR_CODE_WORKER_KIND_INVALID; +/// Error code used when a Worker snapshot sequence cannot advance without wrapping. +pub use self::error::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED; +/// Error code used when a requested Worker lifecycle transition is not allowed. +pub use self::error::ERROR_CODE_WORKER_TRANSITION_INVALID; +/// Maximum UTF-8 byte length admitted for one Worker identifier. +pub use self::identity::MAX_WORKER_ID_BYTES; +/// Maximum UTF-8 byte length admitted for one Worker kind code. +pub use self::identity::MAX_WORKER_KIND_CODE_BYTES; +/// Bounded caller-supplied identity of one logical Worker instance. +pub use self::identity::WorkerId; +/// Bounded stable code identifying one concrete family of Workers. +pub use self::identity::WorkerKindCode; +/// Minimal generic activity classification for a continuous Worker. +pub use self::lifecycle::WorkerActivity; +/// Operational health classification independent from Worker lifecycle phase. +pub use self::lifecycle::WorkerHealth; +/// Passive owner of one Worker identity and its validated lifecycle state. +pub use self::lifecycle::WorkerLifecycle; +/// Current lifecycle state of one continuous Worker. +pub use self::lifecycle::WorkerState; +/// Fixed common latest-value snapshot exposed by every Worker implementation. +pub use self::snapshot::WorkerSnapshot; +/// Runtime-neutral future returned while observing a latest-value Worker snapshot source. +pub use self::snapshot::WorkerSnapshotFuture; +/// Monotone sequence attached to one latest-value Worker snapshot stream. +pub use self::snapshot::WorkerSnapshotSequence; +/// Runtime-neutral read and change-wait contract for one latest-value Worker snapshot stream. +pub use self::snapshot::WorkerSnapshotSource; +/// Runtime-neutral cloneable token carrying cooperative Worker stop intent. +pub use self::stop::WorkerStopToken; +/// Common KSP error type used by Worker-facing contracts. +pub use ksp_core_lib::Error; +/// Stable structured code identifying a KSP error category and condition. +pub use ksp_core_lib::ErrorCode; +/// Structured contextual field attached to a KSP error. +pub use ksp_core_lib::ErrorContext; +/// Common KSP result alias using [`Error`]. +pub use ksp_core_lib::Result; diff --git a/crates/ksp-worker-api/src/lifecycle.rs b/crates/ksp-worker-api/src/lifecycle.rs new file mode 100644 index 0000000..6ed85e6 --- /dev/null +++ b/crates/ksp-worker-api/src/lifecycle.rs @@ -0,0 +1,199 @@ +// file: crates/ksp-worker-api/src/lifecycle.rs +// version: 1 + +/// Current lifecycle state of one continuous Worker. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum WorkerState { + /// The Worker exists but has not started initialization. + Created, + /// The Worker is initializing resources before entering steady service. + Starting, + /// The Worker is actively providing its continuous service. + Running, + /// The Worker observed stop intent and is draining or releasing resources. + Stopping, + /// The Worker reached its normal terminal stopped state. + Stopped, + /// The Worker reached a terminal fault classified by one stable KSP error code. + Faulted(crate::ErrorCode), +} + +impl WorkerState { + /// Returns the stable safe lifecycle code without rendering Worker data. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Created => "created", + Self::Starting => "starting", + Self::Running => "running", + Self::Stopping => "stopping", + Self::Stopped => "stopped", + Self::Faulted(_) => "faulted", + }; + } + + /// Returns the terminal fault code when the state is [`Self::Faulted`]. + #[must_use] + pub const fn fault_code(&self) -> std::option::Option { + return match self { + Self::Faulted(code) => std::option::Option::Some(*code), + _ => std::option::Option::None, + }; + } + + /// Reports whether no later lifecycle transition is permitted. + #[must_use] + pub const fn is_terminal(&self) -> bool { + return matches!(self, Self::Stopped | Self::Faulted(_)); + } +} + +/// Operational health classification independent from Worker lifecycle phase. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum WorkerHealth { + /// Health has not yet been established or cannot currently be classified. + Unknown, + /// The Worker is operating within its expected healthy envelope. + Healthy, + /// The Worker is operating with a known degradation while service remains available. + Degraded, + /// The Worker is currently unable to satisfy its expected service health contract. + Unhealthy, +} + +impl WorkerHealth { + /// Returns the stable safe code for this health classification. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Unknown => "unknown", + Self::Healthy => "healthy", + Self::Degraded => "degraded", + Self::Unhealthy => "unhealthy", + }; + } +} + +/// Minimal generic activity classification for a continuous Worker. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum WorkerActivity { + /// Activity has not yet been established or cannot currently be classified. + Unknown, + /// The Worker is alive but not currently processing concrete work. + Idle, + /// The Worker is currently processing concrete work. + Active, +} + +impl WorkerActivity { + /// Returns the stable safe code for this activity classification. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Unknown => "unknown", + Self::Idle => "idle", + Self::Active => "active", + }; + } +} + +/// Passive owner of one Worker identity and its validated lifecycle state. +#[derive(Eq, PartialEq)] +pub struct WorkerLifecycle { + id: crate::WorkerId, + kind: crate::WorkerKindCode, + state: crate::WorkerState, +} + +impl WorkerLifecycle { + /// Creates one lifecycle in [`WorkerState::Created`] state. + #[must_use] + pub const fn new(id: crate::WorkerId, kind: crate::WorkerKindCode) -> Self { + return Self { id, kind, state: crate::WorkerState::Created }; + } + + /// Returns the logical Worker identity. + #[must_use] + pub const fn id(&self) -> &crate::WorkerId { + return &self.id; + } + + /// Returns the stable Worker family code. + #[must_use] + pub const fn kind(&self) -> &crate::WorkerKindCode { + return &self.kind; + } + + /// Returns the current lifecycle state. + #[must_use] + pub const fn state(&self) -> crate::WorkerState { + return self.state; + } + + /// Begins initialization of a newly created Worker. + pub fn start(&mut self) -> crate::Result<()> { + return self.transition(crate::WorkerState::Starting); + } + + /// Records that initialization completed and steady service is running. + pub fn mark_running(&mut self) -> crate::Result<()> { + return self.transition(crate::WorkerState::Running); + } + + /// Records that a starting or running Worker observed cooperative stop intent. + pub fn mark_stopping(&mut self) -> crate::Result<()> { + return self.transition(crate::WorkerState::Stopping); + } + + /// Records normal terminal stop after draining, or before initialization began. + pub fn mark_stopped(&mut self) -> crate::Result<()> { + return self.transition(crate::WorkerState::Stopped); + } + + /// Records a terminal fault using only a stable KSP error code. + pub fn fault(&mut self, code: crate::ErrorCode) -> crate::Result<()> { + return self.transition(crate::WorkerState::Faulted(code)); + } + + fn transition(&mut self, target: crate::WorkerState) -> crate::Result<()> { + if !allowed_transition(self.state, target) { + return std::result::Result::Err(transition_error(self.state, target)); + } + self.state = target; + return std::result::Result::Ok(()); + } +} + +impl std::fmt::Debug for WorkerLifecycle { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("WorkerLifecycle").field("id", &self.id).field("kind", &self.kind).field("state", &self.state).finish(); + } +} + +fn allowed_transition(source: crate::WorkerState, target: crate::WorkerState) -> bool { + return matches!( + (source, target), + (crate::WorkerState::Created, crate::WorkerState::Starting) + | (crate::WorkerState::Created, crate::WorkerState::Stopped) + | (crate::WorkerState::Starting, crate::WorkerState::Running) + | (crate::WorkerState::Starting, crate::WorkerState::Stopping) + | (crate::WorkerState::Starting, crate::WorkerState::Faulted(_)) + | (crate::WorkerState::Running, crate::WorkerState::Stopping) + | (crate::WorkerState::Running, crate::WorkerState::Faulted(_)) + | (crate::WorkerState::Stopping, crate::WorkerState::Stopped) + | (crate::WorkerState::Stopping, crate::WorkerState::Faulted(_)) + ); +} + +fn transition_error(source: crate::WorkerState, target: crate::WorkerState) -> crate::Error { + return crate::Error::new(crate::ERROR_CODE_WORKER_TRANSITION_INVALID, "invalid Worker lifecycle transition") + .with_context("source_state", source.code()) + .with_context("target_state", target.code()); +} + +#[cfg(test)] +#[path = "../unit_tests/lifecycle.rs"] +mod tests; diff --git a/crates/ksp-worker-api/src/snapshot.rs b/crates/ksp-worker-api/src/snapshot.rs new file mode 100644 index 0000000..d830212 --- /dev/null +++ b/crates/ksp-worker-api/src/snapshot.rs @@ -0,0 +1,138 @@ +// file: crates/ksp-worker-api/src/snapshot.rs +// version: 1 + +/// Monotone sequence attached to one latest-value Worker snapshot stream. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WorkerSnapshotSequence(u64); + +impl WorkerSnapshotSequence { + /// Creates the initial sequence position for one Worker snapshot stream. + #[must_use] + pub const fn initial() -> Self { + return Self(0); + } + + /// Returns the opaque numeric position carried by this sequence. + #[must_use] + pub const fn value(&self) -> u64 { + return self.0; + } + + /// Advances the sequence exactly once or reports exhaustion without wrapping. + pub fn next(&self) -> crate::Result { + let next = match self.0.checked_add(1) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + crate::Error::new(crate::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED, "Worker snapshot sequence exhausted") + .with_context("sequence", self.0.to_string()), + ); + }, + }; + return std::result::Result::Ok(Self(next)); + } + + /// Reports whether this sequence is strictly newer than an observed sequence. + #[must_use] + pub const fn is_after(&self, observed: Self) -> bool { + return self.0 > observed.0; + } +} + +/// Fixed common latest-value snapshot exposed by every Worker implementation. +#[derive(Clone, Eq, PartialEq)] +pub struct WorkerSnapshot { + id: crate::WorkerId, + kind: crate::WorkerKindCode, + sequence: crate::WorkerSnapshotSequence, + state: crate::WorkerState, + health: crate::WorkerHealth, + activity: crate::WorkerActivity, +} + +impl WorkerSnapshot { + /// Creates one immutable common Worker snapshot from already validated values. + #[must_use] + pub const fn new( + id: crate::WorkerId, + kind: crate::WorkerKindCode, + sequence: crate::WorkerSnapshotSequence, + state: crate::WorkerState, + health: crate::WorkerHealth, + activity: crate::WorkerActivity, + ) -> Self { + return Self { id, kind, sequence, state, health, activity }; + } + + /// Returns the logical Worker identity. + #[must_use] + pub const fn id(&self) -> &crate::WorkerId { + return &self.id; + } + + /// Returns the stable Worker family code. + #[must_use] + pub const fn kind(&self) -> &crate::WorkerKindCode { + return &self.kind; + } + + /// Returns the monotone sequence of this latest value. + #[must_use] + pub const fn sequence(&self) -> crate::WorkerSnapshotSequence { + return self.sequence; + } + + /// Returns the lifecycle state represented by this snapshot. + #[must_use] + pub const fn state(&self) -> crate::WorkerState { + return self.state; + } + + /// Returns the operational health represented by this snapshot. + #[must_use] + pub const fn health(&self) -> crate::WorkerHealth { + return self.health; + } + + /// Returns the generic activity represented by this snapshot. + #[must_use] + pub const fn activity(&self) -> crate::WorkerActivity { + return self.activity; + } +} + +impl std::fmt::Debug for WorkerSnapshot { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("WorkerSnapshot") + .field("id", &self.id) + .field("kind", &self.kind) + .field("sequence", &self.sequence) + .field("state", &self.state) + .field("health", &self.health) + .field("activity", &self.activity) + .finish(); + } +} + +/// Runtime-neutral future returned while observing a latest-value Worker snapshot source. +pub type WorkerSnapshotFuture<'a> = std::pin::Pin + std::marker::Send + 'a>>; + +/// Runtime-neutral read and change-wait contract for one latest-value Worker snapshot stream. +pub trait WorkerSnapshotSource: std::marker::Send + std::marker::Sync { + /// Returns the complete current common Worker snapshot without replaying prior updates. + #[must_use] + fn current(&self) -> crate::WorkerSnapshot; + + /// Waits for a snapshot newer than `observed`, returning the complete current value after coalescing intermediate updates. + fn wait_for_change(&self, observed: crate::WorkerSnapshotSequence) -> crate::WorkerSnapshotFuture<'_>; +} + +#[cfg(test)] +fn exhausted_snapshot_sequence() -> crate::WorkerSnapshotSequence { + return WorkerSnapshotSequence(u64::MAX); +} + +#[cfg(test)] +#[path = "../unit_tests/snapshot.rs"] +mod tests; diff --git a/crates/ksp-worker-api/src/stop.rs b/crates/ksp-worker-api/src/stop.rs new file mode 100644 index 0000000..5e55256 --- /dev/null +++ b/crates/ksp-worker-api/src/stop.rs @@ -0,0 +1,44 @@ +// file: crates/ksp-worker-api/src/stop.rs +// version: 1 + +/// Runtime-neutral cloneable token carrying cooperative Worker stop intent. +#[derive(Clone)] +pub struct WorkerStopToken { + requested: std::sync::Arc, +} + +impl WorkerStopToken { + /// Creates a token with no stop request. + #[must_use] + pub fn new() -> Self { + return Self { requested: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)) }; + } + + /// Requests stop and returns `true` only for the first request shared by all clones. + #[must_use] + pub fn request_stop(&self) -> bool { + return !self.requested.swap(true, std::sync::atomic::Ordering::AcqRel); + } + + /// Reports whether stop has been requested through any clone. + #[must_use] + pub fn is_stop_requested(&self) -> bool { + return self.requested.load(std::sync::atomic::Ordering::Acquire); + } +} + +impl std::default::Default for WorkerStopToken { + fn default() -> Self { + return Self::new(); + } +} + +impl std::fmt::Debug for WorkerStopToken { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("WorkerStopToken").field("stop_requested", &self.is_stop_requested()).finish(); + } +} + +#[cfg(test)] +#[path = "../unit_tests/stop.rs"] +mod tests; diff --git a/crates/ksp-worker-api/tests/dependency_boundary.rs b/crates/ksp-worker-api/tests/dependency_boundary.rs new file mode 100644 index 0000000..3fed900 --- /dev/null +++ b/crates/ksp-worker-api/tests/dependency_boundary.rs @@ -0,0 +1,82 @@ +// file: crates/ksp-worker-api/tests/dependency_boundary.rs +// version: 1 + +//! Dependency and runtime-neutrality canaries for the Worker API foundation. + +#[test] +fn pre_002_manifest_has_exact_core_only_dependency_graph() { + let manifest = include_str!("../Cargo.toml"); + assert!(!manifest.contains("[features]")); + assert!(!manifest.contains("[dev-dependencies]")); + assert!(!manifest.contains("[build-dependencies]")); + assert_eq!(manifest.matches("[dependencies]").count(), 1); + let dependencies_tail = manifest.split("[dependencies]").nth(1); + assert!(dependencies_tail.is_some()); + let dependencies_tail = match dependencies_tail { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let dependencies = match dependencies_tail.split("[lints]").next() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert_eq!(manifest_dependency_names(dependencies), std::vec!["ksp-core-lib"]); + return; +} + +#[test] +fn pre_002_production_sources_forbid_job_runtime_domain_and_wire_dependencies() { + let sources = [ + include_str!("../src/error.rs"), + include_str!("../src/identity.rs"), + include_str!("../src/lib.rs"), + include_str!("../src/lifecycle.rs"), + include_str!("../src/snapshot.rs"), + include_str!("../src/stop.rs"), + ]; + for source in sources { + for forbidden in [ + "ksp_config_lib::", + "ksp_interface_lib::", + "ksp_job_api::", + "ksp_logging_lib::", + "ksp_offchain_transport_lib::", + "ksp_onchain_transport_lib::", + "ksp_store_api::", + "ksp_store_lib::", + "reqwest::", + "serde::", + "serde_json::", + "solana_", + "tauri::", + "tokio::", + "tonic::", + concat!("tracing", "::"), + ] { + assert!(!source.contains(forbidden), "forbidden Worker API dependency path detected: {forbidden}"); + } + } + return; +} + +fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> { + let mut names = std::vec::Vec::new(); + for line in section.lines() { + let content = match line.split('#').next() { + std::option::Option::Some(value) => value.trim(), + std::option::Option::None => continue, + }; + if content.is_empty() { + continue; + } + let name = match content.split('=').next() { + std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"), + std::option::Option::None => continue, + }; + if !name.is_empty() { + names.push(name); + } + } + names.sort_unstable(); + return names; +} diff --git a/crates/ksp-worker-api/tests/public_api.rs b/crates/ksp-worker-api/tests/public_api.rs new file mode 100644 index 0000000..91e9d07 --- /dev/null +++ b/crates/ksp-worker-api/tests/public_api.rs @@ -0,0 +1,52 @@ +// file: crates/ksp-worker-api/tests/public_api.rs +// version: 1 + +//! External-consumer canaries for the Worker API crate-root facade. + +#[test] +fn pre_002_identity_lifecycle_stop_and_snapshot_are_consumable_from_crate_root() { + let id = ksp_worker_api::WorkerId::new("worker-public-001"); + let kind = ksp_worker_api::WorkerKindCode::new("example_worker"); + assert!(id.is_ok()); + assert!(kind.is_ok()); + let id = match id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let kind = match kind { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id.clone(), kind.clone()); + assert_eq!(lifecycle.state(), ksp_worker_api::WorkerState::Created); + assert!(lifecycle.start().is_ok()); + assert!(lifecycle.mark_running().is_ok()); + let stop = ksp_worker_api::WorkerStopToken::new(); + assert!(stop.request_stop()); + assert!(lifecycle.mark_stopping().is_ok()); + let snapshot = ksp_worker_api::WorkerSnapshot::new( + id, + kind, + ksp_worker_api::WorkerSnapshotSequence::initial(), + lifecycle.state(), + ksp_worker_api::WorkerHealth::Healthy, + ksp_worker_api::WorkerActivity::Idle, + ); + assert_eq!(snapshot.state(), ksp_worker_api::WorkerState::Stopping); + assert_eq!(snapshot.health(), ksp_worker_api::WorkerHealth::Healthy); + assert_eq!(snapshot.activity(), ksp_worker_api::WorkerActivity::Idle); + return; +} + +#[test] +fn pre_002_public_bounds_and_error_codes_are_exact() { + assert_eq!(ksp_worker_api::MAX_WORKER_ID_BYTES, 128); + assert_eq!(ksp_worker_api::MAX_WORKER_KIND_CODE_BYTES, 128); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID.domain(), "worker_api"); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID.code(), "worker_id_invalid"); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_KIND_INVALID.domain(), "worker_api"); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_KIND_INVALID.code(), "worker_kind_invalid"); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_TRANSITION_INVALID.code(), "worker_transition_invalid"); + assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED.code(), "worker_snapshot_sequence_exhausted"); + return; +} diff --git a/crates/ksp-worker-api/unit_tests/identity.rs b/crates/ksp-worker-api/unit_tests/identity.rs new file mode 100644 index 0000000..f92bd7d --- /dev/null +++ b/crates/ksp-worker-api/unit_tests/identity.rs @@ -0,0 +1,64 @@ +// file: crates/ksp-worker-api/unit_tests/identity.rs +// version: 1 + +#[test] +fn pre_002_identity_accepts_safe_codes_at_exact_bounds() { + let worker_id = crate::WorkerId::new("a".repeat(crate::MAX_WORKER_ID_BYTES)); + let kind = crate::WorkerKindCode::new("continuous.raw:example-1"); + assert!(worker_id.is_ok()); + assert!(kind.is_ok()); + let worker_id = match worker_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let kind = match kind { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(worker_id.as_str().len(), crate::MAX_WORKER_ID_BYTES); + assert_eq!(kind.as_str(), "continuous.raw:example-1"); + return; +} + +#[test] +fn pre_002_identity_rejects_empty_oversized_and_unsafe_values() { + for value in [ + std::string::String::new(), + "a".repeat(crate::MAX_WORKER_ID_BYTES + 1), + "unsafe/value".to_string(), + "unsafe\\value".to_string(), + "space value".to_string(), + "unicode-é".to_string(), + ] { + let rejected = crate::WorkerId::new(value); + assert!(rejected.is_err()); + let error = match rejected { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => continue, + }; + assert_eq!(error.code(), crate::ERROR_CODE_WORKER_ID_INVALID); + assert_eq!(error.context().len(), 1); + assert_eq!(error.context()[0].key(), "field"); + assert_eq!(error.context()[0].value(), "worker_id"); + } + for value in [std::string::String::new(), "b".repeat(crate::MAX_WORKER_KIND_CODE_BYTES + 1), "worker kind".to_string()] { + let rejected = crate::WorkerKindCode::new(value); + assert!(rejected.is_err()); + let error = match rejected { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => continue, + }; + assert_eq!(error.code(), crate::ERROR_CODE_WORKER_KIND_INVALID); + assert_eq!(error.context()[0].value(), "worker_kind"); + } + return; +} + +#[test] +fn pre_002_private_validator_uses_exact_safe_code_alphabet() { + assert!(super::valid_worker_code("AZaz09_.:-", 10)); + for rejected in ["slash/value", "back\\slash", "space value", "line\nbreak", "é"] { + assert!(!super::valid_worker_code(rejected, crate::MAX_WORKER_ID_BYTES)); + } + return; +} diff --git a/crates/ksp-worker-api/unit_tests/lifecycle.rs b/crates/ksp-worker-api/unit_tests/lifecycle.rs new file mode 100644 index 0000000..666d517 --- /dev/null +++ b/crates/ksp-worker-api/unit_tests/lifecycle.rs @@ -0,0 +1,151 @@ +// file: crates/ksp-worker-api/unit_tests/lifecycle.rs +// version: 1 + +const TEST_FAULT: crate::ErrorCode = crate::ErrorCode::new("worker_test", "fault"); + +fn new_lifecycle() -> std::option::Option { + let id = match crate::WorkerId::new("worker-001") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + let kind = match crate::WorkerKindCode::new("continuous_worker") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + return std::option::Option::Some(crate::WorkerLifecycle::new(id, kind)); +} + +#[test] +fn pre_002_lifecycle_accepts_every_planned_terminal_path() { + let lifecycle = new_lifecycle(); + assert!(lifecycle.is_some()); + let mut lifecycle = match lifecycle { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!(lifecycle.mark_stopped().is_ok()); + assert_eq!(lifecycle.state(), crate::WorkerState::Stopped); + assert!(lifecycle.state().is_terminal()); + let lifecycle = new_lifecycle(); + assert!(lifecycle.is_some()); + let mut lifecycle = match lifecycle { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!(lifecycle.start().is_ok()); + assert!(lifecycle.mark_running().is_ok()); + assert!(lifecycle.mark_stopping().is_ok()); + assert!(lifecycle.mark_stopped().is_ok()); + assert_eq!(lifecycle.state(), crate::WorkerState::Stopped); + for fault_from_running in [false, true] { + let lifecycle = new_lifecycle(); + assert!(lifecycle.is_some()); + let mut lifecycle = match lifecycle { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!(lifecycle.start().is_ok()); + if fault_from_running { + assert!(lifecycle.mark_running().is_ok()); + } + assert!(lifecycle.fault(TEST_FAULT).is_ok()); + assert_eq!(lifecycle.state(), crate::WorkerState::Faulted(TEST_FAULT)); + assert_eq!(lifecycle.state().fault_code(), std::option::Option::Some(TEST_FAULT)); + } + return; +} + +#[test] +fn pre_002_stopping_allows_normal_stop_or_fault() { + for terminal in [crate::WorkerState::Stopped, crate::WorkerState::Faulted(TEST_FAULT)] { + let lifecycle = new_lifecycle(); + assert!(lifecycle.is_some()); + let mut lifecycle = match lifecycle { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!(lifecycle.start().is_ok()); + assert!(lifecycle.mark_running().is_ok()); + assert!(lifecycle.mark_stopping().is_ok()); + let result = match terminal { + crate::WorkerState::Stopped => lifecycle.mark_stopped(), + crate::WorkerState::Faulted(code) => lifecycle.fault(code), + _ => return, + }; + assert!(result.is_ok()); + assert_eq!(lifecycle.state(), terminal); + } + return; +} + +#[test] +fn pre_002_invalid_transition_preserves_source_state_and_reports_safe_context() { + let lifecycle = new_lifecycle(); + assert!(lifecycle.is_some()); + let mut lifecycle = match lifecycle { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let rejected = lifecycle.mark_running(); + assert!(rejected.is_err()); + assert_eq!(lifecycle.state(), crate::WorkerState::Created); + let error = match rejected { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_WORKER_TRANSITION_INVALID); + assert_eq!(error.context().len(), 2); + assert_eq!(error.context()[0].key(), "source_state"); + assert_eq!(error.context()[0].value(), "created"); + assert_eq!(error.context()[1].key(), "target_state"); + assert_eq!(error.context()[1].value(), "running"); + return; +} + +#[test] +fn pre_002_transition_matrix_is_exact() { + let states = [ + crate::WorkerState::Created, + crate::WorkerState::Starting, + crate::WorkerState::Running, + crate::WorkerState::Stopping, + crate::WorkerState::Stopped, + crate::WorkerState::Faulted(TEST_FAULT), + ]; + for source in states { + for target in states { + let expected = matches!( + (source, target), + (crate::WorkerState::Created, crate::WorkerState::Starting) + | (crate::WorkerState::Created, crate::WorkerState::Stopped) + | (crate::WorkerState::Starting, crate::WorkerState::Running) + | (crate::WorkerState::Starting, crate::WorkerState::Stopping) + | (crate::WorkerState::Starting, crate::WorkerState::Faulted(_)) + | (crate::WorkerState::Running, crate::WorkerState::Stopping) + | (crate::WorkerState::Running, crate::WorkerState::Faulted(_)) + | (crate::WorkerState::Stopping, crate::WorkerState::Stopped) + | (crate::WorkerState::Stopping, crate::WorkerState::Faulted(_)) + ); + assert_eq!(super::allowed_transition(source, target), expected, "unexpected transition matrix cell: {source:?} -> {target:?}"); + } + } + return; +} + +#[test] +fn pre_002_health_activity_and_state_codes_are_stable() { + assert_eq!(crate::WorkerState::Created.code(), "created"); + assert_eq!(crate::WorkerState::Starting.code(), "starting"); + assert_eq!(crate::WorkerState::Running.code(), "running"); + assert_eq!(crate::WorkerState::Stopping.code(), "stopping"); + assert_eq!(crate::WorkerState::Stopped.code(), "stopped"); + assert_eq!(crate::WorkerState::Faulted(TEST_FAULT).code(), "faulted"); + assert_eq!(crate::WorkerHealth::Unknown.code(), "unknown"); + assert_eq!(crate::WorkerHealth::Healthy.code(), "healthy"); + assert_eq!(crate::WorkerHealth::Degraded.code(), "degraded"); + assert_eq!(crate::WorkerHealth::Unhealthy.code(), "unhealthy"); + assert_eq!(crate::WorkerActivity::Unknown.code(), "unknown"); + assert_eq!(crate::WorkerActivity::Idle.code(), "idle"); + assert_eq!(crate::WorkerActivity::Active.code(), "active"); + return; +} diff --git a/crates/ksp-worker-api/unit_tests/snapshot.rs b/crates/ksp-worker-api/unit_tests/snapshot.rs new file mode 100644 index 0000000..ca23b0d --- /dev/null +++ b/crates/ksp-worker-api/unit_tests/snapshot.rs @@ -0,0 +1,66 @@ +// file: crates/ksp-worker-api/unit_tests/snapshot.rs +// version: 1 + +#[test] +fn pre_002_snapshot_sequence_advances_strictly_and_orders_positions() { + let first = crate::WorkerSnapshotSequence::initial(); + assert_eq!(first.value(), 0); + let second = first.next(); + assert!(second.is_ok()); + let second = match second { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(second.value(), 1); + assert!(second.is_after(first)); + assert!(!first.is_after(second)); + assert!(!first.is_after(first)); + return; +} + +#[test] +fn pre_002_snapshot_sequence_exhaustion_is_explicit_and_non_wrapping() { + let exhausted = super::exhausted_snapshot_sequence(); + let result = exhausted.next(); + assert!(result.is_err()); + let error = match result { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => return, + }; + assert_eq!(error.code(), crate::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED); + assert_eq!(error.context().len(), 1); + assert_eq!(error.context()[0].key(), "sequence"); + assert_eq!(error.context()[0].value(), u64::MAX.to_string()); + return; +} + +#[test] +fn pre_002_snapshot_preserves_exact_common_dimensions() { + let id = crate::WorkerId::new("worker-001"); + let kind = crate::WorkerKindCode::new("example_worker"); + assert!(id.is_ok()); + assert!(kind.is_ok()); + let id = match id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let kind = match kind { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let snapshot = crate::WorkerSnapshot::new( + id, + kind, + crate::WorkerSnapshotSequence::initial(), + crate::WorkerState::Running, + crate::WorkerHealth::Healthy, + crate::WorkerActivity::Active, + ); + assert_eq!(snapshot.id().as_str(), "worker-001"); + assert_eq!(snapshot.kind().as_str(), "example_worker"); + assert_eq!(snapshot.sequence().value(), 0); + assert_eq!(snapshot.state(), crate::WorkerState::Running); + assert_eq!(snapshot.health(), crate::WorkerHealth::Healthy); + assert_eq!(snapshot.activity(), crate::WorkerActivity::Active); + return; +} diff --git a/crates/ksp-worker-api/unit_tests/stop.rs b/crates/ksp-worker-api/unit_tests/stop.rs new file mode 100644 index 0000000..e9c85f8 --- /dev/null +++ b/crates/ksp-worker-api/unit_tests/stop.rs @@ -0,0 +1,22 @@ +// file: crates/ksp-worker-api/unit_tests/stop.rs +// version: 1 + +#[test] +fn pre_002_stop_token_is_shared_and_idempotent() { + let token = crate::WorkerStopToken::new(); + let clone = token.clone(); + assert!(!token.is_stop_requested()); + assert!(clone.request_stop()); + assert!(token.is_stop_requested()); + assert!(!token.request_stop()); + assert!(!clone.request_stop()); + return; +} + +#[test] +fn pre_002_default_stop_token_starts_without_stop_request() { + let token = crate::WorkerStopToken::default(); + assert!(!token.is_stop_requested()); + assert_eq!(std::format!("{token:?}"), "WorkerStopToken { stop_requested: false }"); + return; +} diff --git a/deltas/0.3.9/pre.002.md b/deltas/0.3.9/pre.002.md new file mode 100644 index 0000000..6119bd7 --- /dev/null +++ b/deltas/0.3.9/pre.002.md @@ -0,0 +1,202 @@ + + + +# Delta `0.3.9-pre.002` — noyau `ksp-worker-api` + +## Base requise + +```text +0.3.9-pre.001-fix.002 +workspace.package.version = 0.3.9-pre.1.fix.2 +``` + +Le gate opérateur de la base a exécuté `cargo clean`, format, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace `--all-targets --all-features -- -D warnings`, tests workspace `--all-targets --all-features`, les cinq builds Tauri `deb,rpm` et `cargo tree --duplicates`. Les commandes Cargo ont terminé sans échec signalé ; les tests live/smoke explicitement opt-in sont restés `ignored` comme prévu. + +## Objectif + +Créer uniquement le noyau passif et runtime-neutral de `ksp-worker-api` décidé en `pre.001`, sans commencer le worker RAW Transaction ni anticiper le hardening de `pre.003`. + +## Version + +Cette tranche est une nouvelle prerelease non-fix : + +```text +workspace.package.version = 0.3.9-pre.2 +``` + +Le `Cargo.toml` racine passe du header `476` au header `477` et ajoute `crates/ksp-worker-api` aux membres du workspace. + +## Surface ajoutée + +```text +WorkerId +WorkerKindCode +MAX_WORKER_ID_BYTES = 128 +MAX_WORKER_KIND_CODE_BYTES = 128 + +WorkerState +WorkerHealth +WorkerActivity +WorkerLifecycle + +WorkerStopToken + +WorkerSnapshotSequence +WorkerSnapshot +WorkerSnapshotFuture<'a> +WorkerSnapshotSource + +ERROR_CODE_WORKER_ID_INVALID +ERROR_CODE_WORKER_KIND_INVALID +ERROR_CODE_WORKER_TRANSITION_INVALID +ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED + +Error / ErrorCode / ErrorContext / Result +``` + +Tous les exports sont accessibles depuis le crate-root ; les modules restent privés. + +## Lifecycle matérialisé + +```text +Created -> Starting +Created -> Stopped +Starting -> Running +Starting -> Stopping +Starting -> Faulted(code) +Running -> Stopping +Running -> Faulted(code) +Stopping -> Stopped +Stopping -> Faulted(code) +``` + +`Stopped` et `Faulted(ErrorCode)` sont terminaux. `Faulted` conserve uniquement un `ErrorCode` KSP statique. Une transition invalide retourne `ERROR_CODE_WORKER_TRANSITION_INVALID` avec les seuls codes source/cible et laisse l'état inchangé. + +`WorkerHealth` (`Unknown`, `Healthy`, `Degraded`, `Unhealthy`) et `WorkerActivity` (`Unknown`, `Idle`, `Active`) restent orthogonaux au lifecycle ; aucune completion, progression, ETA, slot, provider ou métrique métier n'entre dans cette API. + +## Snapshot et stop + +`WorkerSnapshot` est fixe et contient exactement les dimensions communes décidées : identité, kind, séquence, lifecycle, health et activity. Aucun payload arbitraire ou champ diagnostique libre n'est admis. + +`WorkerSnapshotSequence` part de zéro, avance par `checked_add(1)`, expose `is_after` et retourne une erreur explicite en cas d'épuisement. + +`WorkerSnapshotSource` expose `current()` et `wait_for_change(...)` via un future `std` boxed ; il n'impose aucun runtime concret. Les preuves object-safety, implémentation externe et listeners lents restent dans `pre.003`. + +`WorkerStopToken` partage une intention atomique cloneable ; seule la première demande retourne `true`. Le token ne modifie pas directement le lifecycle et ne possède ni join, ni timeout, ni thread/task. + +## Firewalls + +Dépendance normale exacte : + +```text +ksp-worker-api -> ksp-core-lib +``` + +Absents du manifest et des sources de production : + +```text +features / dev-dependencies / build-dependencies +ksp-job-api +ksp-interface-lib +ksp-config-lib +ksp-logging-lib +ksp-onchain-transport-lib +ksp-store-api / ksp-store-lib +tokio / futures / serde / tracing / Tauri +Solana / provider / RawTransaction / backfill / checkpoint +``` + +## Tests ajoutés + +Treize tests unitaires : + +- identité : admission aux bornes, rejets hostiles, alphabet exact ; +- lifecycle : chemins terminaux, arrêt/fault depuis Stopping, conservation sur transition invalide, matrice exacte, codes state/health/activity ; +- stop : partage/idempotence et état par défaut ; +- snapshot : séquence monotone, exhaustion explicite et projection exacte des six dimensions communes. + +Quatre canaries d'intégration : + +- deux canaries dependency boundary : manifest Core-only et absence de chemins runtime/domain interdits dans les sources de production ; +- deux canaries public API : consommation crate-root du lifecycle/stop/snapshot et stabilité des bornes/error codes. + +Le hardening adversarial complet, l'object-safety, l'implémentation externe std-only, les listeners lents/indépendants, le late-listener et les inventaires exacts restent réservés à `pre.003`. + +## Fichiers ajoutés + +```text +crates/ksp-worker-api/Cargo.toml +crates/ksp-worker-api/src/error.rs +crates/ksp-worker-api/src/identity.rs +crates/ksp-worker-api/src/lib.rs +crates/ksp-worker-api/src/lifecycle.rs +crates/ksp-worker-api/src/snapshot.rs +crates/ksp-worker-api/src/stop.rs +crates/ksp-worker-api/unit_tests/identity.rs +crates/ksp-worker-api/unit_tests/lifecycle.rs +crates/ksp-worker-api/unit_tests/snapshot.rs +crates/ksp-worker-api/unit_tests/stop.rs +crates/ksp-worker-api/tests/dependency_boundary.rs +crates/ksp-worker-api/tests/public_api.rs +deltas/0.3.9/pre.002.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md +docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md +``` + +## Fichiers supprimés + +Aucun. + +## Documentation volontairement différée + +`crates/ksp-worker-api/README.md` et `USAGE.md` ne sont pas créés dans cette tranche de code. Le plan réserve leur réconciliation version-neutral à `pre.008`, après freeze fonctionnelle et hardening de la surface publique. + +## Validations exécutées dans l'environnement d'assemblage + +```text +python3 scripts/audit_rust_workspace_rules.py +-> General Rust rule audit: clean +-> Rust export completeness audit: 0 candidate(s) +-> KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +-> Markdown table audit: clean (320 table(s), 731 file(s)) +``` + +Contrôles statiques complémentaires : + +```text +workspace member ksp-worker-api présent +workspace.package.version = 0.3.9-pre.2 +manifest Worker API = Core-only, aucune feature/dev/build dependency +6 sources de production prévues présentes +4 fichiers unit_tests présents +2 suites d'intégration présentes +aucun pub mod dans src/lib.rs +aucun runtime concret ajouté +``` + +L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo de `pre.002` n'est donc déclaré PASS localement. + +## Gate opérateur demandé + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-worker-api +cargo tree -p ksp-worker-api --edges normal +cargo tree -p ksp-worker-api -e features +``` + +## Questions ouvertes + +Aucune question ne bloque `pre.003` après un gate opérateur vert. Les questions volontairement différées (`WorkerHandle`, registry/factory, restart generation, remote protocol, serialization, metrics communes) restent hors scope conformément au plan `pre.001`. diff --git a/docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md b/docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md index f4f3779..0e6b328 100644 --- a/docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md +++ b/docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.9 — Worker API générique + audit RAW Transaction @@ -544,9 +544,22 @@ L’audit de `docs/rules/VERSION_WORKFLOW.md` a mis en évidence que `VER-ID-009 ### `pre.002` — crate + contrats Worker API décidés -**Statut : prévu.** +**Statut : réalisé.** -Budget cible : **15-20 min**. Créer le noyau `ksp-worker-api` selon les contrats décidés, avec les tests unitaires, public API et dependency boundary prévus. +Budget cible : **15-20 min**. Le noyau `ksp-worker-api` est matérialisé selon les contrats décidés, avec tests unitaires, public API et dependency boundary. + +Surface effectivement ouverte : + +```text +WorkerId / WorkerKindCode +WorkerState / WorkerHealth / WorkerActivity / WorkerLifecycle +WorkerStopToken +WorkerSnapshotSequence / WorkerSnapshot / WorkerSnapshotFuture / WorkerSnapshotSource +4 ErrorCode worker_api +reexports Error / ErrorCode / ErrorContext / Result +``` + +La crate dépend uniquement de `ksp-core-lib`, ne possède aucune feature/dev/build dependency et n'ouvre ni runtime, ni Job, ni Transport/Store/Config, ni contrat Solana. Le snapshot commun reste fixe et sans payload métier. Les preuves adversariales, object-safety, implémentation externe et latest-value multi-listener restent volontairement dans `pre.003`. ### `pre.003` — hardening, races, object-safety, impl externe et freeze diff --git a/docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md b/docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md index 976b01c..00b47af 100644 --- a/docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md +++ b/docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md @@ -1,5 +1,5 @@ - + # Validation v0.3.9 — Worker API + audit RAW Transaction @@ -38,6 +38,20 @@ Cette matrice suit les preuves de `0.3.9` sans remplacer les deltas. `pre.001` f Le ZIP ne contient pas `.git`; l’existence du tag n’est pas inspectée directement. Le `rel.001` stable décrit l’opération `tag v0.3.8` et le prompt autorise l’archive stable explicitement fournie comme base. +### Gate opérateur avant `pre.002` + +L’état `0.3.9-pre.1.fix.2` a ensuite été validé par l’opérateur avant ouverture de `pre.002` : + +- [X] `cargo clean` exécuté. +- [X] `cargo fmt --all` exécuté. +- [X] audits Rust complets propres. +- [X] audit Markdown propre : 320 tables / 730 fichiers. +- [X] `cargo check --workspace` PASS. +- [X] `cargo clippy --workspace --all-targets --all-features -- -D warnings` PASS. +- [X] `cargo test --workspace --all-targets --all-features` PASS ; seuls les smokes opt-in explicitement `ignored` restent non exécutés. +- [X] les cinq Desks produisent chacun leurs bundles `deb` et `rpm` avec la version `0.3.9-pre.1.fix.2`. +- [X] `cargo tree --duplicates` exécuté jusqu'au retour shell, sans échec de commande signalé. + ## 3. Architecture Worker API décidée en `pre.001` - [X] Job et Worker restent deux lifecycle APIs sémantiquement distinctes. @@ -66,18 +80,20 @@ Le ZIP ne contient pas `.git`; l’existence du tag n’est pas inspectée direc ### `pre.002` -- [ ] `crates/ksp-worker-api` créée dans le workspace. -- [ ] Manifest sans feature/dev/build dependency et avec Core uniquement. -- [ ] `identity.rs`, `error.rs`, `lifecycle.rs`, `snapshot.rs`, `stop.rs`, `lib.rs` matérialisés. -- [ ] Façade crate-root uniquement, aucun `pub mod`. -- [ ] Quatre error codes stables `worker_api` matérialisés. -- [ ] Bounds exacts identity testés. -- [ ] Lifecycle transition matrix exacte testée. -- [ ] Transition invalide conserve l’état source. -- [ ] Stop token idempotent/shared testée. -- [ ] Snapshot sequence/exhaustion testée. -- [ ] Public API consumer depuis crate root testée. -- [ ] Dependency firewall initial testée. +Les cases cochées de cette sous-section attestent la matérialisation de la surface et des canaries prévues. L'environnement d'assemblage ne disposant pas de Cargo, leur exécution Rust reste un gate opérateur avant `pre.003`. + +- [X] `crates/ksp-worker-api` créée dans le workspace. +- [X] Manifest sans feature/dev/build dependency et avec Core uniquement. +- [X] `identity.rs`, `error.rs`, `lifecycle.rs`, `snapshot.rs`, `stop.rs`, `lib.rs` matérialisés. +- [X] Façade crate-root uniquement, aucun `pub mod`. +- [X] Quatre error codes stables `worker_api` matérialisés. +- [X] Bounds exacts identity testés. +- [X] Lifecycle transition matrix exacte testée. +- [X] Transition invalide conserve l’état source. +- [X] Stop token idempotent/shared testée. +- [X] Snapshot sequence/exhaustion testée. +- [X] Public API consumer depuis crate root testée. +- [X] Dependency firewall initial testée. ### `pre.003` @@ -197,10 +213,10 @@ Aucun item ci-dessous n’est déclaré exécuté en `pre.001`. La livraison initiale `pre.001` avait conservé `workspace.package.version = 0.3.8` en suivant l’exception du prompt 028. Cette exception est supplantée par la règle normative `VER-ID-009`. -Après `pre.001-fix.002`, l’état courant est : +Après `pre.002`, l’état courant est : ```text -workspace.package.version = 0.3.9-pre.1.fix.2 +workspace.package.version = 0.3.9-pre.2 ``` -Ce fix modifie `Cargo.toml` et suit donc `VER-ID-007`/`VER-ID-010`. `0.3.9-pre.2` reste la version Cargo de la future tranche non-fix `pre.002`. +`pre.002` est une nouvelle tranche non-fix et synchronise donc Cargo conformément à `VER-ID-006` et `VER-ID-009`. L'état précédent `0.3.9-pre.1.fix.2` reste documenté dans `pre.001-fix.002`.