0.3.15-pre.010-fix.001

This commit is contained in:
2026-09-15 05:53:36 +02:00
parent 9fd51c5a41
commit e919945d1e
20 changed files with 1013 additions and 28 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 617 # version: 619
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-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"] members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-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] [workspace.package]
version = "0.3.15-pre.9.fix.3" version = "0.3.15-pre.10.fix.1"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md --> <!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 10 --> <!-- version: 11 -->
# `ksp-app-raw-transaction-ingest-desk` # `ksp-app-raw-transaction-ingest-desk`
@@ -48,7 +48,7 @@ http-block-polling
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime. Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Les états spontanés après Start ne sont pas encore streamés vers le frontend ; l'observabilité continue est ajoutée par la tranche monitoring dédiée. Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. L'UI détaillée de supervision reste la tranche `pre.011`.
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées. Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md --> <!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 9 --> <!-- version: 10 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk` # Utilisation de `ksp-app-raw-transaction-ingest-desk`
@@ -42,7 +42,7 @@ Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `S
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert. Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
Le frontend ne fournit pas encore de streaming continu des snapshots runtime : une terminaison spontanée est nettoyée côté backend et un Stop tardif peut resynchroniser la route avec son dernier terminal sûr. Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. L'affichage frontend complet et les contrôles de refresh dédiés sont finalisés en `pre.011`.
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée. Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs // file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
// version: 6 // version: 7
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application. //! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
@@ -137,6 +137,11 @@ impl crate::AppState {
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await; return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
} }
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current same-network runtime session.
pub(crate) fn route_monitoring(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
return self.route_runtime.monitoring_statuses();
}
/// Requests cooperative Stop for one exact logical route and waits until its Worker cleanup is complete. /// Requests cooperative Stop for one exact logical route and waits until its Worker cleanup is complete.
pub(crate) async fn stop_route(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> { pub(crate) async fn stop_route(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
return self.route_runtime.stop_and_wait(request).await; return self.route_runtime.stop_and_wait(request).await;
@@ -175,7 +180,7 @@ impl crate::AppState {
application_version: env!("CARGO_PKG_VERSION").to_owned(), application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count, config_document_count: document_count,
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.009-multi-route-mainnet".to_owned(), shell_phase: "pre.010-runtime-monitoring".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(), startup_diagnostic: runtime.startup_diagnostic.clone(),
}); });
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs // file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
// version: 6 // version: 7
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes. //! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
@@ -16,6 +16,7 @@ mod errors;
mod frontend_logging; mod frontend_logging;
mod logging_runtime; mod logging_runtime;
mod route_inventory; mod route_inventory;
mod route_monitoring;
mod route_runtime; mod route_runtime;
mod route_start; mod route_start;
mod splash; mod splash;
@@ -158,6 +159,14 @@ pub(crate) use self::logging_runtime::launch_identity;
pub(crate) use self::route_inventory::build_route_inventory; pub(crate) use self::route_inventory::build_route_inventory;
/// Rebuilds the route inventory from one already captured Config environment snapshot. /// Rebuilds the route inventory from one already captured Config environment snapshot.
pub(crate) use self::route_inventory::build_route_inventory_with_environment; pub(crate) use self::route_inventory::build_route_inventory_with_environment;
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) use self::route_monitoring::RAW_INGEST_ROUTE_STATUS_EVENT_NAME;
/// Frontend-safe bounded projection of one run-local continuity gap.
pub(crate) use self::route_monitoring::RawIngestRouteGapDto;
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
pub(crate) use self::route_monitoring::RawIngestRouteMonitoringDto;
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) use self::route_monitoring::project_route_monitoring;
/// Launch ownership for one independent route Worker sharing the application Store when eligible. /// Launch ownership for one independent route Worker sharing the application Store when eligible.
pub(crate) use self::route_runtime::RouteRuntimeLaunch; pub(crate) use self::route_runtime::RouteRuntimeLaunch;
/// Shared multi-route runtime state with one independent Worker per route and one same-network Store. /// Shared multi-route runtime state with one independent Worker per route and one same-network Store.

View File

@@ -0,0 +1,352 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_monitoring.rs
// version: 1
//! Frontend-safe latest-value monitoring projection for Raw Transaction Ingest Desk route Workers.
use ts_rs::TS; // rust-rules: trait-import
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) const RAW_INGEST_ROUTE_STATUS_EVENT_NAME: &str = "ksp-raw-ingest-route-status";
/// Frontend-safe bounded projection of one run-local continuity gap.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteGapDto.ts")]
pub(crate) struct RawIngestRouteGapDto {
/// Run-local gap identifier encoded as decimal text to avoid JavaScript integer truncation.
pub(crate) gap_id: String,
/// Inclusive first slot encoded as decimal text.
pub(crate) start_slot: String,
/// Inclusive last slot encoded as decimal text.
pub(crate) end_slot: String,
/// Stable source-neutral gap lifecycle code.
pub(crate) state: String,
/// Stable source-neutral reason code.
pub(crate) reason: String,
/// Stable source-neutral latest repair mechanism code, when any.
pub(crate) last_method: std::option::Option<String>,
}
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts")]
pub(crate) struct RawIngestRouteMonitoringDto {
/// Confirmed/finalized commitment bound to this Worker run.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation revalidated before this Worker started.
pub(crate) inventory_generation: u32,
/// Logical network shared by Worker and Store.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Stable logical route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Monotone Worker snapshot sequence encoded as decimal text.
pub(crate) sequence: String,
/// Safe application-owned lifecycle projection.
pub(crate) state: crate::RawIngestRouteState,
/// Stable generic Worker health code.
pub(crate) health: String,
/// Stable generic Worker activity code.
pub(crate) activity: String,
/// Whether this latest Worker snapshot is terminal.
pub(crate) terminal: bool,
/// Stable terminal fault domain when lifecycle is faulted.
pub(crate) fault_domain: std::option::Option<String>,
/// Stable terminal fault code when lifecycle is faulted.
pub(crate) fault_code: std::option::Option<String>,
/// Configured bounded admission queue capacity.
pub(crate) admission_queue_capacity: u32,
/// Latest admission queue depth.
pub(crate) admission_queue_depth: u32,
/// Configured Store persistence concurrency.
pub(crate) persistence_concurrency: u32,
/// Latest number of in-flight Store persistence operations.
pub(crate) in_flight_persistence: u32,
/// Total admitted ingress entries encoded as decimal text.
pub(crate) admitted_total: String,
/// Total canonicalized ingress entries encoded as decimal text.
pub(crate) canonicalized_total: String,
/// Total successful Store persistence outcomes encoded as decimal text.
pub(crate) persisted_total: String,
/// Total newly inserted canonical RAW entities encoded as decimal text.
pub(crate) entity_inserted_total: String,
/// Total canonical RAW entities already present encoded as decimal text.
pub(crate) entity_already_present_total: String,
/// Total purge tombstones respected by persistence encoded as decimal text.
pub(crate) entity_skipped_purged_total: String,
/// Total newly inserted acquisition observations encoded as decimal text.
pub(crate) observation_inserted_total: String,
/// Total acquisition observations already present encoded as decimal text.
pub(crate) observation_already_present_total: String,
/// Total durable content conflicts encoded as decimal text.
pub(crate) content_conflict_total: String,
/// Total non-conflict Store failures encoded as decimal text.
pub(crate) store_failure_total: String,
/// Total source-task failures encoded as decimal text.
pub(crate) source_failure_total: String,
/// Total bounded-admission backpressure waits encoded as decimal text.
pub(crate) backpressure_wait_total: String,
/// Latest number of source signals awaiting hydration/admission processing.
pub(crate) hydration_pending: u32,
/// Run-local processing frontier encoded as decimal text when established.
pub(crate) processing_frontier_slot: std::option::Option<String>,
/// Oldest slot still owning pending source work encoded as decimal text when present.
pub(crate) oldest_pending_slot: std::option::Option<String>,
/// Whether continuity health policy has emitted a run-local projection.
pub(crate) continuity_policy_observed: bool,
/// Latest run-local continuity frontier encoded as decimal text when established.
pub(crate) continuity_frontier_slot: std::option::Option<String>,
/// Whether one or more continuity gaps are currently open.
pub(crate) continuity_has_open_gaps: bool,
/// Whether all failed-source continuity obligations have been reconciled.
pub(crate) failed_source_losses_reconciled: bool,
/// Whether current coverage proves the target can continue into future slots.
pub(crate) future_target_coverage: bool,
/// Stable source-neutral latest productive-source lifecycle code.
pub(crate) source_state: std::option::Option<String>,
/// Configured logical live-source count.
pub(crate) source_total: u32,
/// Latest Active source count.
pub(crate) source_active: u32,
/// Latest Reconnecting source count.
pub(crate) source_reconnecting: u32,
/// Latest Failed source count.
pub(crate) source_failed: u32,
/// Successful source reconnect count encoded as decimal text.
pub(crate) source_reconnect_total: String,
/// Replay-bearing reconnect attempt count encoded as decimal text.
pub(crate) source_replay_attempt_total: String,
/// Proven replay-retention continuity gap count encoded as decimal text.
pub(crate) source_continuity_gap_total: String,
/// Number of currently open continuity gaps.
pub(crate) open_gap_count: u32,
/// Number of continuity gaps currently under repair.
pub(crate) repairing_gap_count: u32,
/// Cumulative repaired-gap count encoded as decimal text.
pub(crate) repaired_gap_total: String,
/// Cumulative unresolved-gap count encoded as decimal text.
pub(crate) unresolved_gap_total: String,
/// Cumulative replay repair count encoded as decimal text.
pub(crate) replay_repair_total: String,
/// Cumulative redundant-coverage repair count encoded as decimal text.
pub(crate) redundant_coverage_repair_total: String,
/// Cumulative bounded HTTP scan repair count encoded as decimal text.
pub(crate) http_scan_repair_total: String,
/// Cumulative direct block-fetch repair count encoded as decimal text.
pub(crate) repair_block_fetch_total: String,
/// Cumulative transaction-hydration repair count encoded as decimal text.
pub(crate) repair_transaction_hydration_total: String,
/// Oldest currently open gap start slot encoded as decimal text when present.
pub(crate) oldest_open_gap_start_slot: std::option::Option<String>,
/// Bounded source-neutral details for current and recent continuity gaps.
pub(crate) gaps: std::vec::Vec<crate::RawIngestRouteGapDto>,
}
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) fn project_route_monitoring(
runtime: &crate::RawIngestRouteRuntimeDto,
snapshot: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot,
) -> ksp_core_lib::Result<crate::RawIngestRouteMonitoringDto> {
let worker = snapshot.worker_snapshot();
let fault = worker.state().fault_code();
let admission_queue_capacity = usize_to_u32(snapshot.admission_queue_capacity(), "admission_queue_capacity");
let admission_queue_capacity = match admission_queue_capacity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admission_queue_depth = usize_to_u32(snapshot.admission_queue_depth(), "admission_queue_depth");
let admission_queue_depth = match admission_queue_depth {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persistence_concurrency = usize_to_u32(snapshot.persistence_concurrency(), "persistence_concurrency");
let persistence_concurrency = match persistence_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let in_flight_persistence = usize_to_u32(snapshot.in_flight_persistence(), "in_flight_persistence");
let in_flight_persistence = match in_flight_persistence {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hydration_pending = usize_to_u32(snapshot.hydration_pending(), "hydration_pending");
let hydration_pending = match hydration_pending {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_total = usize_to_u32(snapshot.source_total(), "source_total");
let source_total = match source_total {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_active = usize_to_u32(snapshot.source_active(), "source_active");
let source_active = match source_active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_reconnecting = usize_to_u32(snapshot.source_reconnecting(), "source_reconnecting");
let source_reconnecting = match source_reconnecting {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_failed = usize_to_u32(snapshot.source_failed(), "source_failed");
let source_failed = match source_failed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let open_gap_count = usize_to_u32(snapshot.open_gap_count(), "open_gap_count");
let open_gap_count = match open_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let repairing_gap_count = usize_to_u32(snapshot.repairing_gap_count(), "repairing_gap_count");
let repairing_gap_count = match repairing_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let gaps = snapshot.gaps().iter().map(project_gap).collect::<std::vec::Vec<_>>();
return std::result::Result::Ok(crate::RawIngestRouteMonitoringDto {
commitment: runtime.commitment,
inventory_generation: runtime.inventory_generation,
network: runtime.network.clone(),
profile_id: runtime.profile_id.clone(),
route_id: runtime.route_id,
sequence: worker.sequence().value().to_string(),
state: project_worker_state(worker.state()),
health: worker.health().code().to_owned(),
activity: worker.activity().code().to_owned(),
terminal: worker.state().is_terminal(),
fault_domain: fault.map(|code| return code.domain().to_owned()),
fault_code: fault.map(|code| return code.code().to_owned()),
admission_queue_capacity,
admission_queue_depth,
persistence_concurrency,
in_flight_persistence,
admitted_total: snapshot.admitted_total().to_string(),
canonicalized_total: snapshot.canonicalized_total().to_string(),
persisted_total: snapshot.persisted_total().to_string(),
entity_inserted_total: snapshot.entity_inserted_total().to_string(),
entity_already_present_total: snapshot.entity_already_present_total().to_string(),
entity_skipped_purged_total: snapshot.entity_skipped_purged_total().to_string(),
observation_inserted_total: snapshot.observation_inserted_total().to_string(),
observation_already_present_total: snapshot.observation_already_present_total().to_string(),
content_conflict_total: snapshot.content_conflict_total().to_string(),
store_failure_total: snapshot.store_failure_total().to_string(),
source_failure_total: snapshot.source_failure_total().to_string(),
backpressure_wait_total: snapshot.backpressure_wait_total().to_string(),
hydration_pending,
processing_frontier_slot: slot_text(snapshot.processing_frontier_slot()),
oldest_pending_slot: slot_text(snapshot.oldest_pending_slot()),
continuity_policy_observed: snapshot.continuity_policy_observed(),
continuity_frontier_slot: slot_text(snapshot.continuity_frontier_slot()),
continuity_has_open_gaps: snapshot.continuity_has_open_gaps(),
failed_source_losses_reconciled: snapshot.failed_source_losses_reconciled(),
future_target_coverage: snapshot.future_target_coverage(),
source_state: snapshot.source_state().map(|state| return source_state_code(state).to_owned()),
source_total,
source_active,
source_reconnecting,
source_failed,
source_reconnect_total: snapshot.source_reconnect_total().to_string(),
source_replay_attempt_total: snapshot.source_replay_attempt_total().to_string(),
source_continuity_gap_total: snapshot.source_continuity_gap_total().to_string(),
open_gap_count,
repairing_gap_count,
repaired_gap_total: snapshot.repaired_gap_total().to_string(),
unresolved_gap_total: snapshot.unresolved_gap_total().to_string(),
replay_repair_total: snapshot.replay_repair_total().to_string(),
redundant_coverage_repair_total: snapshot.redundant_coverage_repair_total().to_string(),
http_scan_repair_total: snapshot.http_scan_repair_total().to_string(),
repair_block_fetch_total: snapshot.repair_block_fetch_total().to_string(),
repair_transaction_hydration_total: snapshot.repair_transaction_hydration_total().to_string(),
oldest_open_gap_start_slot: slot_text(snapshot.oldest_open_gap_start_slot()),
gaps,
});
}
fn project_gap(gap: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapSnapshot) -> crate::RawIngestRouteGapDto {
return crate::RawIngestRouteGapDto {
gap_id: gap.gap_id().value().to_string(),
start_slot: gap.start_slot().to_string(),
end_slot: gap.end_slot().to_string(),
state: gap_state_code(gap.state()).to_owned(),
reason: gap_reason_code(gap.reason()).to_owned(),
last_method: gap.last_method().map(|method| return repair_method_code(method).to_owned()),
};
}
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
return match state {
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
_ => crate::RawIngestRouteState::Faulted,
};
}
fn source_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active => "active",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting => "reconnecting",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closing => "closing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closed => "closed",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Failed => "failed",
};
}
fn gap_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Pending => "pending",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing => "repairing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repaired => "repaired",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Unresolved => "unresolved",
};
}
fn gap_reason_code(reason: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason) -> &'static str {
return match reason {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::HttpProducedBlockUnavailable => "http_produced_block_unavailable",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::KnownReferenceMissing => "known_reference_missing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure => "source_failure",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::TransportOverflow => "transport_overflow",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::WebSocketReconnect => "websocket_reconnect",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::YellowstoneRetention => "yellowstone_retention",
};
}
fn repair_method_code(method: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod) -> &'static str {
return match method {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::Replay => "replay",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::RedundantCoverage => "redundant_coverage",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::HttpScan => "http_scan",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::BlockFetch => "block_fetch",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration => "transaction_hydration",
};
}
fn slot_text(value: std::option::Option<u64>) -> std::option::Option<String> {
return value.map(|slot| return slot.to_string());
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Raw Transaction Ingest Desk cannot project a bounded Worker counter to the frontend",
)
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/route_monitoring.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs // file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
// version: 5 // version: 6
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk. //! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
@@ -8,6 +8,7 @@ const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::fr
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1); const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
struct RouteRuntimeInner { struct RouteRuntimeInner {
last_monitoring: std::vec::Vec<crate::RawIngestRouteMonitoringDto>,
last_terminals: std::vec::Vec<crate::RawIngestRouteRuntimeDto>, last_terminals: std::vec::Vec<crate::RawIngestRouteRuntimeDto>,
network: std::option::Option<String>, network: std::option::Option<String>,
next_sequence: u64, next_sequence: u64,
@@ -28,6 +29,7 @@ impl crate::RouteRuntimeState {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
return Self { return Self {
inner: std::sync::Mutex::new(RouteRuntimeInner { inner: std::sync::Mutex::new(RouteRuntimeInner {
last_monitoring: std::vec::Vec::new(),
last_terminals: std::vec::Vec::new(), last_terminals: std::vec::Vec::new(),
network: std::option::Option::None, network: std::option::Option::None,
next_sequence: 0, next_sequence: 0,
@@ -99,9 +101,11 @@ impl crate::RouteRuntimeState {
route_id: prepared.route_id, route_id: prepared.route_id,
}; };
if inner.network.is_none() { if inner.network.is_none() {
inner.last_monitoring.clear();
inner.last_terminals.clear(); inner.last_terminals.clear();
inner.network = std::option::Option::Some(prepared.network.clone()); inner.network = std::option::Option::Some(prepared.network.clone());
} }
inner.last_monitoring.retain(|status| return !identity.matches_monitoring(status));
inner.last_terminals.retain(|terminal| return !identity.matches_dto(terminal)); inner.last_terminals.retain(|terminal| return !identity.matches_dto(terminal));
inner.next_sequence = sequence; inner.next_sequence = sequence;
if needs_store_open { if needs_store_open {
@@ -196,7 +200,12 @@ impl crate::RouteRuntimeState {
return std::result::Result::Ok(std::option::Option::None); return std::result::Result::Ok(std::option::Option::None);
} }
fn finish(&self, token: u64, terminal: crate::RawIngestRouteRuntimeDto) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> { fn finish(
&self,
token: u64,
terminal: crate::RawIngestRouteRuntimeDto,
monitoring: std::option::Option<crate::RawIngestRouteMonitoringDto>,
) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
let inner = self.inner.lock(); let inner = self.inner.lock();
let mut inner = match inner { let mut inner = match inner {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -209,6 +218,10 @@ impl crate::RouteRuntimeState {
} }
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id); inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal); inner.last_terminals.push(terminal);
if let std::option::Option::Some(status) = monitoring {
inner.last_monitoring.retain(|previous| return previous.profile_id != status.profile_id || previous.route_id != status.route_id);
inner.last_monitoring.push(status);
}
if inner.routes.is_empty() { if inner.routes.is_empty() {
let store = inner.store.take(); let store = inner.store.take();
if store.is_some() { if store.is_some() {
@@ -235,6 +248,30 @@ impl crate::RouteRuntimeState {
} }
} }
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current runtime session.
pub(crate) fn monitoring_statuses(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let mut values = inner.last_monitoring.clone();
for slot in &inner.routes {
if let RouteRuntimeSlot::Active { handle, identity, .. } = slot {
let snapshot = handle.snapshot_source().current();
let runtime = identity.dto(project_worker_state(snapshot.worker_snapshot().state()));
let projected = crate::project_route_monitoring(&runtime, &snapshot);
let projected = match projected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
values.retain(|previous| return previous.profile_id != projected.profile_id || previous.route_id != projected.route_id);
values.push(projected);
}
}
return std::result::Result::Ok(values);
}
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup. /// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> { pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let target = self.stop_target(request); let target = self.stop_target(request);
@@ -389,6 +426,18 @@ impl crate::RouteRuntimeLaunch {
return self.acknowledgement.clone(); return self.acknowledgement.clone();
} }
/// Returns the complete safe logical route identity used to project Worker monitoring snapshots.
#[must_use]
pub(crate) fn monitoring_identity(&self) -> crate::RawIngestRouteRuntimeDto {
return self.acknowledgement.clone();
}
/// Returns an independent latest-value source for frontend-safe monitoring relay.
#[must_use]
pub(crate) fn monitoring_source(&self) -> ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource {
return self.handle.snapshot_source();
}
/// Waits for this Worker terminal state, removes only its route and closes Store only after the final same-network route terminates. /// Waits for this Worker terminal state, removes only its route and closes Store only after the final same-network route terminates.
pub(crate) async fn monitor(self) { pub(crate) async fn monitor(self) {
let terminal = self.handle.wait_terminal().await; let terminal = self.handle.wait_terminal().await;
@@ -415,7 +464,22 @@ impl crate::RouteRuntimeLaunch {
}; };
let mut terminal = self.acknowledgement; let mut terminal = self.acknowledgement;
terminal.state = terminal_state; terminal.state = terminal_state;
let store = self.runtime_state.finish(self.token, terminal); let snapshot = self.handle.snapshot_source().current();
let monitoring = crate::project_route_monitoring(&terminal, &snapshot);
let monitoring = match monitoring {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not retain terminal route monitoring projection"
);
std::option::Option::None
},
};
let store = self.runtime_state.finish(self.token, terminal, monitoring);
let store = match store { let store = match store {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => { std::result::Result::Err(error) => {
@@ -592,6 +656,10 @@ impl RouteRuntimeIdentity {
fn matches_dto(&self, value: &crate::RawIngestRouteRuntimeDto) -> bool { fn matches_dto(&self, value: &crate::RawIngestRouteRuntimeDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id; return self.profile_id == value.profile_id && self.route_id == value.route_id;
} }
fn matches_monitoring(&self, value: &crate::RawIngestRouteMonitoringDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id;
}
} }
enum RouteStopTarget { enum RouteStopTarget {

View File

@@ -1,8 +1,11 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs // file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 6 // version: 7
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application. //! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Runs the Raw Transaction Ingest desktop application. /// Runs the Raw Transaction Ingest desktop application.
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> { pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
@@ -78,6 +81,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
emit_frontend_log, emit_frontend_log,
get_route_foundation, get_route_foundation,
get_route_inventory, get_route_inventory,
get_route_monitoring,
get_runtime_status, get_runtime_status,
splash_frontend_ready, splash_frontend_ready,
start_route, start_route,
@@ -121,6 +125,17 @@ fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result:
}; };
} }
#[tauri::command]
fn get_route_monitoring(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>, crate::CommandErrorDto> {
let result = state.route_monitoring();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_monitoring", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
};
}
#[tauri::command] #[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> { fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
let result = state.shell_status(); let result = state.shell_status();
@@ -145,6 +160,7 @@ async fn splash_frontend_ready(
#[tauri::command] #[tauri::command]
async fn start_route( async fn start_route(
app: tauri::AppHandle,
request: crate::RawIngestRouteStartRequestDto, request: crate::RawIngestRouteStartRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
@@ -156,13 +172,60 @@ async fn start_route(
}, },
}; };
let acknowledgement = launch.acknowledgement(); let acknowledgement = launch.acknowledgement();
let monitor = tauri::async_runtime::spawn(async move { let monitoring_identity = launch.monitoring_identity();
let monitoring_source = launch.monitoring_source();
let monitoring_app = app.clone();
let monitoring_task = tauri::async_runtime::spawn(async move {
monitor_route_status(monitoring_app, monitoring_identity, monitoring_source).await;
});
std::mem::drop(monitoring_task);
let terminal_task = tauri::async_runtime::spawn(async move {
launch.monitor().await; launch.monitor().await;
}); });
std::mem::drop(monitor); std::mem::drop(terminal_task);
return std::result::Result::Ok(acknowledgement); return std::result::Result::Ok(acknowledgement);
} }
async fn monitor_route_status(
app: tauri::AppHandle,
runtime: crate::RawIngestRouteRuntimeDto,
source: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource,
) {
let mut snapshot = source.current();
loop {
let projected = crate::project_route_monitoring(&runtime, &snapshot);
match projected {
std::result::Result::Ok(status) => {
let main = app.get_webview_window("main");
if let std::option::Option::Some(window) = main {
let emitted = window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status);
if emitted.is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
"Raw Transaction Ingest Desk could not emit latest-value route monitoring status"
);
}
}
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not project latest-value route monitoring status"
);
},
}
if snapshot.worker_snapshot().state().is_terminal() {
return;
}
let observed = snapshot.worker_snapshot().sequence();
snapshot = source.wait_for_change(observed).await;
}
}
#[tauri::command] #[tauri::command]
async fn stop_route( async fn stop_route(
request: crate::RawIngestRouteStopRequestDto, request: crate::RawIngestRouteStopRequestDto,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
// version: 4 // version: 5
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Start-resource reconstruction. //! Dependency-boundary canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
@@ -85,3 +85,14 @@ fn pre_008_route_runtime_owns_real_store_worker_start_stop_without_physical_back
assert!(!source.contains(forbidden), "pre.008 crosses physical or secret boundary {forbidden}"); assert!(!source.contains(forbidden), "pre.008 crosses physical or secret boundary {forbidden}");
} }
} }
#[test]
fn pre_010_monitoring_projects_existing_worker_snapshot_facade_without_new_backend_edges() {
let source = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in ["RawTransactionIngestSnapshot", "worker_snapshot()", "source_reconnect_total()", "source_replay_attempt_total()", "gaps()"] {
assert!(source.contains(required), "missing Worker snapshot facade marker {required}");
}
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "reqwest", "tonic", "yellowstone_grpc_client"] {
assert!(!source.contains(forbidden), "monitoring opens forbidden backend edge {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 6 // version: 7
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk. //! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
@@ -201,3 +201,19 @@ fn pre_009_start_stop_controls_are_multi_route_targeted_and_backend_owned() {
assert!(main.contains(required), "missing pre.009 frontend control marker {required}"); assert!(main.contains(required), "missing pre.009 frontend control marker {required}");
} }
} }
#[test]
fn pre_010_runtime_monitoring_is_backend_owned_event_driven_and_resynchronizable() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"get_route_monitoring",
"monitor_route_status",
"window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status)",
"source.wait_for_change(observed).await",
] {
assert!(tauri.contains(required), "missing pre.010 monitoring bridge marker {required}");
}
let state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(state.contains("route_monitoring(&self)"));
assert!(state.contains("pre.010-runtime-monitoring"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 5 // version: 7
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold. //! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
@@ -126,3 +126,37 @@ fn pre_008_runtime_acknowledgement_and_frontend_tracing_remain_secret_and_handle
assert!(!main.contains(forbidden), "frontend runtime trace exposes forbidden marker {forbidden}"); assert!(!main.contains(forbidden), "frontend runtime trace exposes forbidden marker {forbidden}");
} }
} }
#[test]
fn pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"sequence: String",
"processing_frontier_slot: std::option::Option<String>",
"continuity_frontier_slot: std::option::Option<String>",
"fault_domain",
"fault_code",
"gaps: std::vec::Vec<crate::RawIngestRouteGapDto>",
] {
assert!(monitoring.contains(required), "missing pre.010 safe monitoring marker {required}");
}
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "x-token"] {
assert!(!monitoring.contains(forbidden), "monitoring projection exposes forbidden physical marker {forbidden}");
}
let dto_start = monitoring.find("pub(crate) struct RawIngestRouteMonitoringDto");
assert!(dto_start.is_some(), "monitoring DTO declaration is missing");
let dto_start = match dto_start {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dto_end = monitoring[dto_start..].find("/// Projects one complete concrete Worker snapshot");
assert!(dto_end.is_some(), "monitoring DTO declaration boundary is missing");
let dto_end = match dto_end {
std::option::Option::Some(value) => dto_start + value,
std::option::Option::None => return,
};
let dto_surface = &monitoring[dto_start..dto_end];
for forbidden in ["raw_transaction", "signature", "payload", "canonical_bytes", "wire_bytes", "transaction_bytes"] {
assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs // file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 8 // version: 9
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction. //! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
@@ -21,7 +21,7 @@ fn read_text(path: &std::path::Path) -> String {
} }
#[test] #[test]
fn pre_008_production_module_inventory_adds_only_route_runtime_to_pre_007_surface() { fn pre_010_production_module_inventory_adds_only_route_monitoring_to_pre_009_surface() {
let lib = read_text(app_root().join("src/lib.rs").as_path()); let lib = read_text(app_root().join("src/lib.rs").as_path());
let expected = [ let expected = [
"mod app_state;", "mod app_state;",
@@ -33,6 +33,7 @@ fn pre_008_production_module_inventory_adds_only_route_runtime_to_pre_007_surfac
"mod frontend_logging;", "mod frontend_logging;",
"mod logging_runtime;", "mod logging_runtime;",
"mod route_inventory;", "mod route_inventory;",
"mod route_monitoring;",
"mod route_runtime;", "mod route_runtime;",
"mod route_start;", "mod route_start;",
"mod splash;", "mod splash;",
@@ -100,7 +101,7 @@ fn pre_009_multi_route_runtime_keeps_independent_workers_on_one_same_network_sto
"ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH", "ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH",
"already owns one Worker for the selected logical route", "already owns one Worker for the selected logical route",
"shared_store(prepared.network.as_str())", "shared_store(prepared.network.as_str())",
"self.runtime_state.finish(self.token, terminal)", "self.runtime_state.finish(self.token, terminal, monitoring)",
"Arc::try_unwrap", "Arc::try_unwrap",
] { ] {
assert!(runtime.contains(required), "missing pre.009 shared-Store marker {required}"); assert!(runtime.contains(required), "missing pre.009 shared-Store marker {required}");
@@ -141,3 +142,28 @@ fn pre_009_yellowstone_uses_block_subscription_and_one_get_block_path_instead_of
assert!(workspace.contains("RAW_TRANSACTION_INGEST_YELLOWSTONE_BLOCK_RECONCILIATION_ATTEMPTS")); assert!(workspace.contains("RAW_TRANSACTION_INGEST_YELLOWSTONE_BLOCK_RECONCILIATION_ATTEMPTS"));
assert!(workspace.contains("stop_receiver.changed()")); assert!(workspace.contains("stop_receiver.changed()"));
} }
#[test]
fn pre_010_latest_value_monitoring_reuses_worker_snapshot_source_and_tauri_event_bridge() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"RawIngestRouteGapDto",
"project_route_monitoring",
"continuity_frontier_slot",
"future_target_coverage",
"source_reconnect_total",
"source_replay_attempt_total",
"repaired_gap_total",
"backpressure_wait_total",
] {
assert!(monitoring.contains(required), "missing pre.010 monitoring marker {required}");
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["get_route_monitoring", "RAW_INGEST_ROUTE_STATUS_EVENT_NAME", "monitor_route_status", "source.wait_for_change(observed).await"] {
assert!(tauri.contains(required), "missing pre.010 event/resync marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("monitoring_statuses"));
assert!(runtime.contains("last_monitoring"));
}

View File

@@ -0,0 +1,43 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_monitoring.rs
// version: 1
#[test]
fn pre_010_source_gap_and_repair_codes_are_exact_and_source_neutral() {
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active), "active");
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting), "reconnecting");
assert_eq!(super::gap_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing), "repairing");
assert_eq!(super::gap_reason_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure), "source_failure");
assert_eq!(
super::repair_method_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration),
"transaction_hydration"
);
}
#[test]
fn pre_010_slot_projection_uses_decimal_text_for_javascript_safe_exactness() {
assert_eq!(super::slot_text(std::option::Option::Some(u64::MAX)), std::option::Option::Some(u64::MAX.to_string()));
assert_eq!(super::slot_text(std::option::Option::None), std::option::Option::None);
}
#[test]
fn pre_010_bounded_cardinality_projection_rejects_only_unrepresentable_values() {
let zero = super::usize_to_u32(0, "zero");
assert!(matches!(zero, std::result::Result::Ok(0)));
let maximum = usize::try_from(u32::MAX);
assert!(maximum.is_ok());
if let std::result::Result::Ok(maximum) = maximum {
let converted = super::usize_to_u32(maximum, "maximum");
assert!(matches!(converted, std::result::Result::Ok(u32::MAX)));
}
if usize::BITS > u32::BITS {
let overflow_value = usize::try_from(u64::from(u32::MAX) + 1);
assert!(overflow_value.is_ok());
if let std::result::Result::Ok(overflow_value) = overflow_value {
let overflow = super::usize_to_u32(overflow_value, "overflow");
assert!(overflow.is_err());
if let std::result::Result::Err(error) = overflow {
assert_eq!(error.code(), crate::ERROR_CODE_APP_STATE_INVALID);
}
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs // file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
// version: 2 // version: 3
#[test] #[test]
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() { fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
@@ -8,6 +8,7 @@ fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material()
assert!(inner.is_ok()); assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner { if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty()); assert!(inner.routes.is_empty());
assert!(inner.last_monitoring.is_empty());
assert!(inner.store.is_none()); assert!(inner.store.is_none());
assert!(inner.network.is_none()); assert!(inner.network.is_none());
assert!(inner.store_opening_token.is_none()); assert!(inner.store_opening_token.is_none());

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs // file: crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs
// version: 9 // version: 10
/// Runtime-neutral boxed future resolving to one newer concrete RAW transaction ingest Worker snapshot. /// Runtime-neutral boxed future resolving to one newer concrete RAW transaction ingest Worker snapshot.
pub type RawTransactionIngestSnapshotFuture<'a> = pub type RawTransactionIngestSnapshotFuture<'a> =
@@ -533,6 +533,36 @@ impl crate::RawTransactionIngestSnapshot {
return &self.worker; return &self.worker;
} }
/// Reports whether continuity policy has emitted a run-local health projection.
#[must_use]
pub const fn continuity_policy_observed(&self) -> bool {
return self.continuity_policy_observed;
}
/// Returns the latest run-local continuity frontier when continuity policy has observed one.
#[must_use]
pub const fn continuity_frontier_slot(&self) -> std::option::Option<u64> {
return self.continuity_frontier_slot;
}
/// Reports whether the continuity policy currently retains one or more open gaps.
#[must_use]
pub const fn continuity_has_open_gaps(&self) -> bool {
return self.continuity_has_open_gaps;
}
/// Reports whether all failed-source continuity obligations have been reconciled for this run.
#[must_use]
pub const fn failed_source_losses_reconciled(&self) -> bool {
return self.failed_source_losses_reconciled;
}
/// Reports whether current source coverage proves the active target can continue into future slots.
#[must_use]
pub const fn future_target_coverage(&self) -> bool {
return self.future_target_coverage;
}
/// Returns the configured bounded admission queue capacity. /// Returns the configured bounded admission queue capacity.
#[must_use] #[must_use]
pub const fn admission_queue_capacity(&self) -> usize { pub const fn admission_queue_capacity(&self) -> usize {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs // file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs
// version: 7 // version: 8
fn snapshot_foundation_with_source_total( fn snapshot_foundation_with_source_total(
source_total: usize, source_total: usize,
@@ -346,3 +346,24 @@ fn v0_3_14_pre_011_snapshot_carries_checked_gap_observability() {
assert_eq!(snapshot.oldest_open_gap_start_slot(), std::option::Option::Some(100)); assert_eq!(snapshot.oldest_open_gap_start_slot(), std::option::Option::Some(100));
return; return;
} }
#[test]
fn v0_3_15_pre_010_public_continuity_health_getters_are_source_neutral() {
let (mut publisher, source) = match snapshot_foundation_with_source_total(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let projection = crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(90), std::option::Option::None)
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 1, 0)
.with_source_counts(2, 2, 0, 0)
.with_continuity_health(std::option::Option::Some(90), false, true)
.with_failed_source_losses_reconciled(true);
assert!(publisher.record_processing_frontier(ksp_worker_api::WorkerState::Running, 0, 0, projection).is_ok());
let snapshot = source.current();
assert!(snapshot.continuity_policy_observed());
assert_eq!(snapshot.continuity_frontier_slot(), std::option::Option::Some(90));
assert!(!snapshot.continuity_has_open_gaps());
assert!(snapshot.failed_source_losses_reconciled());
assert!(snapshot.future_target_coverage());
return;
}

View File

@@ -0,0 +1,113 @@
<!-- file: deltas/0.3.15/pre.010-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.15-pre.010-fix.001`
## Base
```text
base archive : ksp-general-0.3.15-pre.010.zip
base SHA-256 : c7365dad72c306c4781e351c7b4f1111ebcdce0ed34946b158f332d6b1523fa5
base version : 0.3.15-pre.10
```
## Objectif
Corriger le faux positif du canari Desktop Security introduit par `pre.010` sans modifier le contrat de monitoring, le runtime, l'acquisition ni l'IPC.
Le test `pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact` interdisait la sous-chaîne générique `raw_transaction` dans l'ensemble du fichier `route_monitoring.rs`. Cette chaîne est légitime dans le nom de crate `ksp_worker_raw_transaction_ingest_lib` et dans les chemins d'export TypeScript de l'application ; sa présence ne prouve aucune exposition de payload RAW.
Le correctif borne le scan RAW à la déclaration de `RawIngestRouteMonitoringDto`, c'est-à-dire à la surface effectivement sérialisable vers le frontend. Dans cette surface, les marqueurs suivants restent interdits :
```text
raw_transaction
signature
payload
canonical_bytes
wire_bytes
transaction_bytes
```
Les interdictions physiques/secrets restent en parallèle appliquées à l'ensemble de `route_monitoring.rs` :
```text
endpoint_url
connection_uri
api_key
source_key
worker_id
x-token
```
Aucune structure DTO, aucun getter Worker, aucun événement Tauri, aucune commande IPC et aucune logique runtime n'est modifié.
## Version
```text
workspace.package.version : 0.3.15-pre.10.fix.1
root Cargo header counter : 619
```
Le bump est requis car le correctif modifie un test Rust participant au code/build de validation de la prerelease.
## Fichiers ajoutés
```text
deltas/0.3.15/pre.010-fix.001.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
```
## Fichiers supprimés
Aucun.
## Validations exécutées dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
```
Des canaris statiques complémentaires vérifient que :
```text
workspace.package.version vaut 0.3.15-pre.10.fix.1
le scan interdit encore les marqueurs endpoint/secret/handle
le scan RAW est borné à la déclaration sérialisable RawIngestRouteMonitoringDto
la chaîne raw_transaction reste interdite dans cette surface DTO mais n'est plus confondue avec les noms de crate/bindings
aucun fichier runtime de pre.010 n'est modifié par ce fix
```
## Validations non exécutées dans l'environnement d'assemblage
Cargo/rustfmt ne sont pas disponibles dans cet environnement ; les gates suivants ne sont donc pas déclarés PASS ici :
```bash
cargo fmt --all
cargo fmt --all -- --check
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Aucun `npm run build` manuel n'est requis ; le frontend reste géré par le workflow Tauri/Vite du projet.
## Décisions
- Conserver le contrat monitoring `pre.010` inchangé : le gate n'a détecté aucune fuite DTO/runtime, seulement un scan textuel trop large.
- Borner le scan RAW à la déclaration DTO sérialisable et conserver un scan fichier complet séparé pour les marqueurs physiques/secrets.
- Ne pas toucher README, USAGE, plan, Worker ni runtime dans ce correctif ciblé.
## Questions ouvertes
Aucune pour ce correctif. La poursuite fonctionnelle reste `pre.011` après validation opérateur de `pre.010-fix.001`.

153
deltas/0.3.15/pre.010.md Normal file
View File

@@ -0,0 +1,153 @@
<!-- file: deltas/0.3.15/pre.010.md -->
<!-- version: 1 -->
# Delta `0.3.15-pre.010`
## Base
```text
base archive : ksp-general-0.3.15-pre.009-fix.003.zip
base SHA-256 : 7882f39d8ac213712f81b4dbef1681e4b63064e1ebdf0b8515e343cf3d2094ef
base version : 0.3.15-pre.9.fix.3
```
Le gate opérateur de la base confirme les audits Rust/Markdown, `cargo check --workspace`, Clippy strict, les tests Worker Raw Transaction Ingest, `workspace_logging`, le workspace all-targets/all-features et le lancement Tauri. Le correctif `pre.009-fix.003` supprime le fault synthétique `source.admission_closed` observé lors du Stop coopératif des routes Mainnet Yellowstone et HTTP Block Polling.
## Objectif
Matérialiser l'observabilité runtime prévue pour la Desk sans modifier l'acquisition : relayer les snapshots latest-value de chaque Worker indépendant vers un contrat IPC sûr couvrant lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair.
## Projection latest-value sûre
Le nouveau module `route_monitoring` consomme exclusivement `RawTransactionIngestSnapshotSource` et `RawTransactionIngestSnapshot`. Il construit deux DTO frontend-safe :
```text
RawIngestRouteMonitoringDto
RawIngestRouteGapDto
```
La projection comprend notamment :
```text
lifecycle / terminal / fault code stable
health / activity
admission queue / persistence concurrency
admitted / canonicalized / persisted
entity / observation outcomes
store/source failures
backpressure waits
hydration pending / processing frontier
continuity policy / frontier / open-gap state / future coverage
source active/reconnecting/failed
reconnect / replay-attempt / replay-retention-gap counters
gap details bornés
repair totals par mécanisme
```
Aucune URL, credential, provider physique, source key, `WorkerId`, handle, URI Store, signature ou payload `RawTransaction` n'est projeté.
Les séquences, compteurs et slots issus de `u64` traversent IPC sous forme de texte décimal afin d'éviter toute perte de précision JavaScript. Les cardinalités bornées issues de `usize` sont converties en `u32` avec échec fermé lorsqu'elles ne sont pas représentables.
## Relais Tauri et resynchronisation
Chaque Start conserve le monitor terminal/cleanup existant et ajoute un monitor latest-value indépendant :
```text
Worker handle
-> RawTransactionIngestSnapshotSource clone
-> projection sûre
-> event Tauri ksp-raw-ingest-route-status
```
Le monitor d'observabilité ne possède ni Store ni lifecycle. Une erreur d'émission UI n'arrête pas le Worker et ne modifie pas le cleanup terminal.
La commande :
```text
get_route_monitoring
```
retourne les latest values des routes actives ainsi que les derniers terminaux sûrs retenus dans la session runtime same-network. Un nouveau Start de la même identité remplace son ancien terminal ; une nouvelle session réseau repart sans terminaux hérités.
Cette commande permet une resynchronisation explicite même si le frontend a manqué un événement intermédiaire.
## Extension minimale du snapshot Worker
Le snapshot concret portait déjà les cinq informations de continuité nécessaires au mapping UI mais ne les exposait pas par getters publics. `pre.010` ouvre uniquement :
```text
continuity_policy_observed()
continuity_frontier_slot()
continuity_has_open_gaps()
failed_source_losses_reconciled()
future_target_coverage()
```
Aucun nouveau type Worker, aucune logique de retry/reconnect/replay/repair, aucune persistence et aucune dépendance ne sont ajoutés.
## Frontend
La tranche fournit le contrat backend/event/resync mais ne finalise pas encore l'affichage détaillé de supervision. La consommation UI des métriques, le refresh dédié et le tracing TypeScript associé restent `pre.011`.
## Version
```text
workspace.package.version : 0.3.15-pre.10
root Cargo header counter : 618
```
## Validation d'assemblage
L'environnement d'assemblage ne fournit ni Cargo ni rustfmt ; aucun résultat Cargo local n'est donc revendiqué.
Le gate opérateur attendu est :
```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/0.3.15
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Aucun script npm applicatif n'est exécuté directement par l'opérateur.
## Inventaire exact du delta
Le delta contient `3` ajouts, `16` modifications et aucune suppression, soit `19` entrées.
Ajouts :
```text
crates/ksp-app-raw-transaction-ingest-desk/src/route_monitoring.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_monitoring.rs
deltas/0.3.15/pre.010.md
```
Modifications :
```text
Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/README.md
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs
crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/snapshot.rs
docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
```
Suppressions : aucune.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md --> <!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
<!-- version: 16 --> <!-- version: 17 -->
# Plan v0.3.15 — Raw Transaction Ingest Desk # Plan v0.3.15 — Raw Transaction Ingest Desk
@@ -811,7 +811,13 @@ Budget cible : une tranche runtime/throughput Mainnet + multi-route.
### `pre.010` — snapshots et monitoring runtime ### `pre.010` — snapshots et monitoring runtime
Relayer les snapshots/events sûrs nécessaires à la Desk : lifecycle, health, activity, backpressure, reconnect, replay, gaps et repair, sans payload RawTransaction ni identité provider sensible. **État : implémenté.** Chaque route active fournit désormais une source latest-value indépendante dérivée directement du `RawTransactionIngestSnapshotSource` de son Worker. Le backend projette lifecycle, health, activity, admission/persistence, backpressure, hydration pending, processing frontier, source counts, reconnect/replay, continuité, gaps et repair sans exposer `WorkerId`, source key, endpoint, provider physique, URI Store, signature ni payload `RawTransaction`.
Le relais Tauri suit le pattern KSP déjà utilisé par Backfill Desk : l'événement `ksp-raw-ingest-route-status` transporte une projection complète à chaque latest value, tandis que `get_route_monitoring` fournit une resynchronisation explicite des Workers actifs et des derniers terminaux retenus dans la session same-network. Le monitor d'événements est séparé du monitor de cleanup Store/Worker : perdre un événement UI ne modifie jamais le lifecycle ni la fermeture du Store.
Les compteurs, séquences et slots issus de `u64` sont sérialisés en texte décimal pour éviter toute perte de précision JavaScript. Les cardinalités bornées `usize` sont converties en `u32` avec échec fermé si elles deviennent non représentables. Le Worker ouvre uniquement cinq getters publics source-neutral déjà portés par son snapshot (`continuity_policy_observed`, `continuity_frontier_slot`, `continuity_has_open_gaps`, `failed_source_losses_reconciled`, `future_target_coverage`) ; aucune logique d'acquisition, retry, repair ou persistence n'est déplacée.
Le frontend détaillé ne consomme pas encore cette surface pour construire la vue de supervision complète : ce rendu et son tracing TypeScript restent `pre.011`.
Budget cible : une tranche observabilité. Budget cible : une tranche observabilité.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md --> <!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
<!-- version: 17 --> <!-- version: 19 -->
# Validation v0.3.15 — Raw Transaction Ingest Desk # Validation v0.3.15 — Raw Transaction Ingest Desk
@@ -431,7 +431,13 @@ Le profil `publicnode_mainnet` contient deux endpoints HTTP de même rôle : Pub
### `pre.010` — monitoring ### `pre.010` — monitoring
Snapshots/events/monitoring lifecycle-health-activity-backpressure-reconnect-replay-gaps-repair. Implémenté dans l'assemblage de tranche. Le nouveau module `route_monitoring` projette les snapshots Worker existants vers un DTO latest-value source-neutral comprenant lifecycle, health, activity, admission/persistence, backpressure, hydration pending, processing frontier, continuité, source counts, reconnect/replay, gaps et repair. Le Worker expose uniquement les cinq getters de continuité qui manquaient à la façade publique de son snapshot ; aucun type ni comportement d'acquisition n'est ajouté.
Le Tauri backend démarre un monitor de snapshot indépendant pour chaque Worker lancé et émet `ksp-raw-ingest-route-status` vers la fenêtre principale. La commande `get_route_monitoring` permet une resynchronisation complète sans dépendre de la livraison de tous les événements intermédiaires. Le runtime conserve le dernier snapshot terminal sûr de chaque route jusqu'à un nouveau Start de la même identité ou au changement de session réseau.
Sécurité attendue : aucune URL, credential, provider physique, source key, `WorkerId`, handle, URI Store, signature ou payload RAW dans le DTO. Les `u64` traversant IPC sont encodés en texte décimal ; les cardinalités bornées sont projetées en `u32`. Le monitor d'observabilité ne possède ni Store ni lifecycle et ne peut pas arrêter/redémarrer un Worker.
La consommation UI détaillée de ces snapshots et le tracing TypeScript associé restent `pre.011`.
### `pre.011` — frontend ### `pre.011` — frontend
@@ -647,3 +653,31 @@ La cause Mainnet est une race interne commune : le superviseur Worker ferme le r
Validation attendue après `fix.003` : le drain coopératif ne ferme plus ladmission au début ; il consomme la file jusquà destruction naturelle des senders après propagation du Stop. Le hard-close reste dans le chemin `shutdown_drain_timeout`. Un test unitaire force cette fenêtre de propagation et exige `Stopped` sans source failure. Validation attendue après `fix.003` : le drain coopératif ne ferme plus ladmission au début ; il consomme la file jusquà destruction naturelle des senders après propagation du Stop. Le hard-close reste dans le chemin `shutdown_drain_timeout`. Un test unitaire force cette fenêtre de propagation et exige `Stopped` sans source failure.
### `pre.010-fix.001` — faux positif du canari de sécurité monitoring
Le gate opérateur de `0.3.15-pre.010` confirme que la tranche compile et que son comportement runtime Mainnet attendu est opérationnel, mais détecte un unique échec déterministe dans le canari Desktop Security :
```text
pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact
FAIL : monitoring projection exposes forbidden marker raw_transaction
```
Le marqueur est trop large : `route_monitoring.rs` doit nécessairement référencer `ksp_worker_raw_transaction_ingest_lib` et les chemins d'export TypeScript propres à `ksp-app-raw-transaction-ingest-desk`. Ces identifiants contiennent la chaîne `raw_transaction` sans exposer aucune donnée RAW.
Le gate opérateur confirme en parallèle :
```text
audit Rust workspace PASS
audit Markdown PASS (318 tables / 212 fichiers)
cargo check --workspace PASS
Clippy workspace strict -D warnings PASS
tests Worker PASS : 160 unitaires + suites d'intégration
Raw Transaction Ingest Desk 1 seul échec : canari desktop_security ci-dessus
workspace all-targets/all-features même échec déterministe
cargo tauri dev lancé et application opérationnelle
```
Le live Mainnet exécuté pendant ce gate confirme aussi que `yellowstone-hydrated` et `http-block-polling` peuvent fonctionner simultanément sur le Store partagé, puis terminer toutes deux en `Stopped` après Stop ciblé. Aucun fault `source.admission_closed` n'est réapparu.
Le correctif ne modifie pas la projection runtime. Il sépare le canari en deux niveaux : les marqueurs physiques/secrets (`endpoint_url`, `connection_uri`, `api_key`, `source_key`, `worker_id`, `x-token`) restent interdits dans tout `route_monitoring.rs`, tandis que les marqueurs RAW (`raw_transaction`, `signature`, `payload`, `canonical_bytes`, `wire_bytes`, `transaction_bytes`) sont interdits dans la seule surface déclarative de `RawIngestRouteMonitoringDto`. Le test vérifie explicitement les bornes de cette déclaration. Il reste donc strict sur l'IPC sans confondre les noms normaux des crates/bindings avec des champs réellement projetés.