diff --git a/Cargo.toml b/Cargo.toml index 04d6f9d..1b2e502 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 398 +# version: 399 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.6-pre.3.fix.1" +version = "0.3.6-pre.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/http_executor.rs b/crates/ksp-onchain-transport-lib/src/http_executor.rs index f4cd338..f48b7bf 100644 --- a/crates/ksp-onchain-transport-lib/src/http_executor.rs +++ b/crates/ksp-onchain-transport-lib/src/http_executor.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/http_executor.rs -// version: 4 +// version: 7 const HTTP_BAD_GATEWAY: u16 = 502; const HTTP_GATEWAY_TIMEOUT: u16 = 504; @@ -8,6 +8,63 @@ const HTTP_REQUEST_TIMEOUT: u16 = 408; const HTTP_SERVICE_UNAVAILABLE: u16 = 503; const HTTP_TOO_MANY_REQUESTS: u16 = 429; +/// Typed value returned by an observed HTTP RPC path together with the safe identity of the endpoint that produced the successful response. +/// +/// Endpoint URLs, headers and raw HTTP bodies are intentionally absent. +#[derive(Clone, PartialEq)] +pub struct HttpObservedValue { + value: T, + endpoint_name: std::string::String, + provider: crate::HttpProviderName, +} + +impl HttpObservedValue { + /// Returns the typed RPC value. + #[must_use] + pub const fn value(&self) -> &T { + return &self.value; + } + + /// Returns the safe configured identity of the endpoint that produced the successful response. + #[must_use] + pub fn endpoint_name(&self) -> &str { + return self.endpoint_name.as_str(); + } + + /// Returns the safe provider descriptor attached to the successful endpoint. + #[must_use] + pub const fn provider(&self) -> &crate::HttpProviderName { + return &self.provider; + } + + /// Consumes the observation and returns only the typed value. + #[must_use] + pub fn into_value(self) -> T { + return self.value; + } + + /// Builds an observed value from a successful Transport attempt and its safe routing identity. + pub(crate) fn new(value: T, endpoint_name: std::string::String, provider: crate::HttpProviderName) -> Self { + return Self { value, endpoint_name, provider }; + } + + /// Consumes the observation into its typed value and safe routing identity for crate-internal typed decoding. + pub(crate) fn into_parts(self) -> (T, std::string::String, crate::HttpProviderName) { + return (self.value, self.endpoint_name, self.provider); + } +} + +impl std::fmt::Debug for HttpObservedValue { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("HttpObservedValue") + .field("endpoint_name", &self.endpoint_name) + .field("provider", &self.provider) + .field("value", &"") + .finish(); + } +} + impl crate::HttpTransportPool { /// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy. /// @@ -19,6 +76,33 @@ impl crate::HttpTransportPool { method: &crate::HttpRpcMethodDescriptor, params: std::vec::Vec, ) -> ksp_core_lib::Result { + return self.execute_standard_rpc_with(role, method, params, |value, _permit| return value).await; + } + + /// Executes one audited standard Solana HTTP JSON-RPC method and retains only safe routing identity for the successful attempt. + pub(crate) async fn execute_standard_rpc_observed( + &self, + role: &crate::HttpRoleName, + method: &crate::HttpRpcMethodDescriptor, + params: std::vec::Vec, + ) -> ksp_core_lib::Result> { + return self + .execute_standard_rpc_with(role, method, params, |value, permit| { + return crate::HttpObservedValue::new(value, permit.selection().endpoint_name().to_owned(), permit.client().provider().clone()); + }) + .await; + } + + async fn execute_standard_rpc_with( + &self, + role: &crate::HttpRoleName, + method: &crate::HttpRpcMethodDescriptor, + params: std::vec::Vec, + on_success: F, + ) -> ksp_core_lib::Result + where + F: std::ops::FnOnce(serde_json::Value, &crate::HttpRequestPermit) -> T, + { let support = method.ensure_runtime_supported(); if let std::result::Result::Err(error) = support { return std::result::Result::Err(error); @@ -156,7 +240,12 @@ impl crate::HttpTransportPool { http_status = status, "completed Solana HTTP JSON-RPC request" ); - return parsed.into_result(); + let value = parsed.into_result(); + let value = match value { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(on_success(value, &permit)); } } } @@ -203,14 +292,11 @@ async fn wait_retry_delay(delay: std::time::Duration, deadline: std::time::Insta return std::time::Instant::now() < deadline; } -fn execution_timeout(method: &crate::HttpRpcMethodDescriptor, message: &str) -> ksp_core_lib::Result { +fn execution_timeout(method: &crate::HttpRpcMethodDescriptor, message: &str) -> ksp_core_lib::Result { return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, message).with_context("rpc_method", method.method())); } -fn rate_limited_error( - method: &crate::HttpRpcMethodDescriptor, - provider_retry_after: std::option::Option, -) -> ksp_core_lib::Result { +fn rate_limited_error(method: &crate::HttpRpcMethodDescriptor, provider_retry_after: std::option::Option) -> ksp_core_lib::Result { let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_RATE_LIMITED, "Solana HTTP endpoint rate-limited the JSON-RPC request") .with_context("rpc_method", method.method()); if let std::option::Option::Some(delay) = provider_retry_after { @@ -219,7 +305,7 @@ fn rate_limited_error( return std::result::Result::Err(error); } -fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ksp_core_lib::Result { +fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ksp_core_lib::Result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "Solana HTTP endpoint returned an unsuccessful status") .with_context("rpc_method", method.method()) diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index f8a8fdd..3579ee0 100644 --- a/crates/ksp-onchain-transport-lib/src/lib.rs +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/lib.rs -// version: 44 +// version: 45 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -271,6 +271,8 @@ pub use self::http_client::HttpEndpointClient; pub use self::http_client::HttpEndpointRoleSnapshot; /// Safe metadata snapshot for one logical HTTP endpoint. pub use self::http_client::HttpEndpointSnapshot; +/// Typed RPC value paired with the safe identity of the HTTP endpoint that produced the successful response. +pub use self::http_executor::HttpObservedValue; /// Result of one logical endpoint selection. pub use self::http_pool::HttpEndpointSelection; /// Runtime admission permit for one HTTP request. diff --git a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs index 7a7125a..a27fbc1 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs -// version: 10 +// version: 11 const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128; const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000; @@ -1249,25 +1249,42 @@ impl crate::HttpTransportPool { signature: &str, config: std::option::Option<&crate::SolanaGetTransactionConfig>, ) -> ksp_core_lib::Result> { - if let std::option::Option::Some(config) = config - && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) - { - return std::result::Result::Err( - ksp_core_lib::Error::new( - crate::ERROR_CODE_INVALID_RPC_PARAMETERS, - "getTransaction commitment must be confirmed or finalized when explicitly provided", - ) - .with_context("rpc_method", "getTransaction") - .with_context("commitment", "processed"), - ); - } - let mut params = std::vec![serde_json::Value::String(signature.to_owned())]; - if let std::option::Option::Some(config) = config { - params.push((*config).to_json_value()); - } + let params = get_transaction_params(signature, config); + let params = match params { + std::result::Result::Ok(params) => params, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; return self.execute_get_transaction(role, params).await; } + /// Executes the current object-form `getTransaction` request and reports the safe identity of the endpoint that produced the successful response. + /// + /// Routing, admission, timeout and retry behavior are identical to [`Self::get_transaction`]. The returned observation never contains an endpoint URL, + /// HTTP headers or a raw HTTP body. + pub async fn get_transaction_observed( + &self, + role: &crate::HttpRoleName, + signature: &str, + config: std::option::Option<&crate::SolanaGetTransactionConfig>, + ) -> ksp_core_lib::Result>> { + let params = get_transaction_params(signature, config); + let params = match params { + std::result::Result::Ok(params) => params, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let observed = self.execute_transaction_rpc_observed("getTransaction", role, params).await; + let observed = match observed { + std::result::Result::Ok(observed) => observed, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let (value, endpoint_name, provider) = observed.into_parts(); + let transaction = decode_get_transaction(value); + return match transaction { + std::result::Result::Ok(transaction) => std::result::Result::Ok(crate::HttpObservedValue::new(transaction, endpoint_name, provider)), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + /// Executes the deprecated bare-encoding `getTransaction` request form retained by Solana RPC for backwards compatibility. #[deprecated(note = "use HttpTransportPool::get_transaction with SolanaGetTransactionConfig; the bare encoding request form is deprecated")] pub async fn get_transaction_legacy( @@ -1297,14 +1314,7 @@ impl crate::HttpTransportPool { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - if value.is_null() { - return std::result::Result::Ok(std::option::Option::None); - } - let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value); - return match transaction { - std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)), - std::result::Result::Err(error) => std::result::Result::Err(error), - }; + return decode_get_transaction(value); } /// Executes typed `requestAirdrop` through the common KSP HTTP transport path. @@ -1466,6 +1476,54 @@ impl crate::HttpTransportPool { }; return self.execute_standard_rpc(role, method, params).await; } + + async fn execute_transaction_rpc_observed( + &self, + method_name: &'static str, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result> { + let method = transaction_descriptor(method_name); + let method = match method { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return self.execute_standard_rpc_observed(role, method, params).await; + } +} + +fn get_transaction_params( + signature: &str, + config: std::option::Option<&crate::SolanaGetTransactionConfig>, +) -> ksp_core_lib::Result> { + if let std::option::Option::Some(config) = config + && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) + { + return std::result::Result::Err( + ksp_core_lib::Error::new( + crate::ERROR_CODE_INVALID_RPC_PARAMETERS, + "getTransaction commitment must be confirmed or finalized when explicitly provided", + ) + .with_context("rpc_method", "getTransaction") + .with_context("commitment", "processed"), + ); + } + let mut params = std::vec![serde_json::Value::String(signature.to_owned())]; + if let std::option::Option::Some(config) = config { + params.push((*config).to_json_value()); + } + return std::result::Result::Ok(params); +} + +fn decode_get_transaction(value: serde_json::Value) -> ksp_core_lib::Result> { + if value.is_null() { + return std::result::Result::Ok(std::option::Option::None); + } + let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value); + return match transaction { + std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; } #[derive(serde::Deserialize)] diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index b0381b1..b8cdc1e 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 48 +// version: 49 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -199,6 +199,15 @@ fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() { assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey)); } +#[test] +fn public_v0_3_6_pre_004_observed_get_transaction_surface_is_available_from_crate_root() { + let _get_transaction_observed = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_observed; + let observed: std::option::Option< + ksp_onchain_transport_lib::HttpObservedValue>, + > = std::option::Option::None; + assert!(observed.is_none()); +} + #[test] fn public_pre_003_account_wrappers_are_available_from_crate_root() { let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info; diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs index 6597558..2e31bdb 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs -// version: 10 +// version: 11 #[test] fn transaction_encoding_strings_match_current_and_legacy_wire_labels() { @@ -966,6 +966,80 @@ async fn typed_get_transaction_preserves_raw_json_meta_version_and_transaction_i ); } +#[tokio::test(flavor = "current_thread")] +async fn typed_get_transaction_observed_reports_actual_winner_after_retry_reroute() { + let (first_url, first_handle) = serve_transaction_status_and_count("429 Too Many Requests"); + let (winner_url, winner_handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.base64.json")); + let urls = [(first_url.as_str(), "first-endpoint", "first-provider"), (winner_url.as_str(), "winner-endpoint", "winner-provider")]; + let mut endpoints = std::vec::Vec::with_capacity(urls.len()); + for (url, endpoint_name, provider) in urls { + let role = crate::HttpEndpointRoleSettings::new( + crate::HttpRoleName::new("default"), + true, + std::vec![crate::HttpRequestKind::wildcard()], + 10, + crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), + ); + endpoints.push(crate::HttpEndpointSettings::new( + endpoint_name, + true, + crate::HttpProviderName::new(provider), + crate::HttpClusterName::new("local"), + crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"), + std::time::Duration::from_millis(100), + std::time::Duration::from_secs(1), + std::option::Option::Some(1), + std::vec![role], + )); + } + let pool = crate::HttpTransportPool::new(crate::HttpTransportSettings::new( + endpoints, + crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)), + )) + .expect("observed fixture pool must build"); + let config = crate::SolanaGetTransactionConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Confirmed), + std::option::Option::Some(crate::SolanaTransactionEncoding::Base64), + std::option::Option::Some(0), + ); + let observed = pool + .get_transaction_observed(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config)) + .await + .expect("retry-safe observed getTransaction must succeed on the second endpoint"); + assert_eq!(observed.endpoint_name(), "winner-endpoint"); + assert_eq!(observed.provider().as_str(), "winner-provider"); + let transaction = observed.value().as_ref().expect("winning response must contain a transaction"); + assert_eq!(transaction.slot(), 431_000_062); + assert!(matches!( + transaction.transaction(), + crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. } + )); + let (first_count, first_request) = first_handle.join().expect("first fixture server must join"); + assert_eq!(first_count, 1); + assert_eq!(transaction_request_body(first_request.as_str())["method"], serde_json::json!("getTransaction")); + let winner_request = winner_handle.join().expect("winner fixture server must join"); + assert_eq!(transaction_request_body(winner_request.as_str())["method"], serde_json::json!("getTransaction")); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_transaction_observed_preserves_null_and_redacts_typed_value_debug() { + let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json")); + let pool = transaction_pool_for_url(url.as_str()); + let observed = pool + .get_transaction_observed(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::None) + .await + .expect("observed null getTransaction must succeed"); + assert!(observed.value().is_none()); + assert_eq!(observed.endpoint_name(), "fixture-0"); + assert_eq!(observed.provider().as_str(), "fixture"); + let rendered = format!("{observed:?}"); + assert!(rendered.contains("fixture-0")); + assert!(rendered.contains("fixture")); + assert!(rendered.contains("")); + assert!(!rendered.contains("fixture-signature")); + handle.join().expect("fixture server must join"); +} + #[tokio::test(flavor = "current_thread")] async fn typed_get_transaction_preserves_unsupported_version_rpc_error() { let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.error_unsupported_version.json")); diff --git a/deltas/0.3.6/pre.004.md b/deltas/0.3.6/pre.004.md new file mode 100644 index 0000000..eed0c4e --- /dev/null +++ b/deltas/0.3.6/pre.004.md @@ -0,0 +1,146 @@ + + + +# Delta `0.3.6-pre.004` — provenance Transport observée pour `getTransaction` + +## Base requise + +```text +0.3.6-pre.003-fix.001 appliquée +workspace.package.version = 0.3.6-pre.3.fix.1 +``` + +Le gate opérateur fourni pour cette base confirme : + +```text +cargo fmt --all PASS +python3 scripts/audit_rust_workspace_rules.py PASS / clean +python3 scripts/audit_markdown_tables.py ... PASS / clean (264 tables / 145 fichiers) +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS +cargo test -p ksp-job-api PASS / 13 unitaires + 14 canaries +cargo tree -p ksp-job-api --edges normal Core-only confirmé +cargo tree -p ksp-job-api -e features aucune feature Job API +``` + +## Objectif + +Matérialiser exclusivement la tranche `pre.004` du plan 027 : permettre au futur backfill de connaître le provider et l'endpoint HTTP qui ont réellement produit le succès de `getTransaction`, y compris après retry/reroutage, sans dupliquer le client RPC ni déplacer la politique Transport vers Job. + +## Conception + +La nouvelle enveloppe générique `HttpObservedValue` contient uniquement : + +- la valeur typée `T` ; +- le nom configuré et validé de l'endpoint victorieux ; +- le descripteur provider de cet endpoint. + +Elle ne contient jamais : + +- URL d'endpoint ; +- headers HTTP ; +- body HTTP brut ; +- credentials ou autre détail de connexion. + +Son `Debug` conserve l'identité sûre de routage mais remplace systématiquement la valeur typée par `` afin qu'un diagnostic générique ne rende pas accidentellement un payload transactionnel volumineux. + +Le moteur HTTP commun n'est pas dupliqué. `execute_standard_rpc_with` possède toujours l'unique boucle de support, request-id, admission, deadline, retry, cooldown, HTTP, parsing JSON-RPC et accounting. Une projection de succès interne décide seulement de la forme de retour : + +- `execute_standard_rpc` retourne la valeur historique sans allouer de provenance ; +- la voie interne observée capture endpoint/provider uniquement après le succès final ; +- `get_transaction_observed` réutilise cette voie et retourne `HttpObservedValue>`. + +Les validations de paramètres `getTransaction` sont factorisées dans un helper commun. `get_transaction` et `get_transaction_observed` rejettent donc exactement les mêmes entrées et utilisent le même décodage typé. L'API historique reste source-compatible. + +## Tests ajoutés + +Deux tests unitaires Transport : + +- `typed_get_transaction_observed_reports_actual_winner_after_retry_reroute` : deux endpoints de même priorité, premier résultat HTTP `429`, retry sur le second ; la valeur observée doit rapporter `winner-endpoint` / `winner-provider`, pas le candidat initial ; +- `typed_get_transaction_observed_preserves_null_and_redacts_typed_value_debug` : `result: null` reste `None`, endpoint/provider restent disponibles et le `Debug` ne rend pas la valeur typée. + +Une canarie publique : + +- `public_v0_3_6_pre_004_observed_get_transaction_surface_is_available_from_crate_root` : méthode et enveloppe observée sont consommables depuis le crate-root. + +Aucun fixture wire nouveau n'est nécessaire : les fixtures `get_transaction.base64.json` et `get_transaction.null.json` existantes sont réutilisées. + +## Fichiers ajoutés + +```text +deltas/0.3.6/pre.004.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/http_executor.rs +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs +docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md +docs/validation/023-V0_3_6_JOB_API_BACKFILL.md +``` + +Mécanique Cargo : + +```text +header version: 398 -> 399 +workspace.package.version: 0.3.6-pre.3.fix.1 -> 0.3.6-pre.4 +workspace members: inchangés +workspace dependencies: inchangées +``` + +## Fichiers supprimés + +Aucun. + +## Frontières conservées + +- aucune crate Job ou Store n'entre dans Transport ; +- aucune dépendance ou feature n'est ajoutée ; +- aucune sélection d'endpoint, pause ou boucle de retry n'est ajoutée dans Job ; +- aucune nouvelle lecture Config/env n'est introduite ; +- aucun code, DTO, client, retry loop ou provenance kbot3 n'est copié ; la matrice fonctionnelle `pre.001` reste seulement une référence de besoin ; +- le legacy `get_transaction_legacy` reste inchangé et non observé ; le backfill v0.3.6 utilisera le formulaire moderne objet ; +- README/USAGE restent fermés jusqu'à la tranche de réconciliation documentaire prévue par le plan. + +## Validations exécutées dans l'environnement d'assemblage + +```text +python3 scripts/audit_rust_workspace_rules.py +-> General Rust rule audit: clean +-> Rust export completeness audit: 0 candidate(s) +-> KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.6 +-> Markdown table audit: clean + +python3 -m unittest scripts/tests/test_audit_markdown_tables.py +-> 5 tests / OK +``` + +Le premier passage de l'auditeur Rust a détecté deux rustdocs manquantes sur les helpers `pub(crate)` de `HttpObservedValue`; elles ont été ajoutées avant assemblage et le second passage est intégralement propre. + +L'environnement d'assemblage ne fournit ni `cargo`, ni `rustfmt`, ni `rustc`. Aucune compilation ou suite Rust de `pre.004` n'est donc annoncée comme exécutée localement. + +## Gate opérateur demandé + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.6 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +cargo tree -p ksp-onchain-transport-lib --edges normal +cargo tree -p ksp-onchain-transport-lib -e features +``` + +Le gate doit notamment confirmer le retry/reroutage déterministe du nouveau test observé et l'absence de nouvelle dépendance/feature Transport. + +## Questions ouvertes + +Aucune question ne bloque `pre.005` après un gate opérateur vert de `pre.004`. diff --git a/docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md b/docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md index 78ef079..de3f693 100644 --- a/docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md +++ b/docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.6 — Job API et premier backfill RAW @@ -406,7 +406,7 @@ Le helper de fixture est renommé `new_lifecycle()` et tous ses appels sont sync ### `pre.003` — Notifications latest-value génériques -**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur du fix à rejouer.** +**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur du fix vert.** Budget cible : **15-20 min**. Entrée : transitions Job stables et gate `pre.002-fix.001` vert. La tranche ajoute `JobNotificationSequence`, `JobNotification`, `JobSnapshotFuture` et `JobSnapshotSource` sans nouvelle dépendance. La séquence est ordonnée, avance par `checked_add` et échoue explicitement avant tout wrap ; l'enveloppe reste immutable et son `Debug` masque le snapshot générique. Le trait source expose la valeur courante et une attente abstraite d'une valeur plus récente ; le futur public n'expose que `std::future::Future`, `Pin` et `Box`, jamais Tokio. @@ -422,9 +422,11 @@ Le gate opérateur de `pre.003` confirme `cargo fmt`, les audits Rust/Markdown e ### `pre.004` — Provenance Transport observée -**Statut : planifié.** +**Statut : matérialisé ; gate opérateur à rejouer.** -Budget cible : **15-20 min**. Entrée : besoin de provenance confirmé par le plan. Ajouter le retour observé de `getTransaction` sans dupliquer le client. Sortie : tests de routage, retry et endpoint réellement victorieux verts. +Budget cible : **15-20 min**. Entrée : besoin de provenance confirmé par le plan et gate `pre.003-fix.001` vert. La tranche ajoute `HttpObservedValue` comme enveloppe typée ne conservant que la valeur, le nom sûr de l'endpoint victorieux et son provider. `HttpTransportPool::get_transaction_observed` reprend exactement les validations et le moteur `execute_standard_rpc` existants ; le moteur commun possède désormais une voie interne observée et l'API historique continue à ne retourner que la valeur. + +Les tests couvrent le succès direct `null`, la surface publique et surtout un retry `429` entre deux endpoints de même priorité : le résultat observé rapporte le second endpoint/provider qui a réellement produit la réponse, jamais le candidat initial. URL, headers et body HTTP brut restent absents du contrat, et le `Debug` de l'enveloppe ne rend pas la valeur typée. Sortie attendue après gate : routage/retry/provenance Transport verts sans nouvelle dépendance ni changement de politique. ### `pre.005` — Fondation Backfill et découverte diff --git a/docs/validation/023-V0_3_6_JOB_API_BACKFILL.md b/docs/validation/023-V0_3_6_JOB_API_BACKFILL.md index e3ab454..5c3c6d7 100644 --- a/docs/validation/023-V0_3_6_JOB_API_BACKFILL.md +++ b/docs/validation/023-V0_3_6_JOB_API_BACKFILL.md @@ -1,5 +1,5 @@ - + # Validation v0.3.6 — Job API et premier backfill RAW @@ -54,6 +54,8 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique - [X] Gate opérateur de `pre.003` : `cargo fmt`, audits Rust/Markdown et `cargo check --workspace` verts ; Clippy `--all-targets` et `cargo test -p ksp-job-api` échouent uniquement sur `E0423` dans le helper `exhausted_notification_sequence()`. - [X] Les arbres Cargo normal/features de ce gate confirment que `ksp-job-api` dépend toujours uniquement de `ksp-core-lib` et n'ouvre aucune feature. - [X] `pre.003-fix.001` remplace uniquement la construction via le re-export racine par le constructeur privé directement visible dans le module `notification`, sans changer l'API publique ni le nombre de tests. +- [X] Gate opérateur de `pre.003-fix.001` : `cargo fmt`, audits Rust/Markdown, `cargo check`, Clippy, treize unitaires + quatorze canaries `ksp-job-api` et arbres Cargo normal/features verts. +- [X] `pre.004` matérialise une voie observée additive de `getTransaction` sans modifier le client, les settings, le routage ni la politique de retry Transport. - [X] Le workspace compte 14 crates et aucune crate Job au point de départ. ## 4. Autorités et cohérence documentaire @@ -121,9 +123,9 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique ## 9. Acquisition Transport et retry - [ ] Les wrappers typés officiels restent l'unique chemin RPC. -- [ ] Retour observé additif de `getTransaction` couvert. -- [ ] Fournisseur et endpoint rapportés correspondent au succès réel après reroutage. -- [ ] URL, headers et body brut restent privés. +- [X] Retour observé additif de `getTransaction` matérialisé ; gate opérateur à rejouer. +- [X] Canarie déterministe : un `429` sur le premier endpoint reroute et rapporte le second provider/endpoint victorieux ; gate opérateur à rejouer. +- [X] `HttpObservedValue` ne contient que valeur typée, endpoint sûr et provider ; URL, headers et body HTTP brut restent privés. - [ ] Aucun retry, pacing ou sélection d'endpoint dans Job. - [ ] Aucune détection d'erreur par chaîne. - [ ] Une erreur Transport finale arrête les admissions avec un code Job sûr.