v0.2.5-pre.010.fix.002

This commit is contained in:
2026-08-20 11:53:55 +02:00
parent c0b131bf6f
commit 08c8046262
21 changed files with 467 additions and 37 deletions

5
.pydevproject Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
</pydev_project>

View File

@@ -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"

View File

@@ -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<LoggingStartup> {
) -> ksp_core_lib::Result<crate::LoggingStartup> {
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<LoggingStartup> {
) -> ksp_core_lib::Result<crate::LoggingStartup> {
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<LoggingStartup> {
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard {
std::result::Result::Ok(value) => value,

View File

@@ -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),

View File

@@ -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.

View File

@@ -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.

View File

@@ -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<tauri::Wry>) -> 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),

View File

@@ -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.

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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.

View File

@@ -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";

View File

@@ -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,

View File

@@ -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<u32> {
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<u16> {
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<u64> {
@@ -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<u64> {
@@ -287,20 +311,24 @@ impl SolanaLeaderScheduleConfig {
pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> 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<crate::SolanaCommitment> {
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<crate::SolanaCommitment> {
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<bool> {
return self.keep_unstaked_delinquents;
}
/// Returns the optional delinquent slot distance.
#[must_use]
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
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<u16> {
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] {

View File

@@ -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] {

View File

@@ -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";

View File

@@ -0,0 +1,192 @@
<!-- file: deltas/0.2.5/pre.010-fix.002.md -->
<!-- version: 1 -->
# 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`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_RUST.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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

44
scripts/audit_rust_export_completeness.py Executable file → Normal file
View File

@@ -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():

148
scripts/audit_rust_general_rules.py Executable file → Normal file
View File

@@ -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