v0.3.6-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 396
|
# version: 397
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "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-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "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"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.6-pre.2.fix.1"
|
version = "0.3.6-pre.3"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// file: crates/ksp-job-api/src/error.rs
|
// file: crates/ksp-job-api/src/error.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/// Error code used when a Job identifier violates its bounded safe-code contract.
|
/// Error code used when a Job identifier violates its bounded safe-code contract.
|
||||||
pub const ERROR_CODE_JOB_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_id_invalid");
|
pub const ERROR_CODE_JOB_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_id_invalid");
|
||||||
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
||||||
pub const ERROR_CODE_JOB_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_kind_invalid");
|
pub const ERROR_CODE_JOB_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_kind_invalid");
|
||||||
|
/// Error code used when a Job notification sequence cannot advance without wrapping.
|
||||||
|
pub const ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("job_api", "job_notification_sequence_exhausted");
|
||||||
/// Error code used when a requested Job lifecycle transition is not allowed.
|
/// Error code used when a requested Job lifecycle transition is not allowed.
|
||||||
pub const ERROR_CODE_JOB_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_transition_invalid");
|
pub const ERROR_CODE_JOB_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_transition_invalid");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-api/src/lib.rs
|
// file: crates/ksp-job-api/src/lib.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -8,14 +8,15 @@
|
|||||||
//! Passive runtime-neutral lifecycle contracts for bounded KSP Jobs.
|
//! Passive runtime-neutral lifecycle contracts for bounded KSP Jobs.
|
||||||
//!
|
//!
|
||||||
//! This foundation owns validated Job identity, explicit lifecycle transitions
|
//! This foundation owns validated Job identity, explicit lifecycle transitions
|
||||||
//! and cooperative cancellation intent. Notification delivery, concrete Job
|
//! cooperative cancellation intent and runtime-neutral latest-value observation.
|
||||||
//! behavior, runtime spawning, Transport, Store and Worker contracts remain
|
//! Concrete Job behavior, runtime spawning, Transport, Store and Worker contracts
|
||||||
//! outside this crate.
|
//! remain outside this crate.
|
||||||
|
|
||||||
mod cancellation;
|
mod cancellation;
|
||||||
mod error;
|
mod error;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
|
mod notification;
|
||||||
|
|
||||||
/// Runtime-neutral cloneable token carrying cooperative cancellation intent.
|
/// Runtime-neutral cloneable token carrying cooperative cancellation intent.
|
||||||
pub use self::cancellation::JobCancellationToken;
|
pub use self::cancellation::JobCancellationToken;
|
||||||
@@ -23,6 +24,8 @@ pub use self::cancellation::JobCancellationToken;
|
|||||||
pub use self::error::ERROR_CODE_JOB_ID_INVALID;
|
pub use self::error::ERROR_CODE_JOB_ID_INVALID;
|
||||||
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
||||||
pub use self::error::ERROR_CODE_JOB_KIND_INVALID;
|
pub use self::error::ERROR_CODE_JOB_KIND_INVALID;
|
||||||
|
/// Error code used when a Job notification sequence cannot advance without wrapping.
|
||||||
|
pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;
|
||||||
/// Error code used when a requested Job lifecycle transition is not allowed.
|
/// Error code used when a requested Job lifecycle transition is not allowed.
|
||||||
pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;
|
pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;
|
||||||
/// Bounded caller-supplied identity of one logical Job and its controlled resumptions.
|
/// Bounded caller-supplied identity of one logical Job and its controlled resumptions.
|
||||||
@@ -39,6 +42,14 @@ pub use self::lifecycle::JobCompletion;
|
|||||||
pub use self::lifecycle::JobLifecycle;
|
pub use self::lifecycle::JobLifecycle;
|
||||||
/// Current lifecycle state of one bounded Job.
|
/// Current lifecycle state of one bounded Job.
|
||||||
pub use self::lifecycle::JobState;
|
pub use self::lifecycle::JobState;
|
||||||
|
/// Latest complete observable value for one Job at a monotone sequence position.
|
||||||
|
pub use self::notification::JobNotification;
|
||||||
|
/// Monotone sequence attached to one latest-value Job notification stream.
|
||||||
|
pub use self::notification::JobNotificationSequence;
|
||||||
|
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
|
||||||
|
pub use self::notification::JobSnapshotFuture;
|
||||||
|
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
|
||||||
|
pub use self::notification::JobSnapshotSource;
|
||||||
/// Common KSP error type used by Job-facing contracts.
|
/// Common KSP error type used by Job-facing contracts.
|
||||||
pub use ksp_core_lib::Error;
|
pub use ksp_core_lib::Error;
|
||||||
/// Stable structured code identifying a KSP error category and condition.
|
/// Stable structured code identifying a KSP error category and condition.
|
||||||
|
|||||||
126
crates/ksp-job-api/src/notification.rs
Normal file
126
crates/ksp-job-api/src/notification.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// file: crates/ksp-job-api/src/notification.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Monotone sequence attached to one latest-value Job notification stream.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct JobNotificationSequence(u64);
|
||||||
|
|
||||||
|
impl JobNotificationSequence {
|
||||||
|
/// Creates the initial sequence position for one Job 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_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED, "Job notification 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Latest complete observable value for one Job at a monotone sequence position.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct JobNotification<S> {
|
||||||
|
id: crate::JobId,
|
||||||
|
kind: crate::JobKindCode,
|
||||||
|
sequence: crate::JobNotificationSequence,
|
||||||
|
snapshot: S,
|
||||||
|
state: crate::JobState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> JobNotification<S> {
|
||||||
|
/// Creates one immutable latest-value notification from an already validated Job identity and snapshot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(id: crate::JobId, kind: crate::JobKindCode, sequence: crate::JobNotificationSequence, state: crate::JobState, snapshot: S) -> Self {
|
||||||
|
return Self { id, kind, sequence, snapshot, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the logical Job identity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> &crate::JobId {
|
||||||
|
return &self.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable Job family code.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn kind(&self) -> &crate::JobKindCode {
|
||||||
|
return &self.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the monotone sequence of this latest value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn sequence(&self) -> crate::JobNotificationSequence {
|
||||||
|
return self.sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete safe snapshot owned by the concrete Job contract.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn snapshot(&self) -> &S {
|
||||||
|
return &self.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the lifecycle state represented by this snapshot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn state(&self) -> crate::JobState {
|
||||||
|
return self.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> std::fmt::Debug for JobNotification<S> {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("JobNotification")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("kind", &self.kind)
|
||||||
|
.field("sequence", &self.sequence)
|
||||||
|
.field("state", &self.state)
|
||||||
|
.field("snapshot", &"<redacted>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
|
||||||
|
pub type JobSnapshotFuture<'a, S> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = crate::JobNotification<S>> + std::marker::Send + 'a>>;
|
||||||
|
|
||||||
|
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
|
||||||
|
pub trait JobSnapshotSource: std::marker::Send + std::marker::Sync {
|
||||||
|
/// Complete snapshot type retained by the concrete source.
|
||||||
|
type Snapshot: std::clone::Clone + std::marker::Send + std::marker::Sync + 'static;
|
||||||
|
|
||||||
|
/// Returns the complete current value without requiring replay of prior notifications.
|
||||||
|
#[must_use]
|
||||||
|
fn current(&self) -> crate::JobNotification<Self::Snapshot>;
|
||||||
|
|
||||||
|
/// Waits for a value newer than `observed`, returning the complete current snapshot after coalescing any intermediate updates.
|
||||||
|
fn wait_for_change(&self, observed: crate::JobNotificationSequence) -> crate::JobSnapshotFuture<'_, Self::Snapshot>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn exhausted_notification_sequence() -> crate::JobNotificationSequence {
|
||||||
|
return crate::JobNotificationSequence(u64::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/notification.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-api/tests/dependency_boundary.rs
|
// file: crates/ksp-job-api/tests/dependency_boundary.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Dependency and runtime-neutrality canaries for the Job API foundation.
|
//! Dependency and runtime-neutrality canaries for the Job API foundation.
|
||||||
|
|
||||||
@@ -25,13 +25,14 @@ fn pre_002_manifest_has_exact_core_only_dependency_graph() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_production_sources_forbid_runtime_domain_and_wire_dependencies() {
|
fn pre_003_production_sources_forbid_runtime_domain_and_wire_dependencies() {
|
||||||
let sources = [
|
let sources = [
|
||||||
include_str!("../src/cancellation.rs"),
|
include_str!("../src/cancellation.rs"),
|
||||||
include_str!("../src/error.rs"),
|
include_str!("../src/error.rs"),
|
||||||
include_str!("../src/identity.rs"),
|
include_str!("../src/identity.rs"),
|
||||||
include_str!("../src/lib.rs"),
|
include_str!("../src/lib.rs"),
|
||||||
include_str!("../src/lifecycle.rs"),
|
include_str!("../src/lifecycle.rs"),
|
||||||
|
include_str!("../src/notification.rs"),
|
||||||
];
|
];
|
||||||
for source in sources {
|
for source in sources {
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
|
|||||||
148
crates/ksp-job-api/tests/notifications.rs
Normal file
148
crates/ksp-job-api/tests/notifications.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// file: crates/ksp-job-api/tests/notifications.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! External-consumer canaries for latest-value Job observation.
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
struct TestSnapshot {
|
||||||
|
completed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TestSnapshotSource {
|
||||||
|
current: std::sync::Arc<ksp_job_api::JobNotification<TestSnapshot>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestSnapshotSource {
|
||||||
|
fn new(current: ksp_job_api::JobNotification<TestSnapshot>) -> Self {
|
||||||
|
return Self { current: std::sync::Arc::new(current) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ksp_job_api::JobSnapshotSource for TestSnapshotSource {
|
||||||
|
type Snapshot = TestSnapshot;
|
||||||
|
|
||||||
|
fn current(&self) -> ksp_job_api::JobNotification<Self::Snapshot> {
|
||||||
|
return self.current.as_ref().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_change(&self, observed: ksp_job_api::JobNotificationSequence) -> ksp_job_api::JobSnapshotFuture<'_, Self::Snapshot> {
|
||||||
|
let current = ksp_job_api::JobSnapshotSource::current(self);
|
||||||
|
assert!(current.sequence().is_after(observed));
|
||||||
|
return std::boxed::Box::pin(std::future::ready(current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestWake;
|
||||||
|
|
||||||
|
impl std::task::Wake for TestWake {
|
||||||
|
fn wake(self: std::sync::Arc<Self>) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_ready<S>(mut future: ksp_job_api::JobSnapshotFuture<'_, S>) -> std::option::Option<ksp_job_api::JobNotification<S>> {
|
||||||
|
let waker = std::task::Waker::from(std::sync::Arc::new(TestWake));
|
||||||
|
let mut context = std::task::Context::from_waker(&waker);
|
||||||
|
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||||
|
std::task::Poll::Ready(value) => std::option::Option::Some(value),
|
||||||
|
std::task::Poll::Pending => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notification_sequence(value: u64) -> std::option::Option<ksp_job_api::JobNotificationSequence> {
|
||||||
|
let mut sequence = ksp_job_api::JobNotificationSequence::initial();
|
||||||
|
for _ in 0..value {
|
||||||
|
sequence = match sequence.next() {
|
||||||
|
std::result::Result::Ok(next) => next,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return std::option::Option::Some(sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notification(sequence_value: u64, state: ksp_job_api::JobState, completed: u64) -> std::option::Option<ksp_job_api::JobNotification<TestSnapshot>> {
|
||||||
|
let id = match ksp_job_api::JobId::new("external-job-001") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let kind = match ksp_job_api::JobKindCode::new("backfill_raw") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let sequence = match notification_sequence(sequence_value) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return std::option::Option::Some(ksp_job_api::JobNotification::new(id, kind, sequence, state, TestSnapshot { completed }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_external_notification_contract_is_consumable_from_crate_root() {
|
||||||
|
let notification = notification(3, ksp_job_api::JobState::Running, 2);
|
||||||
|
assert!(notification.is_some());
|
||||||
|
let notification = match notification {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
assert_eq!(notification.id().as_str(), "external-job-001");
|
||||||
|
assert_eq!(notification.kind().as_str(), "backfill_raw");
|
||||||
|
assert_eq!(notification.sequence().value(), 3);
|
||||||
|
assert_eq!(notification.state(), ksp_job_api::JobState::Running);
|
||||||
|
assert_eq!(notification.snapshot().completed, 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_slow_and_independent_listeners_resynchronize_to_latest_value() {
|
||||||
|
let latest = notification(8, ksp_job_api::JobState::Running, 7);
|
||||||
|
assert!(latest.is_some());
|
||||||
|
let source = match latest {
|
||||||
|
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let listener_a = source.clone();
|
||||||
|
let listener_b = source.clone();
|
||||||
|
let observed_a = match notification_sequence(2) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let observed_b = match notification_sequence(6) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let changed_a = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_a, observed_a));
|
||||||
|
let changed_b = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_b, observed_b));
|
||||||
|
assert!(changed_a.is_some());
|
||||||
|
assert!(changed_b.is_some());
|
||||||
|
let changed_a = match changed_a {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let changed_b = match changed_b {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
assert_eq!(changed_a.sequence().value(), 8);
|
||||||
|
assert_eq!(changed_b.sequence().value(), 8);
|
||||||
|
assert_eq!(changed_a.snapshot().completed, 7);
|
||||||
|
assert_eq!(changed_b.snapshot().completed, 7);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_terminal_snapshot_remains_readable_from_shared_source() {
|
||||||
|
let terminal = notification(9, ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial), 8);
|
||||||
|
assert!(terminal.is_some());
|
||||||
|
let source = match terminal {
|
||||||
|
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let cloned = source.clone();
|
||||||
|
let current = ksp_job_api::JobSnapshotSource::current(&cloned);
|
||||||
|
assert_eq!(current.sequence().value(), 9);
|
||||||
|
assert_eq!(current.state(), ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial));
|
||||||
|
assert!(current.state().is_terminal());
|
||||||
|
assert_eq!(current.snapshot().completed, 8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// file: crates/ksp-job-api/tests/release_completeness.rs
|
// file: crates/ksp-job-api/tests/release_completeness.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Completeness canaries for the initial Job API foundation.
|
//! Completeness canaries for the current Job API foundation.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_crate_root_export_inventory_is_exact() {
|
fn pre_003_crate_root_export_inventory_is_exact() {
|
||||||
let crate_root = include_str!("../src/lib.rs");
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
let mut actual = std::vec::Vec::new();
|
let mut actual = std::vec::Vec::new();
|
||||||
for line in crate_root.lines() {
|
for line in crate_root.lines() {
|
||||||
@@ -18,6 +18,7 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
|||||||
"pub use self::cancellation::JobCancellationToken;",
|
"pub use self::cancellation::JobCancellationToken;",
|
||||||
"pub use self::error::ERROR_CODE_JOB_ID_INVALID;",
|
"pub use self::error::ERROR_CODE_JOB_ID_INVALID;",
|
||||||
"pub use self::error::ERROR_CODE_JOB_KIND_INVALID;",
|
"pub use self::error::ERROR_CODE_JOB_KIND_INVALID;",
|
||||||
|
"pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;",
|
||||||
"pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;",
|
"pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;",
|
||||||
"pub use self::identity::JobId;",
|
"pub use self::identity::JobId;",
|
||||||
"pub use self::identity::JobKindCode;",
|
"pub use self::identity::JobKindCode;",
|
||||||
@@ -26,6 +27,10 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
|||||||
"pub use self::lifecycle::JobCompletion;",
|
"pub use self::lifecycle::JobCompletion;",
|
||||||
"pub use self::lifecycle::JobLifecycle;",
|
"pub use self::lifecycle::JobLifecycle;",
|
||||||
"pub use self::lifecycle::JobState;",
|
"pub use self::lifecycle::JobState;",
|
||||||
|
"pub use self::notification::JobNotification;",
|
||||||
|
"pub use self::notification::JobNotificationSequence;",
|
||||||
|
"pub use self::notification::JobSnapshotFuture;",
|
||||||
|
"pub use self::notification::JobSnapshotSource;",
|
||||||
"pub use ksp_core_lib::Error;",
|
"pub use ksp_core_lib::Error;",
|
||||||
"pub use ksp_core_lib::ErrorCode;",
|
"pub use ksp_core_lib::ErrorCode;",
|
||||||
"pub use ksp_core_lib::ErrorContext;",
|
"pub use ksp_core_lib::ErrorContext;",
|
||||||
@@ -38,7 +43,7 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_production_module_inventory_is_exact() -> std::io::Result<()> {
|
fn pre_003_production_module_inventory_is_exact() -> std::io::Result<()> {
|
||||||
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||||
let entries = match std::fs::read_dir(source_root) {
|
let entries = match std::fs::read_dir(source_root) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -66,35 +71,29 @@ fn pre_002_production_module_inventory_is_exact() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
names.sort_unstable();
|
names.sort_unstable();
|
||||||
assert_eq!(names, std::vec!["cancellation.rs", "error.rs", "identity.rs", "lib.rs", "lifecycle.rs"]);
|
assert_eq!(names, std::vec!["cancellation.rs", "error.rs", "identity.rs", "lib.rs", "lifecycle.rs", "notification.rs"]);
|
||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_surface_does_not_open_notifications_backfill_or_worker_contracts() {
|
fn pre_003_surface_opens_only_generic_notifications_without_backfill_or_worker_contracts() {
|
||||||
let sources = [
|
let sources = [
|
||||||
include_str!("../src/cancellation.rs"),
|
include_str!("../src/cancellation.rs"),
|
||||||
include_str!("../src/identity.rs"),
|
include_str!("../src/identity.rs"),
|
||||||
include_str!("../src/lib.rs"),
|
include_str!("../src/lib.rs"),
|
||||||
include_str!("../src/lifecycle.rs"),
|
include_str!("../src/lifecycle.rs"),
|
||||||
|
include_str!("../src/notification.rs"),
|
||||||
];
|
];
|
||||||
for source in sources {
|
for source in sources {
|
||||||
for forbidden in [
|
for forbidden in ["BackfillRequest", "WorkerControl", "WorkerState", "spawn(", "RawTransaction", "provider", "endpoint"] {
|
||||||
"JobNotification",
|
|
||||||
"JobNotificationSequence",
|
|
||||||
"JobSnapshot",
|
|
||||||
"JobSnapshotSource",
|
|
||||||
"BackfillRequest",
|
|
||||||
"WorkerControl",
|
|
||||||
"WorkerState",
|
|
||||||
"spawn(",
|
|
||||||
"RawTransaction",
|
|
||||||
"provider",
|
|
||||||
"endpoint",
|
|
||||||
] {
|
|
||||||
assert!(!source.contains(forbidden), "future or domain-specific Job contract leaked early: {forbidden}");
|
assert!(!source.contains(forbidden), "future or domain-specific Job contract leaked early: {forbidden}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let notification_source = include_str!("../src/notification.rs");
|
||||||
|
assert!(notification_source.contains("pub struct JobNotification<S>"));
|
||||||
|
assert!(notification_source.contains("pub trait JobSnapshotSource"));
|
||||||
|
assert!(notification_source.contains("std::future::Future"));
|
||||||
|
assert!(!notification_source.contains("tokio::"));
|
||||||
let lifecycle_source = include_str!("../src/lifecycle.rs");
|
let lifecycle_source = include_str!("../src/lifecycle.rs");
|
||||||
assert!(lifecycle_source.contains("#[derive(Eq, PartialEq)]\npub struct JobLifecycle"));
|
assert!(lifecycle_source.contains("#[derive(Eq, PartialEq)]\npub struct JobLifecycle"));
|
||||||
assert!(!lifecycle_source.contains("#[derive(Clone, Eq, PartialEq)]\npub struct JobLifecycle"));
|
assert!(!lifecycle_source.contains("#[derive(Clone, Eq, PartialEq)]\npub struct JobLifecycle"));
|
||||||
|
|||||||
81
crates/ksp-job-api/unit_tests/notification.rs
Normal file
81
crates/ksp-job-api/unit_tests/notification.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// file: crates/ksp-job-api/unit_tests/notification.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn sequence(value: u64) -> crate::JobNotificationSequence {
|
||||||
|
let mut sequence = crate::JobNotificationSequence::initial();
|
||||||
|
for _ in 0..value {
|
||||||
|
sequence = match sequence.next() {
|
||||||
|
std::result::Result::Ok(next) => next,
|
||||||
|
std::result::Result::Err(_) => return sequence,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_notification_sequence_advances_strictly_and_orders_positions() {
|
||||||
|
let mut first = crate::JobNotificationSequence::initial();
|
||||||
|
for _ in 0..41 {
|
||||||
|
let next = first.next();
|
||||||
|
assert!(next.is_ok());
|
||||||
|
first = match next {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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!(first.value(), 41);
|
||||||
|
assert_eq!(second.value(), 42);
|
||||||
|
assert!(second.is_after(first));
|
||||||
|
assert!(!first.is_after(second));
|
||||||
|
assert!(second > first);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_notification_sequence_exhaustion_is_explicit_and_non_wrapping() {
|
||||||
|
let exhausted = super::exhausted_notification_sequence().next();
|
||||||
|
assert!(exhausted.is_err());
|
||||||
|
let error = match exhausted {
|
||||||
|
std::result::Result::Err(value) => value,
|
||||||
|
std::result::Result::Ok(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_JOB_NOTIFICATION_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_003_notification_preserves_complete_value_and_redacts_snapshot_debug() {
|
||||||
|
let id = crate::JobId::new("job-notify-001");
|
||||||
|
let kind = crate::JobKindCode::new("backfill_raw");
|
||||||
|
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 notification =
|
||||||
|
crate::JobNotification::new(id.clone(), kind.clone(), sequence(7), crate::JobState::Running, "RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG".to_string());
|
||||||
|
assert_eq!(notification.id(), &id);
|
||||||
|
assert_eq!(notification.kind(), &kind);
|
||||||
|
assert_eq!(notification.sequence().value(), 7);
|
||||||
|
assert_eq!(notification.state(), crate::JobState::Running);
|
||||||
|
assert_eq!(notification.snapshot(), "RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG");
|
||||||
|
let debug = std::format!("{notification:?}");
|
||||||
|
assert!(debug.contains("JobNotification"));
|
||||||
|
assert!(debug.contains("<redacted>"));
|
||||||
|
assert!(!debug.contains("RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-program-api/src/program_instruction_decode.rs
|
// file: crates/ksp-program-api/src/program_instruction_decode.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/// Recognition strength reported by one Program instruction implementation.
|
/// Recognition strength reported by one Program instruction implementation.
|
||||||
///
|
///
|
||||||
@@ -38,10 +38,10 @@ pub enum ProgramInstructionDecodeOutcome<Decoded> {
|
|||||||
|
|
||||||
impl<Decoded> std::fmt::Debug for ProgramInstructionDecodeOutcome<Decoded> {
|
impl<Decoded> std::fmt::Debug for ProgramInstructionDecodeOutcome<Decoded> {
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
return match self {
|
||||||
Self::Decoded(_) => return formatter.write_str("Decoded"),
|
Self::Decoded(_) => formatter.write_str("Decoded"),
|
||||||
Self::Unsupported => return formatter.write_str("Unsupported"),
|
Self::Unsupported => formatter.write_str("Unsupported"),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
180
deltas/0.3.6/pre.003.md
Normal file
180
deltas/0.3.6/pre.003.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<!-- file: deltas/0.3.6/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.6-pre.003` — notifications Job latest-value génériques
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.6-pre.002-fix.001
|
||||||
|
workspace.package.version = 0.3.6-pre.2.fix.1
|
||||||
|
SHA-256 archive = 0b6640155786b92e27508637af2bcc7a0a34e5e8e4185d76ac4dd6d71aa7a869
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur fourni pour cette base confirme `cargo fmt`, audits Rust/Markdown, `cargo check --workspace`, Clippy, les dix tests unitaires et onze canaries `ksp-job-api`, les suites individuelles, `cargo test --workspace`, les arbres Cargo Job API et l'examen des doublons.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Matérialiser la tranche `pre.003` du plan 027 : observation Job latest-value runtime-neutral avec séquence monotone, snapshot complet courant, attente abstraite d'une valeur plus récente et resynchronisation multi-listeners, sans ouvrir encore `ksp-job-backfill-lib` ni imposer Tokio à l'API.
|
||||||
|
|
||||||
|
## Audit des archives et des règles
|
||||||
|
|
||||||
|
Les deux archives fournies ont été testées avant modification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP 0.3.6-pre.002-fix.001
|
||||||
|
SHA-256 = 0b6640155786b92e27508637af2bcc7a0a34e5e8e4185d76ac4dd6d71aa7a869
|
||||||
|
1541 entrées / aucune duplication / aucune traversée / aucun chemin absolu / aucun lien symbolique ZIP
|
||||||
|
unzip -t = propre
|
||||||
|
|
||||||
|
kbot3 v0.5.3-pre.005-fix010
|
||||||
|
SHA-256 = ee47643b9f8b582ee8db97b2381ec107e45aef8c009fee44757531514615d318
|
||||||
|
2501 entrées / aucune duplication / aucune traversée / aucun chemin absolu / aucun lien symbolique ZIP
|
||||||
|
unzip -t = propre
|
||||||
|
```
|
||||||
|
|
||||||
|
La base KSP ne contient ni `Cargo.lock`, ni `.env`, ni `target`, ni `node_modules`, ni secret ou artefact de build détecté par l'inventaire statique. L'archive kbot3 reste une référence historique et contient notamment un `scripts/__pycache__`; ce contenu n'est ni copié ni livré.
|
||||||
|
|
||||||
|
Les autorités relues incluent `RULES.md`, `docs/000-README.md`, les règles sous `docs/rules/`, les architectures Layers/Dependencies, Component Contracts, Dependency Graph et Acquisition/Workers/Jobs, le prompt 025, le plan 027, la validation 023 et les deltas `pre.001` à `pre.002-fix.001`.
|
||||||
|
|
||||||
|
Pour cette tranche, kbot3 confirme uniquement le besoin fonctionnel historique : `BackfillObserver::on_progress` appelait directement le consumer applicatif et exposait timestamp, niveau, message libre et compteurs optionnels. KSP conserve l'observabilité mais rejette cette forme callback/producteur ; aucun code, DTO, ID, message ou runtime kbot3 n'est repris.
|
||||||
|
|
||||||
|
## Surface ajoutée
|
||||||
|
|
||||||
|
`JobNotificationSequence` :
|
||||||
|
|
||||||
|
- position initiale contrôlée par l'API ;
|
||||||
|
- progression unitaire par `checked_add` ;
|
||||||
|
- ordre total et test `is_after` ;
|
||||||
|
- erreur stable `job_api.job_notification_sequence_exhausted` avant tout wrap.
|
||||||
|
|
||||||
|
`JobNotification<S>` :
|
||||||
|
|
||||||
|
- `JobId`, `JobKindCode`, séquence, `JobState` et snapshot complet ;
|
||||||
|
- champs privés et valeur immutable ;
|
||||||
|
- `Debug` structurel qui masque toujours le snapshot générique.
|
||||||
|
|
||||||
|
`JobSnapshotFuture<'a, S>` et `JobSnapshotSource` :
|
||||||
|
|
||||||
|
- futur abstrait fondé uniquement sur `std::future::Future`, `Pin` et `Box` ;
|
||||||
|
- lecture du snapshot courant ;
|
||||||
|
- attente d'une valeur plus récente après une séquence observée ;
|
||||||
|
- coalescence autorisée des progressions intermédiaires, avec resynchronisation sur le snapshot complet courant ;
|
||||||
|
- aucun callback producteur, channel Tokio, event log, Tauri ou historique public.
|
||||||
|
|
||||||
|
La source runtime concrète n'est pas créée dans Job API. Elle restera possédée par `ksp-job-backfill-lib` lorsque cette crate ouvrira ; les canaries de `pre.003` prouvent le contrat externe, sans annoncer comme acquises les propriétés runtime O(1), wake-up ou isolation du producteur avant ce consumer réel.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
Trois unitaires :
|
||||||
|
|
||||||
|
- progression stricte et ordre de séquence ;
|
||||||
|
- épuisement `u64::MAX` explicite sans wrap ;
|
||||||
|
- conservation de la valeur et redaction du snapshot en `Debug`.
|
||||||
|
|
||||||
|
Trois canaries externes :
|
||||||
|
|
||||||
|
- consommation de l'enveloppe depuis le crate-root ;
|
||||||
|
- deux listeners indépendants resynchronisés vers la dernière valeur après séquences anciennes différentes ;
|
||||||
|
- snapshot terminal encore lisible depuis un handle partagé.
|
||||||
|
|
||||||
|
Les canaries de dépendances et de complétude existantes sont étendues pour couvrir `notification.rs`, les nouveaux exports et l'absence de fuite Backfill/Worker/runtime.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-api/src/notification.rs
|
||||||
|
crates/ksp-job-api/tests/notifications.rs
|
||||||
|
crates/ksp-job-api/unit_tests/notification.rs
|
||||||
|
deltas/0.3.6/pre.003.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-api/src/error.rs
|
||||||
|
crates/ksp-job-api/src/lib.rs
|
||||||
|
crates/ksp-job-api/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-job-api/tests/release_completeness.rs
|
||||||
|
docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md
|
||||||
|
docs/validation/023-V0_3_6_JOB_API_BACKFILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Mécanique Cargo :
|
||||||
|
|
||||||
|
```text
|
||||||
|
header version: 396 -> 397
|
||||||
|
workspace.package.version: 0.3.6-pre.2.fix.1 -> 0.3.6-pre.3
|
||||||
|
workspace members: inchangés
|
||||||
|
workspace dependencies: inchangées
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
- une séquence publique ne se construit pas depuis un entier arbitraire ; elle part de `initial()` puis progresse via `next()` ;
|
||||||
|
- `JobNotification<S>` reste générique afin que Job API ne possède aucun snapshot Backfill prématuré ;
|
||||||
|
- le `Debug` de l'enveloppe ne demande pas `S: Debug` et ne rend jamais le snapshot ;
|
||||||
|
- le contrat d'attente expose un futur `std` boxed plutôt qu'un type Tokio ou un callback ;
|
||||||
|
- la sémantique publique est latest-value, pas une queue et pas un event log durable ;
|
||||||
|
- une séquence manquée n'exige aucun replay : le listener reprend le snapshot complet le plus récent ;
|
||||||
|
- la source concrète et son wake-up ne sont pas simulés comme fonctionnalité de production dans Job API ; ils seront prouvés avec le premier consumer réel ;
|
||||||
|
- `ksp-job-api` conserve exactement sa dépendance normale Core-only et aucune feature/dev-dependency/build-dependency n'est ajoutée ;
|
||||||
|
- aucun README/USAGE intermédiaire n'est créé avant la surface consommable réconciliée prévue par le plan.
|
||||||
|
|
||||||
|
## Validations exécutées
|
||||||
|
|
||||||
|
```text
|
||||||
|
unzip -tq des deux archives
|
||||||
|
-> propre
|
||||||
|
|
||||||
|
inventaire ZIP
|
||||||
|
-> KSP: 1541 entrées, 0 duplicate, 0 traversal, 0 absolute, 0 symlink
|
||||||
|
-> kbot3: 2501 entrées, 0 duplicate, 0 traversal, 0 absolute, 0 symlink
|
||||||
|
|
||||||
|
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/0.3.6
|
||||||
|
-> Markdown table audit: clean (264 tables / 144 fichiers)
|
||||||
|
|
||||||
|
python3 -m unittest scripts/tests/test_audit_markdown_tables.py
|
||||||
|
-> 5 tests / OK
|
||||||
|
|
||||||
|
inventaire statique ksp-job-api
|
||||||
|
-> 13 tests unitaires
|
||||||
|
-> 14 canaries d'intégration
|
||||||
|
```
|
||||||
|
|
||||||
|
L'inventaire différentiel base/travail confirme que seuls les fichiers listés par ce delta sont ajoutés ou modifiés, hors caches Python temporaires supprimés avant assemblage.
|
||||||
|
|
||||||
|
## Validations non exécutées
|
||||||
|
|
||||||
|
L'environnement d'assemblage ne fournit ni `cargo` ni `rustfmt`. Les commandes Rust de `pre.003` ne sont donc pas annoncées comme réussies localement.
|
||||||
|
|
||||||
|
Le smoke réseau et PostgreSQL ne concernent pas cette tranche API-only.
|
||||||
|
|
||||||
|
## 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/0.3.6
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-job-api
|
||||||
|
cargo tree -p ksp-job-api --edges normal
|
||||||
|
cargo tree -p ksp-job-api -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résultat attendu pour `ksp-job-api` après cette tranche est treize tests unitaires et quatorze canaries d'intégration, sans nouvelle dépendance normale ou feature.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque `pre.004` après un gate opérateur vert de `pre.003`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md -->
|
<!-- file: docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan v0.3.6 — Job API et premier backfill RAW
|
# Plan v0.3.6 — Job API et premier backfill RAW
|
||||||
|
|
||||||
@@ -400,15 +400,19 @@ La tranche ajoute dix tests unitaires et onze canaries d'intégration réparties
|
|||||||
|
|
||||||
#### `pre.002-fix.001` — Compilation des tests lifecycle
|
#### `pre.002-fix.001` — Compilation des tests lifecycle
|
||||||
|
|
||||||
**Statut : matérialisé ; gate Cargo opérateur à rejouer.**
|
**Statut : réalisé ; gate opérateur vert.**
|
||||||
|
|
||||||
Le helper de fixture est renommé `new_lifecycle()` et tous ses appels sont synchronisés afin d'éliminer le masquage lexical à l'origine des erreurs Rust `E0618` et `E0282`. Le correctif ne modifie ni l'API publique, ni les transitions, ni le nombre de tests. Comme un fichier Rust est corrigé, la version workspace devient `0.3.6-pre.2.fix.1`.
|
Le helper de fixture est renommé `new_lifecycle()` et tous ses appels sont synchronisés afin d'éliminer le masquage lexical à l'origine des erreurs Rust `E0618` et `E0282`. Le correctif ne modifie ni l'API publique, ni les transitions, ni le nombre de tests. Comme un fichier Rust est corrigé, la version workspace devient `0.3.6-pre.2.fix.1`. Le gate opérateur suivant confirme ensuite `cargo fmt`, audits Rust/Markdown, `cargo check`, Clippy, les dix tests unitaires, les onze canaries Job API, `cargo test --workspace` et les deux arbres Cargo Job API verts.
|
||||||
|
|
||||||
### `pre.003` — Notifications latest-value génériques
|
### `pre.003` — Notifications latest-value génériques
|
||||||
|
|
||||||
**Statut : planifié ; dépend du gate opérateur de `pre.002`.**
|
**Statut : matérialisé ; gate Cargo opérateur à rejouer.**
|
||||||
|
|
||||||
Budget cible : **15-20 min**. Entrée : transitions Job stables. Implémenter séquences, snapshot source et resynchronisation multi-listeners. Sortie : listener lent, terminal retenu et canary externe API verts.
|
Budget cible : **15-20 min**. Entrée : transitions Job stables et gate `pre.002-fix.001` vert. La tranche ajoute `JobNotificationSequence`, `JobNotification<S>`, `JobSnapshotFuture` et `JobSnapshotSource` sans nouvelle dépendance. La séquence est ordonnée, avance par `checked_add` et échoue explicitement avant tout wrap ; l'enveloppe reste immutable et son `Debug` masque le snapshot générique. Le trait source expose la valeur courante et une attente abstraite d'une valeur plus récente ; le futur public n'expose que `std::future::Future`, `Pin` et `Box`, jamais Tokio.
|
||||||
|
|
||||||
|
La sémantique est latest-value : le consumer peut perdre des progressions intermédiaires puis se resynchroniser sur un snapshot complet plus récent. Des canaries externes matérialisent deux listeners indépendants reprenant une séquence ancienne et la conservation d'un snapshot terminal par un handle partagé. Elles prouvent le contrat public ; l'implémentation concrète O(1), le réveil runtime et l'isolation du producteur restent à prouver avec `ksp-job-backfill-lib` dans la tranche d'intégration prévue, sans callback de type kbot3.
|
||||||
|
|
||||||
|
Aucune surface Backfill, Worker, Transport, Store, Logging, serde, Tauri ou Tokio n'entre dans `ksp-job-api`. La version workspace devient `0.3.6-pre.3`.
|
||||||
|
|
||||||
### `pre.004` — Provenance Transport observée
|
### `pre.004` — Provenance Transport observée
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/023-V0_3_6_JOB_API_BACKFILL.md -->
|
<!-- file: docs/validation/023-V0_3_6_JOB_API_BACKFILL.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Validation v0.3.6 — Job API et premier backfill RAW
|
# Validation v0.3.6 — Job API et premier backfill RAW
|
||||||
|
|
||||||
@@ -49,7 +49,8 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
|
|||||||
- [X] Gate opérateur de `pre.002` : `cargo fmt`, audits Rust/Markdown, `cargo check --workspace` et arbres Cargo normal/features verts.
|
- [X] Gate opérateur de `pre.002` : `cargo fmt`, audits Rust/Markdown, `cargo check --workspace` et arbres Cargo normal/features verts.
|
||||||
- [X] Échec de `pre.002` constaté dans la preuve opérateur : Clippy `--all-targets` et les tests compilent `unit_tests/lifecycle.rs`, où le helper `lifecycle()` est masqué par une variable homonyme (`E0618`, puis `E0282`).
|
- [X] Échec de `pre.002` constaté dans la preuve opérateur : Clippy `--all-targets` et les tests compilent `unit_tests/lifecycle.rs`, où le helper `lifecycle()` est masqué par une variable homonyme (`E0618`, puis `E0282`).
|
||||||
- [X] `pre.002-fix.001` renomme uniquement le helper en `new_lifecycle()` et synchronise ses six appels sans modifier le contrat public ni la matrice de transitions.
|
- [X] `pre.002-fix.001` renomme uniquement le helper en `new_lifecycle()` et synchronise ses six appels sans modifier le contrat public ni la matrice de transitions.
|
||||||
- [X] L'environnement courant ne fournit pas l'exécutable `cargo` ; aucune validation Rust locale n'est annoncée comme exécutée.
|
- [X] Gate opérateur de `pre.002-fix.001` : `cargo fmt`, audits Rust/Markdown, `cargo check`, Clippy, dix unitaires + onze canaries `ksp-job-api`, toutes les suites workspace, `cargo test --workspace` et arbres Cargo Job API verts.
|
||||||
|
- [X] L'environnement courant ne fournit pas l'exécutable `cargo` ; aucune validation Rust locale de `pre.003` n'est annoncée comme exécutée.
|
||||||
- [X] Le workspace compte 14 crates et aucune crate Job au point de départ.
|
- [X] Le workspace compte 14 crates et aucune crate Job au point de départ.
|
||||||
|
|
||||||
## 4. Autorités et cohérence documentaire
|
## 4. Autorités et cohérence documentaire
|
||||||
@@ -70,15 +71,17 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
|
|||||||
- [X] Transitions `Created`, `Running`, `Cancelling`, `Completed`, `Cancelled` et `Failed` contrôlées.
|
- [X] Transitions `Created`, `Running`, `Cancelling`, `Completed`, `Cancelled` et `Failed` contrôlées.
|
||||||
- [X] États terminaux immuables et lifecycle producteur non cloneable.
|
- [X] États terminaux immuables et lifecycle producteur non cloneable.
|
||||||
- [X] `JobCancellationToken` cloneable, idempotent, `Send + Sync` et sans Tokio public.
|
- [X] `JobCancellationToken` cloneable, idempotent, `Send + Sync` et sans Tokio public.
|
||||||
- [ ] `JobNotificationSequence` strictement monotone avec overflow traité sans panic.
|
- [X] `JobNotificationSequence` strictement monotone avec overflow traité sans panic.
|
||||||
- [ ] `JobNotification<S>` contient un snapshot complet, sûr et borné.
|
- [X] `JobNotification<S>` contient identité, kind, séquence, état et snapshot complet ; son `Debug` masque le snapshot et le contrat impose au snapshot concret de rester sûr et borné.
|
||||||
- [ ] `JobSnapshotSource` permet lecture courante et attente de changement sans callback producteur.
|
- [X] `JobSnapshotSource` permet lecture courante et attente abstraite d'une valeur plus récente sans callback producteur ni type Tokio public.
|
||||||
- [X] Aucun type Transport, Store, Logging, Tokio, serde, Worker ou Backfill dans l'API publique de `pre.002`.
|
- [X] Aucun type Transport, Store, Logging, Tokio, serde, Worker ou Backfill dans l'API publique de `pre.003`; le futur abstrait utilise uniquement `std`.
|
||||||
- [X] Canaries externes de construction, lifecycle, annulation, firewalls, complétude et sécurité matérialisées.
|
- [X] Canaries externes de construction, lifecycle, annulation, notifications latest-value, firewalls, complétude et sécurité matérialisées.
|
||||||
- [ ] Dix unitaires et onze canaries Rust exécutés avec `cargo test -p ksp-job-api` par l'opérateur.
|
- [X] Dix unitaires et onze canaries de `pre.002-fix.001` exécutés avec `cargo test -p ksp-job-api` par l'opérateur.
|
||||||
|
|
||||||
## 6. Notifications multi-listeners
|
## 6. Notifications multi-listeners
|
||||||
|
|
||||||
|
`pre.003` fige le contrat public et ses canaries de resynchronisation. Les cases portant sur le stockage/réveil concret restent volontairement ouvertes jusqu'au producer `ksp-job-backfill-lib` ; un fake de test n'est pas traité comme preuve runtime de production.
|
||||||
|
|
||||||
- [ ] Stockage O(1) de la dernière valeur, sans historique non borné.
|
- [ ] Stockage O(1) de la dernière valeur, sans historique non borné.
|
||||||
- [ ] Un listener lent ne bloque jamais le producteur.
|
- [ ] Un listener lent ne bloque jamais le producteur.
|
||||||
- [ ] Plusieurs listeners progressent indépendamment.
|
- [ ] Plusieurs listeners progressent indépendamment.
|
||||||
@@ -179,8 +182,8 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
|
|||||||
- [ ] Aucun retour de dépendance vers Job depuis les couches basses.
|
- [ ] Aucun retour de dépendance vers Job depuis les couches basses.
|
||||||
- [ ] Versions et features des nouvelles dépendances auditées avant ajout.
|
- [ ] Versions et features des nouvelles dépendances auditées avant ajout.
|
||||||
- [ ] `cargo fmt --all -- --check` vert.
|
- [ ] `cargo fmt --all -- --check` vert.
|
||||||
- [X] `scripts/audit_rust_workspace_rules.py` vert sur `pre.002-fix.001` dans l'environnement d'assemblage.
|
- [X] `scripts/audit_rust_workspace_rules.py` vert sur `pre.003` dans l'environnement d'assemblage.
|
||||||
- [X] `scripts/audit_markdown_tables.py` vert sur `pre.002-fix.001` dans l'environnement d'assemblage.
|
- [X] `scripts/audit_markdown_tables.py` vert sur `pre.003` dans l'environnement d'assemblage (264 tables / 144 fichiers).
|
||||||
- [X] Contrats de fichiers de `FILE_CONTRACTS.md` vérifiés sur chaque ajout de `pre.001`.
|
- [X] Contrats de fichiers de `FILE_CONTRACTS.md` vérifiés sur chaque ajout de `pre.001`.
|
||||||
- [ ] `cargo check --workspace` vert.
|
- [ ] `cargo check --workspace` vert.
|
||||||
- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` vert.
|
- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` vert.
|
||||||
|
|||||||
Reference in New Issue
Block a user