v0.3.9-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 479
|
||||
# version: 480
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.9-pre.2.fix.2"
|
||||
version = "0.3.9-pre.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
67
crates/ksp-worker-api/README.md
Normal file
67
crates/ksp-worker-api/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
<!-- file: crates/ksp-worker-api/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# ksp-worker-api
|
||||
|
||||
`ksp-worker-api` fournit les contrats passifs et runtime-neutral communs aux services continus KSP.
|
||||
|
||||
La crate possède l'identité logique d'un Worker, son lifecycle continu, une classification minimale de health/activity, l'intention de stop coopératif et un contrat latest-value fixe pour l'observation. Elle ne possède aucun runtime concret, aucune politique de restart, aucun Job, aucun Transport, aucun Store et aucun contrat Solana.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
La façade crate-root expose :
|
||||
|
||||
- `WorkerId` et `WorkerKindCode`, bornés et validés ;
|
||||
- `WorkerState`, `WorkerHealth` et `WorkerActivity` ;
|
||||
- `WorkerLifecycle`, propriétaire des transitions admises ;
|
||||
- `WorkerStopToken`, cloneable et idempotent ;
|
||||
- `WorkerSnapshotSequence`, strictement monotone et sans wrap silencieux ;
|
||||
- `WorkerSnapshot`, forme commune fixe sans payload métier ;
|
||||
- `WorkerSnapshotSource`, contrat object-safe de lecture courante et attente d'une valeur plus récente ;
|
||||
- les codes d'erreur Worker et les types `Error`/`Result` communs de Core.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Le lifecycle admis reste explicitement borné :
|
||||
|
||||
```text
|
||||
Created -> Starting | Stopped
|
||||
Starting -> Running | Stopping | Faulted(ErrorCode)
|
||||
Running -> Stopping | Faulted(ErrorCode)
|
||||
Stopping -> Stopped | Faulted(ErrorCode)
|
||||
Stopped -> terminal
|
||||
Faulted -> terminal
|
||||
```
|
||||
|
||||
`Stopped` et `Faulted` sont terminaux et immuables. Une transition invalide retourne `ERROR_CODE_WORKER_TRANSITION_INVALID` sans modifier l'état source.
|
||||
|
||||
## Observation latest-value
|
||||
|
||||
`WorkerSnapshotSource` n'impose ni callback, ni queue d'événements, ni runtime async particulier. Un listener lit d'abord `current()`, mémorise la `WorkerSnapshotSequence`, puis appelle `wait_for_change()` s'il doit attendre une valeur plus récente.
|
||||
|
||||
Les mises à jour intermédiaires peuvent être coalescées : le contrat porte sur la dernière valeur complète, pas sur la livraison de chaque événement. Le snapshot commun ne contient que l'identité, la séquence, le lifecycle, la health et l'activity.
|
||||
|
||||
## Stop
|
||||
|
||||
`WorkerStopToken` représente uniquement une intention coopérative partagée. Il ne tue pas une tâche, ne ferme pas un socket et ne décide pas du résultat terminal. Le runtime concret observe cette intention puis pilote `WorkerLifecycle` selon sa politique de shutdown.
|
||||
|
||||
## Restart et contrôle
|
||||
|
||||
La crate ne possède aucun `restart()`, scheduler, retry/backoff, process manager ou handle runtime générique. Un lifecycle/source terminal n'est jamais réanimé ni rebinding vers une nouvelle exécution. La recréation et la supervision appartiennent au caller ou à une couche de contrôle supérieure.
|
||||
|
||||
## Firewall
|
||||
|
||||
La dépendance normale est volontairement minimale :
|
||||
|
||||
```text
|
||||
ksp-worker-api
|
||||
-> ksp-core-lib
|
||||
```
|
||||
|
||||
La crate ne dépend pas de `ksp-job-api`, Tokio, Futures, serde, Logging, Config, Interface, Transport, Store, Tauri ou d'un SDK provider.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — utilisation durable des contrats Worker ;
|
||||
- [`../../docs/architecture/003-COMPONENT_CONTRACTS.md`](../../docs/architecture/003-COMPONENT_CONTRACTS.md) — contrats de composants ;
|
||||
- [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) — séparation Worker/Job et ownership d'acquisition.
|
||||
141
crates/ksp-worker-api/USAGE.md
Normal file
141
crates/ksp-worker-api/USAGE.md
Normal file
@@ -0,0 +1,141 @@
|
||||
<!-- file: crates/ksp-worker-api/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de ksp-worker-api
|
||||
|
||||
Cette page décrit la façade publique durable de `ksp-worker-api`. Les consumers utilisent uniquement les exports du crate-root.
|
||||
|
||||
## Construire une identité Worker
|
||||
|
||||
```rust
|
||||
fn worker_identity() -> ksp_worker_api::Result<(ksp_worker_api::WorkerId, ksp_worker_api::WorkerKindCode)> {
|
||||
let id = match ksp_worker_api::WorkerId::new("raw-ingest-mainnet-0001") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("raw_transaction_ingest") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok((id, kind));
|
||||
}
|
||||
```
|
||||
|
||||
`WorkerId` identifie une instance logique observée par un lifecycle/source donné. `WorkerKindCode` identifie une famille de Workers. Les deux sont bornés et utilisent un alphabet sûr. Le `Debug` de `WorkerId` masque sa valeur.
|
||||
|
||||
## Piloter un lifecycle passif
|
||||
|
||||
```rust
|
||||
fn start_worker(id: ksp_worker_api::WorkerId, kind: ksp_worker_api::WorkerKindCode) -> ksp_worker_api::Result<ksp_worker_api::WorkerLifecycle> {
|
||||
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
||||
if let std::result::Result::Err(error) = lifecycle.start() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = lifecycle.mark_running() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(lifecycle);
|
||||
}
|
||||
```
|
||||
|
||||
Le producer possède l'autorité de transition. Il ne force jamais un état directement. Une transition invalide retourne une erreur stable et conserve l'état courant.
|
||||
|
||||
Pour un shutdown coopératif après observation du token :
|
||||
|
||||
```rust
|
||||
if let std::result::Result::Err(error) = lifecycle.mark_stopping() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = lifecycle.mark_stopped() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
```
|
||||
|
||||
Pour un fault terminal :
|
||||
|
||||
```rust
|
||||
const IO_FAULT: ksp_worker_api::ErrorCode = ksp_worker_api::ErrorCode::new("example_worker", "io_fault");
|
||||
if let std::result::Result::Err(error) = lifecycle.fault(IO_FAULT) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
```
|
||||
|
||||
`Stopped` et `Faulted(ErrorCode)` sont terminaux. Un ancien lifecycle terminal ne représente jamais une nouvelle exécution.
|
||||
|
||||
## Distinguer lifecycle, health et activity
|
||||
|
||||
`WorkerState` décrit la phase du service. `WorkerHealth` décrit sa qualité opérationnelle. `WorkerActivity` indique seulement `Unknown`, `Idle` ou `Active`.
|
||||
|
||||
```rust
|
||||
let snapshot = ksp_worker_api::WorkerSnapshot::new(
|
||||
lifecycle.id().clone(),
|
||||
lifecycle.kind().clone(),
|
||||
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
||||
lifecycle.state(),
|
||||
ksp_worker_api::WorkerHealth::Healthy,
|
||||
ksp_worker_api::WorkerActivity::Idle,
|
||||
);
|
||||
```
|
||||
|
||||
Le snapshot commun ne porte ni pourcentage, ni total, ni backlog, ni slot, ni transaction, ni métrique métier. Une API Worker concrète peut exposer séparément ses propres métriques.
|
||||
|
||||
## Partager une intention de stop
|
||||
|
||||
```rust
|
||||
let token = ksp_worker_api::WorkerStopToken::new();
|
||||
let listener = token.clone();
|
||||
|
||||
assert!(!listener.is_stop_requested());
|
||||
assert!(token.request_stop());
|
||||
assert!(listener.is_stop_requested());
|
||||
assert!(!token.request_stop());
|
||||
```
|
||||
|
||||
Le premier appel qui change l'intention retourne `true`. Les demandes suivantes sont idempotentes et retournent `false`.
|
||||
|
||||
Le token n'est pas une primitive de kill et ne garantit aucun délai de shutdown. Timeout, drain, join et retry appartiennent au runtime/caller.
|
||||
|
||||
## Observer un snapshot latest-value
|
||||
|
||||
Un consumer portable peut travailler directement avec le trait object-safe :
|
||||
|
||||
```rust
|
||||
async fn observe(source: &dyn ksp_worker_api::WorkerSnapshotSource) {
|
||||
let current = source.current();
|
||||
let observed = current.sequence();
|
||||
let newer = source.wait_for_change(observed).await;
|
||||
assert!(newer.sequence().is_after(observed));
|
||||
}
|
||||
```
|
||||
|
||||
`wait_for_change` retourne la dernière valeur complète disponible après coalescence éventuelle. Un consumer ne doit pas supposer qu'il recevra chaque mise à jour intermédiaire.
|
||||
|
||||
Un listener tardif commence par `current()`. Tant que la source existe, son snapshot terminal courant reste lisible.
|
||||
|
||||
## Faire avancer une séquence
|
||||
|
||||
```rust
|
||||
let first = ksp_worker_api::WorkerSnapshotSequence::initial();
|
||||
let second = match first.next() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(second.is_after(first));
|
||||
```
|
||||
|
||||
L'épuisement de `u64` est une erreur explicite ; la séquence ne wrappe jamais silencieusement.
|
||||
|
||||
## Frontières à respecter
|
||||
|
||||
Ne pas ajouter à `ksp-worker-api` :
|
||||
|
||||
```text
|
||||
runtime Tokio/Futures concret
|
||||
Job lifecycle ou checkpoint/backfill
|
||||
Transport, Store, Config ou Logging
|
||||
DTO Solana/provider
|
||||
restart/retry/scheduler/process manager
|
||||
payload métier dans WorkerSnapshot
|
||||
```
|
||||
|
||||
Ces responsabilités appartiennent aux Workers concrets et aux couches de composition/contrôle supérieures.
|
||||
161
crates/ksp-worker-api/tests/release_completeness.rs
Normal file
161
crates/ksp-worker-api/tests/release_completeness.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: crates/ksp-worker-api/tests/release_completeness.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact frozen-surface and domain-firewall canaries for `ksp-worker-api`.
|
||||
|
||||
#[test]
|
||||
fn pre_003_crate_root_export_inventory_is_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for line in crate_root.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("pub use ") {
|
||||
actual.push(trimmed);
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
let mut expected = std::vec![
|
||||
"pub use self::error::ERROR_CODE_WORKER_ID_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_KIND_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_TRANSITION_INVALID;",
|
||||
"pub use self::identity::MAX_WORKER_ID_BYTES;",
|
||||
"pub use self::identity::MAX_WORKER_KIND_CODE_BYTES;",
|
||||
"pub use self::identity::WorkerId;",
|
||||
"pub use self::identity::WorkerKindCode;",
|
||||
"pub use self::lifecycle::WorkerActivity;",
|
||||
"pub use self::lifecycle::WorkerHealth;",
|
||||
"pub use self::lifecycle::WorkerLifecycle;",
|
||||
"pub use self::lifecycle::WorkerState;",
|
||||
"pub use self::snapshot::WorkerSnapshot;",
|
||||
"pub use self::snapshot::WorkerSnapshotFuture;",
|
||||
"pub use self::snapshot::WorkerSnapshotSequence;",
|
||||
"pub use self::snapshot::WorkerSnapshotSource;",
|
||||
"pub use self::stop::WorkerStopToken;",
|
||||
"pub use ksp_core_lib::Error;",
|
||||
"pub use ksp_core_lib::ErrorCode;",
|
||||
"pub use ksp_core_lib::ErrorContext;",
|
||||
"pub use ksp_core_lib::Result;",
|
||||
];
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 entries = match std::fs::read_dir(source_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut names = std::vec::Vec::new();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = match entry.file_name().into_string() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
if name.ends_with(".rs") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, std::vec!["error.rs", "identity.rs", "lib.rs", "lifecycle.rs", "snapshot.rs", "stop.rs"]);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fixed_snapshot_shape_contains_only_common_worker_dimensions() {
|
||||
let snapshot = include_str!("../src/snapshot.rs");
|
||||
let struct_start = match snapshot.find("pub struct WorkerSnapshot {") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let after_start = &snapshot[struct_start..];
|
||||
let struct_end = match after_start.find("\n}") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let definition = &after_start[..struct_end];
|
||||
for required in ["id:", "kind:", "sequence:", "state:", "health:", "activity:"] {
|
||||
assert!(definition.contains(required), "missing frozen Worker snapshot field: {required}");
|
||||
}
|
||||
for forbidden in ["String", "Vec<", "payload", "slot", "signature", "provider", "endpoint", "checkpoint", "backfill", "transaction"] {
|
||||
assert!(!definition.contains(forbidden), "domain or arbitrary payload leaked into frozen Worker snapshot: {forbidden}");
|
||||
}
|
||||
let fields = definition.lines().filter(|line| {
|
||||
let line = line.trim_start();
|
||||
return line.starts_with("id:")
|
||||
|| line.starts_with("kind:")
|
||||
|| line.starts_with("sequence:")
|
||||
|| line.starts_with("state:")
|
||||
|| line.starts_with("health:")
|
||||
|| line.starts_with("activity:");
|
||||
});
|
||||
assert_eq!(fields.count(), 6);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_surface_remains_runtime_neutral_job_independent_and_domain_free() {
|
||||
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 [
|
||||
"Backfill",
|
||||
"RawTransaction",
|
||||
"WorkerHandle",
|
||||
"checkpoint",
|
||||
"endpoint",
|
||||
"futures::",
|
||||
"futures_util::",
|
||||
"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::",
|
||||
"provider",
|
||||
"reqwest::",
|
||||
"serde::",
|
||||
"serde_json::",
|
||||
"slot",
|
||||
"solana_",
|
||||
"tauri::",
|
||||
"tokio::",
|
||||
"tonic::",
|
||||
concat!("tracing", "::"),
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "forbidden frozen Worker API concern detected: {forbidden}");
|
||||
}
|
||||
}
|
||||
let snapshot = include_str!("../src/snapshot.rs");
|
||||
assert!(snapshot.contains("pub trait WorkerSnapshotSource: std::marker::Send + std::marker::Sync"));
|
||||
assert!(snapshot.contains("std::future::Future"));
|
||||
assert!(!snapshot.contains("std::sync::mpsc"));
|
||||
assert!(!snapshot.contains("VecDeque"));
|
||||
let lifecycle = include_str!("../src/lifecycle.rs");
|
||||
assert!(lifecycle.contains("#[derive(Eq, PartialEq)]\npub struct WorkerLifecycle"));
|
||||
assert!(!lifecycle.contains("#[derive(Clone, Eq, PartialEq)]\npub struct WorkerLifecycle"));
|
||||
return;
|
||||
}
|
||||
185
crates/ksp-worker-api/tests/security_hardening.rs
Normal file
185
crates/ksp-worker-api/tests/security_hardening.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
// file: crates/ksp-worker-api/tests/security_hardening.rs
|
||||
// version: 1
|
||||
|
||||
//! Adversarial lifecycle, stop and redaction canaries for the frozen Worker API.
|
||||
|
||||
const HOSTILE_MARKER: &str = "WORKER-IDENTITY-SECRET-CANARY";
|
||||
const TEST_FAULT: ksp_worker_api::ErrorCode = ksp_worker_api::ErrorCode::new("worker_test", "fault");
|
||||
|
||||
fn running_lifecycle(id_value: &str) -> std::option::Option<ksp_worker_api::WorkerLifecycle> {
|
||||
let id = match ksp_worker_api::WorkerId::new(id_value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("continuous_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
||||
if lifecycle.start().is_err() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
if lifecycle.mark_running().is_err() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return std::option::Option::Some(lifecycle);
|
||||
}
|
||||
|
||||
fn assert_all_mutators_reject(lifecycle: &mut ksp_worker_api::WorkerLifecycle, terminal: ksp_worker_api::WorkerState) {
|
||||
assert!(lifecycle.start().is_err());
|
||||
assert!(lifecycle.mark_running().is_err());
|
||||
assert!(lifecycle.mark_stopping().is_err());
|
||||
assert!(lifecycle.mark_stopped().is_err());
|
||||
assert!(lifecycle.fault(TEST_FAULT).is_err());
|
||||
assert_eq!(lifecycle.state(), terminal);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_worker_identity_lifecycle_and_snapshot_debug_redact_hostile_identity() {
|
||||
let lifecycle = running_lifecycle(HOSTILE_MARKER);
|
||||
assert!(lifecycle.is_some());
|
||||
let lifecycle = match lifecycle {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let id = lifecycle.id();
|
||||
assert_eq!(std::format!("{id:?}"), "WorkerId(..)");
|
||||
let lifecycle_debug = std::format!("{lifecycle:?}");
|
||||
assert!(lifecycle_debug.contains("WorkerLifecycle"));
|
||||
assert!(lifecycle_debug.contains("continuous_worker"));
|
||||
assert!(lifecycle_debug.contains("Running"));
|
||||
assert!(!lifecycle_debug.contains(HOSTILE_MARKER));
|
||||
let snapshot = ksp_worker_api::WorkerSnapshot::new(
|
||||
lifecycle.id().clone(),
|
||||
lifecycle.kind().clone(),
|
||||
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
||||
lifecycle.state(),
|
||||
ksp_worker_api::WorkerHealth::Healthy,
|
||||
ksp_worker_api::WorkerActivity::Active,
|
||||
);
|
||||
let snapshot_debug = std::format!("{snapshot:?}");
|
||||
assert!(snapshot_debug.contains("WorkerSnapshot"));
|
||||
assert!(snapshot_debug.contains("continuous_worker"));
|
||||
assert!(snapshot_debug.contains("Healthy"));
|
||||
assert!(snapshot_debug.contains("Active"));
|
||||
assert!(!snapshot_debug.contains(HOSTILE_MARKER));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_identity_errors_never_echo_hostile_values() {
|
||||
let hostile = "WORKER-SECRET/CANARY";
|
||||
let rejected = ksp_worker_api::WorkerId::new(hostile);
|
||||
assert!(rejected.is_err());
|
||||
let error = match rejected {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID);
|
||||
assert!(!std::format!("{error}").contains(hostile));
|
||||
assert!(!std::format!("{error:?}").contains(hostile));
|
||||
for context in error.context() {
|
||||
assert!(!context.value().contains(hostile));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stopped_and_faulted_are_immutable_under_all_public_mutators() {
|
||||
let id = match ksp_worker_api::WorkerId::new("terminal-created") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("terminal_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut stopped = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
||||
assert!(stopped.mark_stopped().is_ok());
|
||||
assert_all_mutators_reject(&mut stopped, ksp_worker_api::WorkerState::Stopped);
|
||||
let faulted = running_lifecycle("terminal-faulted");
|
||||
assert!(faulted.is_some());
|
||||
let mut faulted = match faulted {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(faulted.fault(TEST_FAULT).is_ok());
|
||||
assert_all_mutators_reject(&mut faulted, ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stop_fault_terminal_orders_are_first_valid_terminal_wins() {
|
||||
let direct_fault = running_lifecycle("race-direct-fault");
|
||||
assert!(direct_fault.is_some());
|
||||
let mut direct_fault = match direct_fault {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(direct_fault.fault(TEST_FAULT).is_ok());
|
||||
assert!(direct_fault.mark_stopping().is_err());
|
||||
assert_eq!(direct_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
let stopping_fault = running_lifecycle("race-stopping-fault");
|
||||
assert!(stopping_fault.is_some());
|
||||
let mut stopping_fault = match stopping_fault {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(stopping_fault.mark_stopping().is_ok());
|
||||
assert!(stopping_fault.fault(TEST_FAULT).is_ok());
|
||||
assert!(stopping_fault.mark_stopped().is_err());
|
||||
assert_eq!(stopping_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
let stopping_stopped = running_lifecycle("race-stopping-stopped");
|
||||
assert!(stopping_stopped.is_some());
|
||||
let mut stopping_stopped = match stopping_stopped {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(stopping_stopped.mark_stopping().is_ok());
|
||||
assert!(stopping_stopped.mark_stopped().is_ok());
|
||||
assert!(stopping_stopped.fault(TEST_FAULT).is_err());
|
||||
assert_eq!(stopping_stopped.state(), ksp_worker_api::WorkerState::Stopped);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stop_token_is_send_sync_cross_thread_and_cannot_reanimate_terminal_lifecycle() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
||||
let lifecycle = running_lifecycle("cross-thread-stop");
|
||||
assert!(lifecycle.is_some());
|
||||
let mut lifecycle = match lifecycle {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(lifecycle.fault(TEST_FAULT).is_ok());
|
||||
let token = ksp_worker_api::WorkerStopToken::new();
|
||||
let worker_token = token.clone();
|
||||
let thread = std::thread::spawn(move || return worker_token.request_stop());
|
||||
let first = match thread.join() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(first);
|
||||
assert!(token.is_stop_requested());
|
||||
assert!(!token.request_stop());
|
||||
assert!(lifecycle.mark_stopping().is_err());
|
||||
assert_eq!(lifecycle.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_public_worker_primitives_keep_promised_send_sync_contracts() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<ksp_worker_api::WorkerId>();
|
||||
require_send_sync::<ksp_worker_api::WorkerKindCode>();
|
||||
require_send_sync::<ksp_worker_api::WorkerState>();
|
||||
require_send_sync::<ksp_worker_api::WorkerHealth>();
|
||||
require_send_sync::<ksp_worker_api::WorkerActivity>();
|
||||
require_send_sync::<ksp_worker_api::WorkerLifecycle>();
|
||||
require_send_sync::<ksp_worker_api::WorkerSnapshotSequence>();
|
||||
require_send_sync::<ksp_worker_api::WorkerSnapshot>();
|
||||
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
||||
return;
|
||||
}
|
||||
219
crates/ksp-worker-api/tests/snapshot_source.rs
Normal file
219
crates/ksp-worker-api/tests/snapshot_source.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
// file: crates/ksp-worker-api/tests/snapshot_source.rs
|
||||
// version: 1
|
||||
|
||||
//! External std-only latest-value source, object-safety and resynchronization canaries.
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestSnapshotSource {
|
||||
state: std::sync::Arc<TestState>,
|
||||
}
|
||||
|
||||
struct TestState {
|
||||
current: std::sync::Mutex<TestInner>,
|
||||
}
|
||||
|
||||
struct TestInner {
|
||||
snapshot: ksp_worker_api::WorkerSnapshot,
|
||||
waiters: std::vec::Vec<std::task::Waker>,
|
||||
}
|
||||
|
||||
impl TestSnapshotSource {
|
||||
fn new(snapshot: ksp_worker_api::WorkerSnapshot) -> Self {
|
||||
return Self {
|
||||
state: std::sync::Arc::new(TestState { current: std::sync::Mutex::new(TestInner { snapshot, waiters: std::vec::Vec::new() }) }),
|
||||
};
|
||||
}
|
||||
|
||||
fn publish(&self, state: ksp_worker_api::WorkerState, health: ksp_worker_api::WorkerHealth, activity: ksp_worker_api::WorkerActivity) -> bool {
|
||||
let mut current = match self.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
let sequence = match current.snapshot.sequence().next() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let id = current.snapshot.id().clone();
|
||||
let kind = current.snapshot.kind().clone();
|
||||
current.snapshot = ksp_worker_api::WorkerSnapshot::new(id, kind, sequence, state, health, activity);
|
||||
let waiters = std::mem::take(&mut current.waiters);
|
||||
std::mem::drop(current);
|
||||
for waiter in waiters {
|
||||
waiter.wake();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
struct TestWaitFuture<'a> {
|
||||
source: &'a TestSnapshotSource,
|
||||
observed: ksp_worker_api::WorkerSnapshotSequence,
|
||||
}
|
||||
|
||||
impl std::future::Future for TestWaitFuture<'_> {
|
||||
type Output = ksp_worker_api::WorkerSnapshot;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
|
||||
let mut current = match self.source.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
if current.snapshot.sequence().is_after(self.observed) {
|
||||
return std::task::Poll::Ready(current.snapshot.clone());
|
||||
}
|
||||
if !current.waiters.iter().any(|registered| return registered.will_wake(context.waker())) {
|
||||
current.waiters.push(context.waker().clone());
|
||||
}
|
||||
return std::task::Poll::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_worker_api::WorkerSnapshotSource for TestSnapshotSource {
|
||||
fn current(&self) -> ksp_worker_api::WorkerSnapshot {
|
||||
let current = match self.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
return current.snapshot.clone();
|
||||
}
|
||||
|
||||
fn wait_for_change(&self, observed: ksp_worker_api::WorkerSnapshotSequence) -> ksp_worker_api::WorkerSnapshotFuture<'_> {
|
||||
return std::boxed::Box::pin(TestWaitFuture { source: self, observed });
|
||||
}
|
||||
}
|
||||
|
||||
struct WakeProbe {
|
||||
woken: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl WakeProbe {
|
||||
fn new() -> Self {
|
||||
return Self { woken: std::sync::atomic::AtomicBool::new(false) };
|
||||
}
|
||||
|
||||
fn is_woken(&self) -> bool {
|
||||
return self.woken.load(std::sync::atomic::Ordering::Acquire);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::task::Wake for WakeProbe {
|
||||
fn wake(self: std::sync::Arc<Self>) {
|
||||
self.woken.store(true, std::sync::atomic::Ordering::Release);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_snapshot(future: &mut ksp_worker_api::WorkerSnapshotFuture<'_>, wake: &std::sync::Arc<WakeProbe>) -> std::task::Poll<ksp_worker_api::WorkerSnapshot> {
|
||||
let waker = std::task::Waker::from(wake.clone());
|
||||
let mut context = std::task::Context::from_waker(&waker);
|
||||
return std::future::Future::poll(future.as_mut(), &mut context);
|
||||
}
|
||||
|
||||
fn initial_snapshot() -> std::option::Option<ksp_worker_api::WorkerSnapshot> {
|
||||
let id = match ksp_worker_api::WorkerId::new("external-source-001") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("external_test_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return std::option::Option::Some(ksp_worker_api::WorkerSnapshot::new(
|
||||
id,
|
||||
kind,
|
||||
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
||||
ksp_worker_api::WorkerState::Running,
|
||||
ksp_worker_api::WorkerHealth::Healthy,
|
||||
ksp_worker_api::WorkerActivity::Idle,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_snapshot_source_is_object_safe_send_sync_and_externally_implementable() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<TestSnapshotSource>();
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let object: &dyn ksp_worker_api::WorkerSnapshotSource = &source;
|
||||
let current = object.current();
|
||||
assert_eq!(current.sequence().value(), 0);
|
||||
let mut wait = object.wait_for_change(current.sequence());
|
||||
fn require_send<T: std::marker::Send>(_: &T) {}
|
||||
require_send(&wait);
|
||||
let wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut wait, &wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(wake.is_woken());
|
||||
let changed = poll_snapshot(&mut wait, &wake);
|
||||
assert!(matches!(changed, std::task::Poll::Ready(_)));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_slow_and_independent_listeners_coalesce_to_latest_value() {
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let observed = ksp_worker_api::WorkerSnapshotSource::current(&source).sequence();
|
||||
let mut slow = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, observed);
|
||||
let mut fast = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, observed);
|
||||
let slow_wake = std::sync::Arc::new(WakeProbe::new());
|
||||
let fast_wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut slow, &slow_wake), std::task::Poll::Pending));
|
||||
assert!(matches!(poll_snapshot(&mut fast, &fast_wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Degraded, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(slow_wake.is_woken());
|
||||
assert!(fast_wake.is_woken());
|
||||
let fast_value = match poll_snapshot(&mut fast, &fast_wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(fast_value.sequence().value(), 1);
|
||||
assert_eq!(fast_value.health(), ksp_worker_api::WorkerHealth::Degraded);
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
let slow_value = match poll_snapshot(&mut slow, &slow_wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(slow_value.sequence().value(), 2);
|
||||
assert_eq!(slow_value.health(), ksp_worker_api::WorkerHealth::Healthy);
|
||||
assert_eq!(slow_value.activity(), ksp_worker_api::WorkerActivity::Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_late_listener_resynchronizes_and_terminal_snapshot_remains_current() {
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Degraded, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Stopping, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
let current = ksp_worker_api::WorkerSnapshotSource::current(&source);
|
||||
assert_eq!(current.sequence().value(), 2);
|
||||
assert_eq!(current.state(), ksp_worker_api::WorkerState::Stopping);
|
||||
let mut wait = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, current.sequence());
|
||||
let wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut wait, &wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Stopped, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
assert!(wake.is_woken());
|
||||
let terminal = match poll_snapshot(&mut wait, &wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(terminal.sequence().value(), 3);
|
||||
assert_eq!(terminal.state(), ksp_worker_api::WorkerState::Stopped);
|
||||
assert!(terminal.state().is_terminal());
|
||||
let retained = ksp_worker_api::WorkerSnapshotSource::current(&source);
|
||||
assert_eq!(retained, terminal);
|
||||
return;
|
||||
}
|
||||
145
deltas/0.3.9/pre.003.md
Normal file
145
deltas/0.3.9/pre.003.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<!-- file: deltas/0.3.9/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.9-pre.003` — hardening et freeze fonctionnelle de `ksp-worker-api`
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
0.3.9-pre.002-fix.002
|
||||
workspace.package.version = 0.3.9-pre.2.fix.2
|
||||
```
|
||||
|
||||
Le gate opérateur de cette base est fermé : audits Rust/Markdown propres, `cargo check --workspace` PASS, Clippy `--all-targets --all-features -- -D warnings` PASS, `cargo test -p ksp-worker-api` PASS avec 13 tests unitaires, 4 tests dependency-boundary et 2 tests public API, puis les deux `cargo tree` ciblés confirment la frontière `ksp-worker-api -> ksp-core-lib`.
|
||||
|
||||
## Objet
|
||||
|
||||
Fermer le contrat Worker générique avant l'audit RAW Transaction. La production API matérialisée en `pre.002` n'est pas élargie : la tranche ajoute les preuves adversariales, external-consumer et release-completeness prévues par le plan, ainsi que les documents durables d'une bibliothèque fonctionnellement complète.
|
||||
|
||||
## Hardening lifecycle et redaction
|
||||
|
||||
`tests/security_hardening.rs` couvre :
|
||||
|
||||
```text
|
||||
Stopped et Faulted immuables sous tous les mutateurs publics
|
||||
Running -> Faulted puis stop lifecycle rejeté
|
||||
Running -> Stopping -> Faulted puis Stopped rejeté
|
||||
Running -> Stopping -> Stopped puis Faulted rejeté
|
||||
WorkerId hostile redacted dans Debug
|
||||
WorkerLifecycle et WorkerSnapshot sans fuite de l'identité hostile
|
||||
valeur d'identité invalide jamais recopiée dans Error/Debug/context
|
||||
WorkerStopToken Send + Sync, partagé et visible cross-thread
|
||||
stop demandé après terminal sans réanimation du lifecycle
|
||||
Send + Sync des primitives publiques promises
|
||||
```
|
||||
|
||||
Le premier état terminal validement enregistré reste donc autoritaire ; le token de stop transporte seulement une intention coopérative.
|
||||
|
||||
## Source latest-value externe std-only
|
||||
|
||||
`tests/snapshot_source.rs` implémente `WorkerSnapshotSource` depuis le contexte d'un vrai test d'intégration, sans dépendance de test supplémentaire et avec uniquement `std` :
|
||||
|
||||
```text
|
||||
Arc
|
||||
Mutex
|
||||
Waker
|
||||
Future
|
||||
Pin
|
||||
AtomicBool
|
||||
```
|
||||
|
||||
Cette implémentation prouve par compilation l'object-safety via `&dyn WorkerSnapshotSource`, le contrat `Send + Sync` et le future `Send`. Les scénarios couvrent :
|
||||
|
||||
```text
|
||||
deux listeners indépendants enregistrés sur la même séquence
|
||||
listener rapide reçoit sequence 1
|
||||
listener lent saute directement à sequence 2 après coalescing
|
||||
late listener commence par current() puis attend sequence > observed
|
||||
publication terminale réveille le listener
|
||||
snapshot Stopped terminal reste lisible par current()
|
||||
```
|
||||
|
||||
Aucune queue d'événements ni runtime async concret n'entre dans `ksp-worker-api`.
|
||||
|
||||
## Freeze de surface
|
||||
|
||||
`tests/release_completeness.rs` verrouille :
|
||||
|
||||
```text
|
||||
six modules de production exacts : error, identity, lib, lifecycle, snapshot, stop
|
||||
inventaire exact des vingt-et-un reexports crate-root
|
||||
aucun pub mod
|
||||
WorkerSnapshot limité à id/kind/sequence/state/health/activity
|
||||
aucun payload/String libre dans le snapshot commun
|
||||
aucun WorkerHandle générique
|
||||
aucun Job/checkpoint/backfill/RawTransaction/provider/endpoint/slot
|
||||
aucun Config/Interface/Logging/Transport/Store
|
||||
aucun Tokio/Futures crate/serde/Tauri/Solana/provider SDK
|
||||
```
|
||||
|
||||
La dependency-boundary existante reste inchangée et continue d'imposer `ksp-core-lib` comme unique dépendance normale.
|
||||
|
||||
## Documentation durable
|
||||
|
||||
La crate reçoit :
|
||||
|
||||
```text
|
||||
crates/ksp-worker-api/README.md
|
||||
crates/ksp-worker-api/USAGE.md
|
||||
```
|
||||
|
||||
Le README décrit responsabilités, lifecycle, latest-value, stop, restart ownership et firewall. `USAGE.md` reste version-neutral et fournit les exemples de consommation crate-root pour identity, lifecycle, health/activity, stop, snapshot, `WorkerSnapshotSource` et sequence. La réconciliation documentaire finale de `pre.008` pourra uniquement ajuster ces documents aux preuves finales sans les transformer en journal de release.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.9-pre.3
|
||||
delivery = 0.3.9-pre.003
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine passe de `479` à `480`.
|
||||
|
||||
## 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 ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-worker-api/README.md
|
||||
crates/ksp-worker-api/USAGE.md
|
||||
crates/ksp-worker-api/tests/release_completeness.rs
|
||||
crates/ksp-worker-api/tests/security_hardening.rs
|
||||
crates/ksp-worker-api/tests/snapshot_source.rs
|
||||
deltas/0.3.9/pre.003.md
|
||||
```
|
||||
|
||||
## Audits d'assemblage
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (320 table(s), 736 file(s))
|
||||
```
|
||||
|
||||
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement d'assemblage ; aucun nouveau gate Rust n'est déclaré PASS ici.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
La freeze fonctionnelle de Worker API devient effective uniquement après fermeture de ce gate. `pre.004` peut alors commencer l'audit RAW standard/KSP sans rouvrir l'API pour des besoins Solana-specific.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Plan v0.3.9 — Worker API générique + audit RAW Transaction
|
||||
|
||||
@@ -575,9 +575,9 @@ Le champ tuple de `WorkerSnapshotSequence` reste privé. L'`impl crate::WorkerSn
|
||||
|
||||
### `pre.003` — hardening, races, object-safety, impl externe et freeze
|
||||
|
||||
**Statut : prévu.**
|
||||
**Statut : réalisé ; gate opérateur demandé avant freeze effective.**
|
||||
|
||||
Budget cible : **15-20 min**. Fermer le hardening du contrat Worker, les races et invariants lifecycle, l’object-safety et la preuve d’implémentation externe. Sortie : Worker API fonctionnellement fermée avant tout audit RAW provider.
|
||||
Budget cible : **15-20 min**. La production API de `pre.002` reste inchangée : aucun nouveau type, module runtime ou edge de dépendance n'est nécessaire. La tranche ferme le hardening par preuves externes et adversariales : immutabilité terminale sous tous les mutateurs, trois ordres stop/fault, redaction hostile, stop cross-thread, `Send + Sync`, object-safety, implémentation std-only de `WorkerSnapshotSource`, coalescing latest-value pour listeners indépendants/lents, late-listener resync, rétention du snapshot terminal, inventaires exacts de modules/exports et firewall runtime/domain. `README.md` et `USAGE.md` durables sont créés pour la bibliothèque désormais fonctionnellement complète ; leur dernière réconciliation reste réservée à `pre.008`. Sortie : Worker API matériellement fermée, freeze effective dès validation du gate Cargo opérateur, avant tout audit RAW provider.
|
||||
|
||||
### `pre.004` — audit RAW A : Solana standard + KSP Transport/Store/Config
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# Validation v0.3.9 — Worker API + audit RAW Transaction
|
||||
|
||||
@@ -128,21 +128,25 @@ Les cases cochées de cette sous-section attestent la matérialisation de la sur
|
||||
|
||||
### `pre.003`
|
||||
|
||||
- [ ] Stop-vs-fault ordres adversariaux couverts.
|
||||
- [ ] Tous mutateurs refusent Stopped/Faulted.
|
||||
- [ ] Debug hostile identity redacted.
|
||||
- [ ] Snapshot ne contient aucun payload/string libre.
|
||||
- [ ] Stop token `Send + Sync` prouvé cross-thread.
|
||||
- [ ] `WorkerSnapshotSource: Send + Sync` prouvé.
|
||||
- [ ] `WorkerSnapshotSource` object-safe prouvé avec trait object.
|
||||
- [ ] Implémentation externe std-only compilée/testée.
|
||||
- [ ] Listeners lents indépendants resynchronisés vers latest value.
|
||||
- [ ] Late listener `current()` + séquence validés.
|
||||
- [ ] Snapshot terminal reste lisible.
|
||||
- [ ] Exact module inventory prouvé.
|
||||
- [ ] Exact crate-root export inventory prouvé.
|
||||
- [ ] Production sources sans Job/Config/Interface/Transport/Store/Tokio/futures/serde/Tauri/Solana.
|
||||
- [ ] Worker API déclarée fonctionnellement frozen avant tout audit RAW détaillé.
|
||||
- [X] Canarie des trois ordres stop-vs-fault ajoutée.
|
||||
- [X] Canarie exhaustive vérifiant que tous les mutateurs refusent `Stopped` et `Faulted` ajoutée.
|
||||
- [X] Canaries Debug hostile identity/lifecycle/snapshot et erreurs d'identité sans echo ajoutées.
|
||||
- [X] Inventaire lexical exact de `WorkerSnapshot` ajouté : six dimensions communes uniquement, aucun payload/string libre.
|
||||
- [X] Stop token `Send + Sync` et visibilité cross-thread couverts par canarie.
|
||||
- [X] `Send + Sync` des primitives Worker publiques promises couvert par canarie de compilation.
|
||||
- [X] `WorkerSnapshotSource: Send + Sync` et object-safety couverts par une implémentation externe via `&dyn WorkerSnapshotSource`.
|
||||
- [X] Implémentation externe std-only matérialisée sans Tokio/Futures crate ni dev-dependency.
|
||||
- [X] Listeners lent et rapide indépendants couverts avec coalescing vers la latest value.
|
||||
- [X] Late listener `current()` puis `wait_for_change(sequence)` couvert.
|
||||
- [X] Snapshot terminal retenu et relisible via `current()` couvert.
|
||||
- [X] Exact production module inventory ajouté.
|
||||
- [X] Exact crate-root export inventory ajouté et `pub mod` interdit.
|
||||
- [X] Firewall production renforcé contre Job/Config/Interface/Transport/Store/Tokio/Futures/serde/Tauri/Solana et préoccupations RAW/provider.
|
||||
- [X] `crates/ksp-worker-api/README.md` descriptif durable ajouté.
|
||||
- [X] `crates/ksp-worker-api/USAGE.md` version-neutral ajouté avec exemples identity/lifecycle/health/activity/stop/snapshot/source/sequence.
|
||||
- [X] Audits Python d'assemblage : Rust clean, export completeness 0, KSP workspace clean, Markdown clean (320 tables / 736 fichiers).
|
||||
- [ ] Gate Cargo opérateur `pre.003` exécuté.
|
||||
- [ ] Worker API déclarée effectivement frozen avant tout audit RAW détaillé ; cette case se ferme uniquement après le gate Cargo opérateur.
|
||||
|
||||
## 5. Threat model à fermer
|
||||
|
||||
@@ -244,10 +248,10 @@ Aucun item ci-dessous n’est déclaré exécuté en `pre.001`.
|
||||
|
||||
La livraison initiale `pre.001` avait conservé `workspace.package.version = 0.3.8` en suivant l’exception du prompt 028. Cette exception est supplantée par la règle normative `VER-ID-009`.
|
||||
|
||||
Après `pre.002-fix.002`, l’état courant est :
|
||||
Après matérialisation de `pre.003`, l’état courant est :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.9-pre.2.fix.2
|
||||
workspace.package.version = 0.3.9-pre.3
|
||||
```
|
||||
|
||||
`pre.002` reste la tranche non-fix `0.3.9-pre.2`. Les correctifs de conformité/compilation portent successivement `0.3.9-pre.2.fix.1` puis `0.3.9-pre.2.fix.2` conformément à `VER-ID-007` et `VER-ID-010`. L'état précédent `0.3.9-pre.1.fix.2` reste documenté dans `pre.001-fix.002`.
|
||||
`pre.002` reste la tranche non-fix `0.3.9-pre.2`. Les correctifs de conformité/compilation portent successivement `0.3.9-pre.2.fix.1` puis `0.3.9-pre.2.fix.2` conformément à `VER-ID-007` et `VER-ID-010`. `pre.003` reprend ensuite la séquence prerelease normale avec `0.3.9-pre.3`. 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