From f636169783467e9065e400ece8388e72324e45bc Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Thu, 10 Sep 2026 21:02:29 +0200 Subject: [PATCH] v0.3.13-pre.010 --- Cargo.toml | 4 +- .../README.md | 12 +- .../USAGE.md | 12 +- .../src/lib.rs | 11 +- .../src/runtime.rs | 9 +- .../src/runtime_resources.rs | 12 +- .../src/snapshot.rs | 116 +++++++++++++++++- .../tests/dependency_boundary.rs | 15 ++- .../tests/hardening.rs | 28 ++++- .../tests/public_api.rs | 19 ++- .../tests/release_completeness.rs | 5 +- .../unit_tests/runtime_resources.rs | 10 +- .../unit_tests/snapshot.rs | 56 ++++++++- deltas/0.3.13/pre.010.md | 48 ++++++++ ...3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md | 75 ++++++++++- ...0-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md | 76 +++++++++++- 16 files changed, 476 insertions(+), 32 deletions(-) create mode 100644 deltas/0.3.13/pre.010.md diff --git a/Cargo.toml b/Cargo.toml index 5cbe6ff..b38aade 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 558 +# version: 559 [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-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] [workspace.package] -version = "0.3.13-pre.9.fix.2" +version = "0.3.13-pre.10" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/README.md b/crates/ksp-worker-raw-transaction-ingest-lib/README.md index cedb38d..2a8b809 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/README.md +++ b/crates/ksp-worker-raw-transaction-ingest-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-worker-raw-transaction-ingest-lib @@ -213,15 +213,23 @@ Un `BlockMeta` ou `Slot` continuity-only est settled localement sans produire de Le reconnect/replay Yellowstone appartient à Transport. Le Worker n'écrit pas `from_slot` et n'interprète pas directement `SubscribeReplayInfo`. -Le snapshot Worker projette uniquement des informations sûres : +Le snapshot Worker projette uniquement des informations sûres et source-neutral : ```text +source_total +source_active +source_reconnecting +source_failed source_state +source_failure_total +backpressure_wait_total source_reconnect_total source_replay_attempt_total source_continuity_gap_total ``` +Les quatre comptes de sources sont des gauges latest-value et ne contiennent aucune identité de source. `source_total` reste le nombre de sources logiques configurées pour le run. Tant que toutes les sources attendues ne sont pas `Active`, la health publique reste conservative : un reconnect transitoire projette `Degraded`, une source `Failed` projette `Unhealthy`, et le retour de toutes les sources attendues à `Active` permet de revenir à `Healthy`. + `RawTransactionIngestSourceState` distingue : ```text diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md b/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md index 29f81cb..6e6fe7d 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md +++ b/crates/ksp-worker-raw-transaction-ingest-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-worker-raw-transaction-ingest-lib @@ -281,6 +281,10 @@ let in_flight = current.in_flight_persistence(); let hydration_pending = current.hydration_pending(); let frontier = current.processing_frontier_slot(); let oldest_pending = current.oldest_pending_slot(); +let source_total = current.source_total(); +let source_active = current.source_active(); +let source_reconnecting = current.source_reconnecting(); +let source_failed = current.source_failed(); let source_state = current.source_state(); ``` @@ -329,7 +333,7 @@ let replay_attempts = snapshot.source_replay_attempt_total(); let proven_gaps = snapshot.source_continuity_gap_total(); ``` -`admission_queue_depth()`, `in_flight_persistence()` et `hydration_pending()` sont des gauges latest-value. Les compteurs cumulés ne wrapent jamais silencieusement ; l'épuisement est terminal avec `worker_raw_transaction_ingest.counter_exhausted`. +`admission_queue_depth()`, `in_flight_persistence()`, `hydration_pending()`, `source_total()`, `source_active()`, `source_reconnecting()` et `source_failed()` sont des gauges latest-value. Les compteurs cumulés ne wrapent jamais silencieusement ; l'épuisement est terminal avec `worker_raw_transaction_ingest.counter_exhausted`. ## Interpréter la processing frontier @@ -348,7 +352,9 @@ Un `Missing` HTTP règle le signal du point de vue source-processing sans créer ## Interpréter reconnect et replay -`source_state()` peut retourner `Active`, `Reconnecting`, `Closing`, `Closed` ou `Failed` après démarrage de la source productive. +`source_state()` peut retourner `Active`, `Reconnecting`, `Closing`, `Closed` ou `Failed` comme état agrégé source-neutral. Les gauges `source_total()`, `source_active()`, `source_reconnecting()` et `source_failed()` permettent d'interpréter une composition multi-source sans exposer provider, endpoint, filtre ou `source_key`. + +La health commune est conservative : pendant `Running`, au moins une source `Failed` donne `Unhealthy`; au moins une source `Reconnecting`, ou une composition dont toutes les sources attendues ne sont pas encore `Active`, donne `Degraded`; toutes les sources attendues `Active` permettent `Healthy`. Une transition Worker `Faulted` reste `Unhealthy`. Les compteurs ont des sémantiques distinctes : diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs index 8e6d4c8..db7980c 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs -// version: 24 +// version: 25 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -10,11 +10,10 @@ //! This tranche owns the concrete Worker family identity, validated technical settings //! and the caller-runtime-owned lifecycle with private child-task supervision. This tranche also //! owns bounded source-neutral admission, common RAW canonicalization/assembly and backend-neutral -//! Store persistence in normal mode plus concrete latest-value snapshots projected onto Worker API. `pre.006` keeps the bounded 1..32 caller-composed -//! aggregate and adds productive HTTP live block polling beside Yellowstone, Standard Logs, Standard Block and Helius Transaction while -//! simultaneous multi-source activation remains gated until the dedicated supervisor tranche. Helius Full notifications, Standard Logs and Yellowstone -//! reference paths converge into one source-neutral hydration coordinator contract; qualified Standard Block Legacy/V0/V1 transactions enter the existing -//! central admission path directly. Yellowstone +//! Store persistence in normal mode plus concrete latest-value snapshots projected onto Worker API. The bounded 1..32 caller-composed aggregate starts +//! all validated Yellowstone, Standard Logs, Standard Block, Helius Transaction and HTTP live block polling sources under one private supervisor. Reference-bearing +//! sources share bounded cross-source hydration and fairness budgets; direct-qualified Standard Block and HTTP polling transactions enter the existing central +//! admission path directly. Public snapshots expose only source-neutral aggregate counts/state and conservative Worker health. Yellowstone //! Transaction/TransactionStatus/Block and standard logs notifications become signature/slot references; BlockMeta/Slot remain continuity-only signals. //! Hydration is coalesced by network/signature/commitment under bounded in-flight and pending budgets. A bounded run-local processing frontier projects //! hydration pending, oldest pending slot and highest unblocked actually observed slot. The productive source also projects safe Transport reconnect/replay diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs index d38a07c..3390146 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs -// version: 15 +// version: 16 type PersistencePort = std::sync::Arc; type PersistenceTasks = tokio::task::JoinSet>; @@ -117,6 +117,7 @@ impl crate::RawTransactionIngestWorker { if let std::result::Result::Err(error) = runtime_resources.validate_network(settings.network()) { return std::result::Result::Err(error); } + let source_total = runtime_resources.source_count(); let source_settings = settings.clone(); let (processing_frontier_sender, processing_frontier_receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty()); @@ -126,6 +127,7 @@ impl crate::RawTransactionIngestWorker { runtime, std::option::Option::Some(port), std::option::Option::Some(processing_frontier_receiver), + source_total, move |children, stop_receiver, admission_sender| { let _abort_handle = children.spawn(async move { return runtime_resources.run_live_sources(source_settings, stop_receiver, admission_sender, processing_frontier_sender).await; @@ -458,7 +460,7 @@ where Spawner: FnOnce(&mut SourceTasks, tokio::sync::watch::Receiver, tokio::sync::mpsc::Sender) + std::marker::Send + 'static, { - return start_foundation_with_port_source_spawner_and_frontier(settings, runtime, port, std::option::Option::None, source_spawner); + return start_foundation_with_port_source_spawner_and_frontier(settings, runtime, port, std::option::Option::None, 0, source_spawner); } fn start_foundation_with_port_source_spawner_and_frontier( @@ -466,6 +468,7 @@ fn start_foundation_with_port_source_spawner_and_frontier( runtime: tokio::runtime::Handle, port: std::option::Option, processing_frontier_receiver: std::option::Option, + source_total: usize, source_spawner: Spawner, ) -> ksp_core_lib::Result where @@ -482,7 +485,7 @@ where } let stop_token = ksp_worker_api::WorkerStopToken::new(); let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false); - let (snapshots, snapshot_source) = crate::RawTransactionIngestSnapshotPublisher::new(&settings, &lifecycle); + let (snapshots, snapshot_source) = crate::RawTransactionIngestSnapshotPublisher::new(&settings, &lifecycle, source_total); let handle = crate::RawTransactionIngestHandle { snapshots: snapshot_source, stop_sender, stop_token }; std::mem::drop(runtime.spawn(run_supervisor(settings, lifecycle, port, stop_receiver, snapshots, processing_frontier_receiver, source_spawner))); return std::result::Result::Ok(handle); diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs index ecc6223..f0031bf 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs -// version: 24 +// version: 25 use sha2::Digest; // rust-rules: trait-import @@ -179,6 +179,10 @@ impl RawTransactionIngestSourceInventory { let mut source_continuity_gap_total = 0_u64; let mut source_reconnect_total = 0_u64; let mut source_replay_attempt_total = 0_u64; + let source_total = self.source_projections.len(); + let mut source_active = 0_usize; + let mut source_reconnecting = 0_usize; + let mut source_failed = 0_usize; let mut any_active = false; let mut any_closing = false; let mut any_failed = false; @@ -200,6 +204,7 @@ impl RawTransactionIngestSourceInventory { source_replay_attempt_total = source_replay_attempt_total.saturating_add(projection.source_replay_attempt_total()); match projection.source_state() { std::option::Option::Some(crate::RawTransactionIngestSourceState::Active) => { + source_active += 1; any_active = true; all_closed = false; }, @@ -209,10 +214,12 @@ impl RawTransactionIngestSourceInventory { }, std::option::Option::Some(crate::RawTransactionIngestSourceState::Closed) => {}, std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed) => { + source_failed += 1; any_failed = true; all_closed = false; }, std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting) => { + source_reconnecting += 1; any_reconnecting = true; all_closed = false; }, @@ -238,7 +245,8 @@ impl RawTransactionIngestSourceInventory { std::option::Option::None }; return crate::RawTransactionIngestProcessingFrontierProjection::new(hydration_pending, processing_frontier_slot, oldest_pending_slot) - .with_source_continuity(source_state, source_reconnect_total, source_replay_attempt_total, source_continuity_gap_total); + .with_source_continuity(source_state, source_reconnect_total, source_replay_attempt_total, source_continuity_gap_total) + .with_source_counts(source_total, source_active, source_reconnecting, source_failed); } fn new(source_keys: std::vec::Vec<[u8; 32]>) -> Self { diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs index 58437dc..94899a8 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs -// version: 4 +// version: 5 /// Runtime-neutral boxed future resolving to one newer concrete RAW transaction ingest Worker snapshot. pub type RawTransactionIngestSnapshotFuture<'a> = @@ -27,6 +27,10 @@ pub(crate) struct RawTransactionIngestProcessingFrontierProjection { processing_frontier_slot: std::option::Option, oldest_pending_slot: std::option::Option, source_state: std::option::Option, + source_total: usize, + source_active: usize, + source_reconnecting: usize, + source_failed: usize, source_reconnect_total: u64, source_replay_attempt_total: u64, source_continuity_gap_total: u64, @@ -40,6 +44,10 @@ impl crate::RawTransactionIngestProcessingFrontierProjection { processing_frontier_slot: std::option::Option::None, oldest_pending_slot: std::option::Option::None, source_state: std::option::Option::None, + source_total: 0, + source_active: 0, + source_reconnecting: 0, + source_failed: 0, source_reconnect_total: 0, source_replay_attempt_total: 0, source_continuity_gap_total: 0, @@ -57,6 +65,10 @@ impl crate::RawTransactionIngestProcessingFrontierProjection { processing_frontier_slot, oldest_pending_slot, source_state: std::option::Option::None, + source_total: 0, + source_active: 0, + source_reconnecting: 0, + source_failed: 0, source_reconnect_total: 0, source_replay_attempt_total: 0, source_continuity_gap_total: 0, @@ -93,6 +105,35 @@ impl crate::RawTransactionIngestProcessingFrontierProjection { return self; } + /// Returns a copy carrying source-neutral multi-source lifecycle counts. + pub(crate) const fn with_source_counts(mut self, source_total: usize, source_active: usize, source_reconnecting: usize, source_failed: usize) -> Self { + self.source_total = source_total; + self.source_active = source_active; + self.source_reconnecting = source_reconnecting; + self.source_failed = source_failed; + return self; + } + + /// Returns the configured logical source count carried by this private projection. + pub(crate) const fn source_total(&self) -> usize { + return self.source_total; + } + + /// Returns the number of sources currently projected Active. + pub(crate) const fn source_active(&self) -> usize { + return self.source_active; + } + + /// Returns the number of sources currently projected Reconnecting. + pub(crate) const fn source_reconnecting(&self) -> usize { + return self.source_reconnecting; + } + + /// Returns the number of sources currently projected Failed. + pub(crate) const fn source_failed(&self) -> usize { + return self.source_failed; + } + /// Returns the latest source-neutral lifecycle state carried by this private projection. pub(crate) const fn source_state(&self) -> std::option::Option { return self.source_state; @@ -138,6 +179,10 @@ pub struct RawTransactionIngestSnapshot { processing_frontier_slot: std::option::Option, oldest_pending_slot: std::option::Option, source_state: std::option::Option, + source_total: usize, + source_active: usize, + source_reconnecting: usize, + source_failed: usize, source_reconnect_total: u64, source_replay_attempt_total: u64, source_continuity_gap_total: u64, @@ -264,6 +309,30 @@ impl crate::RawTransactionIngestSnapshot { return self.oldest_pending_slot; } + /// Returns the configured number of logical live sources for this Worker run. + #[must_use] + pub const fn source_total(&self) -> usize { + return self.source_total; + } + + /// Returns the latest number of sources projected Active. + #[must_use] + pub const fn source_active(&self) -> usize { + return self.source_active; + } + + /// Returns the latest number of sources projected Reconnecting. + #[must_use] + pub const fn source_reconnecting(&self) -> usize { + return self.source_reconnecting; + } + + /// Returns the latest number of sources projected Failed. + #[must_use] + pub const fn source_failed(&self) -> usize { + return self.source_failed; + } + /// Returns the latest source-neutral lifecycle state when the productive source has started. #[must_use] pub const fn source_state(&self) -> std::option::Option { @@ -314,6 +383,10 @@ impl std::fmt::Debug for crate::RawTransactionIngestSnapshot { .field("processing_frontier_slot", &self.processing_frontier_slot) .field("oldest_pending_slot", &self.oldest_pending_slot) .field("source_state", &self.source_state) + .field("source_total", &self.source_total) + .field("source_active", &self.source_active) + .field("source_reconnecting", &self.source_reconnecting) + .field("source_failed", &self.source_failed) .field("source_reconnect_total", &self.source_reconnect_total) .field("source_replay_attempt_total", &self.source_replay_attempt_total) .field("source_continuity_gap_total", &self.source_continuity_gap_total) @@ -403,6 +476,7 @@ impl crate::RawTransactionIngestSnapshotPublisher { pub(crate) fn new( settings: &crate::RawTransactionIngestSettings, lifecycle: &ksp_worker_api::WorkerLifecycle, + source_total: usize, ) -> (crate::RawTransactionIngestSnapshotPublisher, crate::RawTransactionIngestSnapshotSource) { let worker = ksp_worker_api::WorkerSnapshot::new( lifecycle.id().clone(), @@ -434,6 +508,10 @@ impl crate::RawTransactionIngestSnapshotPublisher { processing_frontier_slot: std::option::Option::None, oldest_pending_slot: std::option::Option::None, source_state: std::option::Option::None, + source_total, + source_active: 0, + source_reconnecting: 0, + source_failed: 0, source_reconnect_total: 0, source_replay_attempt_total: 0, source_continuity_gap_total: 0, @@ -449,7 +527,14 @@ impl crate::RawTransactionIngestSnapshotPublisher { self.snapshot.worker.kind().clone(), self.snapshot.worker.sequence(), state, - health_for_state(state, self.snapshot.worker.health()), + health_for_state( + state, + self.snapshot.worker.health(), + self.snapshot.source_total, + self.snapshot.source_active, + self.snapshot.source_reconnecting, + self.snapshot.source_failed, + ), ksp_worker_api::WorkerActivity::Idle, ); self.snapshot.worker = worker; @@ -530,6 +615,12 @@ impl crate::RawTransactionIngestSnapshotPublisher { self.snapshot.processing_frontier_slot = projection.processing_frontier_slot(); self.snapshot.oldest_pending_slot = projection.oldest_pending_slot(); self.snapshot.source_state = projection.source_state(); + if projection.source_total() > 0 { + self.snapshot.source_total = projection.source_total(); + self.snapshot.source_active = projection.source_active(); + self.snapshot.source_reconnecting = projection.source_reconnecting(); + self.snapshot.source_failed = projection.source_failed(); + } self.snapshot.source_reconnect_total = projection.source_reconnect_total(); self.snapshot.source_replay_attempt_total = projection.source_replay_attempt_total(); self.snapshot.source_continuity_gap_total = projection.source_continuity_gap_total(); @@ -646,7 +737,14 @@ impl crate::RawTransactionIngestSnapshotPublisher { self.snapshot.worker.kind().clone(), sequence, state, - health_for_state(state, self.snapshot.worker.health()), + health_for_state( + state, + self.snapshot.worker.health(), + self.snapshot.source_total, + self.snapshot.source_active, + self.snapshot.source_reconnecting, + self.snapshot.source_failed, + ), activity_for_state(state, admission_queue_depth, in_flight_persistence, self.snapshot.hydration_pending, self.snapshot.source_state), ); self.snapshot.worker = worker; @@ -698,8 +796,18 @@ fn checked_optional_counter(current: u64, increment: bool, field: &'static str) return checked_counter(current, field); } -fn health_for_state(state: ksp_worker_api::WorkerState, previous: ksp_worker_api::WorkerHealth) -> ksp_worker_api::WorkerHealth { +fn health_for_state( + state: ksp_worker_api::WorkerState, + previous: ksp_worker_api::WorkerHealth, + source_total: usize, + source_active: usize, + source_reconnecting: usize, + source_failed: usize, +) -> ksp_worker_api::WorkerHealth { return match state { + ksp_worker_api::WorkerState::Running if source_failed > 0 => ksp_worker_api::WorkerHealth::Unhealthy, + ksp_worker_api::WorkerState::Running if source_reconnecting > 0 => ksp_worker_api::WorkerHealth::Degraded, + ksp_worker_api::WorkerState::Running if source_total > 0 && source_active < source_total => ksp_worker_api::WorkerHealth::Degraded, ksp_worker_api::WorkerState::Running => ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerState::Stopping | ksp_worker_api::WorkerState::Stopped => previous, ksp_worker_api::WorkerState::Faulted(_) => ksp_worker_api::WorkerHealth::Unhealthy, diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs index d5d373a..e571f2a 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs -// version: 26 +// version: 27 //! Dependency firewall canaries for the RAW transaction ingest Worker foundation. @@ -650,3 +650,16 @@ fn v0_3_13_pre_009_global_bounds_and_fairness_stay_inside_worker_facades() { } return; } + +#[test] +fn v0_3_13_pre_010_multi_source_snapshot_health_remains_source_neutral_and_backend_free() { + let snapshot = include_str!("../src/snapshot.rs"); + let resources = include_str!("../src/runtime_resources.rs"); + for required in ["source_total", "source_active", "source_reconnecting", "source_failed", "WorkerHealth::Degraded", "with_source_counts"] { + assert!(snapshot.contains(required) || resources.contains(required), "required pre.010 source-neutral snapshot contract missing: {required}"); + } + for forbidden in ["ksp_store_postgres_lib::", "reqwest::", "tonic::", "yellowstone_grpc_proto::", "SourceSnapshotByKey", "provider_health"] { + assert!(!snapshot.contains(forbidden), "pre.010 snapshot crossed source-neutral/backend boundary: {forbidden}"); + } + return; +} diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs index 7f27bc6..3d7c9cb 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs -// version: 21 +// version: 22 //! External public, security, redaction and release-boundary hardening canaries for `pre.010`. @@ -828,3 +828,29 @@ fn v0_3_13_pre_009_duplicate_storm_disagreement_and_starvation_guards_are_explic } return; } + +#[test] +fn v0_3_13_pre_010_multi_source_health_is_conservative_counted_and_redacted() { + let snapshot = include_str!("../src/snapshot.rs"); + let resources = include_str!("../src/runtime_resources.rs"); + for required in [ + "source_total: usize", + "source_active: usize", + "source_reconnecting: usize", + "source_failed: usize", + "source_reconnecting > 0", + "source_failed > 0", + "source_active < source_total", + "WorkerHealth::Degraded", + "WorkerHealth::Unhealthy", + ] { + assert!(snapshot.contains(required), "required pre.010 health guard missing: {required}"); + } + for required in ["source_total = self.source_projections.len()", "with_source_counts(source_total, source_active, source_reconnecting, source_failed)"] { + assert!(resources.contains(required), "required pre.010 inventory count guard missing: {required}"); + } + for forbidden in ["source_key: [u8; 32]", "provider_url", "endpoint_url", "filter_fingerprint", "credential", "api_key", "token_header"] { + assert!(!snapshot.contains(forbidden), "pre.010 snapshot leaks source/provider material: {forbidden}"); + } + return; +} diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs index 438513b..1d9b9cb 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs -// version: 19 +// version: 20 //! External public-surface proofs for the RAW transaction ingest Worker foundation. @@ -368,3 +368,20 @@ fn v0_3_13_pre_009_fairness_budgets_semaphore_and_canonical_state_remain_private } return; } + +#[test] +fn v0_3_13_pre_010_multi_source_snapshot_counts_are_public_and_source_neutral() { + let _source_total: fn(&ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot) -> usize = + ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot::source_total; + let _source_active: fn(&ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot) -> usize = + ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot::source_active; + let _source_reconnecting: fn(&ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot) -> usize = + ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot::source_reconnecting; + let _source_failed: fn(&ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot) -> usize = + ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot::source_failed; + let root = include_str!("../src/lib.rs"); + for forbidden in ["SourceHealthByKey", "SourceSnapshotByKey", "provider_health", "endpoint_health"] { + assert!(!root.contains(forbidden), "pre.010 public root leaked source-specific health material: {forbidden}"); + } + return; +} diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs index 25f5d3d..35a04eb 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs -// version: 17 +// version: 18 //! Release-completeness canaries through the `pre.010` public/release/security hardening tranche. @@ -130,6 +130,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { "v0_3_13_pre_007_multi_source_supervisor_is_fail_closed_joined_and_does_not_publish_source_identity", "v0_3_13_pre_008_cross_source_convergence_is_bounded_conflict_checked_and_private", "v0_3_13_pre_009_duplicate_storm_disagreement_and_starvation_guards_are_explicit", + "v0_3_13_pre_010_multi_source_health_is_conservative_counted_and_redacted", ] { assert!(hardening.contains(required), "required pre.010 hardening canary missing: {required}"); } @@ -150,6 +151,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { assert!(dependency_boundary.contains("v0_3_13_pre_007_multi_source_supervisor_and_inventory_remain_private_bounded_and_source_neutral")); assert!(dependency_boundary.contains("v0_3_13_pre_008_cross_source_convergence_reuses_store_and_transport_facades_only")); assert!(dependency_boundary.contains("v0_3_13_pre_009_global_bounds_and_fairness_stay_inside_worker_facades")); + assert!(dependency_boundary.contains("v0_3_13_pre_010_multi_source_snapshot_health_remains_source_neutral_and_backend_free")); let public_api = include_str!("public_api.rs"); assert!(public_api.contains("pre_003_kind_code_and_settings_are_consumable_from_crate_root")); assert!(public_api.contains("pre_004_start_handle_and_terminal_future_are_consumable_without_public_join_handle")); @@ -163,6 +165,7 @@ fn pre_010_external_hardening_suite_is_present_and_scoped() { assert!(public_api.contains("v0_3_13_pre_007_source_inventory_and_logical_keys_remain_private")); assert!(public_api.contains("v0_3_13_pre_008_convergence_cache_registry_and_source_keys_remain_private")); assert!(public_api.contains("v0_3_13_pre_009_fairness_budgets_semaphore_and_canonical_state_remain_private")); + assert!(public_api.contains("v0_3_13_pre_010_multi_source_snapshot_counts_are_public_and_source_neutral")); assert!(public_api.contains("v0_3_12_pre_007_processing_frontier_snapshot_getters_are_public_and_processing_only")); assert!(public_api.contains("pre_008_snapshot_surface_and_common_projection_are_public_and_stable")); return; diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs index 9bacc84..94d013b 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs -// version: 21 +// version: 22 fn grpc_endpoint(cluster: &str) -> std::option::Option { return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider"); @@ -984,6 +984,10 @@ fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservati assert_eq!(first_aggregate.processing_frontier_slot(), std::option::Option::None); assert_eq!(first_aggregate.oldest_pending_slot(), std::option::Option::Some(45)); assert_eq!(first_aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Active)); + assert_eq!(first_aggregate.source_total(), 2); + assert_eq!(first_aggregate.source_active(), 1); + assert_eq!(first_aggregate.source_reconnecting(), 0); + assert_eq!(first_aggregate.source_failed(), 0); second.publish( crate::RawTransactionIngestProcessingFrontierProjection::new(2, std::option::Option::Some(42), std::option::Option::Some(40)).with_source_continuity( std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting), @@ -997,6 +1001,10 @@ fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservati assert_eq!(aggregate.processing_frontier_slot(), std::option::Option::Some(42)); assert_eq!(aggregate.oldest_pending_slot(), std::option::Option::Some(40)); assert_eq!(aggregate.source_state(), std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting)); + assert_eq!(aggregate.source_total(), 2); + assert_eq!(aggregate.source_active(), 1); + assert_eq!(aggregate.source_reconnecting(), 1); + assert_eq!(aggregate.source_failed(), 0); assert_eq!(aggregate.source_reconnect_total(), 4); assert_eq!(aggregate.source_replay_attempt_total(), 6); assert_eq!(aggregate.source_continuity_gap_total(), 1); diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs index 7ddc15d..a10e9c9 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs @@ -1,7 +1,9 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs -// version: 4 +// version: 5 -fn snapshot_foundation() -> std::option::Option<(crate::RawTransactionIngestSnapshotPublisher, crate::RawTransactionIngestSnapshotSource)> { +fn snapshot_foundation_with_source_total( + source_total: usize, +) -> std::option::Option<(crate::RawTransactionIngestSnapshotPublisher, crate::RawTransactionIngestSnapshotSource)> { let network = match ksp_store_lib::RawNetworkId::new("mainnet") { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::option::Option::None, @@ -19,7 +21,11 @@ fn snapshot_foundation() -> std::option::Option<(crate::RawTransactionIngestSnap if lifecycle.start().is_err() { return std::option::Option::None; } - return std::option::Option::Some(crate::RawTransactionIngestSnapshotPublisher::new(&settings, &lifecycle)); + return std::option::Option::Some(crate::RawTransactionIngestSnapshotPublisher::new(&settings, &lifecycle, source_total)); +} + +fn snapshot_foundation() -> std::option::Option<(crate::RawTransactionIngestSnapshotPublisher, crate::RawTransactionIngestSnapshotSource)> { + return snapshot_foundation_with_source_total(0); } #[test] @@ -54,6 +60,10 @@ fn pre_008_initial_snapshot_and_common_projection_are_exact() { assert_eq!(concrete.processing_frontier_slot(), std::option::Option::None); assert_eq!(concrete.oldest_pending_slot(), std::option::Option::None); assert_eq!(concrete.source_state(), std::option::Option::None); + assert_eq!(concrete.source_total(), 0); + assert_eq!(concrete.source_active(), 0); + assert_eq!(concrete.source_reconnecting(), 0); + assert_eq!(concrete.source_failed(), 0); assert_eq!(concrete.source_reconnect_total(), 0); assert_eq!(concrete.source_replay_attempt_total(), 0); assert_eq!(concrete.source_continuity_gap_total(), 0); @@ -199,3 +209,43 @@ fn pre_008_source_continuity_projection_preserves_processing_frontier_and_distin assert_eq!(resumed.worker_snapshot().activity(), ksp_worker_api::WorkerActivity::Idle); return; } + +#[test] +fn v0_3_13_pre_010_multi_source_counts_and_health_are_conservative_and_source_neutral() { + let (mut publisher, source) = match snapshot_foundation_with_source_total(3) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let initial = source.current(); + assert_eq!(initial.source_total(), 3); + assert_eq!(initial.source_active(), 0); + assert_eq!(initial.source_reconnecting(), 0); + assert_eq!(initial.source_failed(), 0); + assert_eq!(initial.worker_snapshot().health(), ksp_worker_api::WorkerHealth::Unknown); + let reconnecting = crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(50), std::option::Option::Some(50)) + .with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting), 0, 1, 0) + .with_source_counts(3, 2, 1, 0); + assert!(publisher.record_processing_frontier(ksp_worker_api::WorkerState::Running, 0, 0, reconnecting).is_ok()); + let degraded = source.current(); + assert_eq!(degraded.source_total(), 3); + assert_eq!(degraded.source_active(), 2); + assert_eq!(degraded.source_reconnecting(), 1); + assert_eq!(degraded.source_failed(), 0); + assert_eq!(degraded.worker_snapshot().health(), ksp_worker_api::WorkerHealth::Degraded); + assert_eq!(degraded.worker_snapshot().activity(), ksp_worker_api::WorkerActivity::Active); + let active = crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(51), std::option::Option::None) + .with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 1, 0) + .with_source_counts(3, 3, 0, 0); + assert!(publisher.record_processing_frontier(ksp_worker_api::WorkerState::Running, 0, 0, active).is_ok()); + let healthy = source.current(); + assert_eq!(healthy.worker_snapshot().health(), ksp_worker_api::WorkerHealth::Healthy); + assert_eq!(healthy.worker_snapshot().activity(), ksp_worker_api::WorkerActivity::Idle); + let failed = crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(51), std::option::Option::None) + .with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Failed), 1, 1, 0) + .with_source_counts(3, 2, 0, 1); + assert!(publisher.record_processing_frontier(ksp_worker_api::WorkerState::Running, 0, 0, failed).is_ok()); + let unhealthy = source.current(); + assert_eq!(unhealthy.source_failed(), 1); + assert_eq!(unhealthy.worker_snapshot().health(), ksp_worker_api::WorkerHealth::Unhealthy); + return; +} diff --git a/deltas/0.3.13/pre.010.md b/deltas/0.3.13/pre.010.md new file mode 100644 index 0000000..0f64b37 --- /dev/null +++ b/deltas/0.3.13/pre.010.md @@ -0,0 +1,48 @@ + + + +# Delta 0.3.13-pre.010 — snapshots/health multi-source + +## Objet + +Fermer la projection publique source-neutral prévue pour le runtime multi-source sans exposer les identités logiques, providers, endpoints, filtres ou matériaux Transport. + +## Runtime + +`RawTransactionIngestSnapshot` expose désormais quatre gauges latest-value : `source_total`, `source_active`, `source_reconnecting` et `source_failed`. + +`source_total` est initialisé dès le démarrage depuis la collection `RawTransactionIngestRuntimeResources` validée. L'inventaire privé borné agrège ensuite les états locaux et publie uniquement les comptes numériques, l'état source agrégé et les compteurs de continuité déjà existants. + +La health commune devient conservative pendant `Running` : une source failed donne `Unhealthy`; un reconnect ou une composition dont toutes les sources attendues ne sont pas encore actives donne `Degraded`; toutes les sources attendues actives permettent `Healthy`. `Faulted` reste `Unhealthy` et `Stopping`/`Stopped` conservent la dernière health qualifiée. + +L'activity reste liée au travail concret, aux pending, à la persistence, au reconnect ou au closing. Une source simplement ouverte n'est pas artificiellement classée `Active`. + +## Preuves ajoutées + +Les tests couvrent les comptes d'inventaire, la health `Degraded -> Healthy -> Unhealthy`, les quatre getters publics, la redaction du snapshot, l'absence de backend/network client direct et la présence des canaris dans `release_completeness`. + +## Frontière + +Aucun changement de convergence, fairness, quota, Store, Transport, dépendance ou feature. Aucun failover, provider health public, snapshot par source ou coverage inference. Les races et shutdown concurrents restent `pre.011`. + +## Gate reçu avant cette tranche + +Le gate opérateur `0.3.13-pre.009-fix.002` est vert pour toutes les commandes exécutées : fmt, audits, workspace check, Clippy strict, Transport `389 + 52 + 44 + 4` avec `5` smokes live opt-in ignorés, puis Worker `99 + 4 + 16 + 23 + 17 + 4`, sans échec. Aucun `cargo tree` n'était requis ni exécuté. + +## Validation locale d'assemblage + +Le toolchain Cargo/Rustfmt n'est pas disponible dans l'environnement d'assemblage. Aucun résultat Cargo post-modification n'est déclaré PASS localement. Les audits statiques et contrôles de packaging effectivement exécutés sont les seules preuves locales consignées pour cette livraison. + +## Gate opérateur avant pre.011 + +```bash +cargo fmt --all +cargo fmt --all -- --check +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-raw-transaction-ingest-lib +``` + +Aucun `cargo tree` n'est requis ; aucune dépendance ni feature n'a changé. Le Transport n'est pas modifié par cette tranche snapshot/health. diff --git a/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md b/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md index fc1bf9b..91f3bd9 100644 --- a/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md +++ b/docs/plans/034-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source RawTransaction @@ -1406,3 +1406,76 @@ cargo test -p ksp-worker-raw-transaction-ingest-lib ``` Aucun `cargo tree` n'est requis pour ce gate : `pre.009` ne modifie ni dépendance ni feature. Les graphes seront réaudités si le graphe change ou à la fermeture technique de `0.3.13`. + +## 74. Gate opérateur pre.009-fix.002 reçu + +Le 10 septembre 2026, le gate complet communiqué est vert pour toutes les commandes exécutées : fmt, audits Rust, Markdown `340/847`, workspace check, Clippy strict, Transport `389` unit + `52` public-api + `44` release-completeness + `4` doc-tests avec `5` smokes live opt-in ignorés, puis Worker `99` unit + `4` cross-layer + `16` dependency-boundary + `23` hardening + `17` public-api + `4` release-completeness et `0` doc-test. + +Aucun `cargo tree` n'était requis ni exécuté pour ce gate. + +## 75. Surface snapshot/health pre.010 + +La projection publique multi-source reste volontairement minimale et source-neutral : + +```text +source_total +source_active +source_reconnecting +source_failed +source_state agrégé +source_failure_total +backpressure_wait_total +``` + +`source_total` est initialisé dès le démarrage depuis la collection de runtime resources validée. Les autres gauges sont recalculées depuis l'inventaire privé borné `source_key -> latest state`; seules les valeurs numériques agrégées atteignent `RawTransactionIngestSnapshot`. + +Aucune clé logique, identité provider, endpoint, filtre, URL, credential ou état Transport spécifique n'entre dans le snapshot public. + +## 76. Health et activity conservatives pre.010 + +Pendant `WorkerState::Running`, la health commune suit l'ordre conservateur suivant : + +```text +source_failed > 0 -> Unhealthy +sinon source_reconnecting > 0 -> Degraded +sinon source_total > 0 et active < total -> Degraded +sinon -> Healthy +``` + +Une composition partiellement démarrée, une source fermée/perdue avant terminal ou un reconnect transitoire ne peut donc pas être présentée comme entièrement saine. `Faulted` reste toujours `Unhealthy`; `Stopping`/`Stopped` conservent la dernière health qualifiée. + +L'activity générique reste fondée sur du travail concret : queue admission, persistence in-flight, hydration pending, reconnect ou closing. Une source simplement `Active` sans travail en cours ne transforme pas artificiellement `Idle` en `Active`. + +## 77. Preuves déterministes pre.010 + +Les preuves ajoutées couvrent : + +```text +source_total disponible dès le snapshot initial d'un run composé +inventaire 2 sources -> comptes Active/Reconnecting exacts +3 sources dont une Reconnecting -> Degraded +retour des 3 sources à Active -> Healthy +une source Failed -> Unhealthy +getters publics source-neutral disponibles depuis crate root +aucune source_key/provider/endpoint/filter dans snapshot +aucune nouvelle dépendance ou backend direct +release-completeness verrouille les nouveaux canaris +``` + +## 78. Frontière de tranche pre.010 + +`pre.010` ne change ni la politique de terminalité source, ni les quotas/fairness, ni la convergence persistence. Elle n'ajoute pas de failover, de coverage inference, de vue publique par source ou de scheduler pondéré. Les races stop/fault pendant connect, hydration, polling et persistence restent la tranche `pre.011`. + +## 79. Gate opérateur requis avant pre.011 + +```bash +cargo fmt --all +cargo fmt --all -- --check +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-raw-transaction-ingest-lib +``` + +Aucun `cargo tree` n'est requis : `pre.010` ne modifie ni dépendance ni feature. Le Transport n'est pas modifié par cette tranche snapshot/health. diff --git a/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md b/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md index 5226dbe..bae5b6f 100644 --- a/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md +++ b/docs/validation/030-V0_3_13_MULTI_SOURCE_LIVE_CONVERGENCE.md @@ -1,5 +1,5 @@ - + # Validation v0.3.13 — WS standard / Helius / HTTP live + convergence multi-source @@ -1411,3 +1411,77 @@ cargo test -p ksp-worker-raw-transaction-ingest-lib ``` Aucun `cargo tree` n'est requis pour ce gate : `pre.009` ne modifie ni dépendance ni feature. Les graphes seront réaudités si le graphe change ou à la fermeture technique de `0.3.13`. + +## 77. Gate complet pre.009-fix.002 reçu + +Preuve opérateur du 10 septembre 2026 : + +```text +fmt : PASS +audits Rust : clean / export completeness 0 +Markdown : clean, 340 tables / 847 files +cargo check --workspace : PASS +clippy strict : PASS +Transport : 389 unit + 52 public-api + 44 release-completeness + 4 doc-tests, 0 échec +smokes Transport live opt-in : 5 ignored comme prévu +Worker : 99 unit + 4 cross-layer + 16 dependency-boundary + 23 hardening + 17 public-api + 4 release-completeness, 0 échec +Worker doc-tests : 0 +``` + +Aucun `cargo tree` n'est revendiqué pour ce gate. + +## 78. Surface pre.010 matérialisée + +```text +RawTransactionIngestSnapshot::source_total +RawTransactionIngestSnapshot::source_active +RawTransactionIngestSnapshot::source_reconnecting +RawTransactionIngestSnapshot::source_failed +projection privée with_source_counts +inventaire privé -> comptes source-neutral +source_total initialisé depuis RawTransactionIngestRuntimeResources::source_count +health commune conservative +``` + +Les informations par source restent dans l'inventaire privé; le snapshot public ne contient aucune `source_key` ni information provider/endpoint/filter. + +## 79. Invariants health/activity pre.010 + +```text +Running + source_failed > 0 -> Unhealthy +Running + source_reconnecting > 0 -> Degraded +Running + source_total > 0 + source_active < source_total -> Degraded +Running + toutes sources attendues Active -> Healthy +Faulted -> Unhealthy +Stopping/Stopped -> dernière health qualifiée +``` + +`WorkerActivity::Active` continue à représenter du travail concret ou un reconnect/closing, pas simplement l'existence d'une source ouverte. + +## 80. Non-claims pre.010 + +```text +pas de snapshot public par source +pas de provider health/tier/endpoint public +pas de failover ou coverage inference +pas de changement des quotas/fairness pre.009 +pas de changement de convergence persistence +pas encore de fermeture exhaustive des races/shutdown pre.011 +pas de nouvelle dépendance/feature +``` + +## 81. Validation locale d'assemblage et gate avant pre.011 + +Le toolchain Cargo/Rustfmt n'est pas disponible dans l'environnement d'assemblage. Les commandes Cargo post-modification restent `NON EXÉCUTÉ LOCAL`. + +Gate opérateur demandé : + +```bash +cargo fmt --all +cargo fmt --all -- --check +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-raw-transaction-ingest-lib +```