diff --git a/Cargo.toml b/Cargo.toml index 4c593ba..0e7130b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 223 +# version: 224 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.8-pre.5" +version = "0.2.8-pre.5.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index e723ba7..7b9c11f 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: 31 +// version: 32 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -415,16 +415,6 @@ pub(crate) use self::rpc_common::decode_wire_json; pub(crate) use self::rpc_common::parse_wire_pubkey; /// Validates endpoint settings. pub(crate) use self::settings::validate_endpoint_settings; -/// Crate-internal decoder for Helius transaction-subscribe acknowledgement IDs. -pub(crate) use self::ws_helius_transactions::decode_helius_transaction_subscribe_result; -/// Crate-internal decoder for Helius transaction-unsubscribe boolean results. -pub(crate) use self::ws_helius_transactions::decode_helius_transaction_unsubscribe_result; -/// Crate-internal exact Helius transaction-subscribe method descriptor. -pub(crate) use self::ws_helius_transactions::helius_transaction_subscribe_method; -/// Crate-internal exact Helius transaction-unsubscribe method descriptor. -pub(crate) use self::ws_helius_transactions::helius_transaction_unsubscribe_method; -/// Crate-internal Helius transaction-unsubscribe parameter encoder. -pub(crate) use self::ws_helius_transactions::helius_transaction_unsubscribe_params; /// Crate-internal command surface shared by the physical session and typed subscription handle. pub(crate) use self::ws_session::WsSessionCommand; /// Crate-internal notification dispatch result. diff --git a/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs b/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs index 4598784..9112835 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs -// version: 1 +// version: 2 const MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS: usize = 50_000; @@ -325,20 +325,6 @@ impl HeliusTransactionSubscribeRequest { } return std::result::Result::Ok(()); } - - /// Builds the exact JSON-RPC params array after deterministic validation. - #[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006. - pub(crate) fn to_params(&self) -> ksp_core_lib::Result> { - let validation = self.validate(); - if let std::result::Result::Err(error) = validation { - return std::result::Result::Err(error); - } - let mut params = std::vec![self.filter.to_json_value()]; - if let std::option::Option::Some(options) = self.options { - params.push(options.to_json_value()); - } - return std::result::Result::Ok(params); - } } impl std::fmt::Debug for HeliusTransactionSubscribeRequest { @@ -347,21 +333,27 @@ impl std::fmt::Debug for HeliusTransactionSubscribeRequest { } } -/// Returns the exact Helius transaction-subscribe JSON-RPC method name. -#[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006. -pub(crate) const fn helius_transaction_subscribe_method() -> &'static str { +fn helius_transaction_subscribe_params(request: &crate::HeliusTransactionSubscribeRequest) -> ksp_core_lib::Result> { + let validation = request.validate(); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + let mut params = std::vec![request.filter.to_json_value()]; + if let std::option::Option::Some(options) = request.options { + params.push(options.to_json_value()); + } + return std::result::Result::Ok(params); +} + +const fn helius_transaction_subscribe_method() -> &'static str { return "transactionSubscribe"; } -/// Returns the exact Helius transaction-unsubscribe JSON-RPC method name. -#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006. -pub(crate) const fn helius_transaction_unsubscribe_method() -> &'static str { +const fn helius_transaction_unsubscribe_method() -> &'static str { return "transactionUnsubscribe"; } -/// Decodes a successful Helius transaction-subscribe acknowledgement without exposing the remote ID publicly. -#[allow(dead_code)] // Consumed by actor-owned transaction subscription registration in pre.006. -pub(crate) fn decode_helius_transaction_subscribe_result(value: serde_json::Value) -> ksp_core_lib::Result { +fn decode_helius_transaction_subscribe_result(value: serde_json::Value) -> ksp_core_lib::Result { return match value.as_u64() { std::option::Option::Some(remote_id) => std::result::Result::Ok(remote_id), std::option::Option::None => std::result::Result::Err( @@ -371,15 +363,11 @@ pub(crate) fn decode_helius_transaction_subscribe_result(value: serde_json::Valu }; } -/// Builds the exact Helius transaction-unsubscribe params array for one actor-owned remote subscription ID. -#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006. -pub(crate) fn helius_transaction_unsubscribe_params(remote_id: u64) -> std::vec::Vec { +fn helius_transaction_unsubscribe_params(remote_id: u64) -> std::vec::Vec { return std::vec![serde_json::Value::Number(remote_id.into())]; } -/// Decodes the boolean Helius transaction-unsubscribe result. -#[allow(dead_code)] // Consumed by actor-owned transaction subscription cleanup in pre.006. -pub(crate) fn decode_helius_transaction_unsubscribe_result(value: serde_json::Value) -> ksp_core_lib::Result { +fn decode_helius_transaction_unsubscribe_result(value: serde_json::Value) -> ksp_core_lib::Result { return match value.as_bool() { std::option::Option::Some(unsubscribed) => std::result::Result::Ok(unsubscribed), std::option::Option::None => std::result::Result::Err( diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs index 7c2fe81..b98b309 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs -// version: 2 +// version: 3 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -91,11 +91,11 @@ fn helius_transaction_subscribe_request_serializes_complete_documented_filter_an std::option::Option::Some(0), ); let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options)); - let params = request.to_params().expect("complete documented Helius request must validate"); + let params = super::helius_transaction_subscribe_params(&request).expect("complete documented Helius request must validate"); assert_eq!( params, - serde_json::json!([ - { + std::vec![ + serde_json::json!({ "vote": false, "failed": false, "signature": "fixture-signature-secret-canary", @@ -103,15 +103,15 @@ fn helius_transaction_subscribe_request_serializes_complete_documented_filter_an "accountExclude": ["SysvarC1ock11111111111111111111111111111111"], "accountRequired": ["Vote111111111111111111111111111111111111111"], "tokenAccounts": "balanceChanged" - }, - { + }), + serde_json::json!({ "commitment": "confirmed", "encoding": "jsonParsed", "transactionDetails": "accounts", "showRewards": true, "maxSupportedTransactionVersion": 0 - } - ]) + }) + ] ); assert_eq!(request.filter().vote(), std::option::Option::Some(false)); assert_eq!(request.filter().failed(), std::option::Option::Some(false)); @@ -131,7 +131,10 @@ fn helius_transaction_subscribe_request_serializes_complete_documented_filter_an #[test] fn helius_transaction_request_preserves_omitted_explicit_empty_and_explicit_none_states() { let omitted = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None); - assert_eq!(omitted.to_params().expect("fully omitted optional request must validate"), serde_json::json!([{}])); + assert_eq!( + super::helius_transaction_subscribe_params(&omitted).expect("fully omitted optional request must validate"), + std::vec![serde_json::json!({})] + ); let explicit = crate::HeliusTransactionSubscribeRequest::new( crate::HeliusTransactionSubscribeFilter::new( std::option::Option::None, @@ -145,8 +148,8 @@ fn helius_transaction_request_preserves_omitted_explicit_empty_and_explicit_none std::option::Option::Some(crate::HeliusTransactionSubscribeOptions::default()), ); assert_eq!( - explicit.to_params().expect("explicit empty Helius request states must validate"), - serde_json::json!([{"accountInclude":[],"accountExclude":[],"accountRequired":[],"tokenAccounts":"none"}, {}]) + super::helius_transaction_subscribe_params(&explicit).expect("explicit empty Helius request states must validate"), + std::vec![serde_json::json!({"accountInclude":[],"accountExclude":[],"accountRequired":[],"tokenAccounts":"none"}), serde_json::json!({})] ); } @@ -235,21 +238,21 @@ fn helius_transaction_details_require_max_supported_version_only_for_accounts_an #[test] fn helius_transaction_subscribe_and_unsubscribe_control_wire_is_exact() { - assert_eq!(crate::helius_transaction_subscribe_method(), "transactionSubscribe"); - assert_eq!(crate::helius_transaction_unsubscribe_method(), "transactionUnsubscribe"); + assert_eq!(super::helius_transaction_subscribe_method(), "transactionSubscribe"); + assert_eq!(super::helius_transaction_unsubscribe_method(), "transactionUnsubscribe"); assert_eq!( - crate::decode_helius_transaction_subscribe_result(serde_json::json!(4_743_323_479_349_712_u64)).expect("numeric ack must decode"), + super::decode_helius_transaction_subscribe_result(serde_json::json!(4_743_323_479_349_712_u64)).expect("numeric ack must decode"), 4_743_323_479_349_712 ); - assert_eq!(crate::helius_transaction_unsubscribe_params(4_743_323_479_349_712), serde_json::json!([4_743_323_479_349_712_u64])); - assert!(crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(true)).expect("boolean true must decode")); - assert!(!crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(false)).expect("boolean false must decode")); + assert_eq!(super::helius_transaction_unsubscribe_params(4_743_323_479_349_712), std::vec![serde_json::json!(4_743_323_479_349_712_u64)]); + assert!(super::decode_helius_transaction_unsubscribe_result(serde_json::json!(true)).expect("boolean true must decode")); + assert!(!super::decode_helius_transaction_unsubscribe_result(serde_json::json!(false)).expect("boolean false must decode")); assert_eq!( - crate::decode_helius_transaction_subscribe_result(serde_json::json!("not-an-id")).expect_err("non-numeric subscribe ack must fail").code(), + super::decode_helius_transaction_subscribe_result(serde_json::json!("not-an-id")).expect_err("non-numeric subscribe ack must fail").code(), crate::ERROR_CODE_INVALID_RESPONSE ); assert_eq!( - crate::decode_helius_transaction_unsubscribe_result(serde_json::json!(1)).expect_err("non-boolean unsubscribe ack must fail").code(), + super::decode_helius_transaction_unsubscribe_result(serde_json::json!(1)).expect_err("non-boolean unsubscribe ack must fail").code(), crate::ERROR_CODE_INVALID_RESPONSE ); } @@ -307,20 +310,20 @@ async fn helius_transaction_control_wire_round_trips_through_shared_physical_act std::option::Option::Some(0), ); let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options)); - let params = request.to_params().expect("typed Helius request must validate before I/O"); + let params = super::helius_transaction_subscribe_params(&request).expect("typed Helius request must validate before I/O"); let subscribe_result = session .physical_session() - .execute_json_rpc(crate::helius_transaction_subscribe_method(), params) + .execute_json_rpc(super::helius_transaction_subscribe_method(), params) .await .expect("transactionSubscribe acknowledgement must arrive"); - let remote_id = crate::decode_helius_transaction_subscribe_result(subscribe_result).expect("transactionSubscribe id must decode"); + let remote_id = super::decode_helius_transaction_subscribe_result(subscribe_result).expect("transactionSubscribe id must decode"); assert_eq!(remote_id, 4242); let unsubscribe_result = session .physical_session() - .execute_json_rpc(crate::helius_transaction_unsubscribe_method(), crate::helius_transaction_unsubscribe_params(remote_id)) + .execute_json_rpc(super::helius_transaction_unsubscribe_method(), super::helius_transaction_unsubscribe_params(remote_id)) .await .expect("transactionUnsubscribe acknowledgement must arrive"); - assert!(crate::decode_helius_transaction_unsubscribe_result(unsubscribe_result).expect("transactionUnsubscribe boolean must decode")); + assert!(super::decode_helius_transaction_unsubscribe_result(unsubscribe_result).expect("transactionUnsubscribe boolean must decode")); session.close().await.expect("Helius fixture session must close"); server.await.expect("local Helius transaction server must finish"); } diff --git a/deltas/0.2.8/pre.005-fix.001.md b/deltas/0.2.8/pre.005-fix.001.md new file mode 100644 index 0000000..3300189 --- /dev/null +++ b/deltas/0.2.8/pre.005-fix.001.md @@ -0,0 +1,95 @@ + + + +# Delta `0.2.8-pre.005-fix.001` — canaris JSON, visibilité test/private et audit des chemins + +## 1. Cause + +Le checkpoint opérateur de `pre.005` compile le workspace normal mais échoue dès la compilation des tests Transport. Quatre assertions comparent un `Vec` produit par les encodeurs de params à un `serde_json::Value` construit par `serde_json::json!([...])`, ce qui produit `E0277`. + +Le même checkpoint révèle cinq warnings `unused import` au crate-root : cinq helpers Helius avaient été rendus `pub(crate)` et réexportés uniquement pour être appelés par les tests. Cette visibilité est contraire aux règles KSP : une visibilité n'est pas élargie pour les tests. + +## 2. Correction des canaris + +Les attentes de params utilisent maintenant des `Vec` explicites : + +```text +transactionSubscribe complet +omission complète +états []/none explicites +transactionUnsubscribe [remote_id] +``` + +Le wire attendu ne change pas. + +## 3. Correction de visibilité + +Les helpers suivants redeviennent strictement privés au module `ws_helius_transactions` : + +```text +helius_transaction_subscribe_params +helius_transaction_subscribe_method +helius_transaction_unsubscribe_method +decode_helius_transaction_subscribe_result +helius_transaction_unsubscribe_params +decode_helius_transaction_unsubscribe_result +``` + +Les cinq réexports `pub(crate)` de `lib.rs` sont supprimés. Les tests séparés y accèdent via `super::...`. Aucun `#[allow(dead_code)]` n'est conservé pour masquer une visibilité prématurée. + +Les types réellement publics de `pre.005` restent réexportés au crate-root et les tests continuent à les consommer via `crate::Item`. + +En `pre.006`, si un helper devient réellement partagé entre modules de production, il pourra être promu en `pub(crate)`, réexporté au crate-root et consommé via `crate::Item` conformément aux règles. + +## 4. Durcissement des règles et de l'audit + +`RULES_RUST.md` explicite désormais : + +```text +private parent dans unit_tests -> super::Item obligatoire +pub/pub(crate) -> crate::Item obligatoire, jamais super:: ni nom nu +visibilité -> jamais élargie uniquement pour tester +``` + +`audit_rust_export_completeness.py` ajoute des canaris mécaniques bidirectionnels pour les fichiers `unit_tests/` rattachés : + +```text +RUST-IMPORT-204 private parent appelé sans super:: +RUST-IMPORT-205 visible parent appelé sans crate-root +RUST-IMPORT-202 visible parent appelé via super:: (déjà présent) +``` + +## 5. Version + +```text +workspace.package.version = 0.2.8-pre.5.fix.1 +commit attendu = v0.2.8-pre.005-fix.001 +tag prerelease = aucun +``` + +## 6. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs +crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs +scripts/audit_rust_export_completeness.py +docs/rules/RULES_RUST.md +docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +deltas/0.2.8/pre.005-fix.001.md +``` + +## 7. Gate opérateur + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +cargo test --workspace +``` + +Le fix reste `PREPARED` jusqu'à ce gate. diff --git a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md index 9c34539..82e1f34 100644 --- a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +++ b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.8` — Helius LaserStream WebSocket -> **Statut : `pre.004` et ses deux fixes sont validés par checkpoint opérateur complet. `0.2.8-pre.005` est préparé : contrat typed `transactionSubscribe`, filtres/options Helius, `tokenAccounts`, limites 50k et wire `transactionUnsubscribe`, sans exposer encore de handle transaction avant l'intégration actor de `pre.006`.** +> **Statut : `pre.004` et ses deux fixes sont validés. Le premier checkpoint de `0.2.8-pre.005` a révélé quatre erreurs de type dans les canaris JSON et une visibilité `pub(crate)` injustifiée pour des helpers utilisés uniquement par le module/tests. `0.2.8-pre.005-fix.001` est préparé pour corriger ces deux points et durcir l'audit des accès `super::PrivateItem` / `crate::VisibleItem`.** ## 1. Objet, base et état courant @@ -30,10 +30,11 @@ pre.003 six familles standard Helius validées pre.004 Config V2 Helius validé pre.004-fix.001 redaction segmentaire + couverture Devnet Helius validées pre.004-fix.002 provenance composée validée -pre.005 contrat typed transactionSubscribe/unsubscribe préparé +pre.005 contrat typed transactionSubscribe/unsubscribe ; fix requis après checkpoint +pre.005-fix.001 correction types de canaris + visibilité/tests/règles préparée -workspace.package.version courant = 0.2.8-pre.5 -commit attendu = v0.2.8-pre.005 +workspace.package.version courant = 0.2.8-pre.5.fix.1 +commit attendu = v0.2.8-pre.005-fix.001 aucun tag prerelease ``` @@ -57,8 +58,9 @@ pre.004 DONE — Config V2 helius_laserstream + schema/fixtures + mapping Confi + secret/redaction Helius mainnet/devnet validés fix.001 DONE — redaction segmentaire corrigée + représentation Devnet Helius ajoutée fix.002 DONE — provenance composée `DocumentLiteral` + `EnvironmentProcess` corrigée -pre.005 PREPARED — transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe +pre.005 FIX REQUIRED — transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe + bounds 50k + maxSupportedTransactionVersion conditionnel ; live handle différé à pre.006 + fix.001 PREPARED — assertions Vec/Value corrigées + helpers test-only privés + audit super/crate durci pre.006 transactionNotification + actor integration + reconnect/resubscribe/unsubscribe races + late notifications + backpressure ciblée pre.007 heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown @@ -995,3 +997,19 @@ new dependency aucune ``` La séparation `pre.005` / `pre.006` est normative : publier dès maintenant un `transaction_subscribe()` public sans registry de notification/reconnect produirait un handle transitoire qui perdrait les notifications. Le contrat public de requête est donc stable dès `pre.005`, tandis que l'abonnement live est ajouté atomiquement avec l'actor integration en `pre.006`. + +### Checkpoint `pre.005` et correctif `pre.005-fix.001` + +Le premier checkpoint opérateur de `pre.005` a donné : + +```text +cargo fmt --all OK +python3 scripts/audit_rust_workspace_rules.py clean mais incomplet pour la règle test/private +cargo check --workspace OK avec 5 warnings unused pub(crate) reexports +cargo clippy --workspace --all-targets FAIL — 4 comparaisons Vec / Value +cargo test -p ksp-onchain-transport-lib FAIL — mêmes 4 erreurs de type +``` + +Le correctif ne change ni le contrat Helius public ni le wire. Il applique la règle de visibilité KSP : un helper utilisé seulement par son module et son sous-module de tests reste strictement privé ; le test l'appelle via `super::Item`. Les éléments `pub` et `pub(crate)` restent réexportés et consommés via `crate::Item`. Les helpers transaction wire de `pre.005` ne deviendront `pub(crate)` qu'en `pre.006` si l'actor les consomme réellement. + +Le script `audit_rust_export_completeness.py` est étendu pour détecter dans les `unit_tests/` séparés les accès non qualifiés aux items privés du parent et les accès non canoniques aux items visibles. diff --git a/docs/rules/RULES_RUST.md b/docs/rules/RULES_RUST.md index 65a3438..2640a4c 100644 --- a/docs/rules/RULES_RUST.md +++ b/docs/rules/RULES_RUST.md @@ -1,5 +1,5 @@ - + # Règles Rust générales @@ -35,7 +35,7 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou - **RUST-IMPORT-009** — Dans sa propre crate, un élément `pub` ou `pub(crate)` partagé est appelé via `crate::Item`, y compris depuis son module de déclaration lorsque le contrat est crate-wide. - **RUST-IMPORT-010** — Un chemin `crate::module::Item` est interdit pour un élément partagé `pub`/`pub(crate)` qui peut être consommé via le crate-root. Le chemin du module interne n'est pas une façade. - **RUST-IMPORT-011** — Un élément strictement privé à un module n'est pas réexporté et est appelé par son nom local dans ce module. -- **RUST-IMPORT-012** — Dans un sous-module de tests, `super::Item` est réservé à un élément strictement privé du module parent. Un élément `pub` ou `pub(crate)` continue d'être appelé via `crate::Item`. +- **RUST-IMPORT-012** — Dans un fichier `unit_tests/...` rattaché au module parent, tout élément strictement privé du parent est appelé explicitement via `super::Item`. Inversement, un élément `pub` ou `pub(crate)` n'est jamais appelé via `super::` ni par un nom nu : il continue d'être appelé via le crate-root `crate::Item`, y compris lorsque le test est rattaché à son module de déclaration. - **RUST-IMPORT-013** — Les réexports internes commencent par `self::`. Un réexport d'une crate externe peut utiliser directement le chemin externe canonique. - **RUST-IMPORT-014** — Un export correspond à une ligne de réexport distincte ; les accolades ne servent jamais à regrouper une façade. - **RUST-IMPORT-015** — Une crate externe rendue publique uniquement pour l'hygiène d'une macro exportée conserve son nom canonique, porte `#[doc(hidden)]` et ne devient pas une API de consommation. `ksp-logging-lib::tracing` est ce bridge technique pour les macros Logging ; les autres crates KSP n'y accèdent jamais directement. @@ -48,6 +48,7 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou - **RUST-API-004** — Un élément `pub(crate)` consommé hors de son module est réexporté au crate-root via `pub(crate) use` puis appelé via `crate::Item`. - **RUST-API-005** — Les chemins internes de modules ne constituent jamais une API stable. - **RUST-API-006** — Si deux éléments crate-wide auraient le même nom au crate-root, ils sont renommés dans leurs modules propriétaires avec des noms canoniques non ambigus ; un alias de réexport n'est pas utilisé pour masquer la collision. +- **RUST-API-007** — La visibilité d'un item n'est jamais élargie uniquement pour permettre son test. Un helper utilisé seulement par son module et ses `unit_tests/` reste privé et les tests y accèdent via `super::Item`; il ne devient `pub(crate)` que lorsqu'un autre module de production le consomme réellement, auquel cas `RUST-API-004` et `RUST-IMPORT-009` s'appliquent. ## Formatage, blocs et ordre @@ -102,7 +103,7 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou - **RUST-AUDIT-001** — `scripts/audit_rust_workspace_rules.py` est le point d'entrée obligatoire de l'audit Rust KSP. Il exécute les audits généraux, la complétude des réexports/chemins et les frontières KSP sans fusionner leurs responsabilités. - **RUST-AUDIT-002** — L'audit est dependency-free côté Python standard et échoue avec un code non nul dès qu'une violation mécanique est détectée. -- **RUST-AUDIT-003** — L'audit contrôle au minimum : headers/version/newline, lints de crate-root, visibilité interdite, `use`/aliases/groupes/globs/scope, rustdocs visibles, structure des blocs de réexports, ordre des imports de traits et constantes lorsque KSP le possède, lignes vides dans fonctions/structs/enums, complétude des réexports crate-root, chemins `crate::module::Item`, usage de `super::` dans les tests séparés et frontières KSP directement vérifiables. L'ordre intra-bloc des `use`/réexports reste la responsabilité canonique de `cargo fmt --all` et n'est pas réimplémenté par le script. +- **RUST-AUDIT-003** — L'audit contrôle au minimum : headers/version/newline, lints de crate-root, visibilité interdite, `use`/aliases/groupes/globs/scope, rustdocs visibles, structure des blocs de réexports, ordre des imports de traits et constantes lorsque KSP le possède, lignes vides dans fonctions/structs/enums, complétude des réexports crate-root, chemins `crate::module::Item`, accès `super::PrivateItem` et `crate::VisibleItem` dans les tests séparés, ainsi que les frontières KSP directement vérifiables. L'ordre intra-bloc des `use`/réexports reste la responsabilité canonique de `cargo fmt --all` et n'est pas réimplémenté par le script. - **RUST-AUDIT-004** — Les règles contextuelles qui ne peuvent pas être prouvées sans interpréter la sémantique restent des critères de revue humaine ; le script ne doit pas produire de faux sentiment de complétude. ## Contrôle avant livraison diff --git a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md index 120f5db..b04f22e 100644 --- a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +++ b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md @@ -1,9 +1,9 @@ - + # Validation `0.2.8` — Helius LaserStream WebSocket -> **Statut : `pre.004` + `fix.001` + `fix.002` validés par checkpoint opérateur complet. `pre.005` est préparé : contrat public typed de requête Helius transaction, validations 50k/version, `tokenAccounts`, wire subscribe/unsubscribe et canari actor local ; le handle live reste volontairement différé à `pre.006`.** +> **Statut : `pre.004` + ses fixes sont validés. Le checkpoint `pre.005` échoue uniquement sur quatre comparaisons de canaris `Vec`/`Value` et révèle cinq réexports `pub(crate)` test-only inutilisés. `pre.005-fix.001` corrige les canaris, rétablit les visibilités privées et durcit l'audit `super::`/`crate::`.** ## 1. Références @@ -21,6 +21,7 @@ pre.004 deltas/0.2.8/pre.004.md pre.004 redaction/devnet fix deltas/0.2.8/pre.004-fix.001.md pre.004 provenance fix deltas/0.2.8/pre.004-fix.002.md pre.005 deltas/0.2.8/pre.005.md +pre.005 visibility/test fix deltas/0.2.8/pre.005-fix.001.md validation standard WS docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md HTTP compliance docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md KSP-TRANSPORT-007 docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md @@ -134,22 +135,22 @@ Verdict : **gate `0.2.8-pre.001` positif ; `pre.002` + `fix.001` et `pre.003` so ### 4.2 Invariants architecture -| Critère | Décision | Preuve cible | État | -|-------------------------------------|------------------------------------------------------------------|---------------------------------|--------| +| Critère | Décision | Preuve cible | État | +|-------------------------------------|------------------------------------------------------------------|---------------------------------|--------------------| | actor physique | un seul `WsSession` actor partagé | source/runtime canary | pre.002 implémenté | | façade standard | `SolanaStandardWsSession` | public API canary | pre.002 implémenté | | façade Helius | `HeliusLaserStreamWsSession` | public API canary | pre.002 implémenté | -| namespace LaserStream WS | `WsProtocolKind` + `ws_endpoints` possèdent `helius_laserstream` | API/Config/docs canary | pre.004 validé | -| LaserStream gRPC | backend/type/Config distincts, hors `0.2.8` | absence de réutilisation WS | décidé | +| namespace LaserStream WS | `WsProtocolKind` + `ws_endpoints` possèdent `helius_laserstream` | API/Config/docs canary | pre.004 validé | +| LaserStream gRPC | backend/type/Config distincts, hors `0.2.8` | absence de réutilisation WS | décidé | | escape hatch Helius | aucun `inner()`/`into_inner()` public | compile-fail/source canary | pre.002 implémenté | | generic Helius `WsSession::connect` | ne doit pas permettre de contourner la façade | invalid protocol pre-I/O canary | pre.002 implémenté | -| Helius unsupported | absent de la façade | compile-fail/API absence canary | pre.003 validé | -| standard Helius commun | délégation vers le même wire/actor | exact fixture | pre.003 validé | -| DTO duplication | seulement si wire/sémantique divergent | public/source audit | décidé | -| Config direction | Config -> Transport uniquement | ownership tests | pre.004 validé | -| heartbeat | Helius-only, actor commun | deterministic timers | décidé | -| secret | query URL derrière `WsEndpointUrl` | redaction canaries | pre.004 validé | -| new Rust dependency | aucune | manifest/tree audit | décidé | +| Helius unsupported | absent de la façade | compile-fail/API absence canary | pre.003 validé | +| standard Helius commun | délégation vers le même wire/actor | exact fixture | pre.003 validé | +| DTO duplication | seulement si wire/sémantique divergent | public/source audit | décidé | +| Config direction | Config -> Transport uniquement | ownership tests | pre.004 validé | +| heartbeat | Helius-only, actor commun | deterministic timers | décidé | +| secret | query URL derrière `WsEndpointUrl` | redaction canaries | pre.004 validé | +| new Rust dependency | aucune | manifest/tree audit | décidé | ## 5. Contrat `transactionSubscribe` à valider @@ -339,6 +340,7 @@ Constats `pre.002-fix.001` : Aucun split en fichiers supplémentaires n'est retenu : les plans historiques `0.2.5`–`0.2.7` sont de taille comparable ou supérieure, et le contenu du plan `015` reste entièrement centré sur une seule release. Le problème identifié était **l'ordre interne et la duplication de responsabilités**, pas la nécessité d'un nouveau type de document. + ## 12. Gates opérateur `pre.004` / `fix.001` / `fix.002` Premier passage `pre.004` reçu le 2026-08-23 : @@ -455,3 +457,34 @@ Gates opérateur `pre.005` à exécuter : ``` Verdict courant : **`pre.004` et ses fixes DONE ; `pre.005` PREPARED.** + +## 14. Gate `pre.005` et `pre.005-fix.001` + +Checkpoint opérateur initial `pre.005` : + +```text +[x] cargo fmt --all +[x] audit Rust général/workspace +[x] cargo check --workspace compile +[!] cargo check — 5 warnings de réexports pub(crate) utilisés uniquement par les tests +[ ] cargo clippy --workspace --all-targets — 4 erreurs E0277 Vec == Value +[ ] cargo test -p ksp-onchain-transport-lib — mêmes 4 erreurs E0277 +[ ] cargo test --workspace — non retenu comme preuve de fermeture tant que Transport ne compile pas en tests +``` + +Critères du fix : + +```text +[ ] les quatre attentes JSON comparent Vec à Vec +[ ] les helpers wire utilisés uniquement par le module/tests sont strictement privés +[ ] aucun pub(crate) test-only ni #[allow(dead_code)] compensatoire +[ ] unit_tests utilise super::Helper pour ces items privés +[ ] unit_tests continue d'utiliser crate::Item pour tous les éléments pub/pub(crate) +[ ] l'audit détecte private parent utilisé sans super:: +[ ] l'audit détecte visible parent utilisé via super:: ou nom nu +[ ] cargo check sans les cinq warnings pre.005 +[ ] clippy Transport/workspace vert +[ ] tests Transport/workspace verts +``` + +Verdict courant : **`pre.005` FIX REQUIRED ; `pre.005-fix.001` PREPARED.** diff --git a/scripts/audit_rust_export_completeness.py b/scripts/audit_rust_export_completeness.py index f0b1bff..97c91d8 100644 --- a/scripts/audit_rust_export_completeness.py +++ b/scripts/audit_rust_export_completeness.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_rust_export_completeness.py -# version: 3 +# version: 4 """Audit crate-root export completeness and canonical same-crate paths.""" @@ -32,6 +32,7 @@ class Declaration: module: str name: str visibility: str + kind: str path: pathlib.Path line: int @@ -62,17 +63,47 @@ def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Decl depths = line_depths(mask_rust_source(text)) found: list[Declaration] = [] pattern = re.compile( - r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)" + r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)" ) for idx, line in enumerate(lines, 1): if depths[idx - 1] != 0: continue match = pattern.match(line) if match is not None: - found.append(Declaration(module_path(crate, path), match.group(2), match.group(1), path, idx)) + found.append(Declaration(module_path(crate, path), match.group(3), match.group(1), match.group(2), path, idx)) return found +def private_declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]: + """Return module-level strictly private declarations from one source module.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + depths = line_depths(mask_rust_source(text)) + found: list[Declaration] = [] + pattern = re.compile(r"^\s*(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)") + for idx, line in enumerate(lines, 1): + if depths[idx - 1] != 0: + continue + if line.lstrip().startswith(("pub ", "pub(crate) ")): + continue + match = pattern.match(line) + if match is not None: + found.append(Declaration(module_path(crate, path), match.group(2), "private", match.group(1), path, idx)) + return found + + +def unqualified_test_reference_pattern(declaration: Declaration) -> re.Pattern[str]: + """Return a conservative pattern for one unqualified parent-item reference in a separated unit test.""" + + name = re.escape(declaration.name) + if declaration.kind == "fn": + return re.compile(rf"(? dict[tuple[str, str], str]: """Return explicit crate-root re-exports keyed by source module and symbol.""" @@ -160,10 +191,12 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate] exports = root_exports(crate) tests = unit_test_parents(crate) declarations: dict[tuple[str, str], Declaration] = {} + private_declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {} candidates: list[Candidate] = [] for path in sorted((crate / "src").rglob("*.rs")): if path == crate_root: continue + private_declarations_by_source[path.resolve()] = {item.name: item for item in private_declaration_candidates(crate, path)} for declaration in declaration_candidates(crate, path): key = (declaration.module, declaration.name) declarations[key] = declaration @@ -198,19 +231,28 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate] if symbol not in root_symbols: candidates.append(Candidate("RUST-IMPORT-203", relative, idx, f"`crate::{symbol}` does not resolve to a declared/re-exported crate-root symbol")) - # `super::Item` is reserved to strictly private parent items in separated unit tests. + # Separated unit tests use `super::Item` only for strictly private parent items; visible items use the crate-root façade. declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {} for declaration in declarations.values(): declarations_by_source.setdefault(declaration.path.resolve(), {})[declaration.name] = declaration super_pattern = re.compile(r"\bsuper::([A-Za-z_][A-Za-z0-9_]*)") for test, parent in tests.items(): parent_declarations = declarations_by_source.get(parent, {}) + parent_private_declarations = private_declarations_by_source.get(parent, {}) relative = test.relative_to(workspace).as_posix() - for idx, line in enumerate(test.read_text(encoding="utf-8").splitlines(), 1): + text = test.read_text(encoding="utf-8") + masked_lines = mask_rust_source(text).splitlines() + for idx, line in enumerate(masked_lines, 1): for match in super_pattern.finditer(line): declaration = parent_declarations.get(match.group(1)) if declaration is not None and declaration.visibility in {"pub", "pub(crate)"}: candidates.append(Candidate("RUST-IMPORT-202", relative, idx, f"`super::{declaration.name}` targets {declaration.visibility}; use crate-root `crate::{declaration.name}`")) + for declaration in parent_private_declarations.values(): + if unqualified_test_reference_pattern(declaration).search(line) is not None: + candidates.append(Candidate("RUST-IMPORT-204", relative, idx, f"strictly private parent item `{declaration.name}` must be accessed as `super::{declaration.name}` in separated unit tests")) + for declaration in parent_declarations.values(): + if unqualified_test_reference_pattern(declaration).search(line) is not None: + candidates.append(Candidate("RUST-IMPORT-205", relative, idx, f"{declaration.visibility} parent item `{declaration.name}` must be accessed through crate-root `crate::{declaration.name}` in separated unit tests")) return candidates