From 72ddabd9f2468b3214ef6d7f78961a25871bb49a Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 14 Aug 2026 15:12:38 +0200 Subject: [PATCH] v0.1.1-pre.002 --- Cargo.toml | 4 +- crates/ksp-core-lib/src/error.rs | 130 +++++++++++ crates/ksp-core-lib/src/lib.rs | 15 +- crates/ksp-core-lib/tests/public_api.rs | 22 ++ crates/ksp-core-lib/unit_tests/error.rs | 78 +++++++ deltas/0.1.1/pre.002.md | 215 ++++++++++++++++++ docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md | 12 +- 7 files changed, 465 insertions(+), 11 deletions(-) create mode 100644 crates/ksp-core-lib/src/error.rs create mode 100644 crates/ksp-core-lib/tests/public_api.rs create mode 100644 crates/ksp-core-lib/unit_tests/error.rs create mode 100644 deltas/0.1.1/pre.002.md diff --git a/Cargo.toml b/Cargo.toml index ae9a556..9343265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 18 +# version: 19 [workspace] resolver = "3" members = ["crates/ksp-core-lib"] [workspace.package] -version = "0.1.1-pre.1" +version = "0.1.1-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-core-lib/src/error.rs b/crates/ksp-core-lib/src/error.rs new file mode 100644 index 0000000..c7f4e9c --- /dev/null +++ b/crates/ksp-core-lib/src/error.rs @@ -0,0 +1,130 @@ +// file: crates/ksp-core-lib/src/error.rs +// version: 1 + +/// Stable structured identifier for a KSP error. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct ErrorCode { + domain: &'static str, + code: &'static str, +} + +impl ErrorCode { + /// Creates an error code from a stable domain and code identifier. + #[must_use] + pub const fn new(domain: &'static str, code: &'static str) -> Self { + return Self { domain, code }; + } + + /// Returns the stable error domain identifier. + #[must_use] + pub const fn domain(&self) -> &'static str { + return self.domain; + } + + /// Returns the stable error code identifier within the domain. + #[must_use] + pub const fn code(&self) -> &'static str { + return self.code; + } +} + +/// Structured contextual field attached to a KSP error. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ErrorContext { + key: &'static str, + value: std::string::String, +} + +impl ErrorContext { + /// Creates one contextual field from a stable key and an owned value. + #[must_use] + pub fn new(key: &'static str, value: impl std::convert::Into) -> Self { + return Self { key, value: value.into() }; + } + + /// Returns the stable contextual key. + #[must_use] + pub fn key(&self) -> &'static str { + return self.key; + } + + /// Returns the contextual value. + #[must_use] + pub fn value(&self) -> &str { + return self.value.as_str(); + } +} + +/// Common KSP error carrying a stable code, human-readable message, structured context and optional source. +#[derive(Debug)] +pub struct Error { + code: crate::ErrorCode, + message: std::string::String, + context: std::vec::Vec, + source: std::option::Option>, +} + +impl Error { + /// Creates a KSP error without context or external source. + #[must_use] + pub fn new(code: crate::ErrorCode, message: impl std::convert::Into) -> Self { + return Self { code, message: message.into(), context: std::vec::Vec::new(), source: std::option::Option::None }; + } + + /// Returns the stable structured error code. + #[must_use] + pub const fn code(&self) -> crate::ErrorCode { + return self.code; + } + + /// Returns the human-readable diagnostic message. + #[must_use] + pub fn message(&self) -> &str { + return self.message.as_str(); + } + + /// Returns the contextual fields in insertion order. + #[must_use] + pub fn context(&self) -> &[crate::ErrorContext] { + return self.context.as_slice(); + } + + /// Appends one contextual field and returns the enriched error. + #[must_use] + pub fn with_context(mut self, key: &'static str, value: impl std::convert::Into) -> Self { + self.context.push(crate::ErrorContext::new(key, value)); + return self; + } + + /// Attaches an external error as the standard source and returns the enriched error. + #[must_use] + pub fn with_source(mut self, source: E) -> Self + where + E: std::error::Error + std::marker::Send + std::marker::Sync + 'static, + { + self.source = std::option::Option::Some(std::boxed::Box::new(source)); + return self; + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return write!(formatter, "{}.{}: {}", self.code.domain(), self.code.code(), self.message); + } +} + +impl std::error::Error for Error { + fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> { + return match self.source.as_deref() { + std::option::Option::Some(source) => std::option::Option::Some(source), + std::option::Option::None => std::option::Option::None, + }; + } +} + +/// Common result type returned by KSP APIs using [`crate::Error`]. +pub type Result = std::result::Result; + +#[cfg(test)] +#[path = "../unit_tests/error.rs"] +mod tests; diff --git a/crates/ksp-core-lib/src/lib.rs b/crates/ksp-core-lib/src/lib.rs index ad43861..96acc07 100644 --- a/crates/ksp-core-lib/src/lib.rs +++ b/crates/ksp-core-lib/src/lib.rs @@ -1,7 +1,18 @@ // file: crates/ksp-core-lib/src/lib.rs -// version: 3 +// version: 4 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] -//! Minimal core-library skeleton for the KSP foundation phase. +//! Core contracts shared by the foundational KSP layers. + +mod error; + +/// Common KSP error type used by higher-level crates. +pub use self::error::Error; +/// Stable structured code identifying a KSP error category and condition. +pub use self::error::ErrorCode; +/// Structured contextual field attached to a KSP error. +pub use self::error::ErrorContext; +/// Common KSP result alias using [`Error`]. +pub use self::error::Result; diff --git a/crates/ksp-core-lib/tests/public_api.rs b/crates/ksp-core-lib/tests/public_api.rs new file mode 100644 index 0000000..f7962d5 --- /dev/null +++ b/crates/ksp-core-lib/tests/public_api.rs @@ -0,0 +1,22 @@ +// file: crates/ksp-core-lib/tests/public_api.rs +// version: 1 + +fn public_result() -> ksp_core_lib::Result<()> { + let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("consumer", "failed"), "consumer failure").with_context("operation", "public_api"); + return std::result::Result::Err(error); +} + +#[test] +fn error_contract_is_consumable_from_crate_root() { + let result = public_result(); + assert!(result.is_err()); + let error = match result { + std::result::Result::Ok(()) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), ksp_core_lib::ErrorCode::new("consumer", "failed")); + assert_eq!(error.message(), "consumer failure"); + assert_eq!(error.context(), &[ksp_core_lib::ErrorContext::new("operation", "public_api")]); + assert_eq!(std::string::ToString::to_string(&error), "consumer.failed: consumer failure"); + return; +} diff --git a/crates/ksp-core-lib/unit_tests/error.rs b/crates/ksp-core-lib/unit_tests/error.rs new file mode 100644 index 0000000..cd0f23f --- /dev/null +++ b/crates/ksp-core-lib/unit_tests/error.rs @@ -0,0 +1,78 @@ +// file: crates/ksp-core-lib/unit_tests/error.rs +// version: 1 + +#[derive(Debug)] +struct TestSource; + +impl std::fmt::Display for TestSource { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("source failure"); + } +} + +impl std::error::Error for TestSource {} + +fn assert_send_sync() +where + T: std::marker::Send + std::marker::Sync, +{ + return; +} + +#[test] +fn error_code_preserves_domain_and_code() { + const CODE: crate::ErrorCode = crate::ErrorCode::new("core", "sample_failure"); + assert_eq!(CODE.domain(), "core"); + assert_eq!(CODE.code(), "sample_failure"); + return; +} + +#[test] +fn error_context_preserves_key_and_value() { + let context = crate::ErrorContext::new("operation", "sample"); + assert_eq!(context.key(), "operation"); + assert_eq!(context.value(), "sample"); + return; +} + +#[test] +fn error_preserves_code_message_and_context_order() { + let code = crate::ErrorCode::new("core", "sample_failure"); + let error = crate::Error::new(code, "sample message").with_context("first", "one").with_context("second", "two"); + assert_eq!(error.code(), code); + assert_eq!(error.message(), "sample message"); + assert_eq!(error.context().len(), 2); + assert_eq!(error.context()[0].key(), "first"); + assert_eq!(error.context()[0].value(), "one"); + assert_eq!(error.context()[1].key(), "second"); + assert_eq!(error.context()[1].value(), "two"); + return; +} + +#[test] +fn display_contains_only_qualified_code_and_message() { + let error = crate::Error::new(crate::ErrorCode::new("core", "sample_failure"), "sample message") + .with_context("secret_free_context", "not rendered") + .with_source(TestSource); + assert_eq!(std::string::ToString::to_string(&error), "core.sample_failure: sample message"); + return; +} + +#[test] +fn standard_source_is_preserved() { + let error = crate::Error::new(crate::ErrorCode::new("core", "sample_failure"), "sample message").with_source(TestSource); + let source = std::error::Error::source(&error); + assert!(source.is_some()); + let source_message = match source { + std::option::Option::Some(value) => std::string::ToString::to_string(value), + std::option::Option::None => std::string::String::new(), + }; + assert_eq!(source_message, "source failure"); + return; +} + +#[test] +fn common_error_is_send_and_sync() { + assert_send_sync::(); + return; +} diff --git a/deltas/0.1.1/pre.002.md b/deltas/0.1.1/pre.002.md new file mode 100644 index 0000000..bac8019 --- /dev/null +++ b/deltas/0.1.1/pre.002.md @@ -0,0 +1,215 @@ + + + +# Delta 0.1.1-pre.002 + +## Base requise + +Commit de livraison attendu : + +```text +v0.1.1-pre.001-fix.002 +``` + +Le plan actif est `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` version documentaire 4, incluant le réalignement du tableau des cas représentatifs effectué avant le commit du correctif précédent. + +## Objectif + +Implémenter la première surface fonctionnelle de `ksp-core-lib` : le contrat commun ouvert `Error` / `Result` validé pendant `pre.001`. + +Cette tranche reste strictement bornée à l'erreur commune et n'ouvre aucune dépendance Solana ni aucun Program ID. + +## Version Cargo + +`workspace.package.version` passe de : + +```text +0.1.1-pre.1 +``` + +à : + +```text +0.1.1-pre.2 +``` + +L'identifiant Cargo respecte SemVer sans zéro initial ; l'identifiant de livraison reste `0.1.1-pre.002`. + +Le header de `Cargo.toml` passe de version 18 à 19. + +## Fichiers ajoutés + +- `crates/ksp-core-lib/src/error.rs` +- `crates/ksp-core-lib/unit_tests/error.rs` +- `crates/ksp-core-lib/tests/public_api.rs` +- `deltas/0.1.1/pre.002.md` + +## Fichiers modifiés + +- `Cargo.toml` +- `crates/ksp-core-lib/src/lib.rs` +- `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` + +## Fichiers supprimés + +Aucun. + +## Contrat implémenté + +### `ErrorCode` + +`ErrorCode` contient uniquement : + +```text +domain: &'static str +code: &'static str +``` + +Décisions : + +- `ErrorCode::new(...)` est `const` afin que chaque crate supérieure puisse définir ses propres codes statiques ; +- Core ne possède aucune enum centrale des domaines ; +- `domain()` et `code()` exposent les deux identifiants stables ; +- `ErrorCode` est `Copy`, `Clone`, `Eq`, `PartialEq`, `Hash` et `Debug` parce qu'il ne contient que deux chaînes statiques. + +### `ErrorContext` + +`ErrorContext` contient : + +```text +key: &'static str +value: String +``` + +Le contexte conserve son ordre d'insertion dans `Error`. + +L'API publique expose `ErrorContext::new(...)`, `key()` et `value()`. + +### `Error` + +`Error` contient : + +```text +code: ErrorCode +message: String +context: Vec +source: Option> +``` + +Décisions stabilisées : + +- `Error::new(...)` construit l'erreur minimale ; +- `with_context(...)` consomme `self`, ajoute un champ puis retourne l'erreur enrichie ; +- `with_source(...)` suit le même modèle pour une cause externe ; +- aucune méthode mutable publique parallèle n'est ajoutée ; +- aucune conversion générique `From` n'est introduite ; +- `Error` ne dérive pas `Clone`, `Eq` ou `PartialEq`, afin de ne pas affaiblir le support d'une vraie cause externe ; +- `Error` implémente `std::fmt::Display` et `std::error::Error` ; +- le rendu `Display` est exactement : + +```text +.: +``` + +Le contexte et la chaîne de causes ne sont pas injectés automatiquement dans ce rendu. + +### `Result` + +La façade expose : + +```text +ksp_core_lib::Result = std::result::Result +``` + +## Façade Core + +`crates/ksp-core-lib/src/lib.rs` ouvre le module d'implémentation en privé puis réexporte explicitement : + +```text +ksp_core_lib::Error +ksp_core_lib::ErrorCode +ksp_core_lib::ErrorContext +ksp_core_lib::Result +``` + +Aucun `pub mod` n'est introduit. + +## Dépendances + +Aucune dépendance n'est ajoutée à `ksp-core-lib` pendant cette tranche. + +En particulier, `pre.002` n'introduit ni `thiserror`, ni `anyhow`, ni crate Solana, ni codec wire. + +## Tests ajoutés + +### Tests unitaires externes + +`crates/ksp-core-lib/unit_tests/error.rs` vérifie : + +- conservation de `domain` et `code` ; +- possibilité de déclarer un `ErrorCode` constant ; +- conservation des champs `ErrorContext` ; +- conservation du message et de l'ordre du contexte ; +- rendu exact de `Display` ; +- absence du contexte et de la cause dans le rendu ; +- conservation de la cause via `std::error::Error::source()` ; +- propriété `Send + Sync` de l'erreur commune. + +Le fichier est rattaché au module privé de production via `#[cfg(test)]` et `#[path = "../unit_tests/error.rs"]`. + +### Test d'intégration + +`crates/ksp-core-lib/tests/public_api.rs` consomme exclusivement la façade crate-root et vérifie que `Error`, `ErrorCode`, `ErrorContext` et `Result` sont utilisables depuis une crate externe. + +## Documentation de plan + +Le plan passe de version documentaire 4 à 5 afin de remplacer les deux questions désormais résolues par les décisions réellement implémentées : + +- `with_context(...)` consomme `self` ; +- `Display` utilise la forme stable `.: `. + +Les questions `pre.003` concernant `solana-pubkey` restent ouvertes et inchangées. + +## Validations exécutées + +Dans l'environnement de préparation : + +- reconstruction de la base `0.1.1-pre.001-fix.002` depuis la release `0.0.3` et les deltas successifs ; +- prise en compte de la version 4 du plan fournie après réalignement manuel du tableau ; +- contrôle du périmètre des fichiers modifiés/ajoutés ; +- parsing TOML statique du manifest racine ; +- contrôle des headers `file:` / `version:` et des fins de ligne des fichiers livrés ; +- recherche statique des usages interdits `unsafe`, `unwrap`, `expect`, `panic` et opérateur `?` dans le code de production ajouté ; +- contrôle de l'absence de `use` dans le code Rust ajouté ; +- contrôle de l'absence de nouvelle dépendance Cargo. + +## Validations non exécutées + +L'environnement de préparation ne contient ni `cargo` ni `rustc`. + +Les commandes suivantes n'ont donc pas pu être exécutées ici : + +```bash +cargo fmt --all +cargo check --workspace +cargo test --workspace +cargo clippy --workspace --all-targets +``` + +Elles doivent être exécutées sur le dépôt réel avant validation du commit. Aucun succès Cargo n'est déclaré par ce delta. + +## Décisions prises + +- Le contrat Error/Result ouvert de `pre.001` est retenu sans enum centrale de domaines. +- `ErrorCode::new(...)` est `const`. +- Les champs des types publics restent privés et sont accessibles par API explicite. +- Le contexte est ordonné et enrichi par consommation de `self`. +- La cause standard est conservée avec les bornes `Error + Send + Sync + 'static`. +- `Display` est volontairement court et stable ; Logging décidera plus tard comment exploiter contexte et causes. +- Core ne possède aucune conversion vers les erreurs des domaines supérieurs. + +## Questions ouvertes + +Aucune question bloquante pour `pre.002`. + +Les questions relatives à `Pubkey`, aux Program IDs et à leur registre restent réservées à `0.1.1-pre.003` conformément au plan actif. diff --git a/docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md b/docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md index 8e0f4b9..d1a9c35 100644 --- a/docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md +++ b/docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan KSP 0.1.1 — Core foundation @@ -59,11 +59,11 @@ Core doit posséder les identifiants fondamentaux du runtime Solana qui ne relè Cette propriété inclut les valeurs canoniques KSP, leur représentation `Pubkey`, leur nomenclature et un registre descriptif enumerable permettant de les inventorier/rechercher. Ce registre de constantes n'est pas le registry de dispatch de `ksp-program-lib` : il ne sélectionne aucun decoder, executor ou implémentation de programme et ne crée aucune enum fermée des protocoles. -## Contrat d'erreur proposé +## Contrat d'erreur retenu ### Forme générale -La direction retenue pour `pre.002` est un type structuré et extensible plutôt qu'une enum fermée. +`pre.002` stabilise un type structuré et extensible plutôt qu'une enum fermée. Surface conceptuelle : @@ -85,7 +85,7 @@ Error Result = std::result::Result ``` -Le détail syntaxique Rust exact reste à implémenter et tester dans `pre.002`, mais les invariants suivants font partie du plan. +La surface Rust de `pre.002` retient les constructeurs/getters explicites ainsi que des enrichissements consommant `self` : `Error::with_context(...)` et `Error::with_source(...)`. `ErrorCode::new(...)` est `const` afin que les crates supérieures puissent déclarer leurs codes sous forme de constantes. Les invariants suivants font partie du contrat. ### Invariants @@ -101,7 +101,7 @@ Le détail syntaxique Rust exact reste à implémenter et tester dans `pre.002`, - Une cause externe peut être conservée par `source` lorsqu'elle implémente `std::error::Error + Send + Sync + 'static`. - Une erreur externe qui ne respecte pas ces bornes peut toujours être transformée explicitement en message/contexte sans être conservée comme `source`. - `Error` implémente `std::fmt::Display` et `std::error::Error`. -- `Display` expose le code qualifié et le message principal ; il ne concatène pas automatiquement tout le contexte ou toute la chaîne de causes. +- `Display` utilise exactement la forme `.: ` ; il ne concatène pas automatiquement le contexte ou la chaîne de causes. - Aucun `From` générique ou inventaire de conversions propres aux futurs domaines n'est ajouté dans Core. Les crates propriétaires enveloppent explicitement leur cause avec leur propre `ErrorCode`. - Aucun besoin de `Clone`, `Eq` ou `PartialEq` n'est imposé à `Error` : préserver une vraie cause d'erreur est prioritaire sur ces dérivations. @@ -657,8 +657,6 @@ Un `pre.NNN-fix.NNN` corrige la tranche correspondante sans réécrire son histo ## Questions ouvertes non bloquantes -- Confirmer, pendant `pre.002`, si `ErrorContext` doit être ajouté par une méthode consommant `self` (`with_context`) ou par une méthode mutable ; privilégier l'API la plus simple compatible avec le style explicite KSP. -- Confirmer le format exact de `Display` par les tests avant de le considérer stable. - Revérifier en `pre.003` la version publiée de `solana-pubkey`, son API compile-time pertinente et le MSRV officiel au jour du code. - Décider en `pre.005`, à partir de l'API réellement stabilisée, si un `README.md`/`USAGE.md` de crate apporte suffisamment de valeur pour être créé maintenant.