v0.3.9-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 476
|
# version: 477
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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]
|
[workspace.package]
|
||||||
version = "0.3.9-pre.1.fix.2"
|
version = "0.3.9-pre.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
14
crates/ksp-worker-api/Cargo.toml
Normal file
14
crates/ksp-worker-api/Cargo.toml
Normal file
@@ -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
|
||||||
12
crates/ksp-worker-api/src/error.rs
Normal file
12
crates/ksp-worker-api/src/error.rs
Normal file
@@ -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");
|
||||||
76
crates/ksp-worker-api/src/identity.rs
Normal file
76
crates/ksp-worker-api/src/identity.rs
Normal file
@@ -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<std::string::String>) -> crate::Result<Self> {
|
||||||
|
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<std::string::String>) -> crate::Result<Self> {
|
||||||
|
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;
|
||||||
62
crates/ksp-worker-api/src/lib.rs
Normal file
62
crates/ksp-worker-api/src/lib.rs
Normal file
@@ -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;
|
||||||
199
crates/ksp-worker-api/src/lifecycle.rs
Normal file
199
crates/ksp-worker-api/src/lifecycle.rs
Normal file
@@ -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<crate::ErrorCode> {
|
||||||
|
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;
|
||||||
138
crates/ksp-worker-api/src/snapshot.rs
Normal file
138
crates/ksp-worker-api/src/snapshot.rs
Normal file
@@ -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<Self> {
|
||||||
|
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::boxed::Box<dyn std::future::Future<Output = crate::WorkerSnapshot> + 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;
|
||||||
44
crates/ksp-worker-api/src/stop.rs
Normal file
44
crates/ksp-worker-api/src/stop.rs
Normal file
@@ -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<std::sync::atomic::AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
82
crates/ksp-worker-api/tests/dependency_boundary.rs
Normal file
82
crates/ksp-worker-api/tests/dependency_boundary.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
52
crates/ksp-worker-api/tests/public_api.rs
Normal file
52
crates/ksp-worker-api/tests/public_api.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
64
crates/ksp-worker-api/unit_tests/identity.rs
Normal file
64
crates/ksp-worker-api/unit_tests/identity.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
151
crates/ksp-worker-api/unit_tests/lifecycle.rs
Normal file
151
crates/ksp-worker-api/unit_tests/lifecycle.rs
Normal file
@@ -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<crate::WorkerLifecycle> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
66
crates/ksp-worker-api/unit_tests/snapshot.rs
Normal file
66
crates/ksp-worker-api/unit_tests/snapshot.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
22
crates/ksp-worker-api/unit_tests/stop.rs
Normal file
22
crates/ksp-worker-api/unit_tests/stop.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
202
deltas/0.3.9/pre.002.md
Normal file
202
deltas/0.3.9/pre.002.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<!-- file: deltas/0.3.9/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md -->
|
<!-- file: docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Plan v0.3.9 — Worker API générique + audit RAW Transaction
|
# 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
|
### `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
|
### `pre.003` — hardening, races, object-safety, impl externe et freeze
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md -->
|
<!-- file: docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# Validation v0.3.9 — Worker API + audit RAW Transaction
|
# 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.
|
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`
|
## 3. Architecture Worker API décidée en `pre.001`
|
||||||
|
|
||||||
- [X] Job et Worker restent deux lifecycle APIs sémantiquement distinctes.
|
- [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`
|
### `pre.002`
|
||||||
|
|
||||||
- [ ] `crates/ksp-worker-api` créée dans le workspace.
|
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`.
|
||||||
- [ ] 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.
|
- [X] `crates/ksp-worker-api` créée dans le workspace.
|
||||||
- [ ] Façade crate-root uniquement, aucun `pub mod`.
|
- [X] Manifest sans feature/dev/build dependency et avec Core uniquement.
|
||||||
- [ ] Quatre error codes stables `worker_api` matérialisés.
|
- [X] `identity.rs`, `error.rs`, `lifecycle.rs`, `snapshot.rs`, `stop.rs`, `lib.rs` matérialisés.
|
||||||
- [ ] Bounds exacts identity testés.
|
- [X] Façade crate-root uniquement, aucun `pub mod`.
|
||||||
- [ ] Lifecycle transition matrix exacte testée.
|
- [X] Quatre error codes stables `worker_api` matérialisés.
|
||||||
- [ ] Transition invalide conserve l’état source.
|
- [X] Bounds exacts identity testés.
|
||||||
- [ ] Stop token idempotent/shared testée.
|
- [X] Lifecycle transition matrix exacte testée.
|
||||||
- [ ] Snapshot sequence/exhaustion testée.
|
- [X] Transition invalide conserve l’état source.
|
||||||
- [ ] Public API consumer depuis crate root testée.
|
- [X] Stop token idempotent/shared testée.
|
||||||
- [ ] Dependency firewall initial 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`
|
### `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`.
|
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
|
```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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user