diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..2b04565 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,5 @@ + + + Default + python interpreter + diff --git a/Cargo.toml b/Cargo.toml index 57a72f1..508b6e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 160 +# version: 161 [workspace] resolver = "3" members = ["crates/ksp-app-config-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.5-pre.10.fix.1" +version = "0.2.5-pre.10.fix.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-app-config-desk/src/bootstrap.rs b/crates/ksp-app-config-desk/src/bootstrap.rs index d41a2f3..c2c758d 100644 --- a/crates/ksp-app-config-desk/src/bootstrap.rs +++ b/crates/ksp-app-config-desk/src/bootstrap.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/bootstrap.rs -// version: 5 +// version: 6 //! Config and Logging bootstrap for the desktop application. @@ -49,7 +49,7 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li pub(crate) fn initialize_logging( management: &ksp_config_lib::ConfigManagement, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, -) -> ksp_core_lib::Result { +) -> ksp_core_lib::Result { let plan = resolve_logging_startup(management); return match plan { LoggingStartupPlan::Managed { active_profile_id, settings } => { @@ -110,7 +110,7 @@ fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPl fn initialize_fallback_logging( initial_error: ksp_core_lib::Error, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, -) -> ksp_core_lib::Result { +) -> ksp_core_lib::Result { let diagnostic = crate::CommandErrorDto::from_error(&initial_error); return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity); } @@ -120,7 +120,7 @@ fn initialize_planned_fallback_logging( diagnostic: crate::CommandErrorDto, settings: ksp_logging_lib::LoggingSettings, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, -) -> ksp_core_lib::Result { +) -> ksp_core_lib::Result { let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = match guard { std::result::Result::Ok(value) => value, diff --git a/crates/ksp-app-config-desk/src/documents.rs b/crates/ksp-app-config-desk/src/documents.rs index dfdccd6..33e0f77 100644 --- a/crates/ksp-app-config-desk/src/documents.rs +++ b/crates/ksp-app-config-desk/src/documents.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/documents.rs -// version: 2 +// version: 3 //! Generic Config document inventory, diagnostics and validated repair services. @@ -148,7 +148,7 @@ pub(crate) fn save_document_source( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)), }; - let document = detail(state, file_id.as_str()); + let document = crate::document_detail(state, file_id.as_str()); let document = match document { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/crates/ksp-app-config-desk/src/environment.rs b/crates/ksp-app-config-desk/src/environment.rs index ab9b3fd..193624f 100644 --- a/crates/ksp-app-config-desk/src/environment.rs +++ b/crates/ksp-app-config-desk/src/environment.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/environment.rs -// version: 3 +// version: 4 //! Safe Config environment reports and `.env` management projections for Config Desk. diff --git a/crates/ksp-app-config-desk/src/profiles.rs b/crates/ksp-app-config-desk/src/profiles.rs index 198a053..95221d4 100644 --- a/crates/ksp-app-config-desk/src/profiles.rs +++ b/crates/ksp-app-config-desk/src/profiles.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/profiles.rs -// version: 3 +// version: 4 //! Safe Config profile inspection and provenance projections for Config Desk. diff --git a/crates/ksp-app-config-desk/src/tw_main.rs b/crates/ksp-app-config-desk/src/tw_main.rs index ec1d0e5..a752357 100644 --- a/crates/ksp-app-config-desk/src/tw_main.rs +++ b/crates/ksp-app-config-desk/src/tw_main.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/tw_main.rs -// version: 3 +// version: 4 //! Tauri-window helpers for the Config Desk main window. @@ -22,7 +22,7 @@ pub(crate) fn require_main_window(manager: &impl Manager) -> ksp_cor /// Shows main window. pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> { - let window = require_window(app); + let window = crate::require_main_window(app); let window = match window { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/crates/ksp-app-config-desk/src/tw_splash.rs b/crates/ksp-app-config-desk/src/tw_splash.rs index e20849b..ef90805 100644 --- a/crates/ksp-app-config-desk/src/tw_splash.rs +++ b/crates/ksp-app-config-desk/src/tw_splash.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/tw_splash.rs -// version: 4 +// version: 5 //! Tauri-window lifecycle for the Config Desk splash window. @@ -8,6 +8,7 @@ use tauri::Manager; // rust-rules: trait-import /// Crate-internal `WINDOW_LABEL_SPLASH` constant. pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash"; + const SPLASH_EVENT_NAME: &str = "ksp-splash-order"; /// Resolves the required splash window or returns a typed error. diff --git a/crates/ksp-app-config-desk/unit_tests/environment.rs b/crates/ksp-app-config-desk/unit_tests/environment.rs index a13b067..f76e85b 100644 --- a/crates/ksp-app-config-desk/unit_tests/environment.rs +++ b/crates/ksp-app-config-desk/unit_tests/environment.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/unit_tests/environment.rs -// version: 3 +// version: 4 #[test] fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() { @@ -53,7 +53,7 @@ fn committed_environment_report_is_safe_and_deterministic_when_present() { let management = committed_management(); assert!(management.is_ok(), "committed management should construct: {management:?}"); if let std::result::Result::Ok(management) = management { - let report = crate::environment_report_from_management(&management); + let report = super::report_from_management(&management); assert!(report.is_ok(), "environment report should project safely: {report:?}"); if let std::result::Result::Ok(report) = report { let mut previous = std::option::Option::<&str>::None; diff --git a/crates/ksp-app-config-desk/unit_tests/logging_editor.rs b/crates/ksp-app-config-desk/unit_tests/logging_editor.rs index b4a9ae4..0007dd8 100644 --- a/crates/ksp-app-config-desk/unit_tests/logging_editor.rs +++ b/crates/ksp-app-config-desk/unit_tests/logging_editor.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs -// version: 6 +// version: 7 #[test] fn committed_logging_document_maps_complete_read_only_editor_contract() { @@ -8,7 +8,7 @@ fn committed_logging_document_maps_complete_read_only_editor_contract() { if let std::result::Result::Ok(management) = management { let source = management.load_logging_document(); assert!(source.is_ok(), "typed Logging source should load: {source:?}"); - let document = crate::logging_document_from_management(&management); + let document = super::document_from_management(&management); assert!(document.is_ok(), "Logging editor document should load: {document:?}"); if let (std::result::Result::Ok(source), std::result::Result::Ok(document)) = (source, document) { assert_eq!(document.file_id, ksp_config_lib::FILE_ID_STD_LOGGING); @@ -63,7 +63,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() { let management = committed_management(); assert!(management.is_ok(), "committed management should construct: {management:?}"); if let std::result::Result::Ok(management) = management { - let document = crate::logging_document_from_management(&management); + let document = super::document_from_management(&management); assert!(document.is_ok(), "Logging editor document should load: {document:?}"); if let std::result::Result::Ok(document) = document { let candidate = crate::LoggingDocumentCandidateDto { diff --git a/crates/ksp-app-config-desk/unit_tests/profiles.rs b/crates/ksp-app-config-desk/unit_tests/profiles.rs index 06067e3..5f09000 100644 --- a/crates/ksp-app-config-desk/unit_tests/profiles.rs +++ b/crates/ksp-app-config-desk/unit_tests/profiles.rs @@ -1,12 +1,12 @@ // file: crates/ksp-app-config-desk/unit_tests/profiles.rs -// version: 4 +// version: 5 #[test] fn profile_inventory_exposes_registered_standard_profile_documents() { let management = fixture_management(); assert!(management.is_ok(), "fixture management should construct: {management:?}"); if let std::result::Result::Ok(management) = management { - let inventory = crate::profile_inventory_from_management(&management); + let inventory = super::inventory_from_management(&management); assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}"); if let std::result::Result::Ok(inventory) = inventory { assert!(inventory.iter().any(|document| -> bool { @@ -32,7 +32,7 @@ fn default_profile_detail_uses_safe_effective_value_and_dotenv_provenance() { if let std::result::Result::Ok(management) = management { let source = management.load_logging_document(); assert!(source.is_ok(), "typed Logging source should load: {source:?}"); - let detail = crate::profile_detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::None); + let detail = super::detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::None); assert!(detail.is_ok(), "default profile should inspect safely: {detail:?}"); if let (std::result::Result::Ok(source), std::result::Result::Ok(detail)) = (source, detail) { assert_eq!(detail.selected_profile, source.default_profile()); @@ -60,8 +60,7 @@ fn explicit_profile_inspection_reports_explicit_selection_source() { assert!(source.is_ok(), "typed Logging source should load: {source:?}"); if let std::result::Result::Ok(source) = source { let profile_id = source.default_profile().to_owned(); - let detail = - crate::profile_detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::Some(profile_id.as_str())); + let detail = super::detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::Some(profile_id.as_str())); assert!(detail.is_ok(), "explicit profile should inspect: {detail:?}"); if let std::result::Result::Ok(detail) = detail { assert_eq!(detail.selected_profile, profile_id); diff --git a/crates/ksp-config-lib/src/environment.rs b/crates/ksp-config-lib/src/environment.rs index 288c38f..bd1003a 100644 --- a/crates/ksp-config-lib/src/environment.rs +++ b/crates/ksp-config-lib/src/environment.rs @@ -1,10 +1,11 @@ // file: crates/ksp-config-lib/src/environment.rs -// version: 7 +// version: 8 /// Versioned environment contract template expected at the repository/runtime root. pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example"; /// Default local environment file read by Config from the process launch directory. pub const DEFAULT_DOTENV_PATH: &str = ".env"; + const LOGGING_DOMAIN: &str = "config.environment"; /// Source that supplied one resolved Config environment variable. diff --git a/crates/ksp-core-lib/src/program_ids.rs b/crates/ksp-core-lib/src/program_ids.rs index d4d0879..69c0683 100644 --- a/crates/ksp-core-lib/src/program_ids.rs +++ b/crates/ksp-core-lib/src/program_ids.rs @@ -1,5 +1,5 @@ // file: crates/ksp-core-lib/src/program_ids.rs -// version: 3 +// version: 4 const DOMAIN_SOLANA: &str = "solana"; const FAMILY_CONSENSUS: &str = "consensus"; diff --git a/crates/ksp-logging-lib/src/runtime.rs b/crates/ksp-logging-lib/src/runtime.rs index 80ba3dc..8c56a63 100644 --- a/crates/ksp-logging-lib/src/runtime.rs +++ b/crates/ksp-logging-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/runtime.rs -// version: 15 +// version: 16 use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import @@ -473,6 +473,7 @@ const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::f crate::LogFilterLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, }; } + const fn map_file_rotation(rotation: crate::FileRotation) -> tracing_appender::rolling::Rotation { return match rotation { crate::FileRotation::Never => tracing_appender::rolling::Rotation::NEVER, diff --git a/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs b/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs index 4c41103..5da3783 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs -// version: 6 +// version: 7 const MAX_GET_SLOT_LEADERS: u64 = 5_000; @@ -29,71 +29,85 @@ impl SolanaClusterNode { pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.pubkey; } + /// Returns the optional feature-set identifier. #[must_use] pub const fn feature_set(&self) -> std::option::Option { return self.feature_set; } + /// Returns the optional gossip endpoint. #[must_use] pub fn gossip(&self) -> std::option::Option<&str> { return self.gossip.as_deref(); } + /// Returns the optional PubSub endpoint. #[must_use] pub fn pubsub(&self) -> std::option::Option<&str> { return self.pubsub.as_deref(); } + /// Returns the optional JSON-RPC endpoint. #[must_use] pub fn rpc(&self) -> std::option::Option<&str> { return self.rpc.as_deref(); } + /// Returns the optional repair endpoint. #[must_use] pub fn serve_repair(&self) -> std::option::Option<&str> { return self.serve_repair.as_deref(); } + /// Returns the optional shred version. #[must_use] pub const fn shred_version(&self) -> std::option::Option { return self.shred_version; } + /// Returns the optional TPU endpoint. #[must_use] pub fn tpu(&self) -> std::option::Option<&str> { return self.tpu.as_deref(); } + /// Returns the optional TPU forwards endpoint. #[must_use] pub fn tpu_forwards(&self) -> std::option::Option<&str> { return self.tpu_forwards.as_deref(); } + /// Returns the optional TPU forwards QUIC endpoint. #[must_use] pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> { return self.tpu_forwards_quic.as_deref(); } + /// Returns the optional TPU QUIC endpoint. #[must_use] pub fn tpu_quic(&self) -> std::option::Option<&str> { return self.tpu_quic.as_deref(); } + /// Returns the optional TPU vote endpoint. #[must_use] pub fn tpu_vote(&self) -> std::option::Option<&str> { return self.tpu_vote.as_deref(); } + /// Returns the optional TVU endpoint. #[must_use] pub fn tvu(&self) -> std::option::Option<&str> { return self.tvu.as_deref(); } + /// Returns the optional software-version string. #[must_use] pub fn version(&self) -> std::option::Option<&str> { return self.version.as_deref(); } + /// Returns the optional Agave client identifier extension. #[must_use] pub fn client_id(&self) -> std::option::Option<&str> { @@ -149,26 +163,31 @@ impl SolanaEpochInfo { pub const fn absolute_slot(&self) -> u64 { return self.absolute_slot; } + /// Returns the block height. #[must_use] pub const fn block_height(&self) -> u64 { return self.block_height; } + /// Returns the epoch number. #[must_use] pub const fn epoch(&self) -> u64 { return self.epoch; } + /// Returns the slot index within the epoch. #[must_use] pub const fn slot_index(&self) -> u64 { return self.slot_index; } + /// Returns the number of slots in the epoch. #[must_use] pub const fn slots_in_epoch(&self) -> u64 { return self.slots_in_epoch; } + /// Returns the nullable transaction count. #[must_use] pub const fn transaction_count(&self) -> std::option::Option { @@ -208,21 +227,25 @@ impl SolanaEpochSchedule { pub const fn first_normal_epoch(&self) -> u64 { return self.first_normal_epoch; } + /// Returns the first normal slot. #[must_use] pub const fn first_normal_slot(&self) -> u64 { return self.first_normal_slot; } + /// Returns the leader-schedule slot offset. #[must_use] pub const fn leader_schedule_slot_offset(&self) -> u64 { return self.leader_schedule_slot_offset; } + /// Returns the number of slots per epoch. #[must_use] pub const fn slots_per_epoch(&self) -> u64 { return self.slots_per_epoch; } + /// Returns whether epoch warmup is enabled. #[must_use] pub const fn warmup(&self) -> bool { @@ -258,6 +281,7 @@ impl SolanaSnapshotSlotInfo { pub const fn full(&self) -> u64 { return self.full; } + /// Returns the optional highest incremental snapshot slot. #[must_use] pub const fn incremental(&self) -> std::option::Option { @@ -287,20 +311,24 @@ impl SolanaLeaderScheduleConfig { pub const fn new(identity: std::option::Option, commitment: std::option::Option) -> Self { return Self { identity, commitment }; } + /// Returns the optional validator identity filter. #[must_use] pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.identity.as_ref(); } + /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } + /// Returns whether empty. pub(crate) const fn is_empty(&self) -> bool { return self.identity.is_none() && self.commitment.is_none(); } + /// Executes the crate-internal to json value operation for `SolanaLeaderScheduleConfig`. pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); @@ -402,30 +430,36 @@ impl SolanaVoteAccountsConfig { ) -> Self { return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance }; } + /// Returns the optional commitment. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } + /// Returns the optional vote-account public key filter. #[must_use] pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.vote_pubkey.as_ref(); } + /// Returns whether unstaked delinquent validators should be kept. #[must_use] pub const fn keep_unstaked_delinquents(&self) -> std::option::Option { return self.keep_unstaked_delinquents; } + /// Returns the optional delinquent slot distance. #[must_use] pub const fn delinquent_slot_distance(&self) -> std::option::Option { return self.delinquent_slot_distance; } + /// Returns whether empty. pub(crate) const fn is_empty(&self) -> bool { return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none(); } + /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { @@ -460,11 +494,13 @@ impl SolanaEpochCredits { pub const fn epoch(&self) -> u64 { return self.epoch; } + /// Returns cumulative credits at the end of the epoch. #[must_use] pub const fn credits(&self) -> u64 { return self.credits; } + /// Returns cumulative credits before the epoch. #[must_use] pub const fn previous_credits(&self) -> u64 { @@ -492,41 +528,49 @@ impl SolanaVoteAccountInfo { pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.vote_pubkey; } + /// Returns the validator identity public key. #[must_use] pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.node_pubkey; } + /// Returns the activated stake in lamports. #[must_use] pub const fn activated_stake(&self) -> u64 { return self.activated_stake; } + /// Returns the legacy/effective percentage commission field. #[must_use] pub const fn commission(&self) -> u8 { return self.commission; } + /// Returns the optional raw inflation-rewards commission in basis points. #[must_use] pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option { return self.inflation_rewards_commission_bps; } + /// Returns whether the vote account is staked for the current epoch. #[must_use] pub const fn epoch_vote_account(&self) -> bool { return self.epoch_vote_account; } + /// Returns the bounded RPC epoch-credit history. #[must_use] pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] { return self.epoch_credits.as_slice(); } + /// Returns the latest voted slot or zero when no vote exists. #[must_use] pub const fn last_vote(&self) -> u64 { return self.last_vote; } + /// Returns the current root slot or zero when no root exists. #[must_use] pub const fn root_slot(&self) -> u64 { @@ -581,6 +625,7 @@ impl SolanaVoteAccountStatus { pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] { return self.current.as_slice(); } + /// Returns delinquent vote accounts. #[must_use] pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] { diff --git a/crates/ksp-onchain-transport-lib/src/rpc_method.rs b/crates/ksp-onchain-transport-lib/src/rpc_method.rs index 535ce1e..5a79657 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_method.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_method.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_method.rs -// version: 4 +// version: 5 const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [ crate::HttpRpcMethodDescriptor::new( @@ -1080,6 +1080,7 @@ impl HttpRpcMethodDescriptor { pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] { return &CURRENT_HTTP_RPC_METHODS; } + /// Returns the 14 historically documented deprecated HTTP RPC descriptors retained for compliance history. #[must_use] pub const fn historical_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] { diff --git a/crates/ksp-wallet-lib/src/constants.rs b/crates/ksp-wallet-lib/src/constants.rs index f6d372e..993bea6 100644 --- a/crates/ksp-wallet-lib/src/constants.rs +++ b/crates/ksp-wallet-lib/src/constants.rs @@ -1,5 +1,5 @@ // file: crates/ksp-wallet-lib/src/constants.rs -// version: 6 +// version: 7 //! Wallet-owned constants. @@ -85,5 +85,6 @@ pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE"; pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT"; /// Byte length of an XChaCha20-Poly1305 nonce. pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24; + /// Owning tracing target for events emitted by the Wallet crate. pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib"; diff --git a/deltas/0.2.5/pre.010-fix.002.md b/deltas/0.2.5/pre.010-fix.002.md new file mode 100644 index 0000000..fad0353 --- /dev/null +++ b/deltas/0.2.5/pre.010-fix.002.md @@ -0,0 +1,192 @@ + + + +# Delta `0.2.5-pre.010-fix.002` — correction normalisation Rust et garde-fous structurels + +## 1. Base et version + +Base : + +```text +0.2.5-pre.010-fix.001 +workspace.package.version = 0.2.5-pre.10.fix.1 +``` + +Version technique cible : + +```text +workspace.package.version = 0.2.5-pre.10.fix.2 +``` + +Identifiant de livraison : + +```text +0.2.5-pre.010-fix.002 +``` + +Commit attendu après validation : + +```text +v0.2.5-pre.010-fix.002 +``` + +Aucun tag stable n'est créé par ce fix et `rel.001` reste bloqué jusqu'à validation complète. + +## 2. Cause du fix + +La validation opérateur de `pre.010-fix.001` a montré que la normalisation automatisée avait introduit plusieurs erreurs de chemin dans `ksp-app-config-desk` : + +```text +helpers privés de module référencés comme faux crate-root items +fonctions pub(crate) renommées encore appelées sous leur ancien nom local +réexport crate-root LoggingStartup déclaré mais non consommé explicitement +``` + +La revue manuelle a également détecté dans `ksp-core-lib/src/program_ids.rs` une accolade fermante restée à l'ancien emplacement après déplacement de `entries()`. Le code restait syntaxiquement valide parce que `PROGRAM_ID_ENTRIES` devenait un item local situé après un `return`, mais la structure du fichier était incorrecte. + +Enfin, le gate du fix précédent ne contrôlait pas assez précisément l'espacement entre items : plusieurs `const fn` et méthodes restaient accolées sans ligne vide. + +## 3. Corrections Config Desk + +Les tests séparés respectent désormais explicitement la règle de visibilité : + +```text +item strictement privé du module parent -> super::Item +item pub/pub(crate) partagé -> crate::Item via crate-root +``` + +Corrections principales : + +```text +unit_tests/environment.rs + crate::environment_report_from_management + -> super::report_from_management + +unit_tests/logging_editor.rs + crate::logging_document_from_management + -> super::document_from_management + +unit_tests/profiles.rs + crate::profile_inventory_from_management + -> super::inventory_from_management + crate::profile_detail_from_management + -> super::detail_from_management + +src/documents.rs + detail(...) + -> crate::document_detail(...) + +src/tw_main.rs + require_window(...) + -> crate::require_main_window(...) +``` + +`LoggingStartup` reste `pub(crate)` parce qu'il traverse la frontière `bootstrap -> app_state`. Les signatures crate-wide utilisent désormais explicitement `crate::LoggingStartup`, ce qui rend le réexport crate-root effectif et supprime le faux `unused import`. + +## 4. Correction `program_ids.rs` + +L'accolade fermante de : + +```rust +pub const fn entries() -> &'static [crate::ProgramIdEntry] +``` + +est replacée immédiatement après : + +```rust +return PROGRAM_ID_ENTRIES; +``` + +`PROGRAM_ID_ENTRIES` redevient ainsi un item de module et non un `const` local déclaré après un `return`. + +## 5. Espacement Rust normalisé + +Le workspace applique maintenant les règles explicites suivantes : + +```text +fonction/méthode/const fn distincte -> exactement 1 ligne vide entre items +struct/enum/union/trait/impl voisins -> exactement 1 ligne vide +const de même visibilité dans un même bloc -> 0 ligne vide +type/static homogènes de même visibilité -> 0 ligne vide +changement de visibilité/catégorie -> exactement 1 ligne vide +aucune ligne vide dans le corps d'une fonction/méthode/struct/enum +``` + +Le renforcement a détecté puis corrigé 53 séparations manquantes héritées de `fix.001`, notamment dans : + +```text +ksp-app-config-desk +ksp-config-lib +ksp-logging-lib +ksp-onchain-transport-lib +ksp-wallet-lib +``` + +La majorité concernait les méthodes de `rpc_cluster.rs`. + +## 6. Audits renforcés + +### `audit_rust_general_rules.py` + +Ajouts : + +```text +RUST-FMT-110 + détecte un item déclaré après un return inconditionnel au niveau principal + d'une fonction/méthode ; signal d'une accolade ou restructuration incorrecte + +RUST-FMT-111 + impose exactement une ligne vide entre items fonctionnels/type-level voisins + +RUST-FMT-112 + interdit une ligne vide dans un bloc homogène const/static/type de même visibilité + +RUST-FMT-113 + impose exactement une ligne vide entre les blocs pub use et pub(crate) use +``` + +### `audit_rust_export_completeness.py` + +Ajout d'un contrôle de résolution des chemins simples `crate::Item`. Le nouvel audit vérifie qu'un symbole référencé directement depuis le crate-root est réellement déclaré ou réexporté à cette racine. + +Ce contrôle aurait détecté les faux noms `crate::environment_report_from_management`, `crate::logging_document_from_management`, etc. avant la compilation. + +Les macros `#[macro_export]`, modules crate-root et bridges `pub extern crate` sont intégrés à l'inventaire afin de ne pas produire de faux positifs. + +## 7. Règles normatives + +`docs/rules/RULES_RUST.md` précise désormais sans ambiguïté : + +- séparation exacte d'une ligne entre fonctions, méthodes et `const fn` ; +- séparation exacte entre types/blocs `impl` voisins ; +- absence de ligne vide entre constantes de même visibilité ; +- séparation d'une ligne entre niveaux de visibilité distincts ; +- règle analogue pour blocs homogènes `type`/`static` ; +- interdiction d'un item placé après un `return` inconditionnel au niveau principal d'une fonction. + +## 8. Validation exécutée dans l'environnement de préparation + +Les contrôles Python ont réellement été exécutés sur l'arbre cible : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean +``` + +Les scripts modifiés ont aussi été vérifiés par `python3 -m py_compile` pendant la préparation. + +Aucun toolchain Cargo/Rust n'est disponible dans l'environnement de préparation ; aucune compilation Rust locale n'est donc revendiquée. + +## 9. Validation opérateur requise + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-wallet-lib +cargo test --workspace +``` + +Les arbres de dépendances Dalek/Solana ne nécessitent pas d'être répétés pour ce fix : aucune dépendance n'est modifiée par `fix.002`. diff --git a/docs/rules/RULES_RUST.md b/docs/rules/RULES_RUST.md index 44d225f..f3f556c 100644 --- a/docs/rules/RULES_RUST.md +++ b/docs/rules/RULES_RUST.md @@ -1,5 +1,5 @@ - + # Règles Rust générales @@ -54,13 +54,15 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou - **RUST-FMT-001** — `rustfmt.toml` à la racine est la configuration canonique du formatage Rust. - **RUST-FMT-002** — `cargo fmt --all` est exécuté après chaque modification Rust avant l'audit structurel et les validations Cargo. - **RUST-FMT-003** — Aucune ligne vide n'est conservée à l'intérieur du corps d'une fonction ou méthode, ni à l'intérieur d'une définition `struct` ou `enum`. -- **RUST-FMT-004** — Les lignes vides séparent les fonctions/méthodes, types, blocs `impl` et grandes sections logiques ; elles ne fragmentent pas un bloc homogène de déclarations. +- **RUST-FMT-004** — Deux fonctions ou méthodes distinctes, y compris deux `const fn`, sont séparées par exactement une ligne vide. La même séparation exacte s'applique entre `struct`, `enum`, `union`, `trait`, bloc `impl` et fonction/méthode lorsqu'ils constituent des items voisins d'un même scope. - **RUST-FMT-005** — Les blocs homogènes sont ordonnés alphabétiquement par nom lorsque l'ordre n'a pas de signification sémantique. -- **RUST-FMT-006** — Les constantes de module forment des blocs sans ligne vide interne, ordonnés par visibilité `pub`, puis `pub(crate)`, puis privée, et alphabétiquement dans chaque niveau. +- **RUST-FMT-006** — Les constantes de module ou associées sont ordonnées par visibilité `pub`, puis `pub(crate)`, puis privée, et alphabétiquement dans chaque niveau. Deux constantes de même visibilité appartenant au même bloc ne sont séparées par aucune ligne vide ; exactement une ligne vide sépare deux niveaux de visibilité distincts. - **RUST-FMT-007** — Dans une façade, le bloc `pub use` précède le bloc `pub(crate) use`; une seule ligne vide sépare les deux blocs, aucune ligne vide n'existe à l'intérieur d'un bloc, et les symboles sont ordonnés alphabétiquement. - **RUST-FMT-008** — Les déclarations `mod` d'un même bloc sont ordonnées alphabétiquement ; les modules de tests conditionnels restent après les modules de production. - **RUST-FMT-009** — Lorsqu'un `struct` et ses blocs `impl` forment une unité locale sans contrainte de séparation, l'implémentation suit la structure. Les éléments visibles précèdent les helpers privés lorsqu'aucun ordre métier ou protocolaire n'impose l'inverse. - **RUST-FMT-010** — L'ordre alphabétique n'écrase jamais un ordre contractuel ou sémantique : wire fields, comptes Solana, étapes de protocole, priorités, transitions d'état, tableaux de dispatch et séquences explicitement normatives conservent leur ordre défini. +- **RUST-FMT-011** — Les blocs homogènes de `type` ou `static` de même visibilité suivent la même règle d'espacement compact que les constantes ; un changement de catégorie ou de visibilité recrée une séparation d'exactement une ligne vide. +- **RUST-FMT-012** — Une déclaration d'item placée après un `return` inconditionnel au niveau principal d'une fonction/méthode est interdite ; elle est traitée comme un signal probable d'accolade fermante déplacée ou de bloc mal restructuré. ## Contrôle de flux et erreurs diff --git a/scripts/audit_rust_export_completeness.py b/scripts/audit_rust_export_completeness.py old mode 100755 new mode 100644 index 7835b44..f0b1bff --- 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: 1 +# version: 3 """Audit crate-root export completeness and canonical same-crate paths.""" @@ -124,6 +124,33 @@ def external_usage(crate: pathlib.Path, declaration: Declaration, test_parents: return False + + +def crate_root_symbols(crate: pathlib.Path) -> set[str]: + """Return names that can resolve directly after `crate::`.""" + + crate_root = crate / "src/lib.rs" + if not crate_root.is_file(): + crate_root = crate / "src/main.rs" + symbols: set[str] = set() + if crate_root.is_file(): + text = crate_root.read_text(encoding="utf-8") + for match in re.finditer(r"^\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", text, re.MULTILINE): + symbols.add(match.group(1)) + for match in re.finditer(r"^\s*(?:(?:pub|pub\(crate\))\s+)?(?:(?:async|const|unsafe)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)", text, re.MULTILINE): + symbols.add(match.group(1)) + for match in re.finditer(r"^\s*pub(?:\(crate\))?\s+use\s+[^;]*::([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE): + symbols.add(match.group(1)) + for match in re.finditer(r"^\s*pub\s+extern\s+crate\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE): + symbols.add(match.group(1)) + for path in sorted((crate / "src").rglob("*.rs")): + text = path.read_text(encoding="utf-8") + pattern = re.compile(r"#\[macro_export\]\s*\n\s*macro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)") + for match in pattern.finditer(text): + symbols.add(match.group(1)) + return symbols + + def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]: """Audit one crate for export completeness and canonical paths.""" @@ -156,6 +183,21 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate] alias = exports.get((match.group(1), match.group(2))) if alias is not None: candidates.append(Candidate("RUST-IMPORT-201", relative, idx, f"long internal path `crate::{match.group(1)}::{match.group(2)}` must use `crate::{alias}`")) + # Simple `crate::Item` references must resolve at the crate root. + root_symbols = crate_root_symbols(crate) + simple_root = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_]*)\b(?!::)") + for path in sorted(crate.rglob("*.rs")): + if "target" in path.parts: + continue + relative = path.relative_to(workspace).as_posix() + text = path.read_text(encoding="utf-8") + masked = mask_rust_source(text) + for idx, line in enumerate(masked.splitlines(), 1): + for match in simple_root.finditer(line): + symbol = match.group(1) + 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. declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {} for declaration in declarations.values(): diff --git a/scripts/audit_rust_general_rules.py b/scripts/audit_rust_general_rules.py old mode 100755 new mode 100644 index a685ddb..1343b94 --- a/scripts/audit_rust_general_rules.py +++ b/scripts/audit_rust_general_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_rust_general_rules.py -# version: 1 +# version: 4 """Audit mechanically verifiable Rust normalization rules used by KSP.""" @@ -222,6 +222,139 @@ def declaration_start(stripped: str, kind: str) -> bool: return re.match(rf"^{prefixes}{kind}\b", stripped) is not None + + +@dataclasses.dataclass(frozen=True) +class RustItem: + """One mechanically detected Rust item used for spacing checks.""" + + kind: str + visibility: str + start: int + declaration: int + end: int + depth: int + braced: bool + + +def item_visibility(stripped: str) -> str: + """Return normalized visibility for one item declaration.""" + + if stripped.startswith("pub(crate) "): + return "pub(crate)" + if stripped.startswith("pub "): + return "pub" + return "private" + + +def item_kind(stripped: str) -> str | None: + """Return the normalized declaration kind for spacing checks.""" + + prefix = r"(?:(?:pub|pub\(crate\))\s+)?" + qualifiers = r"(?:(?:async|const|unsafe)\s+)*" + if re.match(rf"^{prefix}{qualifiers}fn\b", stripped): + return "fn" + for kind in ("struct", "enum", "union", "trait", "impl", "const", "static", "type"): + if re.match(rf"^{prefix}{qualifiers}{kind}\b", stripped): + return kind + return None + + +def item_leading_line(lines: list[str], declaration_index: int) -> int: + """Return the first rustdoc/attribute line attached to an item.""" + + cursor = declaration_index - 2 + while cursor >= 0: + stripped = lines[cursor].strip() + if stripped.startswith("///") or stripped.startswith("#["): + cursor -= 1 + continue + break + return cursor + 2 + + +def item_end_line(masked_lines: list[str], depths: list[int], declaration_index: int, kind: str) -> tuple[int, bool]: + """Return the end line and whether the item owns a braced body.""" + + start_depth = depths[declaration_index - 1] + if kind in {"const", "static", "type"}: + for line_index in range(declaration_index - 1, len(masked_lines)): + if ";" in masked_lines[line_index]: + return line_index + 1, False + return declaration_index, False + body_started = False + for line_index in range(declaration_index - 1, len(masked_lines)): + masked_line = masked_lines[line_index] + if not body_started and ";" in masked_line and "{" not in masked_line: + return line_index + 1, False + if "{" in masked_line: + body_started = True + if body_started: + end_depth = depths[line_index] + masked_line.count("{") - masked_line.count("}") + if end_depth == start_depth: + return line_index + 1, True + return declaration_index, body_started + + +def rust_items(lines: list[str], masked_lines: list[str], depths: list[int]) -> list[RustItem]: + """Return mechanically detected Rust items with source spans.""" + + result: list[RustItem] = [] + for idx, line in enumerate(lines, 1): + stripped = line.strip() + kind = item_kind(stripped) + if kind is None: + continue + end, braced = item_end_line(masked_lines, depths, idx, kind) + result.append(RustItem(kind, item_visibility(stripped), item_leading_line(lines, idx), idx, end, depths[idx - 1], braced)) + return result + + +def item_parent(item: RustItem, items: list[RustItem]) -> RustItem | None: + """Return the smallest braced item that contains another item.""" + + parents = [candidate for candidate in items if candidate.braced and candidate.declaration < item.declaration <= candidate.end and candidate.depth < item.depth] + if not parents: + return None + return max(parents, key=lambda candidate: candidate.depth) + + +def audit_item_spacing_and_nesting(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]: + """Enforce exact item separation and reject declarations nested in functions.""" + + violations: list[Violation] = [] + items = rust_items(lines, masked_lines, depths) + for item in items: + parent = item_parent(item, items) + if parent is not None and parent.kind == "fn": + body_depth = parent.depth + 1 + preceding = lines[parent.declaration:item.declaration - 1] + preceding_depths = depths[parent.declaration:item.declaration - 1] + returned = any(depth == body_depth and re.match(r"^\s*return\b.*;\s*$", line) is not None for line, depth in zip(preceding, preceding_depths, strict=True)) + if returned: + violations.append(Violation("RUST-FMT-110", relative, item.declaration, f"item `{item.kind}` follows an unconditional function-level return; probable misplaced closing brace")) + children: dict[tuple[int, int] | None, list[RustItem]] = {} + for item in items: + parent = item_parent(item, items) + key = None if parent is None else (parent.declaration, parent.end) + children.setdefault(key, []).append(item) + for siblings in children.values(): + siblings.sort(key=lambda item: item.declaration) + for previous, current in zip(siblings, siblings[1:]): + # Do not infer spacing across unparsed syntax such as macro invocations. + between = lines[previous.end:current.start - 1] + if any(candidate.strip() for candidate in between): + continue + blank_count = sum(1 for candidate in between if not candidate.strip()) + same_homogeneous_block = previous.kind == current.kind and previous.kind in {"const", "static", "type"} and previous.visibility == current.visibility + expected = 0 if same_homogeneous_block else 1 + if blank_count != expected: + rule = "RUST-FMT-111" if expected == 1 else "RUST-FMT-112" + expectation = "exactly one blank line between Rust items" if expected == 1 else "no blank line inside one homogeneous declaration block" + violations.append(Violation(rule, relative, current.start, f"{expectation}; found {blank_count}")) + return violations + + def audit_blank_lines_in_bodies(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]: """Reject blank lines inside functions/methods and struct/enum bodies.""" @@ -277,9 +410,6 @@ def audit_top_level_const_blocks(relative: str, lines: list[str], depths: list[i name = match.group(2) if previous is not None: previous_line, previous_rank, previous_name = previous - between = lines[previous_line:idx - 1] - if any(not candidate.strip() for candidate in between): - violations.append(Violation("RUST-FMT-102", relative, idx, "blank line inside one top-level const block")) if rank < previous_rank: violations.append(Violation("RUST-FMT-103", relative, idx, "const visibility order must be pub, pub(crate), then private")) if rank == previous_rank and natural_key(name) < natural_key(previous_name): @@ -404,6 +534,15 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: source_match = re.match(r"^pub(?:\(crate\))?\s+use\s+(?:self::)?([A-Za-z_][A-Za-z0-9_]*)::", stripped) if source_match is not None and source_match.group(1) in local_modules and not re.match(r"^pub(?:\(crate\))?\s+use\s+self::", stripped): violations.append(Violation("RUST-IMPORT-106", relative, idx, "crate-root internal re-export must start with self::")) + public_exports = [item for item in exports if item[1] == "pub"] + crate_exports = [item for item in exports if item[1] == "pub(crate)"] + if public_exports and crate_exports: + public_end = public_exports[-1][0] + crate_start = crate_exports[0][0] + transition = lines[public_end:crate_start - 1] + blank_count = sum(1 for candidate in transition if not candidate.strip()) + if blank_count != 1: + violations.append(Violation("RUST-FMT-113", relative, crate_start, f"pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}")) crate_seen = False previous_by_visibility: dict[str, tuple[int, str]] = {} for idx, visibility, symbol in exports: @@ -422,6 +561,7 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: violations.append(Violation("RUST-FMT-107", relative, idx, f"{visibility} use block is not alphabetically ordered by exported symbol")) previous_by_visibility[visibility] = (idx, symbol) violations.extend(audit_blank_lines_in_bodies(relative, lines, masked_lines, depths)) + violations.extend(audit_item_spacing_and_nesting(relative, lines, masked_lines, depths)) violations.extend(audit_module_order(relative, lines, depths)) violations.extend(audit_top_level_const_blocks(relative, lines, depths)) return violations