Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a7219fa59 | |||
| 77f6eaf487 | |||
| 8a0c119878 | |||
| d96f41fb8e | |||
| ea87a58e32 | |||
| 20c5643701 | |||
| d55903b13e | |||
| 37f53f3080 | |||
| 9f23eb950c | |||
| 151590422d | |||
| 488d8ae0a8 | |||
| 6e06802e38 | |||
| 697527675a | |||
| 7b4444fb07 | |||
| bd8401c0d0 | |||
| 18ae0e4873 | |||
| 033912fb2c | |||
| 11ca53ba48 | |||
| b01fb3fa53 | |||
| 4a86a76ca8 | |||
| 75e5ec047f | |||
| 81eda2e5ad | |||
| 72ddabd9f2 | |||
| 27a6715a3e | |||
| 37a1480c72 | |||
| ecc82f681d |
13
Cargo.toml
13
Cargo.toml
@@ -1,18 +1,25 @@
|
||||
# file: Cargo.toml
|
||||
# version: 17
|
||||
# version: 39
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-core-lib"]
|
||||
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.3"
|
||||
version = "0.1.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
unreachable_pub = "deny"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -31,8 +31,8 @@ Regrouper les releases consacrées aux fondations N1. Chaque release concrète e
|
||||
|
||||
### Releases concrètes
|
||||
|
||||
- [ ] `0.1.1` — Stabiliser `ksp-core-lib` : `Error`/`Result`, Program IDs fondamentaux et primitives réellement N1.
|
||||
- [ ] `0.1.2` — Introduire `ksp-logging-lib` comme façade KSP de `tracing`, `tracing-appender` et `tracing-subscriber`.
|
||||
- [X] `0.1.1` — Stabiliser `ksp-core-lib` : `Error`/`Result`, Program IDs fondamentaux et primitives réellement N1.
|
||||
- [X] `0.1.2` — Introduire `ksp-logging-lib` comme façade KSP de `tracing`, `tracing-appender` et `tracing-subscriber`.
|
||||
- [ ] `0.1.3` — Introduire `ksp-config-lib` : documents, profils, résolution, validation et modifications autorisées.
|
||||
- [ ] `0.1.4` — Introduire `ksp-app-config-desk` pour valider réellement Config et la frontière Tauri.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-core-lib/Cargo.toml
|
||||
# version: 1
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "ksp-core-lib"
|
||||
@@ -7,5 +7,8 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
solana-pubkey.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
130
crates/ksp-core-lib/src/error.rs
Normal file
130
crates/ksp-core-lib/src/error.rs
Normal file
@@ -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<std::string::String>) -> 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<crate::ErrorContext>,
|
||||
source: std::option::Option<std::boxed::Box<dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static>>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Creates a KSP error without context or external source.
|
||||
#[must_use]
|
||||
pub fn new(code: crate::ErrorCode, message: impl std::convert::Into<std::string::String>) -> 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<std::string::String>) -> 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<E>(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<T> = std::result::Result<T, crate::Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/error.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,120 @@
|
||||
// file: crates/ksp-core-lib/src/lib.rs
|
||||
// version: 3
|
||||
// version: 6
|
||||
#![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.
|
||||
//!
|
||||
//! `ksp-core-lib` owns the common KSP error contract, the Solana [`Pubkey`]
|
||||
//! primitive used by the project, and the KSP-owned registry of fundamental
|
||||
//! Solana Program IDs. Higher-level domains extend these contracts without
|
||||
//! introducing reverse dependencies from Core.
|
||||
|
||||
mod error;
|
||||
mod program_ids;
|
||||
|
||||
/// 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;
|
||||
/// Canonical Address Lookup Table Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_ADDRESS_LOOKUP_TABLE;
|
||||
/// Canonical Compute Budget Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_COMPUTE_BUDGET;
|
||||
/// Canonical Config Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_CONFIG;
|
||||
/// Canonical Feature Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_FEATURE;
|
||||
/// Canonical upgradeable BPF Loader Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_LOADER_BPF_UPGRADEABLE;
|
||||
/// Canonical deprecated BPF Loader Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_LOADER_BPF_V1;
|
||||
/// Canonical BPF Loader v2 Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_LOADER_BPF_V2;
|
||||
/// Canonical Native Loader Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_LOADER_NATIVE;
|
||||
/// Canonical Loader v4 Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_LOADER_V4;
|
||||
/// Canonical Ed25519 precompile Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_PRECOMPILE_ED25519;
|
||||
/// Canonical Secp256k1 precompile Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_PRECOMPILE_SECP256K1;
|
||||
/// Canonical Secp256r1 precompile Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_PRECOMPILE_SECP256R1;
|
||||
/// Canonical Slashing Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_SLASHING;
|
||||
/// Canonical Stake Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_STAKE;
|
||||
/// Canonical System Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_SYSTEM;
|
||||
/// Canonical Vote Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_VOTE;
|
||||
/// Canonical ZK ElGamal Proof Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_ZK_ELGAMAL_PROOF;
|
||||
/// Canonical ZK Token Proof Program ID as Base58 text.
|
||||
pub use self::program_ids::PRGID_SOLANA_ZK_TOKEN_PROOF;
|
||||
/// Canonical Address Lookup Table Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_ADDRESS_LOOKUP_TABLE;
|
||||
/// Canonical Compute Budget Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_COMPUTE_BUDGET;
|
||||
/// Canonical Config Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_CONFIG;
|
||||
/// Canonical Feature Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_FEATURE;
|
||||
/// Canonical upgradeable BPF Loader Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_LOADER_BPF_UPGRADEABLE;
|
||||
/// Canonical deprecated BPF Loader Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_LOADER_BPF_V1;
|
||||
/// Canonical BPF Loader v2 Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_LOADER_BPF_V2;
|
||||
/// Canonical Native Loader Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_LOADER_NATIVE;
|
||||
/// Canonical Loader v4 Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_LOADER_V4;
|
||||
/// Canonical Ed25519 precompile Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_PRECOMPILE_ED25519;
|
||||
/// Canonical Secp256k1 precompile Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_PRECOMPILE_SECP256K1;
|
||||
/// Canonical Secp256r1 precompile Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_PRECOMPILE_SECP256R1;
|
||||
/// Canonical Slashing Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_SLASHING;
|
||||
/// Canonical Stake Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_STAKE;
|
||||
/// Canonical System Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_SYSTEM;
|
||||
/// Canonical Vote Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_VOTE;
|
||||
/// Canonical ZK ElGamal Proof Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_ZK_ELGAMAL_PROOF;
|
||||
/// Canonical ZK Token Proof Program ID as a typed [`Pubkey`].
|
||||
pub use self::program_ids::PRGIDPK_SOLANA_ZK_TOKEN_PROOF;
|
||||
/// Immutable descriptor for one registered Program ID.
|
||||
pub use self::program_ids::ProgramIdEntry;
|
||||
/// Borrowed multi-axis filter for the canonical Program ID registry.
|
||||
pub use self::program_ids::ProgramIdFilter;
|
||||
/// Technical classification of one registered Program ID.
|
||||
pub use self::program_ids::ProgramIdKind;
|
||||
/// Returns the canonical Program ID registry.
|
||||
pub use self::program_ids::entries;
|
||||
/// Finds one registered Program ID by its Base58 representation.
|
||||
pub use self::program_ids::find_program_id;
|
||||
/// Finds one registered Program ID by its typed [`Pubkey`] representation.
|
||||
pub use self::program_ids::find_program_pubkey;
|
||||
/// Returns the Solana core/native Program ID view.
|
||||
pub use self::program_ids::native_program_ids;
|
||||
/// Returns Program IDs matching a multi-axis filter.
|
||||
pub use self::program_ids::program_ids;
|
||||
/// Returns Program IDs belonging to one domain.
|
||||
pub use self::program_ids::program_ids_by_domain;
|
||||
/// Returns Program IDs belonging to one family.
|
||||
pub use self::program_ids::program_ids_by_family;
|
||||
/// Returns Program IDs belonging to one protocol or project.
|
||||
pub use self::program_ids::program_ids_by_protocol;
|
||||
/// Solana account address primitive used by KSP Program IDs.
|
||||
pub use solana_pubkey::Pubkey;
|
||||
|
||||
492
crates/ksp-core-lib/src/program_ids.rs
Normal file
492
crates/ksp-core-lib/src/program_ids.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
// file: crates/ksp-core-lib/src/program_ids.rs
|
||||
// version: 2
|
||||
|
||||
const DOMAIN_SOLANA: &str = "solana";
|
||||
const FAMILY_CONSENSUS: &str = "consensus";
|
||||
const FAMILY_LOADER: &str = "loader";
|
||||
const FAMILY_PRECOMPILE: &str = "precompile";
|
||||
const FAMILY_PROOF: &str = "proof";
|
||||
const FAMILY_RUNTIME: &str = "runtime";
|
||||
const PROTOCOL_SOLANA: &str = "solana";
|
||||
|
||||
/// Declares one KSP-owned Solana Program ID as matching Base58 and typed constants.
|
||||
///
|
||||
/// The Base58 literal is written once and decoded at compile time through [`crate::Pubkey`].
|
||||
#[macro_export]
|
||||
macro_rules! declare_program_id {
|
||||
($string_name:ident, $pubkey_name:ident, $value:literal) => {
|
||||
#[doc = concat!("Base58 Program ID declared as `", stringify!($string_name), "`.")]
|
||||
pub const $string_name: &str = $value;
|
||||
#[doc = concat!("Typed Program ID corresponding to `", stringify!($string_name), "`.")]
|
||||
pub const $pubkey_name: $crate::Pubkey = $crate::Pubkey::from_str_const($string_name);
|
||||
};
|
||||
}
|
||||
|
||||
crate::declare_program_id!(PRGID_SOLANA_ADDRESS_LOOKUP_TABLE, PRGIDPK_SOLANA_ADDRESS_LOOKUP_TABLE, "AddressLookupTab1e1111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_LOADER_BPF_V1, PRGIDPK_SOLANA_LOADER_BPF_V1, "BPFLoader1111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_LOADER_BPF_V2, PRGIDPK_SOLANA_LOADER_BPF_V2, "BPFLoader2111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_LOADER_BPF_UPGRADEABLE, PRGIDPK_SOLANA_LOADER_BPF_UPGRADEABLE, "BPFLoaderUpgradeab1e11111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_COMPUTE_BUDGET, PRGIDPK_SOLANA_COMPUTE_BUDGET, "ComputeBudget111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_CONFIG, PRGIDPK_SOLANA_CONFIG, "Config1111111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_PRECOMPILE_ED25519, PRGIDPK_SOLANA_PRECOMPILE_ED25519, "Ed25519SigVerify111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_FEATURE, PRGIDPK_SOLANA_FEATURE, "Feature111111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_LOADER_V4, PRGIDPK_SOLANA_LOADER_V4, "LoaderV411111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_LOADER_NATIVE, PRGIDPK_SOLANA_LOADER_NATIVE, "NativeLoader1111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_PRECOMPILE_SECP256K1, PRGIDPK_SOLANA_PRECOMPILE_SECP256K1, "KeccakSecp256k11111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_PRECOMPILE_SECP256R1, PRGIDPK_SOLANA_PRECOMPILE_SECP256R1, "Secp256r1SigVerify1111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_SLASHING, PRGIDPK_SOLANA_SLASHING, "S1ashing11111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_STAKE, PRGIDPK_SOLANA_STAKE, "Stake11111111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_SYSTEM, PRGIDPK_SOLANA_SYSTEM, "11111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_VOTE, PRGIDPK_SOLANA_VOTE, "Vote111111111111111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_ZK_ELGAMAL_PROOF, PRGIDPK_SOLANA_ZK_ELGAMAL_PROOF, "ZkE1Gama1Proof11111111111111111111111111111");
|
||||
crate::declare_program_id!(PRGID_SOLANA_ZK_TOKEN_PROOF, PRGIDPK_SOLANA_ZK_TOKEN_PROOF, "ZkTokenProof1111111111111111111111111111111");
|
||||
|
||||
/// Technical classification of a registered Program ID.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ProgramIdKind {
|
||||
/// A regular executable program belonging to the registered protocol surface.
|
||||
Program,
|
||||
/// A Solana program loader.
|
||||
Loader,
|
||||
/// A runtime precompile exposed through a Program ID.
|
||||
Precompile,
|
||||
/// An enshrined on-chain program deployed as part of the Solana protocol.
|
||||
EnshrinedProgram,
|
||||
}
|
||||
|
||||
/// Immutable descriptor for one KSP-owned Program ID.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ProgramIdEntry {
|
||||
code: &'static str,
|
||||
name: &'static str,
|
||||
program_id: &'static str,
|
||||
pubkey: crate::Pubkey,
|
||||
domain: &'static str,
|
||||
family: &'static str,
|
||||
protocol: &'static str,
|
||||
subfamily: std::option::Option<&'static str>,
|
||||
program_version: std::option::Option<&'static str>,
|
||||
kind: crate::ProgramIdKind,
|
||||
}
|
||||
|
||||
impl ProgramIdEntry {
|
||||
const fn new(code: &'static str, name: &'static str, program_id: &'static str, pubkey: crate::Pubkey) -> Self {
|
||||
return Self {
|
||||
code,
|
||||
name,
|
||||
program_id,
|
||||
pubkey,
|
||||
domain: "",
|
||||
family: "",
|
||||
protocol: "",
|
||||
subfamily: std::option::Option::None,
|
||||
program_version: std::option::Option::None,
|
||||
kind: crate::ProgramIdKind::Program,
|
||||
};
|
||||
}
|
||||
|
||||
const fn with_taxonomy(
|
||||
mut self,
|
||||
domain: &'static str,
|
||||
family: &'static str,
|
||||
protocol: &'static str,
|
||||
subfamily: std::option::Option<&'static str>,
|
||||
program_version: std::option::Option<&'static str>,
|
||||
kind: crate::ProgramIdKind,
|
||||
) -> Self {
|
||||
self.domain = domain;
|
||||
self.family = family;
|
||||
self.protocol = protocol;
|
||||
self.subfamily = subfamily;
|
||||
self.program_version = program_version;
|
||||
self.kind = kind;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the stable KSP machine-readable code of this entry.
|
||||
#[must_use]
|
||||
pub const fn code(&self) -> &'static str {
|
||||
return self.code;
|
||||
}
|
||||
|
||||
/// Returns the human-readable program name.
|
||||
#[must_use]
|
||||
pub const fn name(&self) -> &'static str {
|
||||
return self.name;
|
||||
}
|
||||
|
||||
/// Returns the canonical Base58 Program ID.
|
||||
#[must_use]
|
||||
pub const fn program_id(&self) -> &'static str {
|
||||
return self.program_id;
|
||||
}
|
||||
|
||||
/// Returns the typed Solana Program ID.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> crate::Pubkey {
|
||||
return self.pubkey;
|
||||
}
|
||||
|
||||
/// Returns the broad functional domain.
|
||||
#[must_use]
|
||||
pub const fn domain(&self) -> &'static str {
|
||||
return self.domain;
|
||||
}
|
||||
|
||||
/// Returns the functional family within the domain.
|
||||
#[must_use]
|
||||
pub const fn family(&self) -> &'static str {
|
||||
return self.family;
|
||||
}
|
||||
|
||||
/// Returns the owning protocol or project identifier.
|
||||
#[must_use]
|
||||
pub const fn protocol(&self) -> &'static str {
|
||||
return self.protocol;
|
||||
}
|
||||
|
||||
/// Returns the optional architectural branch or product subfamily.
|
||||
#[must_use]
|
||||
pub const fn subfamily(&self) -> std::option::Option<&'static str> {
|
||||
return self.subfamily;
|
||||
}
|
||||
|
||||
/// Returns the optional public generation of this program lineage.
|
||||
#[must_use]
|
||||
pub const fn program_version(&self) -> std::option::Option<&'static str> {
|
||||
return self.program_version;
|
||||
}
|
||||
|
||||
/// Returns the technical Program ID classification.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> crate::ProgramIdKind {
|
||||
return self.kind;
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed filter used to select Program IDs from the canonical KSP registry.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ProgramIdFilter<'a> {
|
||||
domain: std::option::Option<&'a str>,
|
||||
family: std::option::Option<&'a str>,
|
||||
protocol: std::option::Option<&'a str>,
|
||||
subfamily: std::option::Option<&'a str>,
|
||||
program_version: std::option::Option<&'a str>,
|
||||
kind: std::option::Option<crate::ProgramIdKind>,
|
||||
}
|
||||
|
||||
impl<'a> ProgramIdFilter<'a> {
|
||||
/// Creates an empty filter matching every registered Program ID.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
return Self {
|
||||
domain: std::option::Option::None,
|
||||
family: std::option::Option::None,
|
||||
protocol: std::option::Option::None,
|
||||
subfamily: std::option::Option::None,
|
||||
program_version: std::option::Option::None,
|
||||
kind: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Restricts the filter to one functional domain.
|
||||
#[must_use]
|
||||
pub const fn with_domain(mut self, domain: &'a str) -> Self {
|
||||
self.domain = std::option::Option::Some(domain);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Restricts the filter to one functional family.
|
||||
#[must_use]
|
||||
pub const fn with_family(mut self, family: &'a str) -> Self {
|
||||
self.family = std::option::Option::Some(family);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Restricts the filter to one protocol or project.
|
||||
#[must_use]
|
||||
pub const fn with_protocol(mut self, protocol: &'a str) -> Self {
|
||||
self.protocol = std::option::Option::Some(protocol);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Restricts the filter to one architectural subfamily.
|
||||
#[must_use]
|
||||
pub const fn with_subfamily(mut self, subfamily: &'a str) -> Self {
|
||||
self.subfamily = std::option::Option::Some(subfamily);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Restricts the filter to one public program generation.
|
||||
#[must_use]
|
||||
pub const fn with_program_version(mut self, program_version: &'a str) -> Self {
|
||||
self.program_version = std::option::Option::Some(program_version);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Restricts the filter to one technical Program ID kind.
|
||||
#[must_use]
|
||||
pub const fn with_kind(mut self, kind: crate::ProgramIdKind) -> Self {
|
||||
self.kind = std::option::Option::Some(kind);
|
||||
return self;
|
||||
}
|
||||
|
||||
fn matches(&self, entry: &crate::ProgramIdEntry) -> bool {
|
||||
if let std::option::Option::Some(domain) = self.domain
|
||||
&& entry.domain() != domain
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let std::option::Option::Some(family) = self.family
|
||||
&& entry.family() != family
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let std::option::Option::Some(protocol) = self.protocol
|
||||
&& entry.protocol() != protocol
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let std::option::Option::Some(subfamily) = self.subfamily
|
||||
&& entry.subfamily() != std::option::Option::Some(subfamily)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let std::option::Option::Some(program_version) = self.program_version
|
||||
&& entry.program_version() != std::option::Option::Some(program_version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let std::option::Option::Some(kind) = self.kind
|
||||
&& entry.kind() != kind
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.address_lookup_table",
|
||||
"Address Lookup Table Program",
|
||||
crate::PRGID_SOLANA_ADDRESS_LOOKUP_TABLE,
|
||||
crate::PRGIDPK_SOLANA_ADDRESS_LOOKUP_TABLE,
|
||||
)
|
||||
.with_taxonomy(DOMAIN_SOLANA, FAMILY_RUNTIME, PROTOCOL_SOLANA, std::option::Option::None, std::option::Option::None, crate::ProgramIdKind::Program),
|
||||
crate::ProgramIdEntry::new("solana.loader.bpf.v1", "Deprecated BPF Loader", crate::PRGID_SOLANA_LOADER_BPF_V1, crate::PRGIDPK_SOLANA_LOADER_BPF_V1)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_LOADER,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("bpf"),
|
||||
std::option::Option::Some("v1"),
|
||||
crate::ProgramIdKind::Loader,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.loader.bpf.v2", "BPF Loader v2", crate::PRGID_SOLANA_LOADER_BPF_V2, crate::PRGIDPK_SOLANA_LOADER_BPF_V2).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_LOADER,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("bpf"),
|
||||
std::option::Option::Some("v2"),
|
||||
crate::ProgramIdKind::Loader,
|
||||
),
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.loader.bpf_upgradeable",
|
||||
"Upgradeable BPF Loader",
|
||||
crate::PRGID_SOLANA_LOADER_BPF_UPGRADEABLE,
|
||||
crate::PRGIDPK_SOLANA_LOADER_BPF_UPGRADEABLE,
|
||||
)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_LOADER,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("bpf"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Loader,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.compute_budget", "Compute Budget Program", crate::PRGID_SOLANA_COMPUTE_BUDGET, crate::PRGIDPK_SOLANA_COMPUTE_BUDGET)
|
||||
.with_taxonomy(DOMAIN_SOLANA, FAMILY_RUNTIME, PROTOCOL_SOLANA, std::option::Option::None, std::option::Option::None, crate::ProgramIdKind::Program),
|
||||
crate::ProgramIdEntry::new("solana.config", "Config Program", crate::PRGID_SOLANA_CONFIG, crate::PRGIDPK_SOLANA_CONFIG).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_RUNTIME,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.precompile.ed25519",
|
||||
"Ed25519 Signature Verification Precompile",
|
||||
crate::PRGID_SOLANA_PRECOMPILE_ED25519,
|
||||
crate::PRGIDPK_SOLANA_PRECOMPILE_ED25519,
|
||||
)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_PRECOMPILE,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("ed25519"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Precompile,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.feature", "Feature Program", crate::PRGID_SOLANA_FEATURE, crate::PRGIDPK_SOLANA_FEATURE).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_RUNTIME,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.loader.v4", "Loader v4", crate::PRGID_SOLANA_LOADER_V4, crate::PRGIDPK_SOLANA_LOADER_V4).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_LOADER,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some("v4"),
|
||||
crate::ProgramIdKind::Loader,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.loader.native", "Native Loader", crate::PRGID_SOLANA_LOADER_NATIVE, crate::PRGIDPK_SOLANA_LOADER_NATIVE).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_LOADER,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("native"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Loader,
|
||||
),
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.precompile.secp256k1",
|
||||
"Secp256k1 Signature Verification Precompile",
|
||||
crate::PRGID_SOLANA_PRECOMPILE_SECP256K1,
|
||||
crate::PRGIDPK_SOLANA_PRECOMPILE_SECP256K1,
|
||||
)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_PRECOMPILE,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("secp256k1"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Precompile,
|
||||
),
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.precompile.secp256r1",
|
||||
"Secp256r1 Signature Verification Precompile",
|
||||
crate::PRGID_SOLANA_PRECOMPILE_SECP256R1,
|
||||
crate::PRGIDPK_SOLANA_PRECOMPILE_SECP256R1,
|
||||
)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_PRECOMPILE,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("secp256r1"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Precompile,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.slashing", "Slashing Program", crate::PRGID_SOLANA_SLASHING, crate::PRGIDPK_SOLANA_SLASHING).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_CONSENSUS,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::EnshrinedProgram,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.stake", "Stake Program", crate::PRGID_SOLANA_STAKE, crate::PRGIDPK_SOLANA_STAKE).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_CONSENSUS,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.system", "System Program", crate::PRGID_SOLANA_SYSTEM, crate::PRGIDPK_SOLANA_SYSTEM).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_RUNTIME,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.vote", "Vote Program", crate::PRGID_SOLANA_VOTE, crate::PRGIDPK_SOLANA_VOTE).with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_CONSENSUS,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.proof.zk_elgamal",
|
||||
"ZK ElGamal Proof Program",
|
||||
crate::PRGID_SOLANA_ZK_ELGAMAL_PROOF,
|
||||
crate::PRGIDPK_SOLANA_ZK_ELGAMAL_PROOF,
|
||||
)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_PROOF,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("zk_elgamal"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
crate::ProgramIdEntry::new("solana.proof.zk_token", "ZK Token Proof Program", crate::PRGID_SOLANA_ZK_TOKEN_PROOF, crate::PRGIDPK_SOLANA_ZK_TOKEN_PROOF)
|
||||
.with_taxonomy(
|
||||
DOMAIN_SOLANA,
|
||||
FAMILY_PROOF,
|
||||
PROTOCOL_SOLANA,
|
||||
std::option::Option::Some("zk_token"),
|
||||
std::option::Option::None,
|
||||
crate::ProgramIdKind::Program,
|
||||
),
|
||||
];
|
||||
|
||||
/// Returns the canonical KSP Program ID registry.
|
||||
#[must_use]
|
||||
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
|
||||
return PROGRAM_ID_ENTRIES;
|
||||
}
|
||||
|
||||
/// Returns a lazy view of Program IDs matching all configured filter axes.
|
||||
pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
|
||||
return PROGRAM_ID_ENTRIES.iter().filter(move |entry| {
|
||||
return filter.matches(entry);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns all Solana core/native Program IDs, including loaders, precompiles and enshrined programs.
|
||||
pub fn native_program_ids() -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> {
|
||||
return crate::program_ids(crate::ProgramIdFilter::new().with_domain(DOMAIN_SOLANA).with_protocol(PROTOCOL_SOLANA));
|
||||
}
|
||||
|
||||
/// Returns a lazy view of Program IDs belonging to one functional domain.
|
||||
pub fn program_ids_by_domain<'a>(domain: &'a str) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
|
||||
return crate::program_ids(crate::ProgramIdFilter::new().with_domain(domain));
|
||||
}
|
||||
|
||||
/// Returns a lazy view of Program IDs belonging to one functional family.
|
||||
pub fn program_ids_by_family<'a>(family: &'a str) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
|
||||
return crate::program_ids(crate::ProgramIdFilter::new().with_family(family));
|
||||
}
|
||||
|
||||
/// Returns a lazy view of Program IDs belonging to one protocol or project.
|
||||
pub fn program_ids_by_protocol<'a>(protocol: &'a str) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
|
||||
return crate::program_ids(crate::ProgramIdFilter::new().with_protocol(protocol));
|
||||
}
|
||||
|
||||
/// Finds one registered Program ID by its canonical Base58 representation.
|
||||
#[must_use]
|
||||
pub fn find_program_id(program_id: &str) -> std::option::Option<&'static crate::ProgramIdEntry> {
|
||||
return PROGRAM_ID_ENTRIES.iter().find(|entry| {
|
||||
return entry.program_id() == program_id;
|
||||
});
|
||||
}
|
||||
|
||||
/// Finds one registered Program ID by its typed Solana representation.
|
||||
#[must_use]
|
||||
pub fn find_program_pubkey(program_id: &crate::Pubkey) -> std::option::Option<&'static crate::ProgramIdEntry> {
|
||||
return PROGRAM_ID_ENTRIES.iter().find(|entry| {
|
||||
return entry.pubkey() == *program_id;
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/program_ids.rs"]
|
||||
mod tests;
|
||||
67
crates/ksp-core-lib/tests/public_api.rs
Normal file
67
crates/ksp-core-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// file: crates/ksp-core-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
|
||||
//! Integration tests for the public `ksp-core-lib` contracts.
|
||||
|
||||
ksp_core_lib::declare_program_id!(TEST_PRGID_SYSTEM, TEST_PRGIDPK_SYSTEM, "11111111111111111111111111111111");
|
||||
|
||||
const TEST_ERROR_CODE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("consumer", "failed");
|
||||
|
||||
fn public_result() -> ksp_core_lib::Result<()> {
|
||||
let error = ksp_core_lib::Error::new(TEST_ERROR_CODE, "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(), TEST_ERROR_CODE);
|
||||
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;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_id_contract_is_consumable_from_crate_root() {
|
||||
assert_eq!(TEST_PRGID_SYSTEM, ksp_core_lib::PRGID_SOLANA_SYSTEM);
|
||||
assert_eq!(TEST_PRGIDPK_SYSTEM, ksp_core_lib::PRGIDPK_SOLANA_SYSTEM);
|
||||
assert_eq!(ksp_core_lib::entries().len(), 18);
|
||||
assert_eq!(ksp_core_lib::native_program_ids().count(), 18);
|
||||
let system = ksp_core_lib::find_program_id(ksp_core_lib::PRGID_SOLANA_SYSTEM);
|
||||
let system = match system {
|
||||
std::option::Option::Some(entry) => entry,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(system.code(), "solana.system");
|
||||
assert_eq!(system.domain(), "solana");
|
||||
assert_eq!(system.family(), "runtime");
|
||||
assert_eq!(system.protocol(), "solana");
|
||||
assert_eq!(system.pubkey(), ksp_core_lib::PRGIDPK_SOLANA_SYSTEM);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_id_registry_supports_public_taxonomy_filters() {
|
||||
let loader_count = ksp_core_lib::program_ids_by_family("loader").count();
|
||||
let precompile_count = ksp_core_lib::program_ids_by_family("precompile").count();
|
||||
let bpf_v2_count = ksp_core_lib::program_ids(
|
||||
ksp_core_lib::ProgramIdFilter::new()
|
||||
.with_domain("solana")
|
||||
.with_family("loader")
|
||||
.with_protocol("solana")
|
||||
.with_subfamily("bpf")
|
||||
.with_program_version("v2")
|
||||
.with_kind(ksp_core_lib::ProgramIdKind::Loader),
|
||||
)
|
||||
.count();
|
||||
assert_eq!(loader_count, 5);
|
||||
assert_eq!(precompile_count, 3);
|
||||
assert_eq!(bpf_v2_count, 1);
|
||||
return;
|
||||
}
|
||||
78
crates/ksp-core-lib/unit_tests/error.rs
Normal file
78
crates/ksp-core-lib/unit_tests/error.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
// file: crates/ksp-core-lib/unit_tests/error.rs
|
||||
// version: 2
|
||||
|
||||
#[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<T>(_: std::marker::PhantomData<T>)
|
||||
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(std::marker::PhantomData::<crate::Error>);
|
||||
return;
|
||||
}
|
||||
89
crates/ksp-core-lib/unit_tests/program_ids.rs
Normal file
89
crates/ksp-core-lib/unit_tests/program_ids.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
// file: crates/ksp-core-lib/unit_tests/program_ids.rs
|
||||
// version: 1
|
||||
|
||||
fn assert_program_id_types(program_id: &'static str, pubkey: crate::Pubkey) {
|
||||
assert_eq!(pubkey, crate::Pubkey::from_str_const(program_id));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_program_ids_have_matching_text_and_pubkey_forms() {
|
||||
assert_program_id_types(crate::PRGID_SOLANA_SYSTEM, crate::PRGIDPK_SOLANA_SYSTEM);
|
||||
assert_program_id_types(crate::PRGID_SOLANA_STAKE, crate::PRGIDPK_SOLANA_STAKE);
|
||||
assert_program_id_types(crate::PRGID_SOLANA_VOTE, crate::PRGIDPK_SOLANA_VOTE);
|
||||
assert_program_id_types(crate::PRGID_SOLANA_SLASHING, crate::PRGIDPK_SOLANA_SLASHING);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_contains_the_eighteen_core_program_ids() {
|
||||
assert_eq!(crate::entries().len(), 18);
|
||||
assert_eq!(crate::native_program_ids().count(), 18);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_codes_program_ids_and_pubkeys_are_unique() {
|
||||
for left_index in 0..crate::entries().len() {
|
||||
for right_index in (left_index + 1)..crate::entries().len() {
|
||||
let left = &crate::entries()[left_index];
|
||||
let right = &crate::entries()[right_index];
|
||||
assert_ne!(left.code(), right.code());
|
||||
assert_ne!(left.program_id(), right.program_id());
|
||||
assert_ne!(left.pubkey(), right.pubkey());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_pubkeys_match_their_owned_base58_values() {
|
||||
for entry in crate::entries() {
|
||||
assert_eq!(entry.pubkey(), crate::Pubkey::from_str_const(entry.program_id()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_combine_domain_family_protocol_subfamily_version_and_kind() {
|
||||
let bpf_v2 = crate::program_ids(
|
||||
crate::ProgramIdFilter::new()
|
||||
.with_domain("solana")
|
||||
.with_family("loader")
|
||||
.with_protocol("solana")
|
||||
.with_subfamily("bpf")
|
||||
.with_program_version("v2")
|
||||
.with_kind(crate::ProgramIdKind::Loader),
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(bpf_v2.len(), 1);
|
||||
assert_eq!(bpf_v2[0].program_id(), crate::PRGID_SOLANA_LOADER_BPF_V2);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_views_cover_expected_core_groups() {
|
||||
assert_eq!(crate::program_ids_by_family("runtime").count(), 5);
|
||||
assert_eq!(crate::program_ids_by_family("consensus").count(), 3);
|
||||
assert_eq!(crate::program_ids_by_family("loader").count(), 5);
|
||||
assert_eq!(crate::program_ids_by_family("precompile").count(), 3);
|
||||
assert_eq!(crate::program_ids_by_family("proof").count(), 2);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_lookup_supports_text_and_typed_program_ids() {
|
||||
let by_text = crate::find_program_id(crate::PRGID_SOLANA_SLASHING);
|
||||
let by_pubkey = crate::find_program_pubkey(&crate::PRGIDPK_SOLANA_SLASHING);
|
||||
assert_eq!(by_text.map(crate::ProgramIdEntry::code), std::option::Option::Some("solana.slashing"));
|
||||
assert_eq!(by_pubkey.map(crate::ProgramIdEntry::code), std::option::Option::Some("solana.slashing"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_program_well_known_accounts_are_absent() {
|
||||
assert!(crate::find_program_id("1nc1nerator11111111111111111111111111111111").is_none());
|
||||
assert!(crate::find_program_id("StakeConfig11111111111111111111111111111111").is_none());
|
||||
assert!(crate::find_program_id("SysvarC1ock11111111111111111111111111111111").is_none());
|
||||
return;
|
||||
}
|
||||
20
crates/ksp-logging-lib/Cargo.toml
Normal file
20
crates/ksp-logging-lib/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
# file: crates/ksp-logging-lib/Cargo.toml
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "ksp-logging-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
35
crates/ksp-logging-lib/README.md
Normal file
35
crates/ksp-logging-lib/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# ksp-logging-lib
|
||||
|
||||
`ksp-logging-lib` est la façade commune de logging/tracing runtime de Khadhroony Solana Project.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
La crate possède :
|
||||
|
||||
- les cinq niveaux KSP `error`, `warn`, `info`, `debug` et `trace` ;
|
||||
- les macros d'événements et de spans qui préservent le callsite du consommateur ;
|
||||
- `LoggingSettings` et les settings console/fichier indépendants de Config ;
|
||||
- l'installation unique du subscriber global ;
|
||||
- le hot reload via `reinitialize` sans second subscriber global ;
|
||||
- le takeover des logs : les targets externes sont silencieux par défaut ;
|
||||
- les sorties console et fichier non bloquantes ;
|
||||
- les `WorkerGuard`, compteurs de lignes abandonnées, rotation fichier et stripping ANSI ;
|
||||
- l'instrumentation de scopes synchrones et de `Future` async.
|
||||
|
||||
L'API async de production reste indépendante de tout executor. Tokio est utilisé uniquement comme `dev-dependency` afin de valider `instrument(...)` sur un executor réel en mode current-thread et multi-thread ; il ne fait pas partie des dépendances runtime de la crate.
|
||||
|
||||
## Frontières
|
||||
|
||||
Une crate KSP comportementale qui journalise son activité dépend de `ksp-logging-lib` et n'utilise pas directement `tracing`, `tracing-subscriber` ou `tracing-appender`.
|
||||
|
||||
Les événements utiles issus d'une dépendance externe ne sont pas renommés : la crate KSP propriétaire de l'opération réémet explicitement l'information utile sous son propre target KSP.
|
||||
|
||||
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config pourra construire un `LoggingSettings` puis appeler `initialize` ou `reinitialize`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — utilisation concrète de la façade et du runtime ;
|
||||
- [`TODO.md`](TODO.md) — capacités explicitement différées ou points restant à fermer.
|
||||
22
crates/ksp-logging-lib/TODO.md
Normal file
22
crates/ksp-logging-lib/TODO.md
Normal file
@@ -0,0 +1,22 @@
|
||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# TODO ksp-logging-lib
|
||||
|
||||
## À fermer avant la stable 0.1.2
|
||||
|
||||
- exécuter les validations Cargo complètes de `pre.006`, y compris les tests Tokio current-thread/multi-thread ;
|
||||
- vérifier que `cargo tree -p ksp-logging-lib -e normal` ne contient pas Tokio et que Tokio apparaît uniquement dans le graphe dev attendu ;
|
||||
- refaire l'audit final du graphe/features et de l'ownership de la stack tracing ;
|
||||
- après validation de la prerelease finale, préparer `rel.001`, publier `workspace.package.version = "0.1.2"` et taguer `v0.1.2` conformément aux règles de release.
|
||||
|
||||
## Capacités différées
|
||||
|
||||
Ces éléments ne font pas partie du contrat `0.1.2` et ne doivent être ajoutés qu'après besoin concret :
|
||||
|
||||
- plusieurs routes fichier indépendantes ;
|
||||
- rotation par taille, rétention/compression et symlink `latest` ;
|
||||
- formats JSON ou autres formats structurés alternatifs ;
|
||||
- OpenTelemetry/export réseau ;
|
||||
- watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ;
|
||||
- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence.
|
||||
130
crates/ksp-logging-lib/USAGE.md
Normal file
130
crates/ksp-logging-lib/USAGE.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Utilisation de ksp-logging-lib
|
||||
|
||||
## Target d'une crate consommatrice
|
||||
|
||||
Chaque crate KSP comportementale fournit explicitement son target, égal au nom Cargo de la crate :
|
||||
|
||||
```rust
|
||||
const LOGGING_TARGET: &str = "ksp-store-lib";
|
||||
|
||||
ksp_logging_lib::trace!(
|
||||
target: LOGGING_TARGET,
|
||||
domain = "store",
|
||||
component = "postgres",
|
||||
operation = "load_transactions",
|
||||
"executing store operation"
|
||||
);
|
||||
```
|
||||
|
||||
Les champs `domain`, `component`, `operation` et autres champs structurés sont ajoutés par le caller lorsqu'ils sont utiles ; ils ne remplacent pas le target propriétaire.
|
||||
|
||||
## Initialisation
|
||||
|
||||
`initialize` installe le subscriber global KSP une seule fois et retourne le `LoggingGuard` qui doit rester vivant pendant la durée du runtime :
|
||||
|
||||
```rust
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(
|
||||
"logs",
|
||||
"worker.log",
|
||||
ksp_logging_lib::FileRotation::Daily,
|
||||
)),
|
||||
);
|
||||
|
||||
let initialize_result = ksp_logging_lib::initialize(&settings);
|
||||
let mut logging_guard = match initialize_result {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Une configuration sans console ni fichier est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
|
||||
|
||||
## Hot reload
|
||||
|
||||
Une nouvelle configuration peut être appliquée sans redémarrer le processus ou le worker :
|
||||
|
||||
```rust
|
||||
let debug_settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(
|
||||
"ksp-store-lib",
|
||||
ksp_logging_lib::LogFilterLevel::Debug,
|
||||
));
|
||||
|
||||
let reload_result = ksp_logging_lib::reinitialize(&mut logging_guard, &debug_settings);
|
||||
if let std::result::Result::Err(error) = reload_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
```
|
||||
|
||||
La nouvelle configuration est préparée avant la bascule. Si sa validation ou la création d'un nouveau sink échoue, l'ancienne configuration reste active.
|
||||
|
||||
## Spans synchrones
|
||||
|
||||
```rust
|
||||
let span = ksp_logging_lib::trace_span!(
|
||||
target: LOGGING_TARGET,
|
||||
"materialize_transaction",
|
||||
domain = "store"
|
||||
);
|
||||
|
||||
let output = span.in_scope(|| {
|
||||
return materialize_transaction();
|
||||
});
|
||||
```
|
||||
|
||||
Avec `SpanEvents::NewAndClose`, le formatter produit les événements de création/fermeture et les temps `busy` / `idle` à la fermeture.
|
||||
|
||||
## Spans async
|
||||
|
||||
Une `Future` doit être instrumentée avec `ksp_logging_lib::instrument` ; un guard d'entrée de span ne doit pas être conservé à travers `.await` :
|
||||
|
||||
```rust
|
||||
let span = ksp_logging_lib::trace_span!(
|
||||
target: LOGGING_TARGET,
|
||||
"fetch_account",
|
||||
domain = "transport"
|
||||
);
|
||||
|
||||
let output = ksp_logging_lib::instrument(span, fetch_account()).await;
|
||||
```
|
||||
|
||||
La future instrumentée entre/sort du span pendant ses polls et lors de son `Drop`, conformément au contrat de la primitive `tracing` sous-jacente.
|
||||
|
||||
## Lignes abandonnées
|
||||
|
||||
Les sorties utilisent des queues lossy afin de ne pas appliquer de backpressure au hot path. Les pertes restent observables :
|
||||
|
||||
```rust
|
||||
let dropped = logging_guard.dropped_lines();
|
||||
ksp_logging_lib::warn!(
|
||||
target: LOGGING_TARGET,
|
||||
console = dropped.console(),
|
||||
file = dropped.file(),
|
||||
total = dropped.total(),
|
||||
"logging queues dropped lines"
|
||||
);
|
||||
```
|
||||
|
||||
## Instrumentation async et executor
|
||||
|
||||
`instrument(span, future)` accepte une `Future` standard et ne dépend d'aucun executor particulier :
|
||||
|
||||
```rust
|
||||
let span = ksp_logging_lib::trace_span!(target: LOGGING_TARGET, "load_transactions");
|
||||
let result = ksp_logging_lib::instrument(span, async_operation()).await;
|
||||
```
|
||||
|
||||
La crate ne requiert pas Tokio en production. Tokio n'est présent qu'en `dev-dependency` pour valider la surface sur un executor réel, y compris après plusieurs suspensions et sur un runtime multi-thread. Un consumer peut donc utiliser l'executor adapté à son propre contexte sans que Logging lui en impose un.
|
||||
|
||||
11
crates/ksp-logging-lib/src/error.rs
Normal file
11
crates/ksp-logging-lib/src/error.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// file: crates/ksp-logging-lib/src/error.rs
|
||||
// version: 3
|
||||
|
||||
/// Error code used when runtime logging settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
|
||||
/// Error code used when a global logging subscriber is already installed.
|
||||
pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized");
|
||||
/// Error code used when a hot reload cannot replace the active runtime layers.
|
||||
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");
|
||||
/// Error code used when the rolling file output cannot be initialized.
|
||||
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
|
||||
60
crates/ksp-logging-lib/src/lib.rs
Normal file
60
crates/ksp-logging-lib/src/lib.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: crates/ksp-logging-lib/src/lib.rs
|
||||
// version: 4
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned logging and tracing facade.
|
||||
//!
|
||||
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
|
||||
//! `tracing` stack. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
|
||||
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
|
||||
|
||||
mod error;
|
||||
mod macros;
|
||||
mod runtime;
|
||||
mod settings;
|
||||
mod span;
|
||||
mod writer;
|
||||
|
||||
/// Error code used when a global logging subscriber is already installed.
|
||||
pub use self::error::ERROR_CODE_ALREADY_INITIALIZED;
|
||||
/// Error code used when the rolling file output cannot be initialized.
|
||||
pub use self::error::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||
/// Error code used when runtime logging settings are invalid.
|
||||
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
|
||||
/// Error code used when a hot reload cannot replace the active runtime layers.
|
||||
pub use self::error::ERROR_CODE_RELOAD_FAILED;
|
||||
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||
pub use self::runtime::DroppedLines;
|
||||
/// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber.
|
||||
pub use self::runtime::LoggingGuard;
|
||||
/// Installs the global KSP tracing subscriber.
|
||||
pub use self::runtime::initialize;
|
||||
/// Replaces the active KSP logging settings without reinstalling the global subscriber.
|
||||
pub use self::runtime::reinitialize;
|
||||
/// Console stream selected for human-readable logs.
|
||||
pub use self::settings::ConsoleOutput;
|
||||
/// Runtime settings for the optional console output.
|
||||
pub use self::settings::ConsoleSettings;
|
||||
/// Rotation cadence for the optional file output.
|
||||
pub use self::settings::FileRotation;
|
||||
/// Runtime settings for the optional file output.
|
||||
pub use self::settings::FileSettings;
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
pub use self::settings::LogFilterLevel;
|
||||
/// Complete runtime settings consumed by Logging initialization and reload.
|
||||
pub use self::settings::LoggingSettings;
|
||||
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||
pub use self::settings::SpanEvents;
|
||||
/// Per-target filter override owned by Logging.
|
||||
pub use self::settings::TargetFilter;
|
||||
/// KSP-owned handle to a tracing span.
|
||||
pub use self::span::Span;
|
||||
/// Instruments an asynchronous future with a KSP span.
|
||||
pub use self::span::instrument;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Internal macro bridge. KSP consumers must not use this reexport directly.
|
||||
pub extern crate tracing as __private_tracing;
|
||||
97
crates/ksp-logging-lib/src/macros.rs
Normal file
97
crates/ksp-logging-lib/src/macros.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
// file: crates/ksp-logging-lib/src/macros.rs
|
||||
// version: 1
|
||||
|
||||
/// Emits a KSP error event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::error!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Emits a KSP warning event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::warn!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Emits a KSP informational event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::info!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Emits a KSP debug event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::debug!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Emits a KSP trace event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! trace {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::trace!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a KSP error span with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! error_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a KSP warning span with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! warn_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a KSP informational span with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! info_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a KSP debug span with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! debug_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a KSP trace span with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! trace_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
288
crates/ksp-logging-lib/src/runtime.rs
Normal file
288
crates/ksp-logging-lib/src/runtime.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 8
|
||||
|
||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
|
||||
/// Cumulative number of log lines dropped by non-blocking KSP outputs.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DroppedLines {
|
||||
console: usize,
|
||||
file: usize,
|
||||
}
|
||||
|
||||
impl DroppedLines {
|
||||
/// Returns an empty dropped-line snapshot.
|
||||
#[must_use]
|
||||
pub const fn zero() -> Self {
|
||||
return Self { console: 0, file: 0 };
|
||||
}
|
||||
|
||||
/// Returns the number of console lines dropped since Logging initialization.
|
||||
#[must_use]
|
||||
pub const fn console(&self) -> usize {
|
||||
return self.console;
|
||||
}
|
||||
|
||||
/// Returns the number of file lines dropped since Logging initialization.
|
||||
#[must_use]
|
||||
pub const fn file(&self) -> usize {
|
||||
return self.file;
|
||||
}
|
||||
|
||||
/// Returns the total number of dropped lines across console and file outputs.
|
||||
#[must_use]
|
||||
pub const fn total(&self) -> usize {
|
||||
return self.console.saturating_add(self.file);
|
||||
}
|
||||
|
||||
const fn saturating_add(self, other: Self) -> Self {
|
||||
return Self { console: self.console.saturating_add(other.console), file: self.file.saturating_add(other.file) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber.
|
||||
pub struct LoggingGuard {
|
||||
reload_handle: RuntimeReloadHandle,
|
||||
settings: crate::LoggingSettings,
|
||||
outputs: RuntimeOutputs,
|
||||
retired_dropped_lines: crate::DroppedLines,
|
||||
}
|
||||
|
||||
impl LoggingGuard {
|
||||
/// Returns the settings currently active in the KSP logging runtime.
|
||||
#[must_use]
|
||||
pub fn settings(&self) -> &crate::LoggingSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Returns cumulative dropped-line counters across active and previously reloaded outputs.
|
||||
#[must_use]
|
||||
pub fn dropped_lines(&self) -> crate::DroppedLines {
|
||||
return self.retired_dropped_lines.saturating_add(self.outputs.dropped_lines());
|
||||
}
|
||||
}
|
||||
|
||||
type BoxedRuntimeLayer = std::boxed::Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + std::marker::Send + std::marker::Sync + 'static>;
|
||||
type RuntimeLayers = std::vec::Vec<BoxedRuntimeLayer>;
|
||||
type RuntimeReloadHandle = tracing_subscriber::reload::Handle<RuntimeLayers, tracing_subscriber::Registry>;
|
||||
|
||||
struct PreparedRuntime {
|
||||
layers: RuntimeLayers,
|
||||
outputs: RuntimeOutputs,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RuntimeOutputs {
|
||||
console: std::option::Option<RuntimeOutput>,
|
||||
file: std::option::Option<RuntimeOutput>,
|
||||
}
|
||||
|
||||
impl RuntimeOutputs {
|
||||
fn dropped_lines(&self) -> crate::DroppedLines {
|
||||
let console = match self.console.as_ref() {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
let file = match self.file.as_ref() {
|
||||
std::option::Option::Some(output) => output.dropped_lines(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
return crate::DroppedLines { console, file };
|
||||
}
|
||||
}
|
||||
|
||||
struct RuntimeOutput {
|
||||
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
|
||||
error_counter: tracing_appender::non_blocking::ErrorCounter,
|
||||
}
|
||||
|
||||
impl RuntimeOutput {
|
||||
fn dropped_lines(&self) -> usize {
|
||||
return self.error_counter.dropped_lines();
|
||||
}
|
||||
}
|
||||
|
||||
struct PreparedOutput {
|
||||
layer: BoxedRuntimeLayer,
|
||||
output: RuntimeOutput,
|
||||
}
|
||||
|
||||
/// Installs the global KSP tracing subscriber.
|
||||
///
|
||||
/// This function may succeed only once for the lifetime of the process. The returned guard owns all non-blocking writer guards and is then used by
|
||||
/// [`crate::reinitialize`] to replace the active KSP logging configuration without installing a second global subscriber.
|
||||
pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
|
||||
let subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||
let install_result = tracing::subscriber::set_global_default(subscriber);
|
||||
return match install_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
|
||||
reload_handle,
|
||||
settings: settings.clone(),
|
||||
outputs,
|
||||
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
|
||||
///
|
||||
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
|
||||
/// configuration remains unchanged. After a successful layer swap, dropped-line counters from the retired outputs are retained cumulatively. Retired
|
||||
/// layers are then dropped before their worker guards so all retired `NonBlocking` senders are released before shutdown asks the workers to drain/flush.
|
||||
pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
|
||||
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<()> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let mut retired_layers = RuntimeLayers::new();
|
||||
let reload_result = guard.reload_handle.modify(|active_layers| {
|
||||
retired_layers = std::mem::replace(active_layers, layers);
|
||||
});
|
||||
return match reload_result {
|
||||
std::result::Result::Ok(()) => {
|
||||
guard.retired_dropped_lines = guard.retired_dropped_lines.saturating_add(guard.outputs.dropped_lines());
|
||||
let retired_outputs = std::mem::replace(&mut guard.outputs, outputs);
|
||||
guard.settings = settings.clone();
|
||||
drop(retired_layers);
|
||||
drop(retired_outputs);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_RELOAD_FAILED, "unable to reload the KSP logging runtime").with_source(error),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedRuntime> {
|
||||
let validation_error = settings.validate().err();
|
||||
if let std::option::Option::Some(error) = validation_error {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if settings.console().is_none() && settings.file().is_none() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||
}
|
||||
let prepared_file = match settings.file() {
|
||||
std::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let prepared_console = settings.console().map(|console| -> PreparedOutput {
|
||||
return build_console_output(console, settings);
|
||||
});
|
||||
let mut output_layers = RuntimeLayers::new();
|
||||
let mut outputs = RuntimeOutputs::default();
|
||||
if let std::option::Option::Some(console) = prepared_console {
|
||||
output_layers.push(console.layer);
|
||||
outputs.console = std::option::Option::Some(console.output);
|
||||
}
|
||||
if let std::option::Option::Some(file) = prepared_file {
|
||||
output_layers.push(file.layer);
|
||||
outputs.file = std::option::Option::Some(file.output);
|
||||
}
|
||||
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
|
||||
let layers = vec![takeover_layer];
|
||||
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
|
||||
}
|
||||
|
||||
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||
return match console.output() {
|
||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings, true),
|
||||
crate::ConsoleOutput::Stderr => build_non_blocking_output(std::io::stderr(), "ksp-logging-console", settings, true),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result<PreparedOutput> {
|
||||
let appender_result = tracing_appender::rolling::RollingFileAppender::builder()
|
||||
.rotation(map_file_rotation(file.rotation()))
|
||||
.filename_prefix(file.file_name_prefix())
|
||||
.build(file.directory());
|
||||
let appender = match appender_result {
|
||||
std::result::Result::Ok(appender) => appender,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED, "unable to initialize the KSP rolling file appender")
|
||||
.with_context("directory", file.directory().display().to_string())
|
||||
.with_context("file_name_prefix", file.file_name_prefix())
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
||||
return std::result::Result::Ok(build_non_blocking_output(stripped_writer, "ksp-logging-file", settings, false));
|
||||
}
|
||||
|
||||
fn build_non_blocking_output<W>(writer: W, thread_name: &str, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> PreparedOutput
|
||||
where
|
||||
W: std::io::Write + std::marker::Send + 'static,
|
||||
{
|
||||
let (non_blocking, worker_guard) = non_blocking_builder(thread_name).finish(writer);
|
||||
let error_counter = non_blocking.error_counter();
|
||||
let layer = build_format_layer(non_blocking, settings, ansi_sanitization);
|
||||
return PreparedOutput { layer, output: RuntimeOutput { _worker_guard: worker_guard, error_counter } };
|
||||
}
|
||||
|
||||
fn non_blocking_builder(thread_name: &str) -> tracing_appender::non_blocking::NonBlockingBuilder {
|
||||
return tracing_appender::non_blocking::NonBlockingBuilder::default().lossy(true).thread_name(thread_name);
|
||||
}
|
||||
|
||||
fn build_format_layer(writer: tracing_appender::non_blocking::NonBlocking, settings: &crate::LoggingSettings, ansi_sanitization: bool) -> BoxedRuntimeLayer {
|
||||
return tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_span_events(map_span_events(settings.span_events()))
|
||||
.boxed();
|
||||
}
|
||||
|
||||
fn build_target_filter(settings: &crate::LoggingSettings) -> tracing_subscriber::filter::Targets {
|
||||
let mut filter = tracing_subscriber::filter::Targets::new()
|
||||
.with_default(tracing_subscriber::filter::LevelFilter::OFF)
|
||||
.with_target("ksp-", map_filter_level(settings.default_filter()));
|
||||
for target_filter in settings.target_filters() {
|
||||
filter = filter.with_target(target_filter.target_prefix(), map_filter_level(target_filter.level()));
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::filter::LevelFilter {
|
||||
return match level {
|
||||
crate::LogFilterLevel::Off => tracing_subscriber::filter::LevelFilter::OFF,
|
||||
crate::LogFilterLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR,
|
||||
crate::LogFilterLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN,
|
||||
crate::LogFilterLevel::Info => tracing_subscriber::filter::LevelFilter::INFO,
|
||||
crate::LogFilterLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
|
||||
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,
|
||||
crate::FileRotation::Hourly => tracing_appender::rolling::Rotation::HOURLY,
|
||||
crate::FileRotation::Daily => tracing_appender::rolling::Rotation::DAILY,
|
||||
};
|
||||
}
|
||||
|
||||
fn map_span_events(span_events: crate::SpanEvents) -> tracing_subscriber::fmt::format::FmtSpan {
|
||||
return match span_events {
|
||||
crate::SpanEvents::Off => tracing_subscriber::fmt::format::FmtSpan::NONE,
|
||||
crate::SpanEvents::NewAndClose => tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
|
||||
crate::SpanEvents::Full => tracing_subscriber::fmt::format::FmtSpan::FULL,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/runtime.rs"]
|
||||
mod tests;
|
||||
233
crates/ksp-logging-lib/src/settings.rs
Normal file
233
crates/ksp-logging-lib/src/settings.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// file: crates/ksp-logging-lib/src/settings.rs
|
||||
// version: 2
|
||||
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum LogFilterLevel {
|
||||
/// Disables matching logging events and spans.
|
||||
Off,
|
||||
/// Enables only error-level events and spans.
|
||||
Error,
|
||||
/// Enables warning and error events and spans.
|
||||
Warn,
|
||||
/// Enables informational, warning and error events and spans.
|
||||
Info,
|
||||
/// Enables debug and less verbose events and spans.
|
||||
Debug,
|
||||
/// Enables all KSP logging events and spans.
|
||||
Trace,
|
||||
}
|
||||
|
||||
/// Per-target filter override owned by Logging.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TargetFilter {
|
||||
target_prefix: std::string::String,
|
||||
level: crate::LogFilterLevel,
|
||||
}
|
||||
|
||||
impl TargetFilter {
|
||||
/// Creates a filter override for a KSP target prefix.
|
||||
#[must_use]
|
||||
pub fn new(target_prefix: impl std::convert::Into<std::string::String>, level: crate::LogFilterLevel) -> Self {
|
||||
return Self { target_prefix: target_prefix.into(), level };
|
||||
}
|
||||
|
||||
/// Returns the configured target prefix.
|
||||
#[must_use]
|
||||
pub fn target_prefix(&self) -> &str {
|
||||
return self.target_prefix.as_str();
|
||||
}
|
||||
|
||||
/// Returns the configured filter level.
|
||||
#[must_use]
|
||||
pub const fn level(&self) -> crate::LogFilterLevel {
|
||||
return self.level;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SpanEvents {
|
||||
/// Does not synthesize span lifecycle events.
|
||||
Off,
|
||||
/// Emits span creation and closure events for timing-oriented diagnostics.
|
||||
NewAndClose,
|
||||
/// Emits all supported span lifecycle events.
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Console stream selected for human-readable logs.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ConsoleOutput {
|
||||
/// Writes console logs to standard output.
|
||||
Stdout,
|
||||
/// Writes console logs to standard error.
|
||||
Stderr,
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional console output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ConsoleSettings {
|
||||
output: crate::ConsoleOutput,
|
||||
}
|
||||
|
||||
impl ConsoleSettings {
|
||||
/// Creates console settings targeting standard output.
|
||||
#[must_use]
|
||||
pub const fn stdout() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stdout };
|
||||
}
|
||||
|
||||
/// Creates console settings targeting standard error.
|
||||
#[must_use]
|
||||
pub const fn stderr() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stderr };
|
||||
}
|
||||
|
||||
/// Returns the selected console stream.
|
||||
#[must_use]
|
||||
pub const fn output(&self) -> crate::ConsoleOutput {
|
||||
return self.output;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotation cadence for the optional file output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum FileRotation {
|
||||
/// Keeps a single non-rotating file.
|
||||
Never,
|
||||
/// Rotates the file every hour.
|
||||
Hourly,
|
||||
/// Rotates the file every day.
|
||||
Daily,
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional file output.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileSettings {
|
||||
directory: std::path::PathBuf,
|
||||
file_name_prefix: std::string::String,
|
||||
rotation: crate::FileRotation,
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
/// Creates file output settings.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
directory: impl std::convert::Into<std::path::PathBuf>,
|
||||
file_name_prefix: impl std::convert::Into<std::string::String>,
|
||||
rotation: crate::FileRotation,
|
||||
) -> Self {
|
||||
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
|
||||
}
|
||||
|
||||
/// Returns the directory containing log files.
|
||||
#[must_use]
|
||||
pub fn directory(&self) -> &std::path::Path {
|
||||
return self.directory.as_path();
|
||||
}
|
||||
|
||||
/// Returns the file-name prefix passed to the file appender.
|
||||
#[must_use]
|
||||
pub fn file_name_prefix(&self) -> &str {
|
||||
return self.file_name_prefix.as_str();
|
||||
}
|
||||
|
||||
/// Returns the selected file rotation cadence.
|
||||
#[must_use]
|
||||
pub const fn rotation(&self) -> crate::FileRotation {
|
||||
return self.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct LoggingSettings {
|
||||
default_filter: crate::LogFilterLevel,
|
||||
target_filters: std::vec::Vec<crate::TargetFilter>,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
}
|
||||
|
||||
impl LoggingSettings {
|
||||
/// Creates explicit Logging settings without any target override.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
default_filter: crate::LogFilterLevel,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
) -> Self {
|
||||
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
|
||||
}
|
||||
|
||||
/// Adds one target-prefix override and returns the updated settings.
|
||||
#[must_use]
|
||||
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
||||
self.target_filters.push(target_filter);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the default level applied to KSP-owned targets.
|
||||
#[must_use]
|
||||
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
||||
return self.default_filter;
|
||||
}
|
||||
|
||||
/// Returns target-prefix overrides in insertion order.
|
||||
#[must_use]
|
||||
pub fn target_filters(&self) -> &[crate::TargetFilter] {
|
||||
return self.target_filters.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the selected span lifecycle event policy.
|
||||
#[must_use]
|
||||
pub const fn span_events(&self) -> crate::SpanEvents {
|
||||
return self.span_events;
|
||||
}
|
||||
|
||||
/// Returns console settings when console output is enabled.
|
||||
#[must_use]
|
||||
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
||||
return self.console.as_ref();
|
||||
}
|
||||
|
||||
/// Returns file settings when file output is enabled.
|
||||
#[must_use]
|
||||
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
|
||||
return self.file.as_ref();
|
||||
}
|
||||
|
||||
/// Validates backend-independent invariants of the runtime settings.
|
||||
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||
for target_filter in &self.target_filters {
|
||||
if target_filter.target_prefix().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must not be empty")
|
||||
.with_context("field", "target_filters.target_prefix"),
|
||||
);
|
||||
}
|
||||
if !target_filter.target_prefix().starts_with("ksp-") {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "target filter prefix must identify a KSP-owned target")
|
||||
.with_context("field", "target_filters.target_prefix")
|
||||
.with_context("target_prefix", target_filter.target_prefix()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(file) = self.file.as_ref()
|
||||
&& file.file_name_prefix().is_empty()
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||
.with_context("field", "file.file_name_prefix"),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
mod tests;
|
||||
41
crates/ksp-logging-lib/src/span.rs
Normal file
41
crates/ksp-logging-lib/src/span.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
// file: crates/ksp-logging-lib/src/span.rs
|
||||
// version: 2
|
||||
|
||||
/// KSP-owned handle to a tracing span.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Span {
|
||||
inner: tracing::Span,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Runs synchronous work while this span is entered.
|
||||
pub fn in_scope<T>(&self, operation: impl std::ops::FnOnce() -> T) -> T {
|
||||
return self.inner.in_scope(operation);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Constructs the KSP span wrapper for macro expansion support.
|
||||
#[must_use]
|
||||
pub fn __from_tracing(inner: tracing::Span) -> Self {
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
/// Consumes this wrapper and returns the internal tracing span.
|
||||
pub(crate) fn into_tracing(self) -> tracing::Span {
|
||||
return self.inner;
|
||||
}
|
||||
}
|
||||
|
||||
/// Instruments an asynchronous future with a KSP span.
|
||||
///
|
||||
/// The span is entered whenever the future is polled or dropped and exited when that operation returns, so no enter guard is held across an `.await` point.
|
||||
pub fn instrument<F>(span: crate::Span, future: F) -> impl std::future::Future<Output = F::Output>
|
||||
where
|
||||
F: std::future::Future,
|
||||
{
|
||||
return tracing::Instrument::instrument(future, span.into_tracing());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/span.rs"]
|
||||
mod tests;
|
||||
105
crates/ksp-logging-lib/src/writer.rs
Normal file
105
crates/ksp-logging-lib/src/writer.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-logging-lib/src/writer.rs
|
||||
// version: 1
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum StripAnsiState {
|
||||
Text,
|
||||
Escape,
|
||||
Csi,
|
||||
Osc,
|
||||
OscEscape,
|
||||
String,
|
||||
StringEscape,
|
||||
}
|
||||
|
||||
pub(crate) struct StripAnsiWriter<W> {
|
||||
inner: W,
|
||||
state: StripAnsiState,
|
||||
}
|
||||
|
||||
impl<W> StripAnsiWriter<W> {
|
||||
pub(crate) const fn new(inner: W) -> Self {
|
||||
return Self { inner, state: StripAnsiState::Text };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn into_inner(self) -> W {
|
||||
return self.inner;
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> std::io::Write for StripAnsiWriter<W>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let mut stripped = std::vec::Vec::with_capacity(buf.len());
|
||||
for byte in buf {
|
||||
self.consume_byte(*byte, &mut stripped);
|
||||
}
|
||||
let write_result = std::io::Write::write_all(&mut self.inner, stripped.as_slice());
|
||||
return match write_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(buf.len()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
return std::io::Write::flush(&mut self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> StripAnsiWriter<W> {
|
||||
fn consume_byte(&mut self, byte: u8, output: &mut std::vec::Vec<u8>) {
|
||||
self.state = match self.state {
|
||||
StripAnsiState::Text => {
|
||||
if byte == 0x1B {
|
||||
StripAnsiState::Escape
|
||||
} else {
|
||||
output.push(byte);
|
||||
StripAnsiState::Text
|
||||
}
|
||||
},
|
||||
StripAnsiState::Escape => match byte {
|
||||
b'[' => StripAnsiState::Csi,
|
||||
b']' => StripAnsiState::Osc,
|
||||
b'P' | b'X' | b'^' | b'_' => StripAnsiState::String,
|
||||
0x1B => StripAnsiState::Escape,
|
||||
_ => StripAnsiState::Text,
|
||||
},
|
||||
StripAnsiState::Csi => {
|
||||
if (0x40..=0x7E).contains(&byte) {
|
||||
StripAnsiState::Text
|
||||
} else {
|
||||
StripAnsiState::Csi
|
||||
}
|
||||
},
|
||||
StripAnsiState::Osc => match byte {
|
||||
0x07 => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::OscEscape,
|
||||
_ => StripAnsiState::Osc,
|
||||
},
|
||||
StripAnsiState::OscEscape => match byte {
|
||||
b'\\' => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::OscEscape,
|
||||
_ => StripAnsiState::Osc,
|
||||
},
|
||||
StripAnsiState::String => {
|
||||
if byte == 0x1B {
|
||||
StripAnsiState::StringEscape
|
||||
} else {
|
||||
StripAnsiState::String
|
||||
}
|
||||
},
|
||||
StripAnsiState::StringEscape => match byte {
|
||||
b'\\' => StripAnsiState::Text,
|
||||
0x1B => StripAnsiState::StringEscape,
|
||||
_ => StripAnsiState::String,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/writer.rs"]
|
||||
mod tests;
|
||||
163
crates/ksp-logging-lib/tests/callsite.rs
Normal file
163
crates/ksp-logging-lib/tests/callsite.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
// file: crates/ksp-logging-lib/tests/callsite.rs
|
||||
// version: 2
|
||||
|
||||
//! Integration tests for KSP logging callsite and async span instrumentation behavior.
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct CapturedMetadata {
|
||||
target: std::string::String,
|
||||
file: std::option::Option<std::string::String>,
|
||||
module_path: std::option::Option<std::string::String>,
|
||||
line: std::option::Option<u32>,
|
||||
is_event: bool,
|
||||
is_span: bool,
|
||||
}
|
||||
|
||||
impl CapturedMetadata {
|
||||
fn from_metadata(metadata: &tracing::Metadata<'_>) -> Self {
|
||||
return Self {
|
||||
target: metadata.target().to_owned(),
|
||||
file: metadata.file().map(str::to_owned),
|
||||
module_path: metadata.module_path().map(str::to_owned),
|
||||
line: metadata.line(),
|
||||
is_event: metadata.is_event(),
|
||||
is_span: metadata.is_span(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CaptureSubscriber {
|
||||
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
|
||||
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
next_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
}
|
||||
|
||||
impl CaptureSubscriber {
|
||||
fn new(
|
||||
captured: std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>,
|
||||
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
) -> Self {
|
||||
return Self { captured, enters, exits, next_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)) };
|
||||
}
|
||||
|
||||
fn capture(&self, metadata: &tracing::Metadata<'_>) {
|
||||
let lock = self.captured.lock();
|
||||
if let std::result::Result::Ok(mut values) = lock {
|
||||
values.push(CapturedMetadata::from_metadata(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl tracing::Subscriber for CaptureSubscriber {
|
||||
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||
self.capture(span.metadata());
|
||||
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return tracing::span::Id::from_u64(id);
|
||||
}
|
||||
|
||||
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn event(&self, event: &tracing::Event<'_>) {
|
||||
self.capture(event.metadata());
|
||||
return;
|
||||
}
|
||||
|
||||
fn enter(&self, _span: &tracing::span::Id) {
|
||||
self.enters.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
fn exit(&self, _span: &tracing::span::Id) {
|
||||
self.exits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn captured_values(captured: &std::sync::Arc<std::sync::Mutex<std::vec::Vec<CapturedMetadata>>>) -> std::vec::Vec<CapturedMetadata> {
|
||||
let lock = captured.lock();
|
||||
return match lock {
|
||||
std::result::Result::Ok(values) => values.clone(),
|
||||
std::result::Result::Err(error) => error.into_inner().clone(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_macro_preserves_consumer_callsite() {
|
||||
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
|
||||
let expected_line = line!() + 2;
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
ksp_logging_lib::info!(target: TEST_TARGET, domain = "logging", "callsite event");
|
||||
return;
|
||||
});
|
||||
let values = captured_values(&captured);
|
||||
assert_eq!(values.len(), 1);
|
||||
assert_eq!(values[0].target, TEST_TARGET);
|
||||
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
|
||||
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
|
||||
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
|
||||
assert!(values[0].is_event);
|
||||
assert!(!values[0].is_span);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_macro_preserves_consumer_callsite() {
|
||||
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let subscriber = CaptureSubscriber::new(captured.clone(), enters, exits);
|
||||
let expected_line = line!() + 2;
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
let _span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "callsite_span", component = "test");
|
||||
return;
|
||||
});
|
||||
let values = captured_values(&captured);
|
||||
assert_eq!(values.len(), 1);
|
||||
assert_eq!(values[0].target, TEST_TARGET);
|
||||
assert_eq!(values[0].file.as_deref(), std::option::Option::Some(file!()));
|
||||
assert_eq!(values[0].module_path.as_deref(), std::option::Option::Some(module_path!()));
|
||||
assert_eq!(values[0].line, std::option::Option::Some(expected_line));
|
||||
assert!(!values[0].is_event);
|
||||
assert!(values[0].is_span);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_instrumentation_enters_and_exits_span_during_poll_and_drop() {
|
||||
let captured = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let subscriber = CaptureSubscriber::new(captured, std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "async_poll_span", domain = "logging");
|
||||
let future = ksp_logging_lib::instrument(span, std::future::ready(42_u32));
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||
assert_eq!(poll, std::task::Poll::Ready(42_u32));
|
||||
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
assert_eq!(exits.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
std::mem::drop(future);
|
||||
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), 2);
|
||||
assert_eq!(exits.load(std::sync::atomic::Ordering::Relaxed), 2);
|
||||
return;
|
||||
});
|
||||
assert_eq!(enters.load(std::sync::atomic::Ordering::Relaxed), exits.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
47
crates/ksp-logging-lib/tests/overhead.rs
Normal file
47
crates/ksp-logging-lib/tests/overhead.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// file: crates/ksp-logging-lib/tests/overhead.rs
|
||||
// version: 1
|
||||
|
||||
//! Diagnostic gross-overhead probe for the reload layer used by KSP Logging.
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
const ITERATIONS: u64 = 200_000;
|
||||
|
||||
fn emit_probe_events() {
|
||||
for sequence in 0..ITERATIONS {
|
||||
tracing::trace!(target: TEST_TARGET, sequence, "reload overhead probe");
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_filter() -> tracing_subscriber::filter::Targets {
|
||||
return tracing_subscriber::filter::Targets::new()
|
||||
.with_default(tracing_subscriber::filter::LevelFilter::OFF)
|
||||
.with_target(TEST_TARGET, tracing_subscriber::filter::LevelFilter::TRACE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "diagnostic timing probe; run explicitly with --ignored --nocapture"]
|
||||
fn reload_layer_overhead_remains_within_a_gross_regression_guardrail() {
|
||||
let baseline_subscriber = tracing_subscriber::registry().with(trace_filter());
|
||||
let baseline_start = std::time::Instant::now();
|
||||
tracing::subscriber::with_default(baseline_subscriber, || {
|
||||
emit_probe_events();
|
||||
return;
|
||||
});
|
||||
let baseline_elapsed = baseline_start.elapsed();
|
||||
let (reload_layer, _reload_handle) = tracing_subscriber::reload::Layer::new(trace_filter());
|
||||
let reload_subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||
let reload_start = std::time::Instant::now();
|
||||
tracing::subscriber::with_default(reload_subscriber, || {
|
||||
emit_probe_events();
|
||||
return;
|
||||
});
|
||||
let reload_elapsed = reload_start.elapsed();
|
||||
let gross_ceiling = baseline_elapsed.saturating_mul(100).saturating_add(std::time::Duration::from_millis(100));
|
||||
println!("KSP reload overhead probe: baseline={baseline_elapsed:?}, reload={reload_elapsed:?}, iterations={ITERATIONS}");
|
||||
assert!(
|
||||
reload_elapsed <= gross_ceiling,
|
||||
"reload layer exceeded the gross regression guardrail: baseline={baseline_elapsed:?}, reload={reload_elapsed:?}"
|
||||
);
|
||||
}
|
||||
81
crates/ksp-logging-lib/tests/ownership.rs
Normal file
81
crates/ksp-logging-lib/tests/ownership.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// file: crates/ksp-logging-lib/tests/ownership.rs
|
||||
// version: 1
|
||||
|
||||
//! Integration audit ensuring KSP crates do not bypass the logging facade.
|
||||
|
||||
fn collect_rust_files(directory: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
||||
let entries_result = std::fs::read_dir(directory);
|
||||
let entries = match entries_result {
|
||||
std::result::Result::Ok(entries) => entries,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
for entry_result in entries {
|
||||
let entry = match entry_result {
|
||||
std::result::Result::Ok(entry) => entry,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_rust_files(path.as_path(), files);
|
||||
continue;
|
||||
}
|
||||
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_crates_do_not_bypass_ksp_logging_facade() {
|
||||
let logging_manifest_directory = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_root = match logging_manifest_directory.parent().and_then(std::path::Path::parent) {
|
||||
std::option::Option::Some(root) => root,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let crates_directory = workspace_root.join("crates");
|
||||
let entries_result = std::fs::read_dir(crates_directory.as_path());
|
||||
assert!(entries_result.is_ok(), "unable to inspect workspace crates at {}", crates_directory.display());
|
||||
let entries = match entries_result {
|
||||
std::result::Result::Ok(entries) => entries,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
for entry_result in entries {
|
||||
let entry = match entry_result {
|
||||
std::result::Result::Ok(entry) => entry,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
let crate_path = entry.path();
|
||||
if !crate_path.is_dir() || entry.file_name() == std::ffi::OsStr::new("ksp-logging-lib") {
|
||||
continue;
|
||||
}
|
||||
let manifest_path = crate_path.join("Cargo.toml");
|
||||
if manifest_path.exists() {
|
||||
let manifest_result = std::fs::read_to_string(manifest_path.as_path());
|
||||
assert!(manifest_result.is_ok(), "unable to read {}", manifest_path.display());
|
||||
let manifest = match manifest_result {
|
||||
std::result::Result::Ok(manifest) => manifest,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert!(
|
||||
!manifest.contains("tracing.workspace") && !manifest.contains("\ntracing =") && !manifest.contains("[dependencies.tracing]"),
|
||||
"{} depends directly on tracing",
|
||||
manifest_path.display(),
|
||||
);
|
||||
assert!(!manifest.contains("tracing-subscriber"), "{} depends directly on tracing-subscriber", manifest_path.display());
|
||||
assert!(!manifest.contains("tracing-appender"), "{} depends directly on tracing-appender", manifest_path.display());
|
||||
}
|
||||
let mut rust_files = std::vec::Vec::new();
|
||||
collect_rust_files(crate_path.as_path(), &mut rust_files);
|
||||
for rust_file in rust_files {
|
||||
let source_result = std::fs::read_to_string(rust_file.as_path());
|
||||
assert!(source_result.is_ok(), "unable to read {}", rust_file.display());
|
||||
let source = match source_result {
|
||||
std::result::Result::Ok(source) => source,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert!(!source.contains("tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display());
|
||||
assert!(!source.contains("tracing_subscriber::"), "{} bypasses ksp-logging-lib via tracing-subscriber", rust_file.display());
|
||||
assert!(!source.contains("tracing_appender::"), "{} bypasses ksp-logging-lib via tracing-appender", rust_file.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
67
crates/ksp-logging-lib/tests/public_api.rs
Normal file
67
crates/ksp-logging-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// file: crates/ksp-logging-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
|
||||
//! Integration tests for the public crate-root surface of `ksp-logging-lib`.
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
#[test]
|
||||
fn public_settings_surface_is_usable() {
|
||||
let settings = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stdout()),
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new("logs", "ksp", ksp_logging_lib::FileRotation::Daily)),
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(TEST_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||
assert!(settings.validate().is_ok());
|
||||
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
|
||||
assert_eq!(settings.console().map(ksp_logging_lib::ConsoleSettings::output), std::option::Option::Some(ksp_logging_lib::ConsoleOutput::Stdout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_event_macros_are_usable() {
|
||||
ksp_logging_lib::error!(target: TEST_TARGET, operation = "public_api", "error event");
|
||||
ksp_logging_lib::warn!(target: TEST_TARGET, operation = "public_api", "warn event");
|
||||
ksp_logging_lib::info!(target: TEST_TARGET, operation = "public_api", "info event");
|
||||
ksp_logging_lib::debug!(target: TEST_TARGET, operation = "public_api", "debug event");
|
||||
ksp_logging_lib::trace!(target: TEST_TARGET, operation = "public_api", "trace event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_span_surface_is_usable_for_sync_and_async() {
|
||||
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "public_sync", domain = "logging");
|
||||
let value = span.in_scope(|| -> u32 {
|
||||
return 7;
|
||||
});
|
||||
assert_eq!(value, 7);
|
||||
let async_span = ksp_logging_lib::debug_span!(target: TEST_TARGET, "public_async", component = "test");
|
||||
let future = ksp_logging_lib::instrument(async_span, std::future::ready(9_u32));
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||
assert_eq!(poll, std::task::Poll::Ready(9_u32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_span_levels_are_usable() {
|
||||
let _error = ksp_logging_lib::error_span!(target: TEST_TARGET, "error_span");
|
||||
let _warn = ksp_logging_lib::warn_span!(target: TEST_TARGET, "warn_span");
|
||||
let _info = ksp_logging_lib::info_span!(target: TEST_TARGET, "info_span");
|
||||
let _debug = ksp_logging_lib::debug_span!(target: TEST_TARGET, "debug_span");
|
||||
let _trace = ksp_logging_lib::trace_span!(target: TEST_TARGET, "trace_span");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_runtime_surface_is_addressable_without_installing_it() {
|
||||
let _initialize = ksp_logging_lib::initialize;
|
||||
let _reinitialize = ksp_logging_lib::reinitialize;
|
||||
let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED;
|
||||
let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED;
|
||||
let _file_initialization_failed = ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED;
|
||||
let dropped = ksp_logging_lib::DroppedLines::zero();
|
||||
assert_eq!(dropped.console(), 0);
|
||||
assert_eq!(dropped.file(), 0);
|
||||
assert_eq!(dropped.total(), 0);
|
||||
}
|
||||
196
crates/ksp-logging-lib/tests/runtime.rs
Normal file
196
crates/ksp-logging-lib/tests/runtime.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||
// version: 4
|
||||
|
||||
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
|
||||
|
||||
const LOGGING_TARGET: &str = "ksp-logging-lib";
|
||||
const OTHER_KSP_TARGET: &str = "ksp-store-lib";
|
||||
const EXTERNAL_TARGET: &str = "sqlx";
|
||||
|
||||
fn logging_trace_enabled() -> bool {
|
||||
return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE);
|
||||
}
|
||||
|
||||
fn other_ksp_info_enabled() -> bool {
|
||||
return tracing::enabled!(target: OTHER_KSP_TARGET, tracing::Level::INFO);
|
||||
}
|
||||
|
||||
fn other_ksp_debug_enabled() -> bool {
|
||||
return tracing::enabled!(target: OTHER_KSP_TARGET, tracing::Level::DEBUG);
|
||||
}
|
||||
|
||||
fn external_error_enabled() -> bool {
|
||||
return tracing::enabled!(target: EXTERNAL_TARGET, tracing::Level::ERROR);
|
||||
}
|
||||
|
||||
fn test_root_directory() -> std::path::PathBuf {
|
||||
return std::env::temp_dir().join(format!("ksp-logging-lib-runtime-{}", std::process::id()));
|
||||
}
|
||||
|
||||
fn reset_directory(path: &std::path::Path) {
|
||||
if path.exists() {
|
||||
let remove_result = std::fs::remove_dir_all(path);
|
||||
assert!(remove_result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
fn read_directory_text(path: &std::path::Path) -> std::string::String {
|
||||
let read_result = std::fs::read_dir(path);
|
||||
let entries = match read_result {
|
||||
std::result::Result::Ok(entries) => entries,
|
||||
std::result::Result::Err(_) => return std::string::String::new(),
|
||||
};
|
||||
let mut output = std::string::String::new();
|
||||
for entry_result in entries {
|
||||
let entry = match entry_result {
|
||||
std::result::Result::Ok(entry) => entry,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
std::result::Result::Ok(file_type) => file_type,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let content = match std::fs::read_to_string(entry.path()) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
output.push_str(content.as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn exercise_concurrent_reload(guard: &mut ksp_logging_lib::LoggingGuard, disabled: &ksp_logging_lib::LoggingSettings) {
|
||||
let quiet_console = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Off,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let quiet_reload = ksp_logging_lib::reinitialize(guard, &quiet_console);
|
||||
assert!(quiet_reload.is_ok());
|
||||
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let barrier = std::sync::Arc::new(std::sync::Barrier::new(5));
|
||||
let mut threads = std::vec::Vec::new();
|
||||
for worker_index in 0..4_u32 {
|
||||
let worker_stop = std::sync::Arc::clone(&stop);
|
||||
let worker_barrier = std::sync::Arc::clone(&barrier);
|
||||
threads.push(std::thread::spawn(move || {
|
||||
worker_barrier.wait();
|
||||
let mut sequence = 0_u64;
|
||||
while !worker_stop.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
ksp_logging_lib::trace!(target: LOGGING_TARGET, worker_index, sequence, "concurrent reload probe");
|
||||
sequence = sequence.wrapping_add(1);
|
||||
}
|
||||
return;
|
||||
}));
|
||||
}
|
||||
barrier.wait();
|
||||
let mut reloads_succeeded = true;
|
||||
for reload_index in 0..32_u32 {
|
||||
let settings = if reload_index % 2 == 0 { &quiet_console } else { disabled };
|
||||
let reload_result = ksp_logging_lib::reinitialize(guard, settings);
|
||||
if reload_result.is_err() {
|
||||
reloads_succeeded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut joins_succeeded = true;
|
||||
for thread in threads {
|
||||
if thread.join().is_err() {
|
||||
joins_succeeded = false;
|
||||
}
|
||||
}
|
||||
assert!(reloads_succeeded);
|
||||
assert!(joins_succeeded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_initialization() {
|
||||
let root = test_root_directory();
|
||||
reset_directory(root.as_path());
|
||||
let disabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let initialize_result = ksp_logging_lib::initialize(&disabled);
|
||||
assert!(initialize_result.is_ok());
|
||||
let mut guard = match initialize_result {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(guard.dropped_lines(), ksp_logging_lib::DroppedLines::zero());
|
||||
assert!(!logging_trace_enabled());
|
||||
assert!(!other_ksp_info_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
let console_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(ksp_logging_lib::TargetFilter::new(LOGGING_TARGET, ksp_logging_lib::LogFilterLevel::Trace));
|
||||
let reload_result = ksp_logging_lib::reinitialize(&mut guard, &console_enabled);
|
||||
assert!(reload_result.is_ok());
|
||||
assert_eq!(guard.settings(), &console_enabled);
|
||||
assert!(logging_trace_enabled());
|
||||
assert!(other_ksp_info_enabled());
|
||||
assert!(!other_ksp_debug_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
let blocked_directory = root.join("not-a-directory");
|
||||
let create_root = std::fs::create_dir_all(root.as_path());
|
||||
assert!(create_root.is_ok());
|
||||
let create_blocker = std::fs::write(blocked_directory.as_path(), b"file blocks directory creation");
|
||||
assert!(create_blocker.is_ok());
|
||||
let invalid_file = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Error,
|
||||
ksp_logging_lib::SpanEvents::Full,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(blocked_directory.as_path(), "invalid", ksp_logging_lib::FileRotation::Daily)),
|
||||
);
|
||||
let failed_reload = ksp_logging_lib::reinitialize(&mut guard, &invalid_file);
|
||||
assert!(failed_reload.is_err());
|
||||
let file_error = match failed_reload {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(file_error.code(), ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED);
|
||||
assert_eq!(guard.settings(), &console_enabled);
|
||||
assert!(logging_trace_enabled());
|
||||
assert!(!external_error_enabled());
|
||||
exercise_concurrent_reload(&mut guard, &disabled);
|
||||
let log_directory = root.join("logs");
|
||||
let file_enabled = ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_logging_lib::FileSettings::new(log_directory.as_path(), "runtime-test.log", ksp_logging_lib::FileRotation::Never)),
|
||||
);
|
||||
let file_reload = ksp_logging_lib::reinitialize(&mut guard, &file_enabled);
|
||||
assert!(file_reload.is_ok());
|
||||
ksp_logging_lib::info!(target: LOGGING_TARGET, "file \x1b[31moutput\x1b[0m marker");
|
||||
tracing::error!(target: EXTERNAL_TARGET, "external marker must remain silent");
|
||||
let disable_after_file = ksp_logging_lib::reinitialize(&mut guard, &disabled);
|
||||
assert!(disable_after_file.is_ok());
|
||||
let file_text = read_directory_text(log_directory.as_path());
|
||||
assert!(file_text.contains("file output marker"));
|
||||
assert!(file_text.contains(LOGGING_TARGET));
|
||||
assert!(file_text.contains("runtime.rs"));
|
||||
assert!(!file_text.contains("\x1b["));
|
||||
assert!(!file_text.contains("external marker must remain silent"));
|
||||
let dropped = guard.dropped_lines();
|
||||
assert_eq!(dropped.total(), dropped.console().saturating_add(dropped.file()));
|
||||
let second_initialize = ksp_logging_lib::initialize(&disabled);
|
||||
assert!(second_initialize.is_err());
|
||||
let error = match second_initialize {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED);
|
||||
reset_directory(root.as_path());
|
||||
}
|
||||
73
crates/ksp-logging-lib/tests/span_lifecycle.rs
Normal file
73
crates/ksp-logging-lib/tests/span_lifecycle.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
// file: crates/ksp-logging-lib/tests/span_lifecycle.rs
|
||||
// version: 1
|
||||
|
||||
//! Integration tests for formatted KSP span lifecycle timing output.
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SharedWriter {
|
||||
buffer: std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl SharedWriter {
|
||||
fn new(buffer: std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>) -> Self {
|
||||
return Self { buffer };
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for SharedWriter {
|
||||
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
|
||||
let lock_result = self.buffer.lock();
|
||||
let mut buffer = match lock_result {
|
||||
std::result::Result::Ok(buffer) => buffer,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(std::io::Error::other("span test buffer is poisoned")),
|
||||
};
|
||||
buffer.extend_from_slice(bytes);
|
||||
return std::result::Result::Ok(bytes.len());
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
fn captured_text(buffer: &std::sync::Arc<std::sync::Mutex<std::vec::Vec<u8>>>) -> std::string::String {
|
||||
let lock_result = buffer.lock();
|
||||
let bytes = match lock_result {
|
||||
std::result::Result::Ok(bytes) => bytes.clone(),
|
||||
std::result::Result::Err(error) => error.into_inner().clone(),
|
||||
};
|
||||
return std::string::String::from_utf8_lossy(bytes.as_slice()).into_owned();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_and_close_span_events_expose_busy_and_idle_timing_fields() {
|
||||
let buffer = std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new()));
|
||||
let writer_buffer = std::sync::Arc::clone(&buffer);
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(move || -> SharedWriter {
|
||||
return SharedWriter::new(std::sync::Arc::clone(&writer_buffer));
|
||||
})
|
||||
.with_ansi(false)
|
||||
.with_target(true)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE);
|
||||
let subscriber = tracing_subscriber::registry().with(layer);
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
let span = ksp_logging_lib::trace_span!(target: TEST_TARGET, "timed_scope", domain = "logging");
|
||||
span.in_scope(|| {
|
||||
std::hint::black_box(42_u32);
|
||||
return;
|
||||
});
|
||||
drop(span);
|
||||
return;
|
||||
});
|
||||
let text = captured_text(&buffer);
|
||||
assert!(text.contains("timed_scope"));
|
||||
assert!(text.contains("new"));
|
||||
assert!(text.contains("close"));
|
||||
assert!(text.contains("time.busy"));
|
||||
assert!(text.contains("time.idle"));
|
||||
}
|
||||
118
crates/ksp-logging-lib/tests/tokio_span.rs
Normal file
118
crates/ksp-logging-lib/tests/tokio_span.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
// file: crates/ksp-logging-lib/tests/tokio_span.rs
|
||||
// version: 1
|
||||
|
||||
//! Integration tests for KSP span instrumentation on a real Tokio executor.
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CountingSubscriber {
|
||||
enters: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
exits: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
next_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
}
|
||||
|
||||
impl CountingSubscriber {
|
||||
fn new(enters: std::sync::Arc<std::sync::atomic::AtomicU64>, exits: std::sync::Arc<std::sync::atomic::AtomicU64>) -> Self {
|
||||
return Self { enters, exits, next_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)) };
|
||||
}
|
||||
}
|
||||
|
||||
impl tracing::Subscriber for CountingSubscriber {
|
||||
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return tracing::span::Id::from_u64(id);
|
||||
}
|
||||
|
||||
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn event(&self, _event: &tracing::Event<'_>) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn enter(&self, _span: &tracing::span::Id) {
|
||||
self.enters.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
fn exit(&self, _span: &tracing::span::Id) {
|
||||
self.exits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn test_span(enters: std::sync::Arc<std::sync::atomic::AtomicU64>, exits: std::sync::Arc<std::sync::atomic::AtomicU64>) -> ksp_logging_lib::Span {
|
||||
let subscriber = CountingSubscriber::new(enters, exits);
|
||||
return tracing::subscriber::with_default(subscriber, || -> ksp_logging_lib::Span {
|
||||
return ksp_logging_lib::trace_span!(target: TEST_TARGET, "tokio_runtime_span", domain = "logging", executor = "tokio");
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn instrumented_span_reenters_across_real_tokio_suspensions() {
|
||||
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||
let observed_enters = std::sync::Arc::clone(&enters);
|
||||
let future = ksp_logging_lib::instrument(span, async move {
|
||||
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 1);
|
||||
tokio::task::yield_now().await;
|
||||
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 2);
|
||||
tokio::task::yield_now().await;
|
||||
assert!(observed_enters.load(std::sync::atomic::Ordering::Relaxed) >= 3);
|
||||
return 42_u32;
|
||||
});
|
||||
let value = future.await;
|
||||
assert_eq!(value, 42_u32);
|
||||
let enter_count = enters.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let exit_count = exits.load(std::sync::atomic::Ordering::Relaxed);
|
||||
assert!(enter_count >= 3);
|
||||
assert_eq!(enter_count, exit_count);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn instrumented_spans_are_usable_on_tokio_multithread_runtime() {
|
||||
let enters = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let exits = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let first_span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||
let second_span = test_span(std::sync::Arc::clone(&enters), std::sync::Arc::clone(&exits));
|
||||
let first_task = tokio::spawn(ksp_logging_lib::instrument(first_span, async {
|
||||
for _iteration in 0..32 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
return 20_u32;
|
||||
}));
|
||||
let second_task = tokio::spawn(ksp_logging_lib::instrument(second_span, async {
|
||||
for _iteration in 0..32 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
return 22_u32;
|
||||
}));
|
||||
let first_result = first_task.await;
|
||||
assert!(first_result.is_ok(), "first Tokio task must complete successfully");
|
||||
let first_value = match first_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let second_result = second_task.await;
|
||||
assert!(second_result.is_ok(), "second Tokio task must complete successfully");
|
||||
let second_value = match second_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(first_value + second_value, 42_u32);
|
||||
let enter_count = enters.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let exit_count = exits.load(std::sync::atomic::Ordering::Relaxed);
|
||||
assert!(enter_count >= 4);
|
||||
assert_eq!(enter_count, exit_count);
|
||||
}
|
||||
194
crates/ksp-logging-lib/unit_tests/runtime.rs
Normal file
194
crates/ksp-logging-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn level_mapping_covers_all_ksp_levels() {
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Off), tracing_subscriber::filter::LevelFilter::OFF);
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Error), tracing_subscriber::filter::LevelFilter::ERROR);
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Warn), tracing_subscriber::filter::LevelFilter::WARN);
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Info), tracing_subscriber::filter::LevelFilter::INFO);
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Debug), tracing_subscriber::filter::LevelFilter::DEBUG);
|
||||
assert_eq!(super::map_filter_level(crate::LogFilterLevel::Trace), tracing_subscriber::filter::LevelFilter::TRACE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takeover_filter_silences_external_targets_and_applies_ksp_overrides() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-logging-lib", crate::LogFilterLevel::Trace));
|
||||
let filter = super::build_target_filter(&settings);
|
||||
assert!(filter.would_enable("ksp-store-lib", &tracing::Level::INFO));
|
||||
assert!(!filter.would_enable("ksp-store-lib", &tracing::Level::DEBUG));
|
||||
assert!(filter.would_enable("ksp-logging-lib", &tracing::Level::TRACE));
|
||||
assert!(!filter.would_enable("sqlx", &tracing::Level::ERROR));
|
||||
assert!(!filter.would_enable("hyper", &tracing::Level::ERROR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_event_mapping_supports_disabled_timing_and_full_lifecycle() {
|
||||
assert_eq!(super::map_span_events(crate::SpanEvents::Off), tracing_subscriber::fmt::format::FmtSpan::NONE);
|
||||
assert_eq!(
|
||||
super::map_span_events(crate::SpanEvents::NewAndClose),
|
||||
tracing_subscriber::fmt::format::FmtSpan::NEW | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
|
||||
);
|
||||
assert_eq!(super::map_span_events(crate::SpanEvents::Full), tracing_subscriber::fmt::format::FmtSpan::FULL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_rotation_mapping_covers_supported_cadences() {
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Never), tracing_appender::rolling::Rotation::NEVER);
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Hourly), tracing_appender::rolling::Rotation::HOURLY);
|
||||
assert_eq!(super::map_file_rotation(crate::FileRotation::Daily), tracing_appender::rolling::Rotation::DAILY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_runtime_has_no_layers_or_outputs() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let prepared = match result {
|
||||
std::result::Result::Ok(prepared) => prepared,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(prepared.layers.is_empty());
|
||||
assert!(prepared.outputs.console.is_none());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_runtime_composes_takeover_filter_before_formatter_and_owns_guard() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = super::prepare_runtime(&settings);
|
||||
assert!(result.is_ok());
|
||||
let prepared = match result {
|
||||
std::result::Result::Ok(prepared) => prepared,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(prepared.layers.len(), 1);
|
||||
assert!(prepared.outputs.console.is_some());
|
||||
assert!(prepared.outputs.file.is_none());
|
||||
assert_eq!(prepared.outputs.dropped_lines(), crate::DroppedLines::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_line_snapshots_add_saturating_by_sink() {
|
||||
let first = crate::DroppedLines { console: usize::MAX, file: 4 };
|
||||
let second = crate::DroppedLines { console: 1, file: 7 };
|
||||
let combined = first.saturating_add(second);
|
||||
assert_eq!(combined.console(), usize::MAX);
|
||||
assert_eq!(combined.file(), 11);
|
||||
assert_eq!(combined.total(), usize::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takeover_filter_prefers_more_specific_ksp_prefixes_and_supports_off() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-", crate::LogFilterLevel::Debug))
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace))
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-wallet-lib", crate::LogFilterLevel::Off));
|
||||
let filter = super::build_target_filter(&settings);
|
||||
assert!(filter.would_enable("ksp-store-other", &tracing::Level::DEBUG));
|
||||
assert!(!filter.would_enable("ksp-store-other", &tracing::Level::TRACE));
|
||||
assert!(filter.would_enable("ksp-store-lib", &tracing::Level::TRACE));
|
||||
assert!(!filter.would_enable("ksp-wallet-lib", &tracing::Level::ERROR));
|
||||
}
|
||||
|
||||
struct BlockingWriter {
|
||||
first_write: bool,
|
||||
started: std::sync::mpsc::SyncSender<()>,
|
||||
release: std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>,
|
||||
}
|
||||
|
||||
impl BlockingWriter {
|
||||
fn new(started: std::sync::mpsc::SyncSender<()>, release: std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>) -> Self {
|
||||
return Self { first_write: true, started, release };
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for BlockingWriter {
|
||||
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||
if self.first_write {
|
||||
self.first_write = false;
|
||||
if self.started.send(()).is_err() {
|
||||
return std::result::Result::Err(std::io::Error::other("unable to notify saturation test that the writer is blocked"));
|
||||
}
|
||||
let (lock, condition) = self.release.as_ref();
|
||||
let lock_result = lock.lock();
|
||||
let mut released = match lock_result {
|
||||
std::result::Result::Ok(released) => released,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(std::io::Error::other("saturation test release lock is poisoned"));
|
||||
},
|
||||
};
|
||||
while !*released {
|
||||
let wait_result = condition.wait(released);
|
||||
released = match wait_result {
|
||||
std::result::Result::Ok(released) => released,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(std::io::Error::other("saturation test release wait is poisoned"));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(buffer.len());
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
fn release_blocked_writer(release: &std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>) {
|
||||
let (lock, condition) = release.as_ref();
|
||||
let lock_result = lock.lock();
|
||||
let mut released = match lock_result {
|
||||
std::result::Result::Ok(released) => released,
|
||||
std::result::Result::Err(error) => error.into_inner(),
|
||||
};
|
||||
*released = true;
|
||||
condition.notify_all();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lossy_non_blocking_builder_drops_lines_instead_of_blocking_a_stalled_producer() {
|
||||
let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let release = std::sync::Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
|
||||
let writer = BlockingWriter::new(started_sender, std::sync::Arc::clone(&release));
|
||||
let (mut non_blocking, worker_guard) = super::non_blocking_builder("ksp-logging-saturation-test").buffered_lines_limit(1).finish(writer);
|
||||
let error_counter = non_blocking.error_counter();
|
||||
let first_write = std::io::Write::write_all(&mut non_blocking, b"block worker\n");
|
||||
assert!(first_write.is_ok());
|
||||
let writer_started = started_receiver.recv_timeout(std::time::Duration::from_secs(2));
|
||||
assert!(writer_started.is_ok());
|
||||
let mut producer = non_blocking.clone();
|
||||
let (finished_sender, finished_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let producer_thread = std::thread::spawn(move || {
|
||||
let mut succeeded = true;
|
||||
for _ in 0..1_024 {
|
||||
let write_result = std::io::Write::write_all(&mut producer, b"queued line\n");
|
||||
if write_result.is_err() {
|
||||
succeeded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _send_result = finished_sender.send(succeeded);
|
||||
return;
|
||||
});
|
||||
let producer_finished = finished_receiver.recv_timeout(std::time::Duration::from_secs(2));
|
||||
release_blocked_writer(&release);
|
||||
let join_result = producer_thread.join();
|
||||
assert!(join_result.is_ok());
|
||||
assert_eq!(producer_finished, std::result::Result::Ok(true));
|
||||
assert!(error_counter.dropped_lines() > 0);
|
||||
drop(non_blocking);
|
||||
drop(worker_guard);
|
||||
}
|
||||
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal file
102
crates/ksp-logging-lib/unit_tests/settings.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn level_variants_are_distinct() {
|
||||
assert_ne!(crate::LogFilterLevel::Off, crate::LogFilterLevel::Error);
|
||||
assert_ne!(crate::LogFilterLevel::Error, crate::LogFilterLevel::Warn);
|
||||
assert_ne!(crate::LogFilterLevel::Warn, crate::LogFilterLevel::Info);
|
||||
assert_ne!(crate::LogFilterLevel::Info, crate::LogFilterLevel::Debug);
|
||||
assert_ne!(crate::LogFilterLevel::Debug, crate::LogFilterLevel::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_filter_preserves_prefix_and_level() {
|
||||
let filter = crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace);
|
||||
assert_eq!(filter.target_prefix(), "ksp-store-lib");
|
||||
assert_eq!(filter.level(), crate::LogFilterLevel::Trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_settings_select_requested_stream() {
|
||||
assert_eq!(crate::ConsoleSettings::stdout().output(), crate::ConsoleOutput::Stdout);
|
||||
assert_eq!(crate::ConsoleSettings::stderr().output(), crate::ConsoleOutput::Stderr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_settings_preserve_values() {
|
||||
let settings = crate::FileSettings::new("logs", "ksp", crate::FileRotation::Daily);
|
||||
assert_eq!(settings.directory(), std::path::Path::new("logs"));
|
||||
assert_eq!(settings.file_name_prefix(), "ksp");
|
||||
assert_eq!(settings.rotation(), crate::FileRotation::Daily);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_settings_preserve_explicit_values() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::NewAndClose,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "ksp", crate::FileRotation::Hourly)),
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-store-lib", crate::LogFilterLevel::Trace));
|
||||
assert_eq!(settings.default_filter(), crate::LogFilterLevel::Info);
|
||||
assert_eq!(settings.span_events(), crate::SpanEvents::NewAndClose);
|
||||
assert_eq!(settings.target_filters().len(), 1);
|
||||
assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-store-lib");
|
||||
assert_eq!(settings.console(), std::option::Option::Some(&crate::ConsoleSettings::stdout()));
|
||||
assert_eq!(settings.file().map(crate::FileSettings::rotation), std::option::Option::Some(crate::FileRotation::Hourly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_empty_target_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_external_target_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stdout()),
|
||||
std::option::Option::None,
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("sqlx", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_empty_file_prefix() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Off,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "", crate::FileRotation::Never)),
|
||||
);
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_accepts_ksp_outputs_and_filters() {
|
||||
let settings = crate::LoggingSettings::new(
|
||||
crate::LogFilterLevel::Info,
|
||||
crate::SpanEvents::Full,
|
||||
std::option::Option::Some(crate::ConsoleSettings::stderr()),
|
||||
std::option::Option::Some(crate::FileSettings::new("logs", "worker", crate::FileRotation::Daily)),
|
||||
)
|
||||
.with_target_filter(crate::TargetFilter::new("ksp-worker-", crate::LogFilterLevel::Debug));
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_allow_logging_to_be_disabled() {
|
||||
let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Off, crate::SpanEvents::Off, std::option::Option::None, std::option::Option::None);
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal file
22
crates/ksp-logging-lib/unit_tests/span.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/span.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn synchronous_scope_returns_operation_value() {
|
||||
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_span"));
|
||||
let value = span.in_scope(|| -> u32 {
|
||||
return 42;
|
||||
});
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_instrumentation_returns_future_output() {
|
||||
let span = crate::Span::__from_tracing(tracing::info_span!("unit_test_async_span"));
|
||||
let future = crate::instrument(span, std::future::ready(42_u32));
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
let poll = std::future::Future::poll(future.as_mut(), &mut context);
|
||||
assert_eq!(poll, std::task::Poll::Ready(42_u32));
|
||||
}
|
||||
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
30
crates/ksp-logging-lib/unit_tests/writer.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/writer.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_csi_sequences() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
|
||||
assert!(write_result.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"beforeredafter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_preserves_state_across_split_writes() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
|
||||
let second = std::io::Write::write_all(&mut writer, b"32mb");
|
||||
assert!(first.is_ok());
|
||||
assert!(second.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
|
||||
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
|
||||
assert!(first.is_ok());
|
||||
assert!(second.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"abcd");
|
||||
}
|
||||
285
deltas/0.1.1/pre.001-fix.001.md
Normal file
285
deltas/0.1.1/pre.001-fix.001.md
Normal file
@@ -0,0 +1,285 @@
|
||||
<!-- file: deltas/0.1.1/pre.001-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.1-pre.001-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente appliquée et commitée :
|
||||
|
||||
```text
|
||||
0.1.1-pre.001
|
||||
```
|
||||
|
||||
La base stable initiale `khadhroony-solana-project-v0.0.3.zip` provient directement de Gitea depuis le tag `v0.0.3`. L'absence de `.git` dans cette archive n'impose donc aucune vérification supplémentaire du tag pour le cadrage de cette session.
|
||||
|
||||
## Type de livraison
|
||||
|
||||
```text
|
||||
ksp-doc-0.1.1-pre.001-fix.001.zip
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger le plan de `0.1.1-pre.001` après validation du brainstorming Program IDs, sans ouvrir `pre.002` et sans modifier de code/runtime.
|
||||
|
||||
Ce correctif :
|
||||
|
||||
- confirme `solana-pubkey` comme dépendance Solana fondamentale candidate de Core ;
|
||||
- interdit `solana-sdk-ids` comme dépendance KSP, y compris de développement ;
|
||||
- fait posséder à KSP ses chaînes Base58 et représentations `Pubkey` de Program IDs ;
|
||||
- fixe les préfixes `PRGID_` et `PRGIDPK_` ;
|
||||
- fixe la structure générale de nomenclature `<PREFIX>_<DOMAIN>_<SUBDOMAIN?>_<NAME>_<VERSION?>` ;
|
||||
- prévoit une macro KSP `declare_program_id!` produisant les deux représentations depuis une déclaration unique ;
|
||||
- réintroduit et améliore le concept de registre descriptif enumerable inspiré de l'ancien `ks-program-ids` ;
|
||||
- supprime le nombre arbitrairement figé de 17 Program IDs avant l'inventaire final de `pre.003` ;
|
||||
- maintient la séparation stricte entre Program IDs et well-known accounts.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Aucun fichier participant au code, build, runtime, à la configuration exécutable ou aux migrations n'est modifié.
|
||||
|
||||
Conformément à `VER-ID-008`, `workspace.package.version` reste donc :
|
||||
|
||||
```text
|
||||
0.1.1-pre.1
|
||||
```
|
||||
|
||||
Le correctif possède néanmoins son identifiant de livraison/commit propre :
|
||||
|
||||
```text
|
||||
0.1.1-pre.001-fix.001
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `deltas/0.1.1/pre.001-fix.001.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` — version documentaire 1 -> 2.
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Corrections et décisions incorporées
|
||||
|
||||
### Provenance de la base stable
|
||||
|
||||
La réserve de `pre.001` liée à l'absence de `.git` dans l'archive Gitea est retirée du plan actif.
|
||||
|
||||
Dans le workflow KSP fourni, une archive nommée `khadhroony-solana-project-vX.Y.Z.zip` est produite directement par Gitea depuis le tag correspondant. `khadhroony-solana-project-v0.0.3.zip` est donc acceptée comme base stable/taguée `v0.0.3`.
|
||||
|
||||
Le delta `pre.001` déjà livré n'est pas réécrit ; ce correctif trace explicitement la correction.
|
||||
|
||||
### Dépendances Solana/Anza
|
||||
|
||||
Direction acquise :
|
||||
|
||||
```text
|
||||
ksp-core-lib -> solana-pubkey
|
||||
```
|
||||
|
||||
lorsque `pre.003` implémentera réellement la surface Program IDs.
|
||||
|
||||
En revanche :
|
||||
|
||||
```text
|
||||
ksp-core-lib -X-> solana-sdk-ids
|
||||
```
|
||||
|
||||
s'applique aux dépendances runtime **et** de développement.
|
||||
|
||||
`solana-sdk-ids` peut être consultée comme source officielle externe lors des audits, mais elle ne doit pas entrer dans le graphe Cargo KSP.
|
||||
|
||||
### Ownership et représentations des Program IDs
|
||||
|
||||
KSP possède la valeur Base58 canonique de chaque Program ID retenu.
|
||||
|
||||
Chaque ID expose deux représentations publiques liées :
|
||||
|
||||
```text
|
||||
PRGID_<SUFFIXE> : &'static str
|
||||
PRGIDPK_<SUFFIXE> : Pubkey
|
||||
```
|
||||
|
||||
Le suffixe doit être strictement identique entre les deux formes.
|
||||
|
||||
La nomenclature générale est :
|
||||
|
||||
```text
|
||||
<PREFIX>_<DOMAIN>_<SUBDOMAIN?>_<NAME>_<VERSION?>
|
||||
```
|
||||
|
||||
Exemples de convention :
|
||||
|
||||
```text
|
||||
PRGID_SOLANA_SYSTEM
|
||||
PRGIDPK_SOLANA_SYSTEM
|
||||
|
||||
PRGID_SOLANA_LOADER_BPF_V2
|
||||
PRGIDPK_SOLANA_LOADER_BPF_V2
|
||||
|
||||
PRGID_SOLANA_PRECOMPILE_ED25519
|
||||
PRGIDPK_SOLANA_PRECOMPILE_ED25519
|
||||
|
||||
PRGID_SPL_MEMO_V3
|
||||
PRGIDPK_SPL_MEMO_V3
|
||||
```
|
||||
|
||||
L'exemple SPL Memo définit uniquement la convention future ; il n'ouvre pas SPL dans le périmètre fonctionnel de `0.1.1`.
|
||||
|
||||
### Macro de déclaration
|
||||
|
||||
Le plan prévoit une macro publique KSP initialement nommée :
|
||||
|
||||
```text
|
||||
declare_program_id!
|
||||
```
|
||||
|
||||
Elle doit prendre une seule valeur Base58 canonique et produire les deux constantes `PRGID_*` et `PRGIDPK_*` correspondantes à la compilation.
|
||||
|
||||
La macro doit s'inspirer de la mécanique compile-time de `solana_address::declare_id!`/des primitives accessibles via la génération retenue de `solana-pubkey`, tout en conservant une API KSP adaptée à plusieurs Program IDs dans la même crate.
|
||||
|
||||
Elle ne doit notamment pas imposer des symboles génériques `ID`, `id()` ou `check_id()` qui entreraient en collision entre plusieurs déclarations.
|
||||
|
||||
### Registre descriptif enumerable
|
||||
|
||||
Le rejet initial d'un `ProgramIdEntry` enumerable est annulé.
|
||||
|
||||
L'ancien `ks-program-ids` fournissait notamment :
|
||||
|
||||
```text
|
||||
ProgramIdEntry
|
||||
entries()
|
||||
registered_program_ids()
|
||||
native_program_ids()
|
||||
native_well_known_account_ids()
|
||||
find_registered_program_id()
|
||||
```
|
||||
|
||||
La surface KSP doit reprendre/améliorer les capacités utiles sans reprendre les redondances historiques.
|
||||
|
||||
Direction de `pre.003` :
|
||||
|
||||
```text
|
||||
ProgramIdEntry
|
||||
entries()
|
||||
native_program_ids()
|
||||
find_program_id()
|
||||
```
|
||||
|
||||
Une recherche typée par `Pubkey` reste autorisée si son utilité est démontrée pendant l'implémentation.
|
||||
|
||||
`registered_program_ids()` n'est pas repris automatiquement s'il ne fait que dupliquer `entries()`.
|
||||
|
||||
`ProgramIdEntry` doit pouvoir exposer au minimum un code KSP stable, les formes `PRGID_*`/`PRGIDPK_*` et une classification descriptive minimale permettant les sous-ensembles utiles sans dupliquer plusieurs registres.
|
||||
|
||||
### Program IDs fondamentaux
|
||||
|
||||
La liste de `pre.001` n'est plus figée à 17 entrées.
|
||||
|
||||
L'inventaire final sera confirmé dans `pre.003` contre les sources officielles actuelles, en couvrant notamment :
|
||||
|
||||
- System, Stake, Vote, Config, Feature et Compute Budget ;
|
||||
- Address Lookup Table ;
|
||||
- loaders BPF historiques/actuels, Loader v4 et Native Loader ;
|
||||
- précompiles Ed25519, Secp256k1 et Secp256r1 ;
|
||||
- programmes ZK fondamentaux encore pertinents ;
|
||||
- toute surface native/historique supplémentaire réellement justifiée.
|
||||
|
||||
L'ancien `ks-program-ids` reste un inventaire historique utile. Son entrée `slashing` doit par exemple être réévaluée selon son statut officiel actuel plutôt que retenue ou rejetée uniquement parce qu'elle figurait dans bot3.
|
||||
|
||||
### Program IDs et well-known accounts
|
||||
|
||||
Les Program IDs exécutables et les well-known account IDs restent deux concepts distincts.
|
||||
|
||||
`PRGID_*` / `PRGIDPK_*` ne doivent jamais nommer un compte connu non exécutable.
|
||||
|
||||
Le concept historique `native_well_known_account_ids()` est conservé comme direction architecturale possible, mais `0.1.1` ne crée pas une API vide pour ce domaine si aucun well-known account n'est retenu dans sa surface réelle.
|
||||
|
||||
## Impact sur les prereleases suivantes
|
||||
|
||||
`pre.002` ne change pas :
|
||||
|
||||
```text
|
||||
Error / Result
|
||||
```
|
||||
|
||||
`pre.003` est précisé :
|
||||
|
||||
```text
|
||||
Pubkey
|
||||
+ declare_program_id!
|
||||
+ PRGID_* / PRGIDPK_*
|
||||
+ inventaire final des Program IDs fondamentaux
|
||||
+ ProgramIdEntry / entries() / native_program_ids() / find_program_id()
|
||||
+ tests de conformité/unicité
|
||||
```
|
||||
|
||||
Aucune dépendance `solana-sdk-ids` ne doit y être ajoutée.
|
||||
|
||||
## Hors scope inchangé
|
||||
|
||||
Le correctif n'ouvre toujours pas :
|
||||
|
||||
- Logging ;
|
||||
- Config ;
|
||||
- Tauri ;
|
||||
- Wallet/signing ;
|
||||
- codecs wire ;
|
||||
- Interface ;
|
||||
- Program decoding/dispatch registry/`ProgramExecutionPreparer` ;
|
||||
- execution policy/orchestration ;
|
||||
- Transport ;
|
||||
- Store ;
|
||||
- Materializer ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- relecture des règles `VERSION_WORKFLOW.md` et `FILE_CONTRACTS.md` de la base stable ;
|
||||
- confirmation qu'un fix purement documentaire ne modifie pas la version Cargo ;
|
||||
- réaudit ciblé de l'ancien `ks-program-ids` fourni dans l'archive bot3 de référence : `ProgramIdEntry`, `entries()`, `registered_program_ids()`, `native_program_ids()`, `native_well_known_account_ids()` et `find_registered_program_id()` ;
|
||||
- relecture du plan `003-V0_1_1_CORE_FOUNDATION_PLAN.md` après correction ;
|
||||
- contrôle statique du header/version des fichiers livrés ;
|
||||
- contrôle des fins de fichiers ;
|
||||
- contrôle de la structure et du contenu de l'archive ;
|
||||
- vérification de l'absence de fichier Cargo/code/runtime dans ce correctif.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Aucune validation Cargo n'est déclarée pour ce correctif documentaire.
|
||||
|
||||
Les commandes suivantes ne sont pas nécessaires pour démontrer le contenu de ce delta, qui ne modifie aucun artefact compilé :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Elles restent les validations attendues dès la prochaine tranche Rust applicable.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Les décisions nécessaires pour quitter `pre.001` sont considérées validées.
|
||||
|
||||
Les points d'implémentation suivants sont volontairement reportés à leur tranche propriétaire sans bloquer `pre.002` :
|
||||
|
||||
- structure Rust exacte et classification minimale de `ProgramIdEntry` en `pre.003` ;
|
||||
- présence éventuelle d'une recherche dédiée par `Pubkey` ;
|
||||
- inventaire final des IDs natifs/historiques à partir des sources officielles actuelles ;
|
||||
- détail d'expansion de `declare_program_id!` selon l'API exacte de la version `solana-pubkey` retenue.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation/commit de ce correctif :
|
||||
|
||||
```text
|
||||
0.1.1-pre.002 — Error/Result et fondation API
|
||||
```
|
||||
277
deltas/0.1.1/pre.001-fix.002.md
Normal file
277
deltas/0.1.1/pre.001-fix.002.md
Normal file
@@ -0,0 +1,277 @@
|
||||
<!-- file: deltas/0.1.1/pre.001-fix.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.1-pre.001-fix.002
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente appliquée et commitée :
|
||||
|
||||
```text
|
||||
0.1.1-pre.001-fix.001
|
||||
```
|
||||
|
||||
## Type de livraison
|
||||
|
||||
```text
|
||||
ksp-doc-0.1.1-pre.001-fix.002.zip
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Compléter le cadrage Program IDs de `pre.001` avant ouverture de `pre.002`, après réaudit de l'ancien registre/IDLs bot3 et confrontation à des Program IDs publics actuels.
|
||||
|
||||
Ce correctif reste purement documentaire. Il fixe l'architecture de classification/recherche nécessaire pour que le registre KSP puisse ultérieurement retrouver les programmes par domaine, famille, protocole, sous-famille et génération sans dupliquer plusieurs registres statiques.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Aucun fichier de code/build/runtime n'est modifié.
|
||||
|
||||
`workspace.package.version` reste donc :
|
||||
|
||||
```text
|
||||
0.1.1-pre.1
|
||||
```
|
||||
|
||||
Identifiant de livraison/commit :
|
||||
|
||||
```text
|
||||
0.1.1-pre.001-fix.002
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `deltas/0.1.1/pre.001-fix.002.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` — version documentaire 2 -> 3.
|
||||
|
||||
## Décisions acquises
|
||||
|
||||
### Registre canonique unique
|
||||
|
||||
KSP ne doit pas maintenir des tableaux indépendants pour chaque vue (`native`, `amm`, protocole, etc.).
|
||||
|
||||
`entries()` reste la source canonique et les vues/recherches sont dérivées de la classification de chaque `ProgramIdEntry`.
|
||||
|
||||
### Taxonomie minimale
|
||||
|
||||
`ProgramIdEntry` doit être conçu pour porter au minimum les axes descriptifs suivants :
|
||||
|
||||
```text
|
||||
domain
|
||||
family
|
||||
protocol
|
||||
subfamily?
|
||||
program_version?
|
||||
kind
|
||||
```
|
||||
|
||||
auxquels s'ajoutent le code KSP unique, la chaîne Base58 `PRGID_*` et le `Pubkey` `PRGIDPK_*`.
|
||||
|
||||
Les vocabulaires de classification restent extensibles. Core ne doit pas posséder une enum fermée de tous les protocoles/familles futurs.
|
||||
|
||||
### AMM comme famille agrégatrice
|
||||
|
||||
Le réaudit de bot3 montre que les anciens préfixes `AMM`, `CPMM`, `CLMM`, `DLMM`, `STABLE_SWAP` et `WEIGHTED_SWAP` représentent plusieurs spécialisations d'une même grande famille utile pour la recherche.
|
||||
|
||||
La direction KSP devient donc :
|
||||
|
||||
```text
|
||||
family = amm
|
||||
```
|
||||
|
||||
avec des `subfamily` optionnelles telles que :
|
||||
|
||||
```text
|
||||
cpmm
|
||||
clmm
|
||||
dlmm
|
||||
damm
|
||||
stable_swap
|
||||
weighted_swap
|
||||
gamma
|
||||
ssl
|
||||
```
|
||||
|
||||
lorsqu'elles correspondent réellement à une branche/architecture reconnue.
|
||||
|
||||
Une future `amm_program_ids()` doit filtrer le registre canonique sur cette famille et inclure toutes ces sous-familles.
|
||||
|
||||
### `subfamily` et `program_version` sont deux axes indépendants
|
||||
|
||||
La version ne doit pas être détournée en sous-famille.
|
||||
|
||||
Cas audités :
|
||||
|
||||
- SPL Memo : trois Program IDs v1/v3/v4, même lignée fonctionnelle ;
|
||||
- Aldrin AMM : v1/v2, même famille/protocole ;
|
||||
- Meteora DAMM : sous-famille `damm`, versions v1/v2 ;
|
||||
- Meteora DLMM : sous-famille `dlmm` distincte de DAMM ;
|
||||
- Jupiter Aggregator : même lignée, versions v4/v6 ;
|
||||
- GooseFX : GAMMA et SSL sont des branches distinctes ; `SSL v2` possède une version, tandis qu'aucun `v1` ne doit être inventé pour GAMMA.
|
||||
|
||||
### Nom du champ de version
|
||||
|
||||
Le champ retenu conceptuellement est :
|
||||
|
||||
```text
|
||||
program_version
|
||||
```
|
||||
|
||||
et non `protocol_version`.
|
||||
|
||||
Raison : un protocole peut posséder simultanément plusieurs programmes/composants dont les versions évoluent indépendamment. La version doit être attachée à la lignée du Program ID concerné, pas au protocole entier.
|
||||
|
||||
`program_version` est un label de génération reconnu (`v1`, `v2`, `v4`, `v6`, `v0.5`, etc.), pas nécessairement un SemVer.
|
||||
|
||||
### Version d'IDL explicitement distincte
|
||||
|
||||
Une version d'IDL/schema n'est pas une version de Program ID.
|
||||
|
||||
Exemples observés dans les IDLs archivées bot3 :
|
||||
|
||||
- Jupiter V6 : IDL `0.1.0` ;
|
||||
- GooseFX SSL V2 : IDL `0.3.0` ;
|
||||
- GooseFX GAMMA : IDL/schema `0.2.0` ;
|
||||
- Meteora DAMM V2 : IDL/schema distinct de la génération publique `v2`.
|
||||
|
||||
La provenance/version des IDLs appartiendra à la couche Interface/decoder lorsqu'elle sera ouverte ; elle n'entre pas dans `ProgramIdEntry` de Core en `0.1.1`.
|
||||
|
||||
### Nomenclature des constantes
|
||||
|
||||
La nomenclature Rust est séparée de la taxonomie fonctionnelle.
|
||||
|
||||
Le premier segment après `PRGID_`/`PRGIDPK_` est désormais nommé `NAMESPACE`, afin de ne pas confondre le nom public stable avec le champ taxonomique `domain` :
|
||||
|
||||
```text
|
||||
PRGID_<NAMESPACE>_<PROGRAM_OR_FAMILY>_<VARIANT?>_<VERSION?>
|
||||
PRGIDPK_<NAMESPACE>_<PROGRAM_OR_FAMILY>_<VARIANT?>_<VERSION?>
|
||||
```
|
||||
|
||||
Exemples :
|
||||
|
||||
```text
|
||||
PRGID_SPL_MEMO_V3
|
||||
PRGIDPK_SPL_MEMO_V3
|
||||
|
||||
PRGID_METEORA_DAMM_V2
|
||||
PRGIDPK_METEORA_DAMM_V2
|
||||
|
||||
PRGID_GOOSEFX_GAMMA
|
||||
PRGIDPK_GOOSEFX_GAMMA
|
||||
|
||||
PRGID_GOOSEFX_SSL_V2
|
||||
PRGIDPK_GOOSEFX_SSL_V2
|
||||
```
|
||||
|
||||
Le symbole public ne doit pas être renommé uniquement parce qu'une classification fonctionnelle est affinée plus tard.
|
||||
|
||||
### Recherches et vues prévues
|
||||
|
||||
Direction conceptuelle :
|
||||
|
||||
```text
|
||||
ProgramIdEntry
|
||||
ProgramIdFilter
|
||||
entries()
|
||||
program_ids(filter)
|
||||
native_program_ids()
|
||||
find_program_id()
|
||||
```
|
||||
|
||||
Le filtre doit pouvoir combiner plusieurs critères.
|
||||
|
||||
Des helpers de recherche peuvent être proposés :
|
||||
|
||||
```text
|
||||
program_ids_by_domain(...)
|
||||
program_ids_by_family(...)
|
||||
program_ids_by_protocol(...)
|
||||
```
|
||||
|
||||
`native_program_ids()` reste une vue Core réelle de `0.1.1`.
|
||||
|
||||
`amm_program_ids()` est explicitement prévue pour la surface future possédant des IDs AMM, mais n'est pas créée vide pendant `0.1.1` puisque les AMM sont hors scope fonctionnel de la release.
|
||||
|
||||
Les vues doivent pouvoir être des iterators/views du registre canonique afin d'éviter la duplication de tableaux statiques.
|
||||
|
||||
## Réaudit effectué
|
||||
|
||||
### Archive bot3
|
||||
|
||||
Le réaudit a porté sur :
|
||||
|
||||
- `ks-program-ids` et ses 137 entrées historiques ;
|
||||
- `ProgramIdEntry`, `entries()`, `native_program_ids()` et la recherche historique ;
|
||||
- les familles historiques AMM/CLMM/CPMM/DLMM/router/orderbook/etc. ;
|
||||
- les IDLs archivées, notamment GooseFX GAMMA/V2, Meteora DAMM/DLMM, Jupiter v4/v6, CCTP v1/v2, Raydium CLMM/CPMM, OpenBook v2 et Marginfi v2.
|
||||
|
||||
L'inventaire montre également qu'un même protocole traverse plusieurs familles : Jupiter, Raydium, Meteora, Kamino, MetaDAO, Pump, Metaplex, Orca, etc. `protocol` doit donc être un axe séparé de `family`.
|
||||
|
||||
### Sources externes actuelles
|
||||
|
||||
Contrôles représentatifs effectués le 2026-08-14 :
|
||||
|
||||
- Agave runtime `fetch-spl.sh` : Memo 1.0.0, 3.0.0 et 4.0.0 sont associés à trois Program IDs distincts ;
|
||||
- `spl-memo-interface` actuel : modules `v1`, `v3`, `v4` distincts ;
|
||||
- Solana Explorer et Solscan : présence/identification des Program IDs Memo et de programmes DEX audités ;
|
||||
- GooseFX officiel : GAMMA est une lignée AMM distincte ; l'écosystème publie également la lignée SSL ;
|
||||
- Meteora officiel : DAMM v1, DAMM v2 et DLMM sont des surfaces distinctes ;
|
||||
- Jupiter officiel : plusieurs générations du Swap Aggregator sont distinguées par Program ID.
|
||||
|
||||
Ces contrôles sont utilisés comme validation de taxonomie, pas comme dépendances KSP.
|
||||
|
||||
## Impact sur `pre.003`
|
||||
|
||||
`pre.003` devra désormais :
|
||||
|
||||
- implémenter `ProgramIdEntry` avec une taxonomie compatible avec les axes acquis ;
|
||||
- implémenter `ProgramIdFilter` ou une forme équivalente permettant les intersections ;
|
||||
- dériver `native_program_ids()` du registre canonique ;
|
||||
- tester les filtres par domaine/famille/protocole/sous-famille/version/kind ;
|
||||
- ne jamais confondre génération du programme et version d'IDL ;
|
||||
- préserver la possibilité d'ajouter plus tard `amm_program_ids()` sans modifier la structure fondamentale du registre.
|
||||
|
||||
## Hors scope inchangé
|
||||
|
||||
Ce correctif n'ajoute aucun Program ID SPL/DEX au code de `0.1.1` et n'ouvre toujours pas :
|
||||
|
||||
- Logging ;
|
||||
- Config ;
|
||||
- Tauri ;
|
||||
- Wallet/signing ;
|
||||
- codecs wire ;
|
||||
- Interface/IDL runtime ;
|
||||
- Program decoding/dispatch ;
|
||||
- execution ;
|
||||
- Transport ;
|
||||
- Store ;
|
||||
- Materializer ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- réaudit du registre `ks-program-ids` de l'archive bot3 ;
|
||||
- inventaire des Program IDs versionnés et des protocoles présents dans plusieurs familles ;
|
||||
- inspection ciblée des IDLs multi-version/à plusieurs sous-familles ;
|
||||
- confrontation représentative avec Agave/SPL, Solana Explorer, Solscan, GooseFX, Meteora et Jupiter ;
|
||||
- relecture du plan après modification ;
|
||||
- contrôle statique des headers/version documentaire ;
|
||||
- contrôle des fins de fichiers ;
|
||||
- contrôle de l'absence de modification Cargo/code/runtime dans ce fix.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Aucune validation Cargo n'est requise pour ce fix exclusivement documentaire et aucune n'est déclarée réussie.
|
||||
|
||||
## Suite
|
||||
|
||||
Après application et commit de ce correctif, le cadrage `pre.001` peut être considéré comme clôturé et la session peut passer à :
|
||||
|
||||
```text
|
||||
0.1.1-pre.002 — Error/Result et fondation API
|
||||
```
|
||||
219
deltas/0.1.1/pre.001.md
Normal file
219
deltas/0.1.1/pre.001.md
Normal file
@@ -0,0 +1,219 @@
|
||||
<!-- file: deltas/0.1.1/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.1-pre.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Release stable/taguée attendue :
|
||||
|
||||
```text
|
||||
v0.0.3
|
||||
```
|
||||
|
||||
L'archive de base fournie contient bien la version Cargo stable `0.0.3` et le delta final `deltas/0.0.3/rel.001.md`.
|
||||
|
||||
Elle ne contient pas `.git` : le working tree réel, le commit et le tag `v0.0.3` doivent être vérifiés sur le dépôt cible avant commit de ce delta.
|
||||
|
||||
## Objectif
|
||||
|
||||
Ouvrir `0.1.1` par la prerelease obligatoire de brainstorming, audit et planification, sans développement fonctionnel Core.
|
||||
|
||||
Cette tranche :
|
||||
|
||||
- inventorie la surface réelle de `ksp-core-lib` ;
|
||||
- borne les contrats N1 de la release ;
|
||||
- propose le contrat ouvert `Error` / `Result` ;
|
||||
- borne les Program IDs fondamentaux ;
|
||||
- audite les primitives Solana/Anza actuelles nécessaires ;
|
||||
- fixe la stratégie d'API, tests et dépendances ;
|
||||
- dimensionne `pre.002` à `pre.005` ;
|
||||
- confirme les hors-scope.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.0.3
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.1-pre.1
|
||||
```
|
||||
|
||||
L'identifiant Cargo respecte SemVer sans zéro initial ; l'identifiant de livraison reste `0.1.1-pre.001`.
|
||||
|
||||
Le header de `Cargo.toml` passe de version 17 à 18.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`
|
||||
- `deltas/0.1.1/pre.001.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- `ROADMAP.md`
|
||||
- `docs/plans/000-README.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Inventaire Core
|
||||
|
||||
`ksp-core-lib` est encore un squelette volontairement minimal :
|
||||
|
||||
- aucun module fonctionnel ;
|
||||
- aucune dépendance externe ;
|
||||
- aucun type public autre que la documentation de crate ;
|
||||
- aucun test ;
|
||||
- aucune surface Error/Result ou Program IDs existante à préserver pour compatibilité.
|
||||
|
||||
Cette situation permet de définir le contrat sans dette de compatibilité interne KSP.
|
||||
|
||||
## Décisions de planification
|
||||
|
||||
### Error/Result
|
||||
|
||||
Le modèle historique bot3 avec enum centrale de domaines n'est pas migré.
|
||||
|
||||
La direction retenue pour `pre.002` est :
|
||||
|
||||
- `Error` structuré ;
|
||||
- `ErrorCode` ouvert avec domaine/code statiques ;
|
||||
- `ErrorContext` structuré ;
|
||||
- message lisible ;
|
||||
- cause standard optionnelle `Send + Sync` ;
|
||||
- alias `Result<T>` ;
|
||||
- aucune connaissance dans Core des futurs domaines Config/Logging/Wallet/Transport/Store/Tauri/protocoles ;
|
||||
- aucune liste centrale de conversions d'erreurs externes.
|
||||
|
||||
Le détail complet et les invariants figurent dans `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`.
|
||||
|
||||
### Solana/Anza
|
||||
|
||||
Sources officielles consultées le 2026-08-14 : dépôt `anza-xyz/solana-sdk`, notamment la crate Pubkey, la primitive Address, `sdk-ids` et le manifest workspace.
|
||||
|
||||
État vérifié :
|
||||
|
||||
```text
|
||||
solana-pubkey 4.3.0
|
||||
solana-address 2.7.0
|
||||
solana-sdk-ids 3.1.0 package
|
||||
Solana SDK workspace MSRV 1.89.0
|
||||
```
|
||||
|
||||
Direction retenue :
|
||||
|
||||
- runtime Core : `solana-pubkey` seulement, lorsque `pre.003` implémentera réellement les Program IDs ;
|
||||
- `default-features = false` tant qu'aucune feature supplémentaire n'est démontrée nécessaire ;
|
||||
- `Pubkey` réexporté depuis `ksp_core_lib` ;
|
||||
- pas de dépendance directe KSP à `solana-address` ;
|
||||
- `solana-sdk-ids` seulement comme dev-dependency candidate pour les tests de conformité, pas comme dépendance runtime ;
|
||||
- aucune autre primitive Solana autorisée n'est ajoutée par anticipation.
|
||||
|
||||
Les versions seront revérifiées juste avant leur ajout réel au manifeste.
|
||||
|
||||
### Program IDs
|
||||
|
||||
Première surface proposée : 17 Program IDs fondamentaux exposés par la source officielle Solana SDK, incluant les loaders et précompiles de la frontière runtime :
|
||||
|
||||
- Address Lookup Table ;
|
||||
- BPF Loader ;
|
||||
- BPF Loader deprecated ;
|
||||
- BPF Loader Upgradeable ;
|
||||
- Compute Budget ;
|
||||
- Config ;
|
||||
- Ed25519 precompile ;
|
||||
- Feature ;
|
||||
- Loader v4 ;
|
||||
- Native Loader ;
|
||||
- Secp256k1 precompile ;
|
||||
- Secp256r1 precompile ;
|
||||
- Stake ;
|
||||
- System ;
|
||||
- Vote ;
|
||||
- ZK ElGamal Proof ;
|
||||
- ZK Token Proof.
|
||||
|
||||
Sont exclus : sysvars, incinerator, stake config account, SPL/protocoles, registre enumerable et alias bot3 historiques.
|
||||
|
||||
### Autres primitives
|
||||
|
||||
Aucune autre primitive commune n'est justifiée maintenant.
|
||||
|
||||
`Hash`, `Nonce`, Keypair, Signer, identité/version de module et provenance restent reportés jusqu'à un besoin concret.
|
||||
|
||||
## Prereleases prévues
|
||||
|
||||
```text
|
||||
pre.001 audit + brainstorming + plan
|
||||
pre.002 Error/Result + tests publics
|
||||
pre.003 Pubkey + Program IDs + conformité Solana
|
||||
pre.004 intégration Core + audits + compléments strictement justifiés
|
||||
pre.005 validations finales + docs/cleanup + prompt 0.1.2
|
||||
```
|
||||
|
||||
Le découpage reste souple ; une tranche trop large sera scindée plutôt que surchargée.
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
- Logging ;
|
||||
- Config ;
|
||||
- Tauri ;
|
||||
- Wallet/signing ;
|
||||
- codecs wire ;
|
||||
- Interface ;
|
||||
- Program decoding/registry/preparation ;
|
||||
- Execution ;
|
||||
- Transport ;
|
||||
- Store ;
|
||||
- Materializer ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
Dans l'environnement de préparation de ce delta :
|
||||
|
||||
- lecture/audit de l'archive complète `0.0.3` fournie ;
|
||||
- vérification de la version stable `0.0.3` dans le manifest ;
|
||||
- vérification de la présence du delta `0.0.3/rel.001` et du prompt final `0.1.1` ;
|
||||
- inventaire de `ksp-core-lib` ;
|
||||
- lecture des règles, plans et documents d'architecture requis par le prompt ;
|
||||
- audit de l'ancien `ks-core` / `ks-program-ids` de l'archive bot3 fournie comme référence historique, sans le traiter comme source de vérité KSP ;
|
||||
- vérification des versions et surfaces actuelles sur les sources officielles Anza/Solana ;
|
||||
- parsing TOML statique du manifest modifié ;
|
||||
- contrôle statique des headers `file:` / `version:` des fichiers ajoutés/modifiés ;
|
||||
- contrôle statique des liens Markdown locaux après modification.
|
||||
|
||||
## 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 après application du delta. Aucun succès Cargo n'est déclaré par ce delta.
|
||||
|
||||
Le tag Git et le working tree ne peuvent pas non plus être vérifiés depuis l'archive fournie, qui ne contient pas `.git`.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
- validation par le user du modèle Error/Result proposé avant `pre.002` ;
|
||||
- choix exact des méthodes ergonomiques de contexte et du format `Display`, à stabiliser par tests en `pre.002` ;
|
||||
- revérification de la version Solana et du MSRV juste avant `pre.003` ;
|
||||
- confirmation de l'utilité de `solana-sdk-ids` comme dev-dependency de conformité au moment où les tests sont écrits.
|
||||
|
||||
Aucune question ouverte ne justifie de commencer le développement fonctionnel avant validation de ce plan.
|
||||
127
deltas/0.1.1/pre.002-fix.001.md
Normal file
127
deltas/0.1.1/pre.002-fix.001.md
Normal file
@@ -0,0 +1,127 @@
|
||||
<!-- file: deltas/0.1.1/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.1-pre.002-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Commit de livraison attendu :
|
||||
|
||||
```text
|
||||
v0.1.1-pre.002
|
||||
```
|
||||
|
||||
La version Cargo reste :
|
||||
|
||||
```text
|
||||
0.1.1-pre.2
|
||||
```
|
||||
|
||||
Ce correctif ne modifie aucun contrat public de `ksp-core-lib` et ne justifie donc aucune nouvelle prerelease Cargo.
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger les deux warnings remontés par les validations réelles de `0.1.1-pre.002` avant d'ouvrir `0.1.1-pre.003`.
|
||||
|
||||
Les validations exécutées sur le dépôt cible ont confirmé que :
|
||||
|
||||
- `cargo fmt --all` réussit ;
|
||||
- `cargo check --workspace` réussit ;
|
||||
- `cargo test --workspace` réussit avec 6 tests unitaires, 1 test d'intégration et 0 échec ;
|
||||
- `cargo clippy --workspace --all-targets` termine sans erreur mais remonte deux warnings à corriger.
|
||||
|
||||
Warnings observés :
|
||||
|
||||
1. `clippy::extra_unused_type_parameters` sur le helper de vérification `Send + Sync` ;
|
||||
2. `missing_docs` sur la crate de test d'intégration `tests/public_api.rs`.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `crates/ksp-core-lib/unit_tests/error.rs`
|
||||
- `crates/ksp-core-lib/tests/public_api.rs`
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
- `deltas/0.1.1/pre.002-fix.001.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Correction `Send + Sync`
|
||||
|
||||
Le helper de test :
|
||||
|
||||
```text
|
||||
assert_send_sync<T>()
|
||||
```
|
||||
|
||||
utilisait `T` uniquement dans ses bornes de trait. Clippy considère alors le paramètre de type comme inutilisé avec `extra_unused_type_parameters`.
|
||||
|
||||
Le helper reçoit désormais un `std::marker::PhantomData<T>` :
|
||||
|
||||
```text
|
||||
assert_send_sync<T>(PhantomData<T>)
|
||||
```
|
||||
|
||||
et le test fournit `PhantomData<crate::Error>`.
|
||||
|
||||
Cette forme conserve exactement l'objectif du test de compilation : l'appel ne compile que si `crate::Error` satisfait `Send + Sync`, tout en utilisant réellement le paramètre générique et sans ajouter de dépendance, d'import ou de logique runtime significative.
|
||||
|
||||
## Correction `missing_docs`
|
||||
|
||||
Le test d'intégration `crates/ksp-core-lib/tests/public_api.rs` constitue une crate Rust indépendante lors de sa compilation.
|
||||
|
||||
Une rustdoc crate-level est ajoutée :
|
||||
|
||||
```text
|
||||
//! Integration tests for the public `ksp-core-lib` error contract.
|
||||
```
|
||||
|
||||
Cela satisfait le lint workspace `missing_docs = warn` sans désactiver le lint et sans documenter artificiellement les helpers privés du test.
|
||||
|
||||
## Headers de fichiers
|
||||
|
||||
Les deux fichiers Rust modifiés passent de :
|
||||
|
||||
```text
|
||||
version: 1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
version: 2
|
||||
```
|
||||
|
||||
## Contrat public
|
||||
|
||||
Aucun changement.
|
||||
|
||||
Les éléments suivants restent strictement identiques à `0.1.1-pre.002` :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorContext
|
||||
ksp_core_lib::Result<T>
|
||||
```
|
||||
|
||||
Le plan `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` reste en version documentaire 5 : aucune décision d'architecture ou d'API n'est modifiée par ce correctif.
|
||||
|
||||
## Dépendances
|
||||
|
||||
Aucune dépendance ajoutée ou modifiée.
|
||||
|
||||
## Validations à exécuter après application
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Le résultat attendu de ce correctif est l'absence des deux warnings qui ont motivé `pre.002-fix.001`.
|
||||
|
||||
Aucune validation du correctif lui-même n'est déclarée réussie tant que ces commandes n'ont pas été exécutées sur le dépôt cible.
|
||||
215
deltas/0.1.1/pre.002.md
Normal file
215
deltas/0.1.1/pre.002.md
Normal file
@@ -0,0 +1,215 @@
|
||||
<!-- file: deltas/0.1.1/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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<ErrorContext>
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>
|
||||
```
|
||||
|
||||
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<ExternalError>` 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
|
||||
<domain>.<code>: <message>
|
||||
```
|
||||
|
||||
Le contexte et la chaîne de causes ne sont pas injectés automatiquement dans ce rendu.
|
||||
|
||||
### `Result<T>`
|
||||
|
||||
La façade expose :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Result<T> = std::result::Result<T, ksp_core_lib::Error>
|
||||
```
|
||||
|
||||
## 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 `<domain>.<code>: <message>`.
|
||||
|
||||
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.
|
||||
120
deltas/0.1.1/pre.003-fix.001.md
Normal file
120
deltas/0.1.1/pre.003-fix.001.md
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- file: deltas/0.1.1/pre.003-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.1-pre.003-fix.001` — Cargo workspace + Clippy
|
||||
|
||||
## Statut
|
||||
|
||||
Correctif de `0.1.1-pre.003` après application et validation partielle de la tranche par le user.
|
||||
|
||||
Version technique du correctif :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.3.fix.1"
|
||||
```
|
||||
|
||||
Le périmètre fonctionnel de `pre.003` ne change pas.
|
||||
|
||||
## Base et validations reçues
|
||||
|
||||
Sur `0.1.1-pre.3`, le user a exécuté avec succès :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
```
|
||||
|
||||
Résultats communiqués :
|
||||
|
||||
- 14 tests unitaires passent ;
|
||||
- 3 tests d'intégration `public_api` passent ;
|
||||
- `cargo tree` résout `solana-pubkey 4.3.0` puis `solana-address 2.7.0` ;
|
||||
- `cargo tree -d` ne rapporte aucun doublon.
|
||||
|
||||
`cargo clippy --workspace --all-targets` échoue sur trois closures de `program_ids.rs` à cause de la règle workspace `clippy::implicit_return = deny`.
|
||||
|
||||
## Correction Cargo
|
||||
|
||||
La déclaration directe suivante dans `crates/ksp-core-lib/Cargo.toml` est supprimée :
|
||||
|
||||
```toml
|
||||
solana-pubkey = { version = "4.3.0", default-features = false }
|
||||
```
|
||||
|
||||
La dépendance appartient désormais au manifeste workspace :
|
||||
|
||||
```toml
|
||||
[workspace.dependencies]
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
```
|
||||
|
||||
La crate propriétaire la consomme uniquement par héritage :
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
solana-pubkey.workspace = true
|
||||
```
|
||||
|
||||
La contrainte `^4.3` exprime la génération compatible voulue par KSP ; le patch concret reste résolu par Cargo/lockfile.
|
||||
|
||||
## Règles ajoutées
|
||||
|
||||
`docs/rules/RULES_DEPENDENCIES.md` ajoute `DEP-CARGO-001` à `DEP-CARGO-005` afin de rendre obligatoire :
|
||||
|
||||
- la centralisation des dépendances externes sous `[workspace.dependencies]` ;
|
||||
- l'usage de `.workspace = true` dans les crates membres ;
|
||||
- la centralisation des contraintes de version et options communes ;
|
||||
- la convention de contrainte caret `^M.m` pour une génération majeure/mineure compatible ;
|
||||
- la distinction entre contrainte de manifeste et résolution concrète du lockfile.
|
||||
|
||||
## Correction Clippy
|
||||
|
||||
Les trois closures signalées utilisent maintenant un `return` explicite dans leur corps :
|
||||
|
||||
- filtre générique de `program_ids(...)` ;
|
||||
- recherche texte `find_program_id(...)` ;
|
||||
- recherche typée `find_program_pubkey(...)`.
|
||||
|
||||
Aucune signature ni sémantique de l'API publique ne change.
|
||||
|
||||
## Documentation
|
||||
|
||||
`docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` est aligné sur la nouvelle règle Cargo et documente la contrainte workspace `^4.3` au lieu d'une déclaration locale `4.3.0`.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-core-lib/Cargo.toml
|
||||
crates/ksp-core-lib/src/program_ids.rs
|
||||
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||
docs/rules/RULES_DEPENDENCIES.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.1.1/pre.003-fix.001.md
|
||||
```
|
||||
|
||||
## Validations de ce correctif
|
||||
|
||||
Non exécutées dans l'environnement de préparation du delta, qui ne fournit pas la toolchain Cargo/Rust du dépôt.
|
||||
|
||||
À exécuter après application :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
```
|
||||
|
||||
## Hors scope
|
||||
|
||||
Ce correctif n'ajoute ni Program ID, ni dépendance, ni primitive Core supplémentaire et ne démarre pas `0.1.1-pre.004`.
|
||||
284
deltas/0.1.1/pre.003.md
Normal file
284
deltas/0.1.1/pre.003.md
Normal file
@@ -0,0 +1,284 @@
|
||||
<!-- file: deltas/0.1.1/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.1-pre.003` — Pubkey + Program IDs
|
||||
|
||||
## Statut
|
||||
|
||||
Tranche fonctionnelle `0.1.1-pre.003` préparée après validation réussie par le user de `0.1.1-pre.002-fix.001`.
|
||||
|
||||
La base utilisateur observée avant ce delta utilise :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.2.fix.1"
|
||||
```
|
||||
|
||||
Le présent delta ouvre :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.3"
|
||||
```
|
||||
|
||||
## Validation de la base précédente
|
||||
|
||||
Le user a exécuté avec succès avant ce delta :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Les six tests unitaires Error et le test d'intégration public Error passent sans warning dans cette validation.
|
||||
|
||||
## Audit Solana/Anza revérifié
|
||||
|
||||
Au 2026-08-14 :
|
||||
|
||||
- `solana-pubkey 4.3.0` est la version publiée courante observée sur crates.io ;
|
||||
- son MSRV publié est Rust `1.89.0` ;
|
||||
- la validation précédente du user expose une génération Clippy `rust-1.94.0`, donc la toolchain observée satisfait ce MSRV ;
|
||||
- `Pubkey` reste la façade de compatibilité officielle sur l'`Address` Solana actuel ;
|
||||
- `Pubkey::from_str_const` permet le décodage Base58 compile-time nécessaire à la macro KSP ;
|
||||
- `solana-sdk-ids` est consulté uniquement comme source d'audit et n'entre pas dans le graphe Cargo KSP.
|
||||
|
||||
Sources externes revérifiées pendant cette tranche :
|
||||
|
||||
- crates.io / docs.rs pour `solana-pubkey 4.3.0` ;
|
||||
- `anza-xyz/solana-sdk`, `sdk-ids/src/lib.rs`, pour les identifiants fondamentaux actuellement publiés ;
|
||||
- SIMD-0204 et la documentation Anza pour le Slashing Program `S1ashing11111111111111111111111111111111111`.
|
||||
|
||||
## Dépendance Core
|
||||
|
||||
`crates/ksp-core-lib/Cargo.toml` ajoute uniquement :
|
||||
|
||||
```toml
|
||||
solana-pubkey = { version = "4.3.0", default-features = false }
|
||||
```
|
||||
|
||||
Aucun codec wire, client RPC, umbrella SDK ou registre `solana-sdk-ids` n'est ajouté.
|
||||
|
||||
`ksp_core_lib::Pubkey` réexporte `solana_pubkey::Pubkey` depuis la façade Core.
|
||||
|
||||
## Macro KSP de Program ID
|
||||
|
||||
La macro publique :
|
||||
|
||||
```text
|
||||
ksp_core_lib::declare_program_id!
|
||||
```
|
||||
|
||||
possède la chaîne Base58 une seule fois et produit simultanément :
|
||||
|
||||
```text
|
||||
PRGID_* : &'static str
|
||||
PRGIDPK_* : Pubkey
|
||||
```
|
||||
|
||||
La représentation typée est construite avec `Pubkey::from_str_const`, sans parsing runtime, `unwrap`, `expect`, `panic` ni opérateur `?`.
|
||||
|
||||
La macro ne génère pas de symboles génériques `ID`, `id()` ou `check_id()` et peut donc être utilisée plusieurs fois dans une même crate/module.
|
||||
|
||||
## Première surface Program IDs Core
|
||||
|
||||
La tranche fixe le premier registre à 18 Program IDs.
|
||||
|
||||
Les 17 valeurs de la surface officielle actuelle Anza `solana-sdk-ids` sont possédées localement par KSP :
|
||||
|
||||
1. Address Lookup Table ;
|
||||
2. BPF Loader historique v1 ;
|
||||
3. BPF Loader v2 ;
|
||||
4. BPF Loader Upgradeable ;
|
||||
5. Compute Budget ;
|
||||
6. Config ;
|
||||
7. Ed25519 precompile ;
|
||||
8. Feature ;
|
||||
9. Loader v4 ;
|
||||
10. Native Loader ;
|
||||
11. Secp256k1 precompile ;
|
||||
12. Secp256r1 precompile ;
|
||||
13. Stake ;
|
||||
14. System ;
|
||||
15. Vote ;
|
||||
16. ZK ElGamal Proof ;
|
||||
17. ZK Token Proof.
|
||||
|
||||
Le dix-huitième identifiant est :
|
||||
|
||||
```text
|
||||
PRGID_SOLANA_SLASHING = "S1ashing11111111111111111111111111111111111"
|
||||
```
|
||||
|
||||
Son statut de programme enshrined et son adresse sont confirmés séparément par SIMD-0204/Anza ; sa présence ne dépend donc pas de l'ancien registre bot3.
|
||||
|
||||
Les sysvars, `StakeConfig`, l'incinerator et les autres well-known accounts restent exclus du registre Program IDs.
|
||||
|
||||
## Nomenclature publique
|
||||
|
||||
Chaque entrée possède une paire `PRGID_*` / `PRGIDPK_*` au crate-root.
|
||||
|
||||
Exemples :
|
||||
|
||||
```text
|
||||
PRGID_SOLANA_SYSTEM
|
||||
PRGIDPK_SOLANA_SYSTEM
|
||||
|
||||
PRGID_SOLANA_LOADER_BPF_V1
|
||||
PRGIDPK_SOLANA_LOADER_BPF_V1
|
||||
|
||||
PRGID_SOLANA_PRECOMPILE_ED25519
|
||||
PRGIDPK_SOLANA_PRECOMPILE_ED25519
|
||||
|
||||
PRGID_SOLANA_SLASHING
|
||||
PRGIDPK_SOLANA_SLASHING
|
||||
```
|
||||
|
||||
Le suffixe est strictement identique entre les deux représentations.
|
||||
|
||||
## Registre canonique
|
||||
|
||||
`ProgramIdEntry` porte :
|
||||
|
||||
```text
|
||||
code
|
||||
name
|
||||
program_id
|
||||
pubkey
|
||||
domain
|
||||
family
|
||||
protocol
|
||||
subfamily
|
||||
program_version
|
||||
kind
|
||||
```
|
||||
|
||||
Les axes fonctionnels restent extensibles sous forme de chaînes. Aucun enum central fermé des domaines, familles ou protocoles futurs n'est créé.
|
||||
|
||||
`ProgramIdKind` est limité à la classification technique :
|
||||
|
||||
```text
|
||||
Program
|
||||
Loader
|
||||
Precompile
|
||||
EnshrinedProgram
|
||||
```
|
||||
|
||||
La première taxonomie Core utilise :
|
||||
|
||||
```text
|
||||
domain = solana
|
||||
protocol = solana
|
||||
|
||||
family = runtime
|
||||
family = consensus
|
||||
family = loader
|
||||
family = precompile
|
||||
family = proof
|
||||
```
|
||||
|
||||
`program_version` reste distinct de `subfamily`. Les BPF loaders v1/v2 et Loader v4 utilisent l'axe version ; la branche BPF utilise séparément `subfamily = bpf`.
|
||||
|
||||
## API de recherche et vues
|
||||
|
||||
La façade expose :
|
||||
|
||||
```text
|
||||
entries()
|
||||
program_ids(filter)
|
||||
native_program_ids()
|
||||
program_ids_by_domain(...)
|
||||
program_ids_by_family(...)
|
||||
program_ids_by_protocol(...)
|
||||
find_program_id(...)
|
||||
find_program_pubkey(...)
|
||||
```
|
||||
|
||||
`ProgramIdFilter` peut combiner :
|
||||
|
||||
```text
|
||||
domain
|
||||
family
|
||||
protocol
|
||||
subfamily
|
||||
program_version
|
||||
kind
|
||||
```
|
||||
|
||||
Toutes les vues sont construites à partir du registre canonique unique. Elles retournent des iterators paresseux sans dupliquer un tableau statique par catégorie.
|
||||
|
||||
Une future fonction `amm_program_ids()` pourra donc devenir une vue `family = amm` sans changement structurel de `ProgramIdEntry`.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
`unit_tests/program_ids.rs` vérifie notamment :
|
||||
|
||||
- cohérence texte/`Pubkey` des déclarations ;
|
||||
- présence exacte des 18 Program IDs Core ;
|
||||
- unicité des codes, Base58 et `Pubkey` ;
|
||||
- correspondance de chaque `Pubkey` avec la Base58 KSP ;
|
||||
- intersection des six axes de filtre ;
|
||||
- tailles attendues des familles Core actuelles ;
|
||||
- recherche texte et `Pubkey` ;
|
||||
- absence de l'incinerator, `StakeConfig` et du Clock sysvar.
|
||||
|
||||
`tests/public_api.rs` vérifie en consommateur externe :
|
||||
|
||||
- la macro `declare_program_id!` ;
|
||||
- les constantes `PRGID_*` / `PRGIDPK_*` ;
|
||||
- `Pubkey` ;
|
||||
- les vues du registre ;
|
||||
- les getters de `ProgramIdEntry` ;
|
||||
- le filtrage public combiné.
|
||||
|
||||
## Documentation
|
||||
|
||||
`docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` passe en version documentaire 6 afin de :
|
||||
|
||||
- fixer l'inventaire Core à 18 IDs ;
|
||||
- tracer la vérification du Slashing Program ;
|
||||
- enregistrer `solana-pubkey 4.3.0` comme dépendance effectivement introduite ;
|
||||
- fermer la question de MSRV de `pre.003` ;
|
||||
- fixer `ProgramIdKind` et les retours iterator des vues ;
|
||||
- compléter l'API publique réellement implémentée.
|
||||
|
||||
## Hors scope préservé
|
||||
|
||||
Cette tranche n'ajoute toujours pas :
|
||||
|
||||
- SPL Token/Token-2022/ATA/Memo ;
|
||||
- Metaplex ou DEX ;
|
||||
- well-known accounts ;
|
||||
- codecs Borsh/Wincode ;
|
||||
- decoder/executor/IDL ;
|
||||
- RPC/WS ;
|
||||
- Wallet ;
|
||||
- Logging ;
|
||||
- Config ;
|
||||
- Store ;
|
||||
- applications.
|
||||
|
||||
## Validations à exécuter sur le dépôt cible
|
||||
|
||||
Cette livraison ne déclare aucune validation Cargo non exécutée dans l'environnement de génération.
|
||||
|
||||
Après application :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
```
|
||||
|
||||
Les deux commandes `cargo tree` doivent notamment confirmer l'absence de `solana-sdk-ids` et permettre de contrôler le graphe/features réellement résolus.
|
||||
|
||||
## Suite
|
||||
|
||||
Si les validations sont propres, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.1-pre.004 — intégration Core + audits
|
||||
```
|
||||
120
deltas/0.1.1/pre.004.md
Normal file
120
deltas/0.1.1/pre.004.md
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- file: deltas/0.1.1/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.1-pre.004` — intégration Core + audits
|
||||
|
||||
## Statut
|
||||
|
||||
Tranche d'intégration préparée après validation réussie par le user de `0.1.1-pre.003-fix.001`.
|
||||
|
||||
La base validée utilise :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.3.fix.1"
|
||||
```
|
||||
|
||||
Le présent delta ouvre :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.4"
|
||||
```
|
||||
|
||||
## Validation de la base précédente
|
||||
|
||||
Le user a exécuté avec succès :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
```
|
||||
|
||||
Résultats communiqués :
|
||||
|
||||
- 14 tests unitaires passent ;
|
||||
- 3 tests d'intégration publics passent ;
|
||||
- Clippy ne rapporte plus de warning ou d'erreur ;
|
||||
- `cargo tree` résout `solana-pubkey 4.3.0` puis `solana-address 2.7.0` et leurs dépendances fondamentales ;
|
||||
- `cargo tree -d` ne rapporte aucun doublon.
|
||||
|
||||
## Audit d'intégration Core
|
||||
|
||||
L'audit conjoint de `Error` / `Result`, `Pubkey` et Program IDs ne démontre aucun besoin de primitive N1 supplémentaire dans `0.1.1`.
|
||||
|
||||
La façade conserve les propriétés attendues :
|
||||
|
||||
- les modules d'implémentation restent privés ;
|
||||
- les contrats consommables sont réexportés explicitement au crate-root ;
|
||||
- `ksp-core-lib` ne dépend d'aucune couche KSP supérieure ;
|
||||
- `solana-pubkey` reste l'unique dépendance externe directe de Core ;
|
||||
- aucun codec wire, RPC/client, signer/keypair, store, logging ou configuration n'est introduit ;
|
||||
- le registre Program IDs reste descriptif et distinct de tout registry de decoder/executor.
|
||||
|
||||
## Rustdocs
|
||||
|
||||
La rustdoc crate-level de `ksp-core-lib` est complétée afin de rendre explicites :
|
||||
|
||||
- le contrat d'erreur commun ;
|
||||
- la propriété de `Pubkey` dans la façade Core ;
|
||||
- la propriété KSP du registre de Program IDs fondamentaux ;
|
||||
- l'absence de dépendance inverse vers les domaines supérieurs.
|
||||
|
||||
## Test public renforcé
|
||||
|
||||
`tests/public_api.rs` déclare désormais :
|
||||
|
||||
```text
|
||||
const TEST_ERROR_CODE: ksp_core_lib::ErrorCode = ...
|
||||
```
|
||||
|
||||
Le test confirme ainsi depuis une crate consommatrice que `ErrorCode::new(...)` est réellement utilisable en contexte `const`, ce qui permettra aux futures crates de domaine de posséder leurs codes sans faire connaître leurs domaines à Core.
|
||||
|
||||
Aucune signature publique n'est modifiée.
|
||||
|
||||
## Documentation du plan
|
||||
|
||||
`docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` passe en version documentaire 8 pour enregistrer le résultat de l'audit `pre.004` et ajouter le contrôle explicite des features résolues :
|
||||
|
||||
```bash
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-core-lib/src/lib.rs
|
||||
crates/ksp-core-lib/tests/public_api.rs
|
||||
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.1.1/pre.004.md
|
||||
```
|
||||
|
||||
## Validations à exécuter sur le dépôt cible
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
Aucune validation non exécutée dans l'environnement de préparation n'est déclarée réussie pour ce delta.
|
||||
|
||||
## Suite
|
||||
|
||||
Si cette tranche est propre, la suite prévue est :
|
||||
|
||||
```text
|
||||
0.1.1-pre.005 — clôture, documentation finale et prompt 0.1.2
|
||||
```
|
||||
191
deltas/0.1.1/pre.005.md
Normal file
191
deltas/0.1.1/pre.005.md
Normal file
@@ -0,0 +1,191 @@
|
||||
<!-- file: deltas/0.1.1/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.1-pre.005` — clôture Core et prompt Logging
|
||||
|
||||
## Base requise
|
||||
|
||||
`v0.1.1-pre.004` au sens du commit de livraison correspondant, avec :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.4"
|
||||
```
|
||||
|
||||
Le présent delta ouvre :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.5"
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Clôturer la phase de développement `0.1.1` sans élargir la surface Core : enregistrer les validations finales de `pre.004`, confirmer la politique de features `solana-pubkey`, réaligner les documents de référence et produire le prompt final de démarrage `0.1.2`.
|
||||
|
||||
La publication stable reste un delta `0.1.1-rel.001` séparé après validation de cette prerelease.
|
||||
|
||||
## Validation de la base précédente
|
||||
|
||||
Le user a exécuté avec succès le 2026-08-14 :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
Résultats communiqués :
|
||||
|
||||
- 14 tests unitaires passent ;
|
||||
- 3 tests d'intégration publics passent ;
|
||||
- Clippy passe sans warning communiqué ;
|
||||
- `cargo tree` conserve `solana-pubkey 4.3.0` comme unique dépendance externe directe de `ksp-core-lib` ;
|
||||
- `solana-pubkey` résout `solana-address 2.7.0` puis ses dépendances fondamentales ;
|
||||
- `cargo tree -d` ne rapporte aucun doublon ;
|
||||
- `cargo tree -e features` a été inspecté.
|
||||
|
||||
## Décision finale sur les features `solana-pubkey`
|
||||
|
||||
Aucune feature optionnelle supplémentaire n'est activée dans `0.1.1`.
|
||||
|
||||
La déclaration workspace reste :
|
||||
|
||||
```toml
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
```
|
||||
|
||||
Les features optionnelles `alloc`, `borsh`, `bytemuck`, `curve25519`, `rand`, `serde`, `sha2`, `std` et `wincode` ne correspondent à aucun besoin du contrat Core actuellement livré.
|
||||
|
||||
Les features `solana-address` visibles dans le graphe résolu (`copy`, `decode`, `error`, `sanitize`, `syscalls`, `default`) appartiennent à la composition interne de la génération actuelle de `solana-pubkey`/`solana-address`. Elles ne justifient pas une activation KSP supplémentaire.
|
||||
|
||||
Les futures releases activent une feature uniquement lorsque leur propriétaire fonctionnel démontre un besoin concret. En particulier, `borsh`/`wincode` ne sont pas activées dans Core par anticipation d'une future surface wire.
|
||||
|
||||
## Documentation finale
|
||||
|
||||
Le plan `0.1.1` est consolidé pour :
|
||||
|
||||
- refléter la surface réellement implémentée ;
|
||||
- enregistrer les validations réussies de `pre.004` ;
|
||||
- fermer la question des features `solana-pubkey` ;
|
||||
- confirmer qu'aucune primitive N1 supplémentaire n'est nécessaire ;
|
||||
- confirmer qu'aucun `README.md`/`USAGE.md` spécifique à la crate n'est nécessaire pour cette petite surface ;
|
||||
- confirmer que le dépôt ne possède actuellement aucun changelog général à synchroniser.
|
||||
|
||||
La séquence fonctionnelle est mise à jour pour remplacer le périmètre candidat de `0.1.1` par la surface effectivement stabilisée et son lifecycle réellement suivi.
|
||||
|
||||
Les index de documentation/plans/prompts sont réalignés avec les fichiers présents.
|
||||
|
||||
## Prompt `0.1.2`
|
||||
|
||||
Ajout :
|
||||
|
||||
```text
|
||||
prompts/002-V0_1_2_START_PROMPT.md
|
||||
```
|
||||
|
||||
Ce prompt ouvre :
|
||||
|
||||
```text
|
||||
0.1.2 — Logging foundation
|
||||
```
|
||||
|
||||
après publication stable de `0.1.1`.
|
||||
|
||||
Il conserve notamment les décisions suivantes :
|
||||
|
||||
- `ksp-logging-lib` est la façade KSP unique de logging/tracing runtime ;
|
||||
- Logging peut dépendre de `ksp-core-lib`, jamais l'inverse ;
|
||||
- `pre.001` de Logging reste une phase d'audit/brainstorming/planification ;
|
||||
- la stack `tracing` et ses features sont revérifiées depuis les sources officielles avant ajout ;
|
||||
- l'API doit préserver les callsites réels ;
|
||||
- Logging possède ses settings runtime sans dépendre de Config ;
|
||||
- les secrets ne sont jamais loggés automatiquement ;
|
||||
- les dépendances externes restent centralisées sous `[workspace.dependencies]`.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.1.1/pre.005.md
|
||||
prompts/002-V0_1_2_START_PROMPT.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||
prompts/000-README.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Nettoyage/archivage
|
||||
|
||||
Aucun fichier temporaire ou obsolète supplémentaire n'est identifié comme devant être supprimé dans cette tranche.
|
||||
|
||||
Les deltas historiques et le prompt `0.1.1` restent conservés comme historique utile.
|
||||
|
||||
## Validations exécutées pendant la préparation
|
||||
|
||||
Contrôles statiques hors Cargo :
|
||||
|
||||
- parsing TOML ;
|
||||
- headers `file:` / `version:` des fichiers modifiés/ajoutés ;
|
||||
- terminaison EOF ;
|
||||
- liens Markdown locaux ;
|
||||
- cohérence des index documentaires ;
|
||||
- cohérence de la version `0.1.1-pre.5` ;
|
||||
- absence d'ajout de feature `solana-pubkey` ;
|
||||
- intégrité de l'archive delta.
|
||||
|
||||
## Validations non exécutées pendant la préparation
|
||||
|
||||
L'environnement de préparation ne fournit pas `cargo`/`rustc`.
|
||||
|
||||
Après application du delta, exécuter sur le dépôt cible :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
Aucune de ces validations de `pre.005` n'est déclarée réussie avant exécution par le user.
|
||||
|
||||
## Publication suivante
|
||||
|
||||
Si `pre.005` est propre, préparer :
|
||||
|
||||
```text
|
||||
0.1.1-rel.001
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1"
|
||||
```
|
||||
|
||||
Le commit de `rel.001` validé comme stable reçoit ensuite le tag :
|
||||
|
||||
```text
|
||||
v0.1.1
|
||||
```
|
||||
|
||||
La session suivante peut alors démarrer avec :
|
||||
|
||||
```text
|
||||
prompts/002-V0_1_2_START_PROMPT.md
|
||||
```
|
||||
150
deltas/0.1.1/rel.001.md
Normal file
150
deltas/0.1.1/rel.001.md
Normal file
@@ -0,0 +1,150 @@
|
||||
<!-- file: deltas/0.1.1/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.1-rel.001` — publication stable Core
|
||||
|
||||
## Base requise
|
||||
|
||||
`v0.1.1-pre.005` au sens du commit de livraison correspondant, avec :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.1-pre.5"
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Publier la release stable `0.1.1`, clôturer `Core foundation` et préparer l'ouverture de `0.1.2 — Logging foundation` sans modifier la surface fonctionnelle de `ksp-core-lib`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.1-pre.5
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.1
|
||||
```
|
||||
|
||||
Le header de `Cargo.toml` passe de version 24 à 25.
|
||||
|
||||
La politique de dépendance reste inchangée :
|
||||
|
||||
```toml
|
||||
[workspace.dependencies]
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
```
|
||||
|
||||
Aucune feature optionnelle supplémentaire n'est activée.
|
||||
|
||||
## Validations finales exécutées par le user
|
||||
|
||||
Commandes exécutées avec succès le 2026-08-14 sur `0.1.1-pre.5` :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
Résultats communiqués :
|
||||
|
||||
- `cargo check --workspace` : succès ;
|
||||
- `cargo test --workspace` : 14 tests unitaires réussis, 3 tests d'intégration publics réussis, doc-tests réussis ;
|
||||
- `cargo clippy --workspace --all-targets` : succès sans warning communiqué ;
|
||||
- `cargo tree -p ksp-core-lib` : dépendance externe directe unique `solana-pubkey 4.3.0`, résolvant `solana-address 2.7.0` ;
|
||||
- `cargo tree -p ksp-core-lib -d` : aucun doublon ;
|
||||
- `cargo tree -p ksp-core-lib -e features` : graphe inspecté, sans besoin d'activer une feature optionnelle `solana-pubkey` supplémentaire.
|
||||
|
||||
## Surface stable publiée
|
||||
|
||||
`0.1.1` stabilise notamment :
|
||||
|
||||
- `ksp_core_lib::ErrorCode`, `ErrorContext`, `Error` et `Result<T>` ;
|
||||
- `ksp_core_lib::Pubkey` ;
|
||||
- les 18 Program IDs fondamentaux possédés par KSP et leurs paires `PRGID_*` / `PRGIDPK_*` ;
|
||||
- `declare_program_id!` ;
|
||||
- `ProgramIdEntry`, `ProgramIdFilter`, `ProgramIdKind` ;
|
||||
- le registre canonique enumerable/recherchable et ses vues par taxonomie ;
|
||||
- `native_program_ids()`, recherches texte/`Pubkey` et filtres combinables ;
|
||||
- la séparation `subfamily` / `program_version` ;
|
||||
- les règles Cargo workspace introduites pendant la release.
|
||||
|
||||
Aucune nouvelle primitive ou API n'est ajoutée par le présent delta de publication.
|
||||
|
||||
## Documentation de clôture
|
||||
|
||||
Le présent delta :
|
||||
|
||||
- marque `0.1.1` réalisée dans `ROADMAP.md` ;
|
||||
- conserve `003-V0_1_1_CORE_FOUNDATION_PLAN.md` comme plan historique clôturé ;
|
||||
- réaligne la séquence fonctionnelle sur la publication stable ;
|
||||
- réaligne les index de documentation/plans ;
|
||||
- conserve `prompts/002-V0_1_2_START_PROMPT.md` comme prompt de démarrage de la release suivante.
|
||||
|
||||
Aucun changelog général n'existe dans la base actuelle ; aucun changelog artificiel n'est créé.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.1.1/rel.001.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
ROADMAP.md
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Décisions
|
||||
|
||||
Aucune nouvelle décision architecturale.
|
||||
|
||||
La publication stable confirme les décisions et contrats stabilisés pendant les prereleases `0.1.1` et leurs fixes.
|
||||
|
||||
## Publication Git
|
||||
|
||||
Après application de ce delta :
|
||||
|
||||
1. vérifier que le working tree ne contient que les modifications attendues ;
|
||||
2. exécuter au minimum un `cargo check --workspace` final sur la version Cargo stable `0.1.1` ;
|
||||
3. créer le commit de release `v0.1.1-rel.001` ;
|
||||
4. marquer ce commit comme release stable avec le tag :
|
||||
|
||||
```text
|
||||
v0.1.1
|
||||
```
|
||||
|
||||
Aucun autre tag n'est requis pour les prereleases/fixes historiques.
|
||||
|
||||
## Suite
|
||||
|
||||
Après le tag stable `v0.1.1`, ouvrir :
|
||||
|
||||
```text
|
||||
0.1.2-pre.001
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
prompts/002-V0_1_2_START_PROMPT.md
|
||||
```
|
||||
|
||||
La première prerelease de `0.1.2` reste une phase de brainstorming, audit et planification avant développement fonctionnel de `ksp-logging-lib`.
|
||||
162
deltas/0.1.2/pre.001-fix.001.md
Normal file
162
deltas/0.1.2/pre.001-fix.001.md
Normal file
@@ -0,0 +1,162 @@
|
||||
<!-- file: deltas/0.1.2/pre.001-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.001-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.001
|
||||
```
|
||||
|
||||
Ce correctif est documentaire et corrige le plan de `pre.001` sans réécrire son delta historique.
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger le cadrage Logging avant validation du plan afin de fixer :
|
||||
|
||||
- le takeover complet du logging/tracing KSP par `ksp-logging-lib` ;
|
||||
- le target KSP explicite égal au nom Cargo de la crate propriétaire ;
|
||||
- le silence par défaut des targets tiers et la réémission explicite des informations utiles par le composant KSP propriétaire ;
|
||||
- console et fichier non bloquants avec guards et compteurs de lignes abandonnées ;
|
||||
- le stripping ANSI des fichiers ;
|
||||
- un `initialize` global unique suivi d'un hot reload via `reinitialize` sans second subscriber global ;
|
||||
- une surface de spans KSP synchrones et async avec diagnostic de durée `NEW/CLOSE`, `busy` et `idle` ;
|
||||
- la responsabilité des données loggées au caller, sans détection/redaction automatique par Logging.
|
||||
|
||||
Aucun développement fonctionnel de `ksp-logging-lib` n'est introduit par ce fix.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
### Takeover tracing
|
||||
|
||||
`ksp-logging-lib` devient le seul propriétaire KSP direct de la stack tracing et la seule façade autorisée pour les événements/spans KSP.
|
||||
|
||||
Le subscriber applique une politique de takeover :
|
||||
|
||||
```text
|
||||
external targets = Off by default
|
||||
ksp-* targets = configured KSP default
|
||||
specific ksp-* = optional override
|
||||
```
|
||||
|
||||
KSP ne renomme pas un événement tiers. Lorsqu'un détail provenant d'une dépendance externe est utile, la crate KSP propriétaire le réémet sous son propre target.
|
||||
|
||||
Exemple attendu pour Store : les logs SQLx natifs sont désactivés/silencieux ; les opérations SQL utiles sont journalisées explicitement par `ksp-store-lib`, typiquement au niveau `trace`.
|
||||
|
||||
### Targets
|
||||
|
||||
Les macros événements et spans exigent un target explicite correspondant au nom Cargo de la crate propriétaire. `domain`, `component` et autres fields restent des subdivisions structurées, pas des remplacements du target.
|
||||
|
||||
### Settings runtime
|
||||
|
||||
`LoggingSettings` reste propriétaire de Logging et indépendant de Config. Il couvre niveau KSP default, overrides de target, sorties console/fichier et politique d'événements de spans.
|
||||
|
||||
Une future `ksp-config-lib` pourra construire ces settings puis appeler la façade Logging.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
Console et fichier utilisent des writers non bloquants avec leurs `WorkerGuard` possédés par `LoggingGuard`.
|
||||
|
||||
Le mode retenu privilégie l'absence de backpressure sur le hot path : une saturation peut abandonner des lignes. Les `ErrorCounter` sont conservés afin que ces pertes restent observables.
|
||||
|
||||
### Stripping ANSI
|
||||
|
||||
Les fichiers passent par un stripping ANSI générique avant persistence. Logging ne dépend pas de Tauri ; cette protection évite seulement de persister des séquences de terminal déjà présentes dans les données écrites.
|
||||
|
||||
### Initialisation et hot reload
|
||||
|
||||
`initialize(settings)` installe le subscriber global une seule fois et retourne `LoggingGuard`.
|
||||
|
||||
Après succès, `reinitialize(&mut guard, settings)` ou une méthode équivalente peut être appelée 0..N fois. Elle ne réinstalle pas le subscriber global ; elle modifie les filters/layers/sinks de l'infrastructure déjà installée.
|
||||
|
||||
Le reload vise une sémantique transactionnelle : une nouvelle configuration invalide ou impossible à construire laisse l'ancienne configuration active.
|
||||
|
||||
Le mécanisme interne exact (`tracing_subscriber::reload` ciblé ou routing KSP dynamique) sera choisi par implémentation/tests selon correction et overhead, sans modifier le contrat public.
|
||||
|
||||
### Spans sync/async et durée
|
||||
|
||||
`0.1.2` inclut désormais une surface de spans KSP par niveau, sans dépendance directe `tracing` dans les crates consommatrices.
|
||||
|
||||
Le code sync doit pouvoir exécuter un scope dans un span. Le code async doit instrumenter la `Future` elle-même et ne pas maintenir un enter guard à travers `.await`.
|
||||
|
||||
Les settings permettent au minimum `Off` et `NewAndClose`; `NEW | CLOSE` fournit des repères de début/fin et, lorsque les timestamps sont actifs, le close fournit `busy`/`idle`. Cette capacité sert au diagnostic rapide de latence/blocage et n'est pas présentée comme un benchmark de précision absolue.
|
||||
|
||||
### Contenu sensible
|
||||
|
||||
`ksp-logging-lib` n'essaie pas de détecter ou redacter automatiquement les données sensibles. La crate appelante est responsable du contenu qu'elle choisit de logger.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `deltas/0.1.2/pre.001-fix.001.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
- `docs/rules/RULES_DEPENDENCIES.md`
|
||||
- `docs/architecture/003-COMPONENT_CONTRACTS.md`
|
||||
- `docs/architecture/005-DEPENDENCY_GRAPH.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Aucune modification de `Cargo.toml`.
|
||||
|
||||
Ce fix est limité à la documentation et respecte `VER-ID-008` : `workspace.package.version` reste donc :
|
||||
|
||||
```text
|
||||
0.1.2-pre.1
|
||||
```
|
||||
|
||||
L'identifiant de livraison est :
|
||||
|
||||
```text
|
||||
0.1.2-pre.001-fix.001
|
||||
```
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- vérification du delta `pre.001` fourni et des règles de version/delta/archive de la base `0.1.1` ;
|
||||
- vérification de la documentation officielle `tracing` indiquant que le subscriber global ne peut être installé qu'une fois ;
|
||||
- vérification de `tracing-subscriber::reload` pour le remplacement runtime d'une Layer/Filter ;
|
||||
- vérification de l'avertissement officiel contre `Span::enter()` conservé à travers `.await` ;
|
||||
- vérification de l'instrumentation de `Future` fournie par `tracing::Instrument` ;
|
||||
- vérification de `FmtSpan::NEW | FmtSpan::CLOSE` et des champs `busy`/`idle` au close lorsque les timestamps sont actifs ;
|
||||
- vérification du writer non bloquant, de `WorkerGuard` et `ErrorCounter` dans `tracing-appender 0.2.5` ;
|
||||
- contrôle des headers `file:` / `version:` des fichiers livrés ;
|
||||
- contrôle de l'absence de modification Cargo dans ce fix documentaire ;
|
||||
- contrôle du contenu de l'archive selon `VER-ARCHIVE-004`.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Aucune validation Cargo n'est applicable à ce correctif documentaire et aucun code fonctionnel Logging n'existe encore dans la livraison.
|
||||
|
||||
Les commandes suivantes restent à exécuter dès que les tranches de développement les rendent applicables :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question architecturale bloquante.
|
||||
|
||||
Restent à trancher par implémentation/tests dans les prereleases suivantes :
|
||||
|
||||
- mécanisme exact des macros spans/events préservant le callsite sans fuite de types `tracing` ;
|
||||
- abstraction KSP exacte pour instrumenter les futures async ;
|
||||
- composition reloadable interne la moins coûteuse ;
|
||||
- API exacte d'observation des dropped lines.
|
||||
|
||||
Après validation de ce fix, la prochaine tranche reste `0.1.2-pre.002`.
|
||||
375
deltas/0.1.2/pre.001.md
Normal file
375
deltas/0.1.2/pre.001.md
Normal file
@@ -0,0 +1,375 @@
|
||||
<!-- file: deltas/0.1.2/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Release stable/taguée attendue :
|
||||
|
||||
```text
|
||||
v0.1.1
|
||||
```
|
||||
|
||||
L'archive Gitea fournie `khadhroony-solana-project-v0.1.1.zip` contient bien :
|
||||
|
||||
- `workspace.package.version = "0.1.1"` ;
|
||||
- le delta final `deltas/0.1.1/rel.001.md` ;
|
||||
- le prompt final `prompts/002-V0_1_2_START_PROMPT.md` ;
|
||||
- la surface Core stabilisée attendue.
|
||||
|
||||
Dans le workflow KSP, cette archive provient directement du tag correspondant et constitue la base stable suffisante pour ouvrir `0.1.2`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Ouvrir `0.1.2` par la prerelease obligatoire de brainstorming, audit et planification, sans développement fonctionnel Logging.
|
||||
|
||||
Cette tranche :
|
||||
|
||||
- inventorie l'état réel du workspace et confirme l'absence actuelle de `ksp-logging-lib` ;
|
||||
- audite la stack `tracing` officielle actuelle ;
|
||||
- fixe la frontière façade/instrumentation/runtime subscriber ;
|
||||
- retient les macros KSP pour préserver les callsites ;
|
||||
- définit les niveaux et settings runtime candidats ;
|
||||
- borne la sémantique de `target`, `domain` et `component` ;
|
||||
- retient le filtering global + target-prefix via `Targets` ;
|
||||
- retient console + fichier optionnel ;
|
||||
- retient un writer fichier non bloquant non-lossy avec guard possédé explicitement ;
|
||||
- définit le lifecycle d'initialisation/réinitialisation ;
|
||||
- fixe la stratégie d'erreurs Core et de protection des secrets ;
|
||||
- dimensionne `pre.002` à `pre.006` ;
|
||||
- confirme les hors-scope.
|
||||
|
||||
Le détail est consigné dans `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.2-pre.1
|
||||
```
|
||||
|
||||
L'identifiant Cargo respecte SemVer sans zéro initial ; l'identifiant de livraison reste `0.1.2-pre.001`.
|
||||
|
||||
Le header de `Cargo.toml` passe de version 25 à 26.
|
||||
|
||||
Aucune dépendance `tracing*` n'est ajoutée par cette tranche de planification : elles seront introduites uniquement lorsque le code/tests de `ksp-logging-lib` les consommeront réellement.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
- `deltas/0.1.2/pre.001.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- `ROADMAP.md`
|
||||
- `docs/plans/000-README.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Inventaire du workspace
|
||||
|
||||
État de la base stable auditée :
|
||||
|
||||
```text
|
||||
workspace members
|
||||
└── crates/ksp-core-lib
|
||||
```
|
||||
|
||||
`ksp-logging-lib` n'existe pas encore.
|
||||
|
||||
Core fournit déjà les contrats nécessaires à Logging :
|
||||
|
||||
```text
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorContext
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::Result<T>
|
||||
ksp_core_lib::Pubkey
|
||||
```
|
||||
|
||||
ainsi que les Program IDs fondamentaux et leur registre descriptif.
|
||||
|
||||
La relation retenue reste unidirectionnelle :
|
||||
|
||||
```text
|
||||
ksp-logging-lib -> ksp-core-lib
|
||||
ksp-core-lib -X-> ksp-logging-lib
|
||||
```
|
||||
|
||||
## Audit externe tracing
|
||||
|
||||
Audit effectué le 2026-08-14 sur les publications/docs officielles Tokio `tracing`, docs.rs/crates.io et les manifests publiés.
|
||||
|
||||
Versions observées :
|
||||
|
||||
```text
|
||||
tracing 0.1.44 rustc 1.65+
|
||||
tracing-subscriber 0.3.23 rustc 1.65+
|
||||
tracing-appender 0.2.5 rustc 1.63+
|
||||
```
|
||||
|
||||
Contraintes candidates à revérifier au moment de l'ajout effectif :
|
||||
|
||||
```toml
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
```
|
||||
|
||||
Décisions de features :
|
||||
|
||||
- pas de `tracing-attributes`/`attributes` ;
|
||||
- pas de `ansi` ;
|
||||
- pas de `tracing-log` ;
|
||||
- pas d'`env-filter` ;
|
||||
- pas de JSON/Serde ;
|
||||
- pas de chrono/time formatter via `tracing-subscriber` ;
|
||||
- pas de `parking_lot` appender ;
|
||||
- `tracing-appender` tire lui-même `tracing-subscriber` avec `default-features = false`, `fmt` et `std` ainsi que les dépendances internes nécessaires à son fonctionnement.
|
||||
|
||||
Aucune dépendance n'est ajoutée uniquement parce qu'elle figure dans l'architecture candidate.
|
||||
|
||||
## Décisions de planification
|
||||
|
||||
### Façade et callsites
|
||||
|
||||
La surface d'émission KSP sera :
|
||||
|
||||
```text
|
||||
ksp_logging_lib::error!
|
||||
ksp_logging_lib::warn!
|
||||
ksp_logging_lib::info!
|
||||
ksp_logging_lib::debug!
|
||||
ksp_logging_lib::trace!
|
||||
```
|
||||
|
||||
Les événements ne seront pas émis par de simples fonctions wrappers qui déplaceraient les métadonnées source.
|
||||
|
||||
L'implémentation exacte des macros doit réussir un test d'intégration prouvant que file/module/line et target implicite restent ceux du consommateur.
|
||||
|
||||
### Champs structurés
|
||||
|
||||
- `target` : métadonnée native de routage/filtering, naturelle au callsite ou explicitement overridable ;
|
||||
- `domain` : champ structuré KSP optionnel ;
|
||||
- `component` : champ structuré KSP optionnel ;
|
||||
- autres champs : ouverts selon besoin, sans taxonomie fermée.
|
||||
|
||||
`domain` et `component` ne deviennent pas des filtres dans `0.1.2`.
|
||||
|
||||
### Filtering
|
||||
|
||||
Première surface :
|
||||
|
||||
```text
|
||||
default filter level
|
||||
+ zero or more target-prefix overrides
|
||||
```
|
||||
|
||||
`tracing_subscriber::filter::Targets` est retenu comme mécanisme initial.
|
||||
|
||||
`EnvFilter`, `RUST_LOG`, field-based filtering et hot reload restent hors scope.
|
||||
|
||||
### Settings runtime
|
||||
|
||||
Surface conceptuelle retenue :
|
||||
|
||||
```text
|
||||
LogFilterLevel
|
||||
TargetFilter
|
||||
ConsoleOutput
|
||||
ConsoleSettings
|
||||
FileRotation
|
||||
FileSettings
|
||||
LoggingSettings
|
||||
LoggingGuard
|
||||
initialize(...)
|
||||
```
|
||||
|
||||
Les settings ne lisent ni fichier, ni environnement, ni profil Config et ne contiennent aucun secret.
|
||||
|
||||
### Console
|
||||
|
||||
Sortie console avec choix explicite stdout/stderr.
|
||||
|
||||
Le formatter initial reste humain, sans JSON ni ANSI obligatoire.
|
||||
|
||||
### Fichier
|
||||
|
||||
Sortie fichier optionnelle retenue avec :
|
||||
|
||||
```text
|
||||
Never | Hourly | Daily
|
||||
```
|
||||
|
||||
Le builder fallible du `RollingFileAppender` doit être utilisé afin de remonter les erreurs au lieu de paniquer.
|
||||
|
||||
Le writer fichier utilise `NonBlockingBuilder` en mode :
|
||||
|
||||
```text
|
||||
lossy(false)
|
||||
```
|
||||
|
||||
La saturation applique donc de la backpressure plutôt que de supprimer silencieusement des logs.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
`LoggingGuard` possède le ou les `WorkerGuard` nécessaires au backend non bloquant.
|
||||
|
||||
L'appelant conserve le guard jusqu'à la fin ordonnée du processus.
|
||||
|
||||
Le lifecycle global est volontairement :
|
||||
|
||||
```text
|
||||
uninitialized -> initialized -> process shutdown
|
||||
```
|
||||
|
||||
Une initialisation répétée échoue avec une erreur KSP ; elle ne remplace pas silencieusement un subscriber existant et ne panique pas.
|
||||
|
||||
### Erreurs
|
||||
|
||||
Les erreurs Logging utilisent le contrat Core et restent dans le domaine :
|
||||
|
||||
```text
|
||||
logging
|
||||
```
|
||||
|
||||
Codes conceptuels initiaux :
|
||||
|
||||
```text
|
||||
logging.invalid_settings
|
||||
logging.already_initialized
|
||||
logging.file_output_initialization_failed
|
||||
```
|
||||
|
||||
Les causes externes utiles sont conservées via `Error::with_source(...)` lorsque possible.
|
||||
|
||||
### Secrets
|
||||
|
||||
Sont explicitement interdits dans les logs : clés privées, seeds/mnemonics, passwords/passphrases/PIN, tokens API/bearer/session, cookies/auth headers, secrets de chiffrement/signature, credentials de connexion et futurs `*_SECRET_*`.
|
||||
|
||||
Aucun helper de redaction universel n'est introduit : le caller doit omettre ou redacter explicitement la valeur avant émission.
|
||||
|
||||
Les settings Logging ne contiennent eux-mêmes aucun secret.
|
||||
|
||||
### Surface différée
|
||||
|
||||
Ne pas ajouter dans `0.1.2` sans nouveau besoin validé :
|
||||
|
||||
- spans KSP/`#[instrument]` ;
|
||||
- OpenTelemetry ;
|
||||
- JSON ;
|
||||
- ANSI ;
|
||||
- compatibilité `log` ;
|
||||
- `EnvFilter` ;
|
||||
- reload de filtre ;
|
||||
- filtering par fields/domain ;
|
||||
- rotation minutely/weekly/by-size ;
|
||||
- compression/rétention complexe/latest symlink ;
|
||||
- routes multiples avancées.
|
||||
|
||||
## Référence historique bot3
|
||||
|
||||
L'ancien `ks-logging` de l'archive bot3 fournie a été relu comme référence historique uniquement.
|
||||
|
||||
Éléments conservés comme leçons utiles :
|
||||
|
||||
- objet de lifecycle possédant les `WorkerGuard` ;
|
||||
- console + fichier ;
|
||||
- rotation ;
|
||||
- filtering par targets.
|
||||
|
||||
Éléments non migrés :
|
||||
|
||||
- dépendance Logging -> Config ;
|
||||
- document/schema JSON propre à Logging ;
|
||||
- Serde/JSON pour la configuration ;
|
||||
- routes/formats multiples non nécessaires à la première surface KSP.
|
||||
|
||||
## Prereleases prévues
|
||||
|
||||
```text
|
||||
pre.001 audit + brainstorming + plan
|
||||
pre.002 crate + settings + macros/façade
|
||||
pre.003 subscriber + console + filtering + callsite final
|
||||
pre.004 fichier + non-blocking + lifecycle
|
||||
pre.005 intégration + tests + audits
|
||||
pre.006 validation finale + docs/cleanup + prompt 0.1.3
|
||||
```
|
||||
|
||||
Le découpage reste souple ; une tranche trop large sera scindée plutôt que surchargée.
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
- Config/documents/profils ;
|
||||
- Tauri ;
|
||||
- Wallet/signing ;
|
||||
- RPC/WS/providers ;
|
||||
- Program decoding/execution ;
|
||||
- Store/PostgreSQL ;
|
||||
- Materializer ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML ;
|
||||
- observabilité distribuée/OpenTelemetry.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
Dans l'environnement de préparation de ce delta :
|
||||
|
||||
- lecture/audit de l'archive complète `0.1.1` fournie ;
|
||||
- vérification statique de `workspace.package.version = "0.1.1"` ;
|
||||
- vérification de la présence du delta `0.1.1/rel.001` et du prompt final `0.1.2` ;
|
||||
- inventaire des membres workspace et confirmation de l'absence de `ksp-logging-lib` ;
|
||||
- lecture des règles, plans, indexes et documents d'architecture demandés par le prompt ;
|
||||
- lecture de `ksp-core-lib` et de son contrat Error/Result ;
|
||||
- audit de l'ancien `ks-logging` bot3 fourni comme référence historique, sans le traiter comme source de vérité KSP ;
|
||||
- vérification des versions/features/MSRV actuels de `tracing`, `tracing-subscriber` et `tracing-appender` depuis leurs sources de publication officielles ;
|
||||
- audit du manifest publié de `tracing-appender` pour ses dépendances/features ;
|
||||
- audit de `Targets`, `EnvFilter`, du non-blocking, du mode lossy/backpressure, de `WorkerGuard`, du builder fallible et de la rotation ;
|
||||
- parsing TOML statique du manifest modifié ;
|
||||
- contrôle statique des headers `file:` / `version:` des fichiers ajoutés/modifiés ;
|
||||
- contrôle statique des liens Markdown locaux après modification ;
|
||||
- contrôle du contenu de l'archive delta selon `VER-ARCHIVE-004`.
|
||||
|
||||
## 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
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Les trois commandes `cargo tree -p ksp-logging-lib` ne sont de toute façon applicables qu'après création effective de la crate.
|
||||
|
||||
Aucun succès Cargo n'est déclaré par ce delta.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question architecturale bloquante ne justifie de poursuivre le développement dans `pre.001`.
|
||||
|
||||
À confirmer par tests dans les tranches suivantes :
|
||||
|
||||
- mécanisme exact de macro KSP préservant le callsite avec la plus petite surface ;
|
||||
- format visuel exact des lignes humaines sans le figer comme protocole ;
|
||||
- nécessité future de capacités volontairement différées comme rétention, ANSI, JSON, `tracing-log`, `EnvFilter`, spans ou reload.
|
||||
|
||||
La prochaine tranche après validation de ce plan est `0.1.2-pre.002`.
|
||||
125
deltas/0.1.2/pre.002-fix.001.md
Normal file
125
deltas/0.1.2/pre.002-fix.001.md
Normal file
@@ -0,0 +1,125 @@
|
||||
<!-- file: deltas/0.1.2/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.002-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.002
|
||||
```
|
||||
|
||||
Ce correctif traite uniquement les résultats de validation remontés après `pre.002`. Il ne modifie pas le périmètre fonctionnel de la prerelease et n'ouvre pas `pre.003`.
|
||||
|
||||
## Résultats de validation à corriger
|
||||
|
||||
Les commandes exécutées sur le workspace de développement ont montré :
|
||||
|
||||
- `cargo fmt --all` : exécuté sans erreur ;
|
||||
- `cargo check --workspace` : réussi ;
|
||||
- `cargo clippy --workspace --all-targets` : terminé avec quatre catégories de warnings à nettoyer dans Logging/tests ;
|
||||
- `cargo test --workspace` : tous les tests Core et les tests unitaires Logging réussissent, mais `async_instrumentation_enters_and_exits_span_during_poll` échoue avec `enters = 2` au lieu de l'attente `1`.
|
||||
|
||||
## Cause du test async
|
||||
|
||||
Le test `pre.002` supposait qu'une future instrumentée n'entrait dans son span que pendant son unique `poll`.
|
||||
|
||||
Le contrat de `tracing::Instrument` est plus précis : la future instrumentée entre dans le span lors de chaque `poll` **et lors de son `Drop`**. Pour `std::future::ready(42_u32)`, le test observe donc :
|
||||
|
||||
```text
|
||||
poll -> enter + exit
|
||||
Drop -> enter + exit
|
||||
```
|
||||
|
||||
Le compteur final `2` est donc conforme au comportement de `tracing`; c'est l'attente du test qui était incorrecte.
|
||||
|
||||
Le test corrigé vérifie séparément :
|
||||
|
||||
1. une paire `enter` / `exit` immédiatement après le `poll` ;
|
||||
2. une deuxième paire après destruction explicite de la future instrumentée ;
|
||||
3. l'équilibre final entre le nombre d'entrées et de sorties.
|
||||
|
||||
Le plan actif documente désormais explicitement cette sémantique afin qu'un futur test async ne réintroduise pas l'hypothèse erronée d'une seule paire `enter` / `exit` sur toute la durée de vie d'une future.
|
||||
|
||||
## Nettoyage Clippy
|
||||
|
||||
### `collapsible_if`
|
||||
|
||||
La validation du préfixe de fichier utilise désormais un `if let` avec condition chaînée compatible Rust 2024 au lieu de deux `if` imbriqués.
|
||||
|
||||
### `double_must_use`
|
||||
|
||||
L'attribut `#[must_use]` explicite de `ksp_logging_lib::instrument(...)` est supprimé : la fonction retourne déjà un type `Future`, lui-même marqué `must_use` par son contrat standard.
|
||||
|
||||
## Documentation des tests d'intégration
|
||||
|
||||
Les crates de tests d'intégration :
|
||||
|
||||
```text
|
||||
crates/ksp-logging-lib/tests/callsite.rs
|
||||
crates/ksp-logging-lib/tests/public_api.rs
|
||||
```
|
||||
|
||||
reçoivent chacune une documentation crate-root `//! ...` afin de satisfaire `missing_docs = "warn"` lorsque les tests sont compilés comme crates séparées.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
La version reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.2
|
||||
```
|
||||
|
||||
Aucune dépendance et aucun manifest ne sont modifiés.
|
||||
|
||||
L'identifiant de livraison de ce correctif est :
|
||||
|
||||
```text
|
||||
0.1.2-pre.002-fix.001
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `crates/ksp-logging-lib/src/settings.rs`
|
||||
- `crates/ksp-logging-lib/src/span.rs`
|
||||
- `crates/ksp-logging-lib/tests/callsite.rs`
|
||||
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
- `deltas/0.1.2/pre.002-fix.001.md`
|
||||
|
||||
## Validations statiques exécutées lors de la préparation
|
||||
|
||||
- contrôle des headers `file:` / `version:` des fichiers du correctif ;
|
||||
- contrôle que `Cargo.toml` n'est pas inclus dans le delta ;
|
||||
- contrôle que la version Cargo de la base reste `0.1.2-pre.2` ;
|
||||
- contrôle de l'absence de nouvelle dépendance ;
|
||||
- contrôle que le correctif ne contient aucun ajout `unwrap`, `expect`, `panic` ou opérateur `?` dans le code production modifié ;
|
||||
- contrôle que l'archive contient uniquement les cinq fichiers modifiés et le nouveau delta.
|
||||
|
||||
## Validations à réexécuter sur le workspace
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Puis, pour compléter les validations prévues pour `pre.002` si elles ne l'ont pas encore été :
|
||||
|
||||
```bash
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Aucune validation Cargo non exécutable dans l'environnement de préparation n'est déclarée réussie par ce delta.
|
||||
|
||||
## Suite
|
||||
|
||||
Une fois ce correctif validé, `0.1.2-pre.002` peut être considérée propre et la session peut passer à `0.1.2-pre.003` pour le subscriber runtime, le takeover, le filtering, la console non bloquante et la fondation du hot reload.
|
||||
270
deltas/0.1.2/pre.002.md
Normal file
270
deltas/0.1.2/pre.002.md
Normal file
@@ -0,0 +1,270 @@
|
||||
<!-- file: deltas/0.1.2/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.002
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.2-pre.001-fix.001
|
||||
```
|
||||
|
||||
Cette tranche applique le plan corrigé de `pre.001` et ouvre le développement fonctionnel de `ksp-logging-lib` sans encore installer le subscriber runtime.
|
||||
|
||||
## Objectif
|
||||
|
||||
Créer la première surface fonctionnelle de Logging :
|
||||
|
||||
- créer `crates/ksp-logging-lib` et l'ajouter au workspace ;
|
||||
- dépendre de `ksp-core-lib` pour le contrat commun d'erreur ;
|
||||
- ajouter uniquement `tracing` parmi les dépendances de la stack de logging ;
|
||||
- définir les settings runtime propres à Logging, indépendants de Config ;
|
||||
- exposer les cinq niveaux d'événements par macros KSP avec `target:` explicite ;
|
||||
- exposer les cinq niveaux de spans KSP ;
|
||||
- fournir une abstraction `Span` KSP pour les scopes synchrones ;
|
||||
- fournir `instrument(span, future)` pour l'instrumentation async sans demander au consumer d'utiliser `tracing::Instrument` ;
|
||||
- vérifier par tests la préservation du callsite événement/span et le cycle enter/exit d'une future instrumentée.
|
||||
|
||||
Le subscriber global, le takeover effectif, le filtering runtime, les sorties console/fichier non bloquantes, les guards et le hot reload restent réservés aux prereleases suivantes conformément au plan.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.2-pre.1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.2-pre.2
|
||||
```
|
||||
|
||||
L'identifiant de livraison reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.002
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine passe de version 26 à 27.
|
||||
|
||||
## Dépendances
|
||||
|
||||
`tracing` est ajouté à la racine sous `[workspace.dependencies]` :
|
||||
|
||||
```toml
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
```
|
||||
|
||||
`ksp-logging-lib` le consomme avec :
|
||||
|
||||
```toml
|
||||
tracing.workspace = true
|
||||
```
|
||||
|
||||
L'audit de la publication actuelle retient `tracing 0.1.44`. Les default features ne sont pas activées : `attributes` n'est pas nécessaire à cette tranche, car KSP n'utilise pas `#[instrument]`. La feature `std` suffit à la façade retenue et aux tests de subscriber local.
|
||||
|
||||
`tracing-subscriber` et `tracing-appender` ne sont pas ajoutés dans `pre.002` : ils ne sont pas encore consommés par du code runtime.
|
||||
|
||||
## Settings runtime
|
||||
|
||||
La surface publique introduit :
|
||||
|
||||
```text
|
||||
LogFilterLevel
|
||||
TargetFilter
|
||||
SpanEvents
|
||||
ConsoleOutput
|
||||
ConsoleSettings
|
||||
FileRotation
|
||||
FileSettings
|
||||
LoggingSettings
|
||||
```
|
||||
|
||||
Ces types :
|
||||
|
||||
- appartiennent à `ksp-logging-lib` ;
|
||||
- ne lisent aucun document Config ;
|
||||
- ne consultent aucune variable d'environnement ;
|
||||
- ne dépendent pas de `ksp-config-lib` ;
|
||||
- utilisent des champs privés et une construction/getters explicites.
|
||||
|
||||
Une configuration sans console ni fichier est valide et représente un logging KSP désactivé. Les validations actuelles rejettent uniquement les ambiguïtés propres au contrat déjà fixé, notamment les préfixes de target vides/externes et un préfixe de fichier vide.
|
||||
|
||||
## Façade événements
|
||||
|
||||
Les macros crate-root suivantes sont introduites :
|
||||
|
||||
```text
|
||||
ksp_logging_lib::error!
|
||||
ksp_logging_lib::warn!
|
||||
ksp_logging_lib::info!
|
||||
ksp_logging_lib::debug!
|
||||
ksp_logging_lib::trace!
|
||||
```
|
||||
|
||||
Leur syntaxe KSP exige `target:` explicitement. Elles délèguent directement aux macros `tracing` au point d'expansion afin que les métadonnées `file`, `module_path` et `line` correspondent au callsite consumer et non à une fonction wrapper dans Logging.
|
||||
|
||||
Un bridge `tracing` public mais caché de la documentation est nécessaire à l'expansion des macros depuis les crates consommatrices. Il est réservé à l'implémentation des macros ; `DEP-LOG-009` interdit son usage direct comme API consumer.
|
||||
|
||||
## Spans synchrones et async
|
||||
|
||||
Les macros suivantes sont introduites :
|
||||
|
||||
```text
|
||||
ksp_logging_lib::error_span!
|
||||
ksp_logging_lib::warn_span!
|
||||
ksp_logging_lib::info_span!
|
||||
ksp_logging_lib::debug_span!
|
||||
ksp_logging_lib::trace_span!
|
||||
```
|
||||
|
||||
Elles exigent également `target:` explicitement et retournent `ksp_logging_lib::Span`.
|
||||
|
||||
Pour le synchrone :
|
||||
|
||||
```text
|
||||
Span::in_scope(operation)
|
||||
```
|
||||
|
||||
entre dans le span pendant le scope puis en sort à la fin du scope.
|
||||
|
||||
Pour l'async :
|
||||
|
||||
```text
|
||||
ksp_logging_lib::instrument(span, future)
|
||||
```
|
||||
|
||||
retourne une `Future` opaque instrumentée. Le span est entré pendant chaque poll de la future et quitté lorsque ce poll rend la main ; aucun enter guard KSP n'est destiné à être conservé à travers `.await`.
|
||||
|
||||
Cette surface prépare les diagnostics de durée `NEW/CLOSE`, `busy` et `idle` qui seront activés par le formatter/subscriber dans les tranches runtime suivantes.
|
||||
|
||||
## Erreurs
|
||||
|
||||
`ksp-logging-lib` utilise :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Result<T>
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::ErrorCode
|
||||
```
|
||||
|
||||
Le premier code propre à Logging est :
|
||||
|
||||
```text
|
||||
logging.invalid_settings
|
||||
```
|
||||
|
||||
Core ne reçoit aucune connaissance de Logging et aucune dépendance inverse n'est introduite.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
### Unitaires
|
||||
|
||||
- distinction des niveaux ;
|
||||
- construction/getters des target filters ;
|
||||
- console stdout/stderr ;
|
||||
- settings fichier/rotation ;
|
||||
- conservation des settings explicites ;
|
||||
- logging désactivé sans sink ;
|
||||
- rejet des target prefixes vides ou externes ;
|
||||
- rejet du préfixe fichier vide ;
|
||||
- scope synchrone d'un span ;
|
||||
- propagation du résultat d'une future instrumentée.
|
||||
|
||||
### Intégration
|
||||
|
||||
- surface publique des settings sans Config ;
|
||||
- disponibilité des cinq macros événements ;
|
||||
- disponibilité des cinq macros spans ;
|
||||
- usage sync et async sans import consumer de `tracing::Span` ou `tracing::Instrument` ;
|
||||
- préservation de `target`, `file`, `module_path` et `line` au callsite événement ;
|
||||
- préservation de `target`, `file`, `module_path` et `line` au callsite span ;
|
||||
- entrée puis sortie du span lors du poll d'une future instrumentée.
|
||||
|
||||
## Règles ajustées
|
||||
|
||||
`DEP-LOG-009` documente explicitement que le bridge `tracing` caché nécessaire aux macros est un détail d'implémentation de `ksp-logging-lib`, jamais une surface utilisable par une crate consommatrice.
|
||||
|
||||
Le plan `004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` est synchronisé avec l'API effectivement retenue dans `pre.002` et avec la validité d'un logging entièrement désactivé.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `crates/ksp-logging-lib/Cargo.toml`
|
||||
- `crates/ksp-logging-lib/src/error.rs`
|
||||
- `crates/ksp-logging-lib/src/lib.rs`
|
||||
- `crates/ksp-logging-lib/src/macros.rs`
|
||||
- `crates/ksp-logging-lib/src/settings.rs`
|
||||
- `crates/ksp-logging-lib/src/span.rs`
|
||||
- `crates/ksp-logging-lib/unit_tests/settings.rs`
|
||||
- `crates/ksp-logging-lib/unit_tests/span.rs`
|
||||
- `crates/ksp-logging-lib/tests/callsite.rs`
|
||||
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||
- `deltas/0.1.2/pre.002.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
- `docs/rules/RULES_DEPENDENCIES.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
Validations statiques exécutées dans l'environnement de préparation :
|
||||
|
||||
- parsing TOML des manifests ;
|
||||
- contrôle des headers `file:` / `version:` des fichiers livrés ;
|
||||
- contrôle de l'absence de `Cargo.lock` dans le delta ;
|
||||
- contrôle de l'absence de `tracing-subscriber` et `tracing-appender` dans les manifests ;
|
||||
- contrôle de la centralisation de `tracing` sous `[workspace.dependencies]` ;
|
||||
- contrôle que les usages directs de `tracing` restent bornés à `ksp-logging-lib` ;
|
||||
- contrôle des patterns Rust interdits par les règles workspace dans le code production ajouté ;
|
||||
- contrôle des liens Markdown locaux du plan modifié ;
|
||||
- contrôle du contenu de l'archive selon `VER-ARCHIVE-004`.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
L'environnement de préparation ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les validations suivantes ne sont donc **pas** déclarées réussies :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Elles doivent être exécutées sur le workspace de développement avant validation de la tranche. Toute erreur sera corrigée par le delta suivant conformément au workflow KSP.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- `tracing` est la seule dépendance de la stack ajoutée en `pre.002` ;
|
||||
- les macros KSP exigent `target:` ;
|
||||
- le callsite est préservé par expansion de macro et testé ;
|
||||
- l'abstraction publique de span est `ksp_logging_lib::Span` ;
|
||||
- le synchrone utilise `Span::in_scope(...)` ;
|
||||
- l'async utilise `instrument(span, future)` ;
|
||||
- une configuration sans sink est valide et représente Logging désactivé ;
|
||||
- aucune initialisation/subscriber global n'est introduit prématurément dans cette tranche.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.002`.
|
||||
|
||||
Restent à choisir/tester dans les tranches runtime suivantes :
|
||||
|
||||
- la composition interne reloadable la moins coûteuse ;
|
||||
- l'API exacte d'observation des lignes abandonnées ;
|
||||
- les détails finaux du formatter console/fichier ;
|
||||
- la stratégie de swap des sinks garantissant le maintien de l'ancienne configuration si une reconfiguration échoue.
|
||||
|
||||
Après validation de cette tranche, la prochaine étape est `0.1.2-pre.003` : subscriber, takeover, filtering, console initiale et fondation du hot reload.
|
||||
144
deltas/0.1.2/pre.003-fix.001.md
Normal file
144
deltas/0.1.2/pre.003-fix.001.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<!-- file: deltas/0.1.2/pre.003-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.003-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.003
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.3"
|
||||
Cargo.toml header version = 29
|
||||
```
|
||||
|
||||
## Motif du correctif
|
||||
|
||||
Les validations remontées pour `pre.003` sont :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace ECHEC
|
||||
```
|
||||
|
||||
Le test d'intégration :
|
||||
|
||||
```text
|
||||
global_runtime_supports_takeover_hot_reload_and_single_initialization
|
||||
```
|
||||
|
||||
panique pendant le premier `reinitialize()` activant la console :
|
||||
|
||||
```text
|
||||
a `Filtered` layer was used, but it had no `FilterId`; was it registered with the subscriber?
|
||||
```
|
||||
|
||||
## Cause
|
||||
|
||||
`pre.003` construisait le sink console sous cette forme conceptuelle :
|
||||
|
||||
```text
|
||||
fmt layer
|
||||
.with_filter(Targets)
|
||||
-> Filtered<fmt, Targets, Registry>
|
||||
```
|
||||
|
||||
Ce `Filtered` était ensuite boxed dans le `Vec<Box<dyn Layer<Registry>>>` placé derrière `tracing_subscriber::reload::Layer`.
|
||||
|
||||
Au démarrage sans sink, le `Vec` initial était vide. Le premier hot reload construisait donc un nouveau `Filtered` après l'installation du subscriber global puis remplaçait le `Vec` via `Handle::reload`. Or un per-layer `Filtered` a besoin que son `FilterId` soit enregistré lors de son attachement au subscriber. La documentation de `tracing-subscriber 0.3.23` indique explicitement que `Handle::reload` ne doit pas être utilisé pour remplacer directement un `Filtered`.
|
||||
|
||||
Le panic n'indique donc pas un défaut du contrat public KSP de hot reload, mais une composition interne incorrecte des layers de `pre.003`.
|
||||
|
||||
## Correction
|
||||
|
||||
Le runtime conserve :
|
||||
|
||||
```text
|
||||
reload::Layer<Vec<Box<dyn Layer<Registry>>>>
|
||||
```
|
||||
|
||||
mais la composition devient :
|
||||
|
||||
```text
|
||||
Vec reloadable
|
||||
├── Targets global takeover filter
|
||||
└── fmt console layer
|
||||
```
|
||||
|
||||
au lieu de :
|
||||
|
||||
```text
|
||||
Vec reloadable
|
||||
└── Filtered<fmt console layer, Targets>
|
||||
```
|
||||
|
||||
`Targets` est utilisé comme layer de filtrage global. Le layer `fmt` n'appelle plus `with_filter`.
|
||||
|
||||
Conséquences :
|
||||
|
||||
- aucun nouveau `Filtered` n'est injecté par `Handle::reload` ;
|
||||
- aucun `FilterId` tardif n'est nécessaire ;
|
||||
- le takeover reste global : les targets externes restent `OFF` ;
|
||||
- les niveaux KSP et overrides par préfixe restent inchangés ;
|
||||
- le `Vec` complet peut toujours être remplacé pour activer/désactiver des sinks à chaud ;
|
||||
- l'API publique `initialize` / `reinitialize` / `LoggingGuard` ne change pas ;
|
||||
- `pre.004` peut toujours ajouter le backend fichier au même runtime reloadable.
|
||||
|
||||
Une configuration sans sink conserve un `Vec` vide, donc le logging reste effectivement désactivé jusqu'à un `reinitialize()` qui ajoute une sortie.
|
||||
|
||||
## Tests
|
||||
|
||||
Le test d'intégration déjà présent qui a révélé la régression reste le test de non-régression principal :
|
||||
|
||||
```text
|
||||
global_runtime_supports_takeover_hot_reload_and_single_initialization
|
||||
```
|
||||
|
||||
Un test unitaire supplémentaire vérifie que la console prépare deux layers distincts : le takeover filter global et le formatter.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie du Rust. Conformément à la règle KSP de signal technique, la version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.3.fix.1"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 30
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/src/runtime.rs
|
||||
crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.003-fix.001.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Si ces validations sont propres, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004 — non-blocking console/file + guards + ANSI + reload sinks
|
||||
```
|
||||
305
deltas/0.1.2/pre.003.md
Normal file
305
deltas/0.1.2/pre.003.md
Normal file
@@ -0,0 +1,305 @@
|
||||
<!-- file: deltas/0.1.2/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.003
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.2-pre.002-fix.001
|
||||
```
|
||||
|
||||
La base de développement validée porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.2.fix.1"
|
||||
Cargo.toml header version = 28
|
||||
```
|
||||
|
||||
Les validations remontées avant l'ouverture de cette tranche sont propres :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Introduire le runtime subscriber de Logging sans encore ouvrir le backend fichier/non bloquant :
|
||||
|
||||
- ajouter `tracing-subscriber` avec la feature minimale `fmt` ;
|
||||
- installer une seule fois le subscriber global KSP ;
|
||||
- appliquer le takeover KSP et rendre silencieux les targets externes par défaut ;
|
||||
- mapper `LogFilterLevel` vers `LevelFilter` ;
|
||||
- appliquer un niveau KSP global puis les overrides par préfixe de target ;
|
||||
- introduire une première couche console stdout/stderr ;
|
||||
- intégrer les événements de lifecycle des spans `Off`, `NewAndClose` et `Full` ;
|
||||
- introduire `LoggingGuard`, `initialize()` et `reinitialize()` ;
|
||||
- permettre un démarrage sans sink puis une activation à chaud ;
|
||||
- vérifier le hot reload sans second subscriber global.
|
||||
|
||||
La console reste volontairement synchrone dans cette tranche intermédiaire. `pre.004` la remplacera par un writer `tracing-appender` non bloquant et ajoutera fichier, guards, dropped-line counters et stripping ANSI avant toute stabilisation de `0.1.2`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.2-pre.2.fix.1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.2-pre.3
|
||||
```
|
||||
|
||||
L'identifiant de livraison est :
|
||||
|
||||
```text
|
||||
0.1.2-pre.003
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine passe de version 28 à 29.
|
||||
|
||||
## Dépendance `tracing-subscriber`
|
||||
|
||||
L'audit du 2026-08-14 confirme `tracing-subscriber 0.3.23` dans la génération `^0.3`.
|
||||
|
||||
La dépendance est centralisée sous `[workspace.dependencies]` :
|
||||
|
||||
```toml
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||
```
|
||||
|
||||
`ksp-logging-lib` la consomme avec :
|
||||
|
||||
```toml
|
||||
tracing-subscriber.workspace = true
|
||||
```
|
||||
|
||||
La feature `fmt` fournit le formatter et entraîne les capacités `registry`/`std` nécessaires à la composition retenue. Ne sont pas activés par anticipation :
|
||||
|
||||
- `env-filter` ;
|
||||
- `ansi` ;
|
||||
- `tracing-log` ;
|
||||
- `json` ;
|
||||
- `time` ;
|
||||
- `chrono` ;
|
||||
- `parking_lot`.
|
||||
|
||||
`tracing-appender` reste absent jusqu'à `pre.004`.
|
||||
|
||||
## Takeover et filtering
|
||||
|
||||
Le runtime utilise `tracing_subscriber::filter::Targets`.
|
||||
|
||||
La construction est conceptuellement :
|
||||
|
||||
```text
|
||||
default unmatched targets = OFF
|
||||
ksp-* = LoggingSettings.default_filter
|
||||
target overrides = TargetFilter entries
|
||||
```
|
||||
|
||||
Conséquences :
|
||||
|
||||
- un événement `sqlx`, `hyper`, `rustls` ou autre target externe reste silencieux même à `ERROR` tant qu'aucune couche KSP ne le réémet explicitement ;
|
||||
- les crates KSP utilisent leur nom Cargo comme target ;
|
||||
- `ksp-logging-lib`, `ksp-store-lib`, etc. suivent le niveau global KSP ;
|
||||
- un `TargetFilter` plus spécifique peut relever ou abaisser le niveau d'une crate KSP donnée ;
|
||||
- aucune chaîne `RUST_LOG` ou `EnvFilter` n'est introduite.
|
||||
|
||||
## Console initiale
|
||||
|
||||
`ConsoleSettings::stdout()` et `ConsoleSettings::stderr()` construisent une couche `fmt` avec :
|
||||
|
||||
- target affiché ;
|
||||
- ANSI explicitement désactivé ;
|
||||
- lifecycle de span selon `SpanEvents` ;
|
||||
- filtering KSP `Targets`.
|
||||
|
||||
Cette couche utilise encore directement `std::io::stdout` / `std::io::stderr`. Ce writer synchrone est uniquement la fondation de `pre.003`; il n'est pas le contrat final de la release.
|
||||
|
||||
## Spans runtime
|
||||
|
||||
Le mapping retenu est :
|
||||
|
||||
```text
|
||||
SpanEvents::Off -> FmtSpan::NONE
|
||||
SpanEvents::NewAndClose -> FmtSpan::NEW | FmtSpan::CLOSE
|
||||
SpanEvents::Full -> FmtSpan::FULL
|
||||
```
|
||||
|
||||
`NewAndClose` active ainsi la surface nécessaire aux diagnostics de début/fin et de temps busy/idle fournis par le formatter sans obliger les consumers à utiliser directement `tracing-subscriber`.
|
||||
|
||||
## Subscriber global
|
||||
|
||||
La nouvelle API publique est :
|
||||
|
||||
```text
|
||||
ksp_logging_lib::LoggingGuard
|
||||
ksp_logging_lib::initialize(&LoggingSettings) -> Result<LoggingGuard>
|
||||
ksp_logging_lib::reinitialize(&mut LoggingGuard, &LoggingSettings) -> Result<()>
|
||||
```
|
||||
|
||||
`initialize()` :
|
||||
|
||||
1. valide/prépare les layers ;
|
||||
2. crée une unique infrastructure `reload::Layer` ;
|
||||
3. installe le subscriber global avec l'API fallible `tracing::subscriber::set_global_default` ;
|
||||
4. retourne un `LoggingGuard` possédant le handle de reload et les settings actifs.
|
||||
|
||||
Une seconde installation globale retourne :
|
||||
|
||||
```text
|
||||
logging.already_initialized
|
||||
```
|
||||
|
||||
La cause `SetGlobalDefaultError` est conservée comme `source` Core.
|
||||
|
||||
## Hot reload
|
||||
|
||||
La composition interne retenue est :
|
||||
|
||||
```text
|
||||
Registry
|
||||
-> reload::Layer
|
||||
-> Vec<Box<dyn Layer<Registry> + Send + Sync>>
|
||||
```
|
||||
|
||||
Le `Vec` peut être vide. Cela permet :
|
||||
|
||||
```text
|
||||
initialize(no sink)
|
||||
-> subscriber global installé mais silencieux
|
||||
|
||||
reinitialize(console enabled)
|
||||
-> console activée sans second subscriber global
|
||||
```
|
||||
|
||||
Le choix d'un `Vec` de layers boxed prépare directement `pre.004`, qui pourra ajouter ou retirer console/fichier sans changer la surface publique de reload.
|
||||
|
||||
`reinitialize()` prépare d'abord complètement la nouvelle représentation. Une erreur de validation/préparation retourne avant le swap et conserve :
|
||||
|
||||
- les settings actifs du `LoggingGuard` ;
|
||||
- les layers actuellement installés ;
|
||||
- le comportement de filtering en cours.
|
||||
|
||||
Une erreur effective du handle `reload` retourne :
|
||||
|
||||
```text
|
||||
logging.reload_failed
|
||||
```
|
||||
|
||||
et conserve sa cause externe via le contrat `source` Core.
|
||||
|
||||
## File settings pendant `pre.003`
|
||||
|
||||
`FileSettings` reste dans la surface publique définie par `pre.002`, mais le backend fichier n'est pas encore construit dans cette tranche.
|
||||
|
||||
`initialize()` / `reinitialize()` refusent donc temporairement une configuration avec `file = Some(...)` avec `logging.invalid_settings` et contexte `field = file` au lieu d'ignorer silencieusement la demande.
|
||||
|
||||
Cette restriction transitoire disparaîtra lorsque le backend fichier réel sera introduit en `pre.004`.
|
||||
|
||||
## Erreurs ajoutées
|
||||
|
||||
```text
|
||||
logging.already_initialized
|
||||
logging.reload_failed
|
||||
```
|
||||
|
||||
Elles s'ajoutent à :
|
||||
|
||||
```text
|
||||
logging.invalid_settings
|
||||
```
|
||||
|
||||
Aucune connaissance Logging n'est ajoutée à Core.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
### Unitaires runtime
|
||||
|
||||
- mapping complet des niveaux KSP ;
|
||||
- silence des targets externes ;
|
||||
- default KSP `Info` ;
|
||||
- override `ksp-logging-lib = Trace` ;
|
||||
- mapping des événements de span ;
|
||||
- rejet temporaire du backend fichier avant `pre.004`.
|
||||
|
||||
### Intégration runtime global
|
||||
|
||||
Un seul test global dans sa crate de test dédiée vérifie :
|
||||
|
||||
1. `initialize()` avec aucun sink ;
|
||||
2. absence d'admission des callsites tant que Logging est désactivé ;
|
||||
3. `reinitialize()` avec console active ;
|
||||
4. activation `Trace` de `ksp-logging-lib` par override ;
|
||||
5. maintien de `ksp-store-lib` à `Info` ;
|
||||
6. maintien de `sqlx` à `Off` même pour `Error` ;
|
||||
7. échec d'un reload demandant le backend fichier non encore disponible ;
|
||||
8. conservation des anciens settings/filtering après cet échec ;
|
||||
9. refus d'un deuxième `initialize()`.
|
||||
|
||||
Le changement de filtering est observé via des fonctions contenant des callsites `tracing::enabled!` stables, afin de vérifier que le reload invalide correctement l'intérêt mis en cache.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `crates/ksp-logging-lib/src/runtime.rs`
|
||||
- `crates/ksp-logging-lib/unit_tests/runtime.rs`
|
||||
- `crates/ksp-logging-lib/tests/runtime.rs`
|
||||
- `deltas/0.1.2/pre.003.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- `crates/ksp-logging-lib/Cargo.toml`
|
||||
- `crates/ksp-logging-lib/src/error.rs`
|
||||
- `crates/ksp-logging-lib/src/lib.rs`
|
||||
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Validations exécutées pendant la préparation
|
||||
|
||||
- revérification documentaire de `tracing-subscriber 0.3.23` et de ses features ;
|
||||
- contrôle TOML des manifests ;
|
||||
- contrôle des headers `file:` / `version:` ;
|
||||
- contrôle de la centralisation de `tracing-subscriber` sous `[workspace.dependencies]` ;
|
||||
- contrôle que `tracing-appender` reste absent ;
|
||||
- contrôle que le code production ajouté n'utilise ni `unwrap`, ni `expect`, ni `panic`, ni opérateur `?`, ni `unsafe` ;
|
||||
- contrôle que les usages directs de la stack tracing restent dans `ksp-logging-lib` ;
|
||||
- contrôle du contenu du delta contre la base reconstruite `0.1.2-pre.2.fix.1`.
|
||||
|
||||
## Validations à exécuter dans le workspace
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Aucune validation Cargo non exécutable dans l'environnement de préparation n'est déclarée réussie.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation de `pre.003`, passer à `0.1.2-pre.004` :
|
||||
|
||||
- `tracing-appender` ;
|
||||
- console non bloquante ;
|
||||
- fichier Never/Hourly/Daily ;
|
||||
- `WorkerGuard` / `ErrorCounter` ;
|
||||
- stripping ANSI fichier ;
|
||||
- hot reload des sinks non bloquants et de leurs guards.
|
||||
157
deltas/0.1.2/pre.004-fix.001.md
Normal file
157
deltas/0.1.2/pre.004-fix.001.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- file: deltas/0.1.2/pre.004-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.004-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4"
|
||||
Cargo.toml header version = 31
|
||||
```
|
||||
|
||||
## Validations remontées
|
||||
|
||||
Les validations utilisateur de `pre.004` sont :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace ECHEC
|
||||
cargo tree -p ksp-logging-lib OK
|
||||
cargo tree -p ksp-logging-lib -d OK — aucun doublon
|
||||
cargo tree -p ksp-logging-lib -e features inspecté
|
||||
```
|
||||
|
||||
Tous les tests unitaires, de callsite et de façade publique passent. Le seul échec est :
|
||||
|
||||
```text
|
||||
global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_initialization
|
||||
```
|
||||
|
||||
sur :
|
||||
|
||||
```text
|
||||
assertion failed: file_text.contains("file output marker")
|
||||
```
|
||||
|
||||
Le test émet une ligne sur le sink fichier, retire immédiatement ce sink par hot reload, puis lit le fichier. Il constitue donc un test direct du contrat de drain/flush lors d'un reload.
|
||||
|
||||
## Cause de lifecycle
|
||||
|
||||
`pre.004` faisait conceptuellement :
|
||||
|
||||
```text
|
||||
reload_handle.reload(new_layers)
|
||||
retire counters
|
||||
replace outputs
|
||||
drop(old WorkerGuard)
|
||||
```
|
||||
|
||||
Le layer `fmt` retiré possède les clones `NonBlocking` utilisés pour alimenter le worker. KSP ne récupérait cependant pas explicitement l'ancien `Vec` de layers ; l'ordre entre la destruction effective de ces anciens layers et la destruction des `WorkerGuard` n'était donc pas exprimé dans notre lifecycle.
|
||||
|
||||
Pour un sink non bloquant, l'ordre voulu est explicite :
|
||||
|
||||
```text
|
||||
1. préparer complètement le nouveau runtime
|
||||
2. remplacer le Vec actif et récupérer l'ancien Vec
|
||||
3. mémoriser les dropped-line counters
|
||||
4. remplacer les outputs actifs
|
||||
5. détruire les anciens layers / NonBlocking senders
|
||||
6. détruire les anciens WorkerGuard
|
||||
7. retourner du reinitialize()
|
||||
```
|
||||
|
||||
`WorkerGuard` envoie le signal de shutdown au worker et attend son drain/flush de manière bornée. Les anciens senders doivent donc être libérés avant cette étape lorsqu'un sink vient d'être retiré.
|
||||
|
||||
## Correction
|
||||
|
||||
`reinitialize()` n'utilise plus :
|
||||
|
||||
```text
|
||||
Handle::reload(new_layers)
|
||||
```
|
||||
|
||||
pour les changements de runtime.
|
||||
|
||||
Il utilise :
|
||||
|
||||
```text
|
||||
Handle::modify(... mem::replace(active_layers, new_layers) ...)
|
||||
```
|
||||
|
||||
et récupère ainsi l'ancien `RuntimeLayers`.
|
||||
|
||||
Après succès du swap :
|
||||
|
||||
```text
|
||||
drop(retired_layers)
|
||||
drop(retired_outputs)
|
||||
```
|
||||
|
||||
est exécuté dans cet ordre.
|
||||
|
||||
Cette correction :
|
||||
|
||||
- ne change pas l'API publique ;
|
||||
- conserve le subscriber global unique ;
|
||||
- conserve le takeover KSP ;
|
||||
- conserve la préparation transactionnelle des nouveaux sinks avant le swap ;
|
||||
- conserve l'ancienne configuration lorsqu'une validation ou une construction de sink échoue avant le swap ;
|
||||
- rend explicite le lifecycle de retrait des `NonBlocking` writers avant leurs `WorkerGuard` ;
|
||||
- évite d'ajouter un sleep ou un polling temporel au test.
|
||||
|
||||
Le test d'intégration qui a révélé le défaut reste inchangé et sert directement de test de non-régression.
|
||||
|
||||
## Référence backend
|
||||
|
||||
`tracing-appender 0.2.5` documente `WorkerGuard` comme responsable du flush des logs bufferisés à sa destruction. Son implémentation de `Drop` envoie un `Msg::Shutdown` au worker puis attend le signal de fin de drain de manière bornée. KSP doit donc contrôler clairement l'ordre de destruction des senders/layers et du guard au moment d'un hot reload.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie du Rust. La version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.1"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 32
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/src/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.004-fix.001.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le graphe Cargo/features de `pre.004` a déjà été remonté sans doublon. Il pourra être réaudité dans `pre.005` avec les validations d'intégration finales.
|
||||
|
||||
Si ces validations sont propres, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||
```
|
||||
135
deltas/0.1.2/pre.004-fix.002.md
Normal file
135
deltas/0.1.2/pre.004-fix.002.md
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- file: deltas/0.1.2/pre.004-fix.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.004-fix.002
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004-fix.001
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.1"
|
||||
Cargo.toml header version = 32
|
||||
```
|
||||
|
||||
## Validation remontée
|
||||
|
||||
Après application de `pre.004-fix.001`, un rebuild propre a donné :
|
||||
|
||||
```text
|
||||
cargo clean OK
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace ECHEC
|
||||
```
|
||||
|
||||
Tous les tests sauf le test runtime global passent encore. L'échec reste strictement identique :
|
||||
|
||||
```text
|
||||
assertion failed: file_text.contains("file output marker")
|
||||
```
|
||||
|
||||
La reproduction après `cargo clean` invalide donc l'hypothèse selon laquelle cet échec précis provenait de l'ordre de destruction corrigé par `pre.004-fix.001`. Ce lifecycle explicite est néanmoins conservé.
|
||||
|
||||
## Cause réelle
|
||||
|
||||
`tracing-subscriber 0.3.23` active par défaut la sanitization ANSI des valeurs dans `fmt::Layer`. Cette protection intervient pendant le formatage, donc avant l'appel au `MakeWriter`.
|
||||
|
||||
Le sink fichier KSP était composé comme suit :
|
||||
|
||||
```text
|
||||
value containing ESC
|
||||
-> fmt::Layer ANSI sanitization
|
||||
-> NonBlocking
|
||||
-> StripAnsiWriter
|
||||
-> RollingFileAppender
|
||||
```
|
||||
|
||||
Le `StripAnsiWriter` KSP ne recevait donc plus les octets ESC originaux à supprimer. Le test attend volontairement que :
|
||||
|
||||
```text
|
||||
file ESC[31moutput ESC[0m marker
|
||||
```
|
||||
|
||||
devienne dans le fichier :
|
||||
|
||||
```text
|
||||
file output marker
|
||||
```
|
||||
|
||||
La sanitization native et le stripping KSP sont deux politiques différentes : KSP veut supprimer les contrôles du fichier, pas les transformer avant son propre writer.
|
||||
|
||||
## Correction
|
||||
|
||||
Le runtime distingue désormais la politique du formatter selon le sink :
|
||||
|
||||
```text
|
||||
console
|
||||
fmt::Layer.with_ansi(false)
|
||||
fmt::Layer.with_ansi_sanitization(true)
|
||||
-> NonBlocking console
|
||||
|
||||
file
|
||||
fmt::Layer.with_ansi(false)
|
||||
fmt::Layer.with_ansi_sanitization(false)
|
||||
-> NonBlocking
|
||||
-> StripAnsiWriter
|
||||
-> RollingFileAppender
|
||||
```
|
||||
|
||||
La console conserve donc la protection native de `tracing-subscriber`. Le fichier laisse passer jusqu'au worker les séquences présentes dans les valeurs afin que `StripAnsiWriter` les supprime avant persistence.
|
||||
|
||||
Le stripping reste hors du hot path : il est toujours exécuté derrière la queue non bloquante.
|
||||
|
||||
Le test d'intégration runtime reste inchangé. Il continue à vérifier :
|
||||
|
||||
- l'émission fichier après hot reload ;
|
||||
- le retrait immédiat du sink et son drain ;
|
||||
- la présence du target et du callsite ;
|
||||
- l'absence de séquences ANSI ;
|
||||
- le silence des targets externes.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie du Rust. La version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.2"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 33
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/src/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.004-fix.002.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Si ces validations sont propres, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||
```
|
||||
131
deltas/0.1.2/pre.004-fix.003.md
Normal file
131
deltas/0.1.2/pre.004-fix.003.md
Normal file
@@ -0,0 +1,131 @@
|
||||
<!-- file: deltas/0.1.2/pre.004-fix.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.004-fix.003
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004-fix.002
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.2"
|
||||
Cargo.toml header version = 33
|
||||
```
|
||||
|
||||
## Validation remontée
|
||||
|
||||
Après application de `pre.004-fix.002` :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace ECHEC
|
||||
```
|
||||
|
||||
Le marqueur fichier précédemment absent est désormais correctement persisté et les assertions de présence du target, du callsite et d'absence d'ANSI passent. Le test runtime global échoue plus loin sur :
|
||||
|
||||
```text
|
||||
assertion failed: !file_text.contains("external marker must remain silent")
|
||||
```
|
||||
|
||||
Le défaut restant concerne donc exclusivement le takeover : un événement `tracing` émis directement avec le target externe `sqlx` atteint encore le sink fichier alors que la politique KSP exige son silence total.
|
||||
|
||||
## Cause réelle
|
||||
|
||||
`pre.003-fix.001` avait évité le panic `Filtered`/`FilterId` en plaçant `Targets` comme layer global distinct dans le même :
|
||||
|
||||
```text
|
||||
Vec<Box<dyn Layer<Registry>>>
|
||||
```
|
||||
|
||||
que les formatters.
|
||||
|
||||
Cette composition n'est toutefois pas correcte pour le filtrage global au niveau des callsites. L'implémentation `Layer` de `Vec<L>` agrège `register_callsite` en conservant l'intérêt le plus élevé retourné par ses enfants. Un `fmt::Layer` intéressé par le callsite peut donc produire un intérêt actif alors que `Targets` retourne `Interest::never()` pour un target externe.
|
||||
|
||||
Lorsque le callsite est enregistré comme toujours actif, `enabled()` n'est ensuite pas consulté à chaque émission. Le `Targets` frère du formatter ne peut donc plus bloquer l'événement externe.
|
||||
|
||||
Le test avec `sqlx` expose précisément cette fuite.
|
||||
|
||||
## Correction
|
||||
|
||||
Les layers de sortie sont d'abord construits dans un `Vec` :
|
||||
|
||||
```text
|
||||
outputs
|
||||
├── console fmt layer, si actif
|
||||
└── file fmt layer, si actif
|
||||
```
|
||||
|
||||
Le takeover est ensuite composé **devant tout ce groupe** :
|
||||
|
||||
```text
|
||||
Targets
|
||||
.and_then(outputs)
|
||||
```
|
||||
|
||||
et ce composite unique devient l'élément du `Vec` reloadable :
|
||||
|
||||
```text
|
||||
reload::Layer
|
||||
└── Vec
|
||||
└── Targets -> output layers
|
||||
```
|
||||
|
||||
Cette forme rétablit la sémantique de filtre global : un `Interest::never()` produit par `Targets` court-circuite le groupe de sinks avant leur formatter.
|
||||
|
||||
Elle conserve simultanément les propriétés requises :
|
||||
|
||||
- aucun `Layer::with_filter` n'est utilisé sur un layer remplacé à chaud ;
|
||||
- aucun `Filtered` et donc aucun `FilterId` reloadable n'est introduit ;
|
||||
- console et fichier restent activables/désactivables dynamiquement ;
|
||||
- `Handle::modify` continue de récupérer l'ancien composite avant destruction de ses `WorkerGuard` ;
|
||||
- la sanitization console et le stripping ANSI fichier de `fix.002` restent inchangés ;
|
||||
- l'API publique reste inchangée.
|
||||
|
||||
Le test d'intégration runtime conserve son assertion directe sur un événement `tracing::error!` de target `sqlx`. Il reste donc le test de non-régression du takeover effectif, au-delà du test unitaire de `Targets::would_enable`.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie du Rust. La version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.3"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 34
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/src/runtime.rs
|
||||
crates/ksp-logging-lib/unit_tests/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.004-fix.003.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Aucune dépendance n'est modifiée par ce fix. Après validation propre, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||
```
|
||||
95
deltas/0.1.2/pre.004-fix.004.md
Normal file
95
deltas/0.1.2/pre.004-fix.004.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<!-- file: deltas/0.1.2/pre.004-fix.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.004-fix.004
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004-fix.003
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.3"
|
||||
Cargo.toml header version = 34
|
||||
```
|
||||
|
||||
## Validation remontée
|
||||
|
||||
Après application de `pre.004-fix.003` :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo test --workspace OK
|
||||
cargo clippy --workspace --all-targets WARNING
|
||||
```
|
||||
|
||||
Tous les tests fonctionnels passent désormais, y compris le test runtime global couvrant takeover, sorties non bloquantes, hot reload, fichier, stripping ANSI et initialisation unique.
|
||||
|
||||
Clippy signale uniquement :
|
||||
|
||||
```text
|
||||
clippy::vec_init_then_push
|
||||
```
|
||||
|
||||
sur la construction du `Vec` reloadable après création du composite `Targets -> sinks`.
|
||||
|
||||
## Correction
|
||||
|
||||
La construction :
|
||||
|
||||
```rust
|
||||
let mut layers = RuntimeLayers::new();
|
||||
layers.push(takeover_layer);
|
||||
```
|
||||
|
||||
est remplacée par :
|
||||
|
||||
```rust
|
||||
let layers = vec![takeover_layer];
|
||||
```
|
||||
|
||||
Aucun comportement runtime, test, setting, writer, filtre ou contrat public n'est modifié.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie du Rust. La version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.4.fix.4"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 35
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/src/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.004-fix.004.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Aucune dépendance n'est modifiée. Après validation propre, la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005 — intégration + concurrence + saturation + audits
|
||||
```
|
||||
301
deltas/0.1.2/pre.004.md
Normal file
301
deltas/0.1.2/pre.004.md
Normal file
@@ -0,0 +1,301 @@
|
||||
<!-- file: deltas/0.1.2/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.004
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.2-pre.003-fix.001
|
||||
```
|
||||
|
||||
La base de développement validée porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.3.fix.1"
|
||||
Cargo.toml header version = 30
|
||||
```
|
||||
|
||||
Les validations remontées avant l'ouverture de cette tranche sont propres :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Compléter le runtime Logging avec les sorties réellement retenues pour `0.1.2` :
|
||||
|
||||
- ajouter `tracing-appender` ;
|
||||
- rendre console et fichier non bloquants pour le caller ;
|
||||
- posséder les `WorkerGuard` jusqu'au reload/shutdown approprié ;
|
||||
- exposer les dropped-line counters ;
|
||||
- activer le fichier `Never/Hourly/Daily` avec construction fallible ;
|
||||
- supprimer les séquences ANSI avant persistence ;
|
||||
- conserver le takeover et le hot reload transactionnel établis par `pre.003-fix.001`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.2-pre.3.fix.1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.2-pre.4
|
||||
```
|
||||
|
||||
L'identifiant de livraison est :
|
||||
|
||||
```text
|
||||
0.1.2-pre.004
|
||||
```
|
||||
|
||||
Le header du `Cargo.toml` racine passe de version 30 à 31.
|
||||
|
||||
## Dépendance `tracing-appender`
|
||||
|
||||
L'audit du 2026-08-14 confirme `tracing-appender 0.2.5`, publié le 2026-04-17, dans la génération `^0.2`.
|
||||
|
||||
La dépendance est centralisée sous `[workspace.dependencies]` :
|
||||
|
||||
```toml
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
```
|
||||
|
||||
`ksp-logging-lib` la consomme avec :
|
||||
|
||||
```toml
|
||||
tracing-appender.workspace = true
|
||||
```
|
||||
|
||||
Aucune feature optionnelle n'est activée. Le backend expose `NonBlockingBuilder`, `WorkerGuard`, `ErrorCounter` et `RollingFileAppender` sans feature supplémentaire.
|
||||
|
||||
## Console non bloquante
|
||||
|
||||
La console n'utilise plus directement `stdout`/`stderr` dans le formatter.
|
||||
|
||||
Chaque sink console construit :
|
||||
|
||||
```text
|
||||
Stdout | Stderr
|
||||
-> NonBlockingBuilder(lossy = true)
|
||||
-> fmt layer
|
||||
+ WorkerGuard
|
||||
+ ErrorCounter
|
||||
```
|
||||
|
||||
Le mode lossy est explicite : lorsque la queue est saturée, un log peut être abandonné au lieu de bloquer le thread appelant.
|
||||
|
||||
Le thread worker console est nommé :
|
||||
|
||||
```text
|
||||
ksp-logging-console
|
||||
```
|
||||
|
||||
## Fichier et rotation
|
||||
|
||||
`FileSettings` est maintenant réellement consommé par le runtime.
|
||||
|
||||
Le mapping est :
|
||||
|
||||
```text
|
||||
FileRotation::Never -> Rotation::NEVER
|
||||
FileRotation::Hourly -> Rotation::HOURLY
|
||||
FileRotation::Daily -> Rotation::DAILY
|
||||
```
|
||||
|
||||
Le runtime utilise uniquement :
|
||||
|
||||
```text
|
||||
RollingFileAppender::builder()
|
||||
.rotation(...)
|
||||
.filename_prefix(...)
|
||||
.build(directory)
|
||||
```
|
||||
|
||||
La forme builder retourne un `Result`; aucune API de construction qui panique n'est utilisée par KSP.
|
||||
|
||||
Un échec retourne :
|
||||
|
||||
```text
|
||||
logging.file_output_initialization_failed
|
||||
```
|
||||
|
||||
avec le directory, le file-name prefix et l'erreur `InitError` externe conservés dans le contrat Core.
|
||||
|
||||
## Stripping ANSI
|
||||
|
||||
Le fichier est composé comme suit :
|
||||
|
||||
```text
|
||||
fmt layer
|
||||
-> NonBlocking queue
|
||||
-> StripAnsiWriter
|
||||
-> RollingFileAppender
|
||||
```
|
||||
|
||||
Le stripping est donc effectué par le thread logging et non par le caller.
|
||||
|
||||
`StripAnsiWriter` conserve un état entre les appels `Write` afin de retirer correctement une séquence terminal coupée entre plusieurs buffers. La première surface couvre :
|
||||
|
||||
- CSI (`ESC [` ... final byte) ;
|
||||
- OSC terminé par BEL ou ST ;
|
||||
- autres chaînes terminal ESC de type DCS/SOS/PM/APC terminées par ST.
|
||||
|
||||
Ce mécanisme est générique et n'introduit aucune dépendance Tauri.
|
||||
|
||||
## Formatter humain
|
||||
|
||||
Console et fichier partagent le formatter humain KSP avec :
|
||||
|
||||
- timestamp standard `tracing-subscriber` ;
|
||||
- niveau ;
|
||||
- target ;
|
||||
- champs/message ;
|
||||
- source file ;
|
||||
- line number ;
|
||||
- ANSI du formatter désactivé ;
|
||||
- lifecycle de spans selon `SpanEvents`.
|
||||
|
||||
La ponctuation exacte du formatter reste hors contrat public.
|
||||
|
||||
## Ownership et reload
|
||||
|
||||
`LoggingGuard` possède désormais les outputs actifs :
|
||||
|
||||
```text
|
||||
LoggingGuard
|
||||
├── reload handle
|
||||
├── current LoggingSettings
|
||||
├── active console WorkerGuard/ErrorCounter
|
||||
├── active file WorkerGuard/ErrorCounter
|
||||
└── cumulative retired dropped-line counters
|
||||
```
|
||||
|
||||
`reinitialize()` :
|
||||
|
||||
1. valide les nouveaux settings ;
|
||||
2. construit entièrement le nouveau file appender et tous les nouveaux non-blocking writers/guards ;
|
||||
3. construit les nouveaux layers ;
|
||||
4. remplace le `Vec` reloadable ;
|
||||
5. mémorise les dropped lines des anciens sinks ;
|
||||
6. remplace les outputs actifs ;
|
||||
7. détruit les anciens `WorkerGuard`, provoquant leur flush borné par le backend.
|
||||
|
||||
Une erreur avant le swap détruit uniquement les nouveaux outputs préparés et laisse l'ancienne configuration active.
|
||||
|
||||
## Dropped lines
|
||||
|
||||
Nouvelle surface publique :
|
||||
|
||||
```text
|
||||
DroppedLines
|
||||
LoggingGuard::dropped_lines() -> DroppedLines
|
||||
```
|
||||
|
||||
`DroppedLines` expose :
|
||||
|
||||
```text
|
||||
console()
|
||||
file()
|
||||
total()
|
||||
```
|
||||
|
||||
Les valeurs sont cumulées pour toute la durée de vie du `LoggingGuard`, y compris après plusieurs hot reloads. Les `ErrorCounter` de `tracing-appender` ne sont pas exposés directement aux consumers.
|
||||
|
||||
## Tests
|
||||
|
||||
### Unitaires
|
||||
|
||||
- mapping `Never/Hourly/Daily` ;
|
||||
- runtime sans sink ;
|
||||
- console préparée avec filter séparé, non-blocking output et guard ;
|
||||
- addition saturante des dropped-line counters ;
|
||||
- stripping CSI ;
|
||||
- stripping d'une CSI coupée entre deux writes ;
|
||||
- stripping OSC terminé par BEL/ST.
|
||||
|
||||
### Intégration runtime global
|
||||
|
||||
Le test global vérifie désormais :
|
||||
|
||||
1. initialisation silencieuse sans sink ;
|
||||
2. hot reload console non bloquante ;
|
||||
3. takeover KSP et silence `sqlx` ;
|
||||
4. erreur de création d'un file appender sur un chemin invalide ;
|
||||
5. conservation des settings précédents après cet échec ;
|
||||
6. hot reload vers un fichier `Never` ;
|
||||
7. émission d'un message contenant des codes ANSI ;
|
||||
8. retrait du sink fichier par reload, donc drop/flush de son guard ;
|
||||
9. présence du message KSP dans le fichier ;
|
||||
10. absence des codes ANSI persistés ;
|
||||
11. absence du message externe `sqlx` ;
|
||||
12. présence du target et de la source ;
|
||||
13. lecture de la statistique cumulée ;
|
||||
14. refus d'un second `initialize()`.
|
||||
|
||||
La saturation déterministe avec une queue artificiellement petite est reportée à `pre.005`, où un writer de test injecté pourra être utilisé sans rendre la capacité de queue publique dans `LoggingSettings`.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
- `crates/ksp-logging-lib/src/writer.rs`
|
||||
- `crates/ksp-logging-lib/unit_tests/writer.rs`
|
||||
- `deltas/0.1.2/pre.004.md`
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- `crates/ksp-logging-lib/Cargo.toml`
|
||||
- `crates/ksp-logging-lib/src/error.rs`
|
||||
- `crates/ksp-logging-lib/src/lib.rs`
|
||||
- `crates/ksp-logging-lib/src/runtime.rs`
|
||||
- `crates/ksp-logging-lib/unit_tests/runtime.rs`
|
||||
- `crates/ksp-logging-lib/tests/runtime.rs`
|
||||
- `crates/ksp-logging-lib/tests/public_api.rs`
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`
|
||||
|
||||
## Validations exécutées pendant la préparation
|
||||
|
||||
- revérification documentaire officielle de `tracing-appender 0.2.5` ;
|
||||
- vérification de la sémantique lossy de `NonBlockingBuilder` ;
|
||||
- vérification de `WorkerGuard` et `ErrorCounter::dropped_lines()` ;
|
||||
- vérification du builder fallible de `RollingFileAppender` ;
|
||||
- contrôle TOML des manifests ;
|
||||
- contrôle des headers `file:` / `version:` ;
|
||||
- contrôle de la centralisation de `tracing-appender` sous `[workspace.dependencies]` ;
|
||||
- contrôle que le code production ajouté n'utilise ni `unwrap`, ni `expect`, ni `panic`, ni opérateur `?`, ni `unsafe` ;
|
||||
- contrôle que les usages directs de `tracing-appender` restent dans `ksp-logging-lib`.
|
||||
|
||||
## Validations à exécuter dans le workspace
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Aucune validation Cargo non exécutable dans l'environnement de préparation n'est déclarée réussie.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation de `pre.004`, passer à `0.1.2-pre.005` :
|
||||
|
||||
- concurrence/reloads répétés ;
|
||||
- saturation déterministe et dropped lines ;
|
||||
- audits de façade et usages directs de la stack tracing ;
|
||||
- audits Cargo/features/doublons ;
|
||||
- mesure grossière de l'overhead du reload/runtime ;
|
||||
- compléments de tests et documentation de crate avant la tranche finale.
|
||||
88
deltas/0.1.2/pre.005-fix.001.md
Normal file
88
deltas/0.1.2/pre.005-fix.001.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<!-- file: deltas/0.1.2/pre.005-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.005-fix.001
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.5"
|
||||
Cargo.toml header version = 36
|
||||
```
|
||||
|
||||
## Validation remontée
|
||||
|
||||
La validation utilisateur de `pre.005` est fonctionnellement propre :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||
```
|
||||
|
||||
Le probe d'overhead a également passé son garde-fou grossier sur 200000 itérations.
|
||||
|
||||
Le test runtime concurrent produit toutefois un grand volume de lignes `TRACE` sur stderr au démarrage du stress test. Le runtime est encore sur la configuration précédente `console_enabled` avec un override `Trace` lorsque les producteurs sont libérés ; ils peuvent donc émettre avant le premier reload vers la configuration silencieuse.
|
||||
|
||||
## Correction
|
||||
|
||||
`exercise_concurrent_reload` effectue maintenant un `reinitialize` vers `quiet_console` **avant** de créer/libérer les producteurs concurrents.
|
||||
|
||||
Le stress test conserve ensuite exactement :
|
||||
|
||||
- 4 producteurs ;
|
||||
- les appels continus à `ksp_logging_lib::trace!` ;
|
||||
- 32 hot reloads ;
|
||||
- l'alternance entre console non bloquante présente avec filtre KSP `Off` et runtime sans sink ;
|
||||
- les assertions de succès des reloads et des joins.
|
||||
|
||||
La correction supprime uniquement la fenêtre de course initiale qui laissait la configuration `Trace` précédente produire des lignes. Elle ne modifie aucun comportement de production, aucune API publique et aucune dépendance.
|
||||
|
||||
## Version technique
|
||||
|
||||
Ce correctif modifie un fichier Rust de test. La version workspace devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.5.fix.1"
|
||||
```
|
||||
|
||||
et l'en-tête du `Cargo.toml` racine devient :
|
||||
|
||||
```text
|
||||
# version: 37
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/tests/runtime.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
deltas/0.1.2/pre.005-fix.001.md
|
||||
```
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Si ces validations sont propres et que le test runtime ne pollue plus la console, `pre.005` est clôturée et la tranche suivante reste :
|
||||
|
||||
```text
|
||||
0.1.2-pre.006 — validation finale, documentation, cleanup et prompt 0.1.3
|
||||
```
|
||||
173
deltas/0.1.2/pre.005.md
Normal file
173
deltas/0.1.2/pre.005.md
Normal file
@@ -0,0 +1,173 @@
|
||||
<!-- file: deltas/0.1.2/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.005
|
||||
|
||||
## Identité
|
||||
|
||||
```text
|
||||
0.1.2-pre.005 — intégration, concurrence, saturation et audits Logging
|
||||
```
|
||||
|
||||
Version Cargo portée par ce delta :
|
||||
|
||||
```text
|
||||
0.1.2-pre.5
|
||||
```
|
||||
|
||||
## Base
|
||||
|
||||
Base directe : `0.1.2-pre.004-fix.004`, version Cargo `0.1.2-pre.4.fix.4`.
|
||||
|
||||
La validation utilisateur de cette base a exécuté avec succès :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le test runtime global passe alors avec takeover des targets externes, console/fichier non bloquants, stripping ANSI, lifecycle des `WorkerGuard` et hot reload.
|
||||
|
||||
## Mission du delta
|
||||
|
||||
Cette tranche ne modifie pas la surface fonctionnelle publique de Logging. Elle durcit la fondation existante avant la validation finale :
|
||||
|
||||
- saturation déterministe des queues lossy ;
|
||||
- émissions concurrentes pendant hot reload ;
|
||||
- vérification des temps de lifecycle de spans ;
|
||||
- audit automatique du takeover de dépendances ;
|
||||
- documentation consommateur de la crate ;
|
||||
- probe diagnostic de l'overhead du mécanisme reload.
|
||||
|
||||
## Changements Rust
|
||||
|
||||
### Construction non bloquante testable sans setting supplémentaire
|
||||
|
||||
`runtime.rs` centralise la construction du `NonBlockingBuilder` dans un helper privé utilisé par la production.
|
||||
|
||||
La politique reste :
|
||||
|
||||
```text
|
||||
lossy = true
|
||||
```
|
||||
|
||||
La taille de queue n'entre pas dans `LoggingSettings`. Le test unitaire peut cependant dériver le même builder avec `buffered_lines_limit(1)` afin de provoquer une saturation contrôlée.
|
||||
|
||||
### Saturation déterministe
|
||||
|
||||
Le test unitaire runtime introduit un writer qui bloque volontairement son worker sur la première écriture.
|
||||
|
||||
Une fois le worker bloqué :
|
||||
|
||||
1. la queue est limitée à une ligne ;
|
||||
2. un producteur émet 1024 lignes supplémentaires ;
|
||||
3. le producteur doit terminer sous timeout alors que le writer reste bloqué ;
|
||||
4. le compteur `ErrorCounter::dropped_lines()` doit être strictement positif ;
|
||||
5. le writer est ensuite libéré et le `WorkerGuard` peut terminer proprement.
|
||||
|
||||
Ce test distingue directement la politique lossy retenue d'une régression vers une queue exerçant de la backpressure.
|
||||
|
||||
### Concurrence pendant hot reload
|
||||
|
||||
Le test runtime global lance quatre threads qui émettent continuellement via `ksp_logging_lib::trace!` pendant que le thread possédant `LoggingGuard` effectue 32 `reinitialize()` successifs.
|
||||
|
||||
Les settings alternent entre :
|
||||
|
||||
- runtime sans sink ;
|
||||
- console non bloquante présente avec niveau KSP `Off`.
|
||||
|
||||
Le test exerce donc le reload, la création/retrait de worker guards et la lecture concurrente du subscriber sans inonder stdout/stderr.
|
||||
|
||||
### Lifecycle des spans
|
||||
|
||||
Un nouveau test avec subscriber local vérifie que `FmtSpan::NEW | FmtSpan::CLOSE` produit pour un span KSP :
|
||||
|
||||
- l'identité du span ;
|
||||
- l'événement `new` ;
|
||||
- l'événement `close` ;
|
||||
- `time.busy` ;
|
||||
- `time.idle`.
|
||||
|
||||
### Audit de takeover
|
||||
|
||||
`tests/ownership.rs` parcourt les crates du workspace autres que `ksp-logging-lib` et rejette :
|
||||
|
||||
- une dépendance Cargo directe `tracing` ;
|
||||
- une dépendance directe `tracing-subscriber` ;
|
||||
- une dépendance directe `tracing-appender` ;
|
||||
- les usages Rust directs `tracing::`, `tracing_subscriber::` ou `tracing_appender::`.
|
||||
|
||||
La crate Logging elle-même est explicitement exclue de cet audit car elle possède légitimement la stack.
|
||||
|
||||
## Documentation de crate
|
||||
|
||||
Ajouts :
|
||||
|
||||
```text
|
||||
crates/ksp-logging-lib/README.md
|
||||
crates/ksp-logging-lib/USAGE.md
|
||||
crates/ksp-logging-lib/TODO.md
|
||||
```
|
||||
|
||||
Ils documentent notamment :
|
||||
|
||||
- target = nom Cargo de la crate propriétaire ;
|
||||
- champs structurés additionnels ;
|
||||
- `initialize` puis `reinitialize` ;
|
||||
- hot reload transactionnel ;
|
||||
- spans sync/async ;
|
||||
- dropped lines ;
|
||||
- capacités explicitement différées.
|
||||
|
||||
## Probe d'overhead
|
||||
|
||||
`tests/overhead.rs` est ignoré par défaut car il s'agit d'un probe temporel diagnostic, pas d'un benchmark de précision.
|
||||
|
||||
Commande explicite :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Le probe compare un filtre local fixe au même filtre derrière `tracing_subscriber::reload::Layer` sur 200000 événements et n'échoue que si le coût reload dépasse un garde-fou volontairement très large. Le résultat sert à repérer une régression grossière ; il ne constitue pas une mesure HFT ni un engagement de performance absolue.
|
||||
|
||||
## Audit des dépendances
|
||||
|
||||
Aucune dépendance n'est ajoutée par `pre.005`.
|
||||
|
||||
Lors de `pre.004`, les commandes utilisateur ont observé :
|
||||
|
||||
```text
|
||||
tracing 0.1.44
|
||||
tracing-subscriber 0.3.23
|
||||
tracing-appender 0.2.5
|
||||
```
|
||||
|
||||
`cargo tree -p ksp-logging-lib -d` ne signalait aucun doublon. Le graphe de features n'activait pas via KSP `tracing-attributes`, `tracing-log`, `env-filter`, JSON/Serde ou le formatter ANSI. Ces commandes doivent être réexécutées sur `pre.005` avant validation finale de la tranche car une validation d'une base précédente ne vaut pas validation du delta courant.
|
||||
|
||||
## Validations à exécuter
|
||||
|
||||
Non exécutées dans l'environnement de génération :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
La base stable ne contient pas de répertoire `scripts/`; aucun script inexistant n'est déclaré réussi.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation de `pre.005`, la tranche prévue est :
|
||||
|
||||
```text
|
||||
0.1.2-pre.006 — validation finale, documentation, cleanup et prompt 0.1.3
|
||||
```
|
||||
69
deltas/0.1.2/pre.006-fix.001.md
Normal file
69
deltas/0.1.2/pre.006-fix.001.md
Normal file
@@ -0,0 +1,69 @@
|
||||
<!-- file: deltas/0.1.2/pre.006-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.006-fix.001
|
||||
|
||||
## Nature
|
||||
|
||||
Correctif **documentaire uniquement** appliqué après validation complète de `0.1.2-pre.006`.
|
||||
|
||||
La version Cargo reste :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.6"
|
||||
Cargo.toml header version = 38
|
||||
```
|
||||
|
||||
Aucun fichier Rust, manifest Cargo, dépendance ou comportement runtime n'est modifié.
|
||||
|
||||
## Base validée
|
||||
|
||||
La validation utilisateur de `pre.006` est propre :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo build -p ksp-logging-lib OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||
cargo tree -p ksp-logging-lib OK
|
||||
cargo tree -p ksp-logging-lib -d OK (aucun doublon)
|
||||
cargo tree -p ksp-logging-lib -e features OK
|
||||
cargo tree -p ksp-logging-lib -e normal OK (Tokio absent)
|
||||
cargo tree -p ksp-logging-lib -e dev OK (Tokio seul dev-dependency)
|
||||
```
|
||||
|
||||
Les deux tests Tokio réels passent et le build normal de `ksp-logging-lib` confirme que Tokio reste hors du graphe runtime normal.
|
||||
|
||||
## Corrections du prompt `0.1.3`
|
||||
|
||||
Le prompt Config est corrigé avant `rel.001` afin de ne pas démarrer la session suivante avec d'anciens contrats khadhroony-bot3 devenus incorrects.
|
||||
|
||||
Décisions enregistrées :
|
||||
|
||||
1. les vrais fichiers de configuration runtime sont sous `config/` ;
|
||||
2. les schemas sont sous `config/schemas/` ;
|
||||
3. les exemples sont sous `config/examples/`, séparés des fichiers réels ;
|
||||
4. la conception doit reprendre un système de **documents unitaires** spécialisés et de **fichiers composites** qui assemblent ces documents pour un exécutable/application et peuvent sélectionner/remplacer les profils ;
|
||||
5. `ksp-config-lib` est le propriétaire unique de la lecture, résolution, validation et mutation des fichiers Config ainsi que des variables d'environnement applicatives ; les autres crates/apps passent par ses APIs ;
|
||||
6. les namespaces d'environnement deviennent `KSP_*`, `KSP_PUBLIC_*`, `KSP_SECRET_*` pour KSP et `KSPB_*`, `KSPB_PUBLIC_*`, `KSPB_SECRET_*` pour la branche bot ;
|
||||
7. les secrets ne suivent plus une règle absolue « jamais exposés » : ils restent protégés contre toute exposition implicite, mais les composants légitimes et les applications de management Config doivent pouvoir les consulter/modifier via des contrats explicitement autorisés ;
|
||||
8. `ksp-app-config-desk` est cité comme premier consommateur probable d'une telle surface privilégiée, avant une éventuelle application générale disposant d'une section Config.
|
||||
|
||||
Le détail des formats, contrats d'autorisation et découpage fonctionnel reste volontairement à décider lors du brainstorming obligatoire de `0.1.3-pre.001`.
|
||||
|
||||
## Fichiers
|
||||
|
||||
Modifiés :
|
||||
|
||||
- `prompts/003-V0_1_3_START_PROMPT.md` ;
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`.
|
||||
|
||||
Ajouté :
|
||||
|
||||
- `deltas/0.1.2/pre.006-fix.001.md`.
|
||||
|
||||
## Suite
|
||||
|
||||
Après application de ce correctif documentaire, `0.1.2-pre.006` reste la dernière prerelease fonctionnelle validée. La prochaine livraison est `0.1.2-rel.001` avec passage à la version stable `0.1.2` et validations finales de publication.
|
||||
213
deltas/0.1.2/pre.006.md
Normal file
213
deltas/0.1.2/pre.006.md
Normal file
@@ -0,0 +1,213 @@
|
||||
<!-- file: deltas/0.1.2/pre.006.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.2-pre.006
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.2-pre.005-fix.001
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.5.fix.1"
|
||||
Cargo.toml header version = 37
|
||||
```
|
||||
|
||||
## Validation de la base
|
||||
|
||||
La validation utilisateur de `pre.005-fix.001` est propre :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture OK
|
||||
```
|
||||
|
||||
Le stress test concurrent ne produit plus le flux TRACE parasite corrigé par `pre.005-fix.001`.
|
||||
|
||||
Le probe diagnostic d'overhead a passé son garde-fou sur 200000 itérations :
|
||||
|
||||
```text
|
||||
baseline = 19.658553 ms
|
||||
reload = 28.807958 ms
|
||||
```
|
||||
|
||||
Ces nombres restent des observations diagnostiques et non un benchmark contractuel.
|
||||
|
||||
## Objet de pre.006
|
||||
|
||||
`pre.006` est la prerelease finale prévue de `0.1.2`.
|
||||
|
||||
Elle :
|
||||
|
||||
- ajoute une validation des spans async sur un executor Tokio réel ;
|
||||
- garde Tokio hors des dépendances runtime de `ksp-logging-lib` ;
|
||||
- consolide la documentation de la crate ;
|
||||
- ferme les TODO de prerelease ;
|
||||
- prépare le prompt final de `0.1.3 — ksp-config-lib` ;
|
||||
- prépare les validations finales précédant `rel.001`.
|
||||
|
||||
## Tokio uniquement pour les tests
|
||||
|
||||
Version actuelle vérifiée le 2026-08-14 :
|
||||
|
||||
```text
|
||||
tokio 1.53.1
|
||||
```
|
||||
|
||||
Le workspace centralise une contrainte de génération :
|
||||
|
||||
```toml
|
||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
`ksp-logging-lib` le consomme uniquement comme dev-dependency :
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
```
|
||||
|
||||
Aucun source de production de Logging n'importe Tokio. L'API `instrument(span, future)` reste fondée sur `std::future::Future` et reste indépendante de l'executor choisi par le consumer.
|
||||
|
||||
## Tests Tokio réels
|
||||
|
||||
Nouveau fichier :
|
||||
|
||||
```text
|
||||
crates/ksp-logging-lib/tests/tokio_span.rs
|
||||
```
|
||||
|
||||
### Current-thread
|
||||
|
||||
Le premier test utilise :
|
||||
|
||||
```text
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
```
|
||||
|
||||
Il instrumente une future contenant plusieurs `tokio::task::yield_now().await` et utilise un subscriber de test associé au span pour compter les `enter`/`exit`.
|
||||
|
||||
Le test exige plusieurs ré-entrées du span après suspension et un nombre final d'entrées/sorties identique.
|
||||
|
||||
### Multi-thread
|
||||
|
||||
Le second test utilise :
|
||||
|
||||
```text
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
```
|
||||
|
||||
Deux futures instrumentées sont lancées avec `tokio::spawn`, effectuent des suspensions répétées puis doivent terminer normalement. Le test vérifie également l'équilibre des `enter`/`exit`.
|
||||
|
||||
Ce test démontre l'utilisation correcte de la surface KSP sous un runtime Tokio multi-thread ; il ne prétend pas imposer ni mesurer une migration déterministe d'une même future entre worker threads.
|
||||
|
||||
## Documentation finale
|
||||
|
||||
Mises à jour :
|
||||
|
||||
- `crates/ksp-logging-lib/README.md` : indépendance de l'executor en production et statut test-only de Tokio ;
|
||||
- `crates/ksp-logging-lib/USAGE.md` : exemple async et frontière executor ;
|
||||
- `crates/ksp-logging-lib/TODO.md` : seules restent les validations finales et la future livraison stable ;
|
||||
- `docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` : statut validé de `pre.005-fix.001`, contenu `pre.006`, validations finales et absence de question architecturale bloquante ;
|
||||
- `prompts/000-README.md` : ajout du prompt Config ;
|
||||
- `prompts/003-V0_1_3_START_PROMPT.md` : prompt final pour ouvrir `0.1.3` après `v0.1.2`.
|
||||
|
||||
`ROADMAP.md` reste volontairement inchangé : `0.1.2` demeure en cours tant que `rel.001` et le tag `v0.1.2` ne sont pas validés.
|
||||
|
||||
Aucun changelog général n'existe actuellement dans le dépôt ; `pre.006` n'en crée pas artificiellement un.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.6"
|
||||
```
|
||||
|
||||
L'en-tête du manifest racine devient :
|
||||
|
||||
```text
|
||||
# version: 38
|
||||
```
|
||||
|
||||
Le manifest de `ksp-logging-lib` devient :
|
||||
|
||||
```text
|
||||
# version: 4
|
||||
```
|
||||
|
||||
## Fichiers du delta
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-logging-lib/Cargo.toml
|
||||
crates/ksp-logging-lib/README.md
|
||||
crates/ksp-logging-lib/TODO.md
|
||||
crates/ksp-logging-lib/USAGE.md
|
||||
crates/ksp-logging-lib/tests/tokio_span.rs
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
prompts/000-README.md
|
||||
prompts/003-V0_1_3_START_PROMPT.md
|
||||
deltas/0.1.2/pre.006.md
|
||||
```
|
||||
|
||||
## Validations finales à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo build -p ksp-logging-lib
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
cargo tree -p ksp-logging-lib -e normal
|
||||
cargo tree -p ksp-logging-lib -e dev
|
||||
```
|
||||
|
||||
Contrôles attendus en particulier :
|
||||
|
||||
- les deux tests de `tests/tokio_span.rs` passent ;
|
||||
- `cargo build -p ksp-logging-lib` reste un build normal sans Tokio comme dépendance runtime ;
|
||||
- le graphe `-e normal` n'inclut pas Tokio ;
|
||||
- Tokio est visible uniquement via l'usage dev attendu ;
|
||||
- aucune seconde version évitable n'apparaît ;
|
||||
- l'audit ownership continue à interdire les contournements de la façade tracing.
|
||||
|
||||
Aucune validation Cargo n'est déclarée réussie dans ce delta avant exécution dans l'environnement de développement.
|
||||
|
||||
## Suite après validation
|
||||
|
||||
Si `pre.006` est propre, la prochaine livraison est :
|
||||
|
||||
```text
|
||||
0.1.2-rel.001
|
||||
```
|
||||
|
||||
Elle publiera :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2"
|
||||
```
|
||||
|
||||
puis, après validation utilisateur, le commit final recevra :
|
||||
|
||||
```text
|
||||
v0.1.2
|
||||
```
|
||||
|
||||
La session fonctionnelle suivante pourra alors démarrer avec :
|
||||
|
||||
```text
|
||||
prompts/003-V0_1_3_START_PROMPT.md
|
||||
```
|
||||
193
deltas/0.1.2/rel.001.md
Normal file
193
deltas/0.1.2/rel.001.md
Normal file
@@ -0,0 +1,193 @@
|
||||
<!-- file: deltas/0.1.2/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.1.2-rel.001` — publication stable Logging
|
||||
|
||||
## Base requise
|
||||
|
||||
`0.1.2-pre.006` avec le correctif documentaire `0.1.2-pre.006-fix.001`, au sens des commits de livraison correspondants, avec :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.2-pre.6"
|
||||
```
|
||||
|
||||
Le correctif `pre.006-fix.001` ne modifie pas la version Cargo.
|
||||
|
||||
## Objectif
|
||||
|
||||
Publier la release stable `0.1.2`, clôturer `Logging foundation` et préparer l'ouverture de `0.1.3 — Configuration foundation` sans modifier la surface fonctionnelle de `ksp-logging-lib`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
`workspace.package.version` passe de :
|
||||
|
||||
```text
|
||||
0.1.2-pre.6
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.1.2
|
||||
```
|
||||
|
||||
Le header de `Cargo.toml` passe de version 38 à 39.
|
||||
|
||||
Les contraintes de dépendances restent inchangées :
|
||||
|
||||
```toml
|
||||
[workspace.dependencies]
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt"] }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
Tokio reste uniquement une dev-dependency de `ksp-logging-lib` et n'appartient pas à son graphe normal.
|
||||
|
||||
## Validations finales exécutées par le user
|
||||
|
||||
Commandes exécutées avec succès le 2026-08-14 sur `0.1.2-pre.6` :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo build -p ksp-logging-lib
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo test -p ksp-logging-lib --test overhead -- --ignored --nocapture
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
cargo tree -p ksp-logging-lib -e normal
|
||||
cargo tree -p ksp-logging-lib -e dev
|
||||
```
|
||||
|
||||
Résultats communiqués :
|
||||
|
||||
- `cargo check --workspace` : succès ;
|
||||
- `cargo build -p ksp-logging-lib` : succès sur le graphe normal ;
|
||||
- `cargo clippy --workspace --all-targets` : succès sans warning communiqué ;
|
||||
- `cargo test --workspace` : tous les tests exécutés réussissent, dont les tests de takeover, saturation non bloquante, hot reload concurrent, lifecycle span et les deux tests Tokio réels ;
|
||||
- probe d'overhead explicite : succès sur 200000 itérations, `baseline=16.959425ms`, `reload=24.942444ms` ;
|
||||
- `cargo tree -p ksp-logging-lib -d` : aucun doublon ;
|
||||
- `cargo tree -p ksp-logging-lib -e normal` : Tokio absent ;
|
||||
- `cargo tree -p ksp-logging-lib -e dev` : Tokio présent comme seule dev-dependency directe ;
|
||||
- features Tokio observées : `macros`, `rt`, `rt-multi-thread`, sans feature `full`.
|
||||
|
||||
Le correctif documentaire `pre.006-fix.001` appliqué après ces validations ne modifie ni Rust, ni manifest, ni runtime.
|
||||
|
||||
## Surface stable publiée
|
||||
|
||||
`0.1.2` stabilise notamment :
|
||||
|
||||
- `ksp-logging-lib` comme façade runtime KSP unique de logging/tracing ;
|
||||
- les macros `error!`, `warn!`, `info!`, `debug!`, `trace!` avec target KSP explicite et callsite consommateur préservé ;
|
||||
- les spans KSP synchrones et `instrument(span, future)` pour l'async sans dépendance `tracing` directe chez les consumers ;
|
||||
- `LoggingSettings`, `LogFilterLevel`, `TargetFilter`, `SpanEvents`, `ConsoleSettings`, `FileSettings` et `FileRotation` ;
|
||||
- `initialize()` unique, `reinitialize()` à chaud et `LoggingGuard` ;
|
||||
- takeover KSP avec silence externe par défaut et overrides par préfixe `ksp-*` ;
|
||||
- console et fichier non bloquants avec `WorkerGuard` possédés par Logging ;
|
||||
- mode lossy sans backpressure sur le hot path et observation cumulée des lignes abandonnées via `DroppedLines` ;
|
||||
- rotation fichier `Never`, `Hourly`, `Daily` ;
|
||||
- suppression des séquences ANSI avant persistence fichier ;
|
||||
- reconfiguration transactionnelle conservant l'ancienne configuration si la nouvelle préparation échoue ;
|
||||
- lifecycle spans `Off`, `NewAndClose`, `Full`, avec `busy`/`idle` lorsque demandé ;
|
||||
- tests de concurrence/reload, saturation, ownership de la stack tracing, callsites et instrumentation Tokio current-thread/multi-thread ;
|
||||
- ownership exclusif de `tracing`, `tracing-subscriber` et `tracing-appender` par `ksp-logging-lib` dans le workspace KSP.
|
||||
|
||||
Aucune nouvelle primitive ou API n'est ajoutée par le présent delta de publication.
|
||||
|
||||
## Documentation de clôture
|
||||
|
||||
Le présent delta :
|
||||
|
||||
- marque `0.1.2` réalisée dans `ROADMAP.md` ;
|
||||
- conserve `004-V0_1_2_LOGGING_FOUNDATION_PLAN.md` comme plan historique clôturé ;
|
||||
- ajoute ce plan aux index de documentation/plans ;
|
||||
- remplace le périmètre candidat Logging dans la séquence fonctionnelle par la surface réellement stabilisée ;
|
||||
- réaligne la section Config de la séquence fonctionnelle sur les décisions de `pre.006-fix.001` : `KSP_*`/`KSPB_*`, `config/examples/`, documents unitaires + composites, ownership exclusif de Config et accès explicite aux secrets pour les surfaces autorisées ;
|
||||
- conserve `prompts/003-V0_1_3_START_PROMPT.md` comme prompt final d'ouverture de `0.1.3`.
|
||||
|
||||
Aucun changelog général n'existe dans la base actuelle ; aucun changelog artificiel n'est créé.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.1.2/rel.001.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
ROADMAP.md
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Décisions
|
||||
|
||||
Aucune nouvelle décision fonctionnelle concernant Logging.
|
||||
|
||||
La publication stable confirme les décisions, contrats et corrections stabilisés pendant les prereleases `0.1.2` et leurs fixes.
|
||||
|
||||
La synchronisation de la documentation Config en clôture ne remplace pas le brainstorming `0.1.3-pre.001`; elle ne fait qu'enregistrer les décisions déjà prises dans `pre.006-fix.001`.
|
||||
|
||||
## Validations non exécutées dans cette livraison
|
||||
|
||||
L'environnement de génération du delta ne dispose pas de Cargo/Rust. Les commandes Cargo ne sont donc pas réexécutées ici sur la version finale `0.1.2`.
|
||||
|
||||
Après application du delta, le user doit exécuter au minimum :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le build normal et les graphes Cargo peuvent également être rejoués pour confirmer une dernière fois l'absence de Tokio dans le graphe runtime.
|
||||
|
||||
## Publication Git
|
||||
|
||||
Après application et validation de ce delta :
|
||||
|
||||
1. vérifier que le working tree ne contient que les modifications attendues ;
|
||||
2. exécuter les validations finales sur `workspace.package.version = "0.1.2"` ;
|
||||
3. créer le commit de release :
|
||||
|
||||
```text
|
||||
v0.1.2-rel.001
|
||||
```
|
||||
|
||||
4. marquer ce commit comme release stable avec le tag :
|
||||
|
||||
```text
|
||||
v0.1.2
|
||||
```
|
||||
|
||||
Aucun tag supplémentaire n'est requis pour les prereleases/fixes historiques.
|
||||
|
||||
## Suite
|
||||
|
||||
Après le tag stable `v0.1.2`, ouvrir :
|
||||
|
||||
```text
|
||||
0.1.3-pre.001
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
prompts/003-V0_1_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
La première prerelease de `0.1.3` reste une phase de brainstorming, audit et planification avant développement fonctionnel de `ksp-config-lib`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/000-README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Documentation KSP
|
||||
|
||||
@@ -34,7 +34,9 @@ docs/
|
||||
├── plans/
|
||||
│ ├── 000-README.md
|
||||
│ ├── 001-V0_0_3_PLAN.md
|
||||
│ └── 002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
│ ├── 002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
│ ├── 003-V0_1_1_CORE_FOUNDATION_PLAN.md
|
||||
│ └── 004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
|
||||
└── rules/
|
||||
├── FILE_CONTRACTS.md
|
||||
├── PROMPT_STRUCTURE.md
|
||||
@@ -51,7 +53,7 @@ D'autres sous-répertoires seront ajoutés uniquement lorsque leur rôle aura é
|
||||
|
||||
## Documents de planification
|
||||
|
||||
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md).
|
||||
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md).
|
||||
|
||||
`IDEAS.md` conserve les pistes et questions qui ne sont pas encore des engagements du roadmap ni des décisions architecturales.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/003-COMPONENT_CONTRACTS.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Contrats initiaux des composants KSP
|
||||
|
||||
@@ -80,11 +80,17 @@ Le retry d'un appel réseau identique reste une responsabilité transport, disti
|
||||
|
||||
`ksp-logging-lib` est la façade KSP unique pour le logging/tracing runtime. Elle importe/initialise directement `tracing`, `tracing-appender` et `tracing-subscriber` et peut dépendre de `ksp-core-lib` pour `Error` / `Result`.
|
||||
|
||||
Les crates comportementales KSP peuvent dépendre directement de `ksp-logging-lib` et utilisent sa façade pour `error`, `warn`, `info`, `debug` et `trace` avec target/domain/champs structurés selon l'API finale.
|
||||
Les crates comportementales KSP utilisent sa façade pour leurs événements `error`, `warn`, `info`, `debug`, `trace` et pour leurs spans sync/async. Elles n'émettent pas leurs propres logs via une dépendance directe à la stack tracing.
|
||||
|
||||
Chaque émission KSP indique un target correspondant au nom Cargo de la crate propriétaire ; `domain`, `component` et les autres champs structurés décrivent les subdivisions fonctionnelles sans multiplier les targets.
|
||||
|
||||
Le subscriber KSP rend les targets tiers silencieux par défaut. Lorsqu'une information provenant d'une dépendance externe est nécessaire, la crate KSP qui possède l'opération la réémet explicitement sous son propre target ; Logging ne renomme pas les événements tiers.
|
||||
|
||||
Logging possède ses `LoggingSettings`, ses writers/guards et son lifecycle. Le subscriber global est installé une fois, puis la configuration peut être rechargée à chaud via la façade KSP sans dépendance vers Config.
|
||||
|
||||
`ksp-core-lib` n'a pas de dépendance logging requise. Les crates `*-api` purement déclaratives restent sans logging par défaut.
|
||||
|
||||
Une application Tauri peut exceptionnellement avoir une dépendance/framework tracing imposée par un plugin, sans définir une politique parallèle à `ksp-logging-lib`.
|
||||
Une application Tauri peut exceptionnellement avoir une dépendance/framework tracing imposée par un plugin, sans définir une politique parallèle à `ksp-logging-lib`; les événements KSP restent émis via la façade KSP.
|
||||
|
||||
## Frontière materializer / store
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Graphe de dépendances KSP
|
||||
|
||||
@@ -100,9 +100,11 @@ runtime crate
|
||||
-> ksp-logging-lib
|
||||
```
|
||||
|
||||
`ksp-logging-lib` est la seule façade KSP propriétaire de l'initialisation/configuration tracing. Une crate runtime ne dépend normalement pas directement de `tracing`.
|
||||
`ksp-logging-lib` est la seule façade KSP propriétaire de l'initialisation/configuration tracing. Une crate runtime KSP n'utilise pas directement `tracing`, `tracing-subscriber` ou `tracing-appender` pour émettre ses propres événements/spans.
|
||||
|
||||
Exception technique possible : une application/framework, notamment Tauri, peut devoir intégrer un plugin tracing. Cette adaptation ne crée pas une seconde politique de logging parallèle.
|
||||
Le subscriber global KSP est installé une seule fois puis sa configuration peut être rechargée à chaud par `ksp-logging-lib`. Les targets externes sont silencieux par défaut ; les informations tierces utiles sont réémises par le composant KSP propriétaire sous son propre target.
|
||||
|
||||
Exception technique possible : une application/framework, notamment Tauri, peut devoir intégrer un plugin tracing. Cette adaptation ne crée pas une seconde politique de logging parallèle et les événements KSP restent émis via la façade KSP.
|
||||
|
||||
`ksp-core-lib` et les crates `*-api` purement déclaratives n'ont pas de dépendance logging obligatoire.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -10,7 +10,9 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
||||
## Plans de référence
|
||||
|
||||
- [`001-V0_0_3_PLAN.md`](001-V0_0_3_PLAN.md) — plan historique de la phase fondatrice `0.0.3`, clôturée ;
|
||||
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles, dont `0.1.1`.
|
||||
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ;
|
||||
- [`003-V0_1_1_CORE_FOUNDATION_PLAN.md`](003-V0_1_1_CORE_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.1`, établi par `0.1.1-pre.001` puis consolidé jusqu'à `0.1.1-rel.001`.
|
||||
- [`004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](004-V0_1_2_LOGGING_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.2`, établi par `0.1.2-pre.001` puis consolidé jusqu'à `0.1.2-rel.001`.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -48,27 +48,24 @@ Stabiliser `ksp-core-lib` comme fondation N1 minimale et durable.
|
||||
|
||||
Le Core possède uniquement les contrats réellement transversaux nécessaires aux couches supérieures.
|
||||
|
||||
### Périmètre initial
|
||||
### Surface stabilisée
|
||||
|
||||
À auditer précisément dans `0.1.1-pre.001`, avec comme candidats acquis :
|
||||
`0.1.1` stabilise :
|
||||
|
||||
- type public commun `ksp_core_lib::Error` ;
|
||||
- alias public commun `Result<T>` ou forme équivalente validée ;
|
||||
- architecture d'erreur permettant aux domaines supérieurs d'ajouter du contexte sans faire connaître tous les futurs domaines à Core ;
|
||||
- Program IDs fondamentaux appartenant à KSP Core ;
|
||||
- primitives/identités réellement communes et déjà justifiées ;
|
||||
- conventions de version/provenance N1 uniquement si un besoin concret existe ;
|
||||
- exports crate-root et documentation publique ;
|
||||
- tests unitaires/integration appropriés ;
|
||||
- respect complet des règles Rust/workspace.
|
||||
- `ksp_core_lib::ErrorCode`, `ErrorContext`, `Error` et `Result<T>` comme contrat d'erreur ouvert aux domaines supérieurs ;
|
||||
- `ksp_core_lib::Pubkey` comme primitive Solana réexportée par Core ;
|
||||
- 18 Program IDs fondamentaux possédés par KSP avec paires `PRGID_*` / `PRGIDPK_*` ;
|
||||
- `declare_program_id!` pour construire la représentation texte et `Pubkey` depuis une déclaration canonique unique ;
|
||||
- `ProgramIdEntry`, `ProgramIdFilter`, `ProgramIdKind` et le registre enumerable/recherchable ;
|
||||
- des vues par domaine/famille/protocole et `native_program_ids()` sans registres secondaires ;
|
||||
- une taxonomie extensible séparant notamment `subfamily` et `program_version` ;
|
||||
- les réexports crate-root, rustdocs et tests publics correspondants.
|
||||
|
||||
### Dépendances
|
||||
|
||||
Core ne dépend pas de `ksp-logging-lib`, Config, Wallet, Store, Transport, Program ou Materializer.
|
||||
|
||||
Une primitive Solana/Anza officiellement stable peut être ajoutée seulement lorsqu'un item Core concret en a besoin.
|
||||
|
||||
`solana-pubkey` est un candidat naturel pour les Program IDs ; les autres primitives autorisées ne sont pas ajoutées par anticipation.
|
||||
La seule dépendance externe directe de `ksp-core-lib` à la clôture est `solana-pubkey`, déclarée au workspace avec la génération `^4.3`, `default-features = false`, puis héritée par la crate avec `.workspace = true`. Aucune feature optionnelle supplémentaire n'est activée dans `0.1.1`.
|
||||
|
||||
### Hors scope
|
||||
|
||||
@@ -87,23 +84,42 @@ Une primitive Solana/Anza officiellement stable peut être ajoutée seulement lo
|
||||
|
||||
### Lifecycle de la release
|
||||
|
||||
Le nombre de prereleases n'est pas figé avant `pre.001`.
|
||||
|
||||
Trajectoire candidate :
|
||||
Trajectoire réellement suivie :
|
||||
|
||||
```text
|
||||
pre.001 brainstorming + audit + plan détaillé
|
||||
pre.001-fix.001/.002 corrections de cadrage Program IDs/taxonomie
|
||||
pre.002 Error/Result + fondation API
|
||||
pre.003 primitives/Program IDs réellement retenus
|
||||
pre.004 compléments/tests/audits
|
||||
pre.002-fix.001 corrections de tests/lints
|
||||
pre.003 Pubkey + Program IDs
|
||||
pre.003-fix.001 politique Cargo workspace + corrections Clippy
|
||||
pre.004 intégration Core + audits
|
||||
pre.005 validation finale/docs/cleanup/prompt 0.1.2
|
||||
rel.001 publication stable validée de 0.1.1
|
||||
```
|
||||
|
||||
Cette séquence est indicative. `pre.001` peut la modifier.
|
||||
|
||||
## `0.1.2` — Logging foundation
|
||||
|
||||
### Dépendances
|
||||
### Mission
|
||||
|
||||
Faire de `ksp-logging-lib` la façade KSP unique de logging/tracing pour les composants runtime.
|
||||
|
||||
### Surface stabilisée
|
||||
|
||||
`0.1.2` stabilise :
|
||||
|
||||
- les macros KSP `error!`, `warn!`, `info!`, `debug!`, `trace!` avec `target:` KSP explicite et callsite consommateur préservé ;
|
||||
- les spans KSP synchrones et l'instrumentation de futures async sans exposer `tracing` aux consumers ;
|
||||
- `LoggingSettings`, niveaux, overrides par préfixe de target et lifecycle de spans ;
|
||||
- `initialize()` unique et `reinitialize()` à chaud avec `LoggingGuard` ;
|
||||
- le takeover des targets : targets externes silencieux par défaut, targets `ksp-*` gouvernés par la politique KSP ;
|
||||
- console et fichier non bloquants, rotation, ownership des `WorkerGuard` et compteurs cumulés de lignes abandonnées ;
|
||||
- stripping ANSI avant persistence fichier ;
|
||||
- reconfiguration transactionnelle conservant l'ancien runtime en cas d'échec ;
|
||||
- tests de saturation, concurrence/reload, lifecycle spans et instrumentation Tokio réelle ;
|
||||
- audit d'ownership empêchant les autres crates workspace de dépendre directement de la stack `tracing*`.
|
||||
|
||||
### Dépendances runtime
|
||||
|
||||
```text
|
||||
ksp-logging-lib
|
||||
@@ -113,26 +129,29 @@ ksp-logging-lib
|
||||
-> tracing-subscriber
|
||||
```
|
||||
|
||||
### Mission
|
||||
Tokio est uniquement une dev-dependency de `ksp-logging-lib` pour les tests async réels et n'appartient pas à son graphe normal.
|
||||
|
||||
Faire de `ksp-logging-lib` la façade KSP unique de logging/tracing pour les composants runtime.
|
||||
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config pourra convertir ses documents résolus en `LoggingSettings` puis utiliser le lifecycle public de Logging.
|
||||
|
||||
### Périmètre candidat
|
||||
### Lifecycle de la release
|
||||
|
||||
- initialisation ;
|
||||
- settings runtime propres au logging ;
|
||||
- `error`, `warn`, `info`, `debug`, `trace` ;
|
||||
- target/domain/component/champs structurés selon API validée ;
|
||||
- fonctions, macros ou combinaison permettant de préserver correctement les callsites ;
|
||||
- console/fichiers ;
|
||||
- filtering ;
|
||||
- appender/rotation selon besoin concret ;
|
||||
- tests ;
|
||||
- protection contre logging de secrets.
|
||||
Trajectoire réellement suivie :
|
||||
|
||||
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`.
|
||||
|
||||
Config pourra plus tard convertir ses documents résolus en settings de Logging.
|
||||
```text
|
||||
pre.001 brainstorming + audit + plan détaillé
|
||||
pre.001-fix.001 corrections de cadrage takeover/reload/spans
|
||||
pre.002 crate + settings + façade événements/spans
|
||||
pre.002-fix.001 corrections tests/lints
|
||||
pre.003 subscriber + takeover + console + reload
|
||||
pre.003-fix.001 correction du montage reload/filter
|
||||
pre.004 console/fichier non bloquants + guards + ANSI
|
||||
pre.004-fix.001..004 corrections lifecycle, ANSI, takeover et Clippy
|
||||
pre.005 robustesse, concurrence, saturation, audits
|
||||
pre.005-fix.001 suppression du bruit console du stress test
|
||||
pre.006 validation finale, Tokio dev-only, docs, prompt 0.1.3
|
||||
pre.006-fix.001 correction documentaire du prompt Config
|
||||
rel.001 publication stable validée de 0.1.2
|
||||
```
|
||||
|
||||
## `0.1.3` — Configuration foundation
|
||||
|
||||
@@ -157,11 +176,13 @@ Périmètre candidat à revalider dans son `pre.001` :
|
||||
- résolution ;
|
||||
- validation ;
|
||||
- modification/sauvegarde ;
|
||||
- variables d'environnement `KS_*` / `KB_*` ;
|
||||
- variables d'environnement `KSP_*` / `KSPB_*` ;
|
||||
- secret/public/debug exposure policy ;
|
||||
- `logging.config.json` séparé ;
|
||||
- schemas sous `config/schemas/` ;
|
||||
- examples sous `config/`.
|
||||
- vrais fichiers runtime sous `config/`, schemas sous `config/schemas/` et exemples sous `config/examples/` ;
|
||||
- documents unitaires spécialisés + fichiers composites par application/exécutable ;
|
||||
- ownership exclusif de `ksp-config-lib` sur lecture/résolution/validation/mutation des fichiers Config et variables d'environnement ;
|
||||
- accès explicite aux secrets pour les surfaces de management autorisées.
|
||||
|
||||
### Règle de scission
|
||||
|
||||
@@ -325,7 +346,7 @@ Les directions restent celles du roadmap :
|
||||
|
||||
Ces séries sont des objectifs fonctionnels, pas un calendrier contractuel.
|
||||
|
||||
# Sélection de la première release
|
||||
# Progression de la série `0.1.x`
|
||||
|
||||
La première release fonctionnelle est :
|
||||
|
||||
@@ -333,10 +354,15 @@ La première release fonctionnelle est :
|
||||
0.1.1 — Core foundation
|
||||
```
|
||||
|
||||
Le prompt de démarrage associé est :
|
||||
Son prompt historique d'ouverture reste :
|
||||
|
||||
```text
|
||||
prompts/001-V0_1_1_START_PROMPT.md
|
||||
```
|
||||
|
||||
`0.0.3` est désormais la base fondatrice stable. La prochaine phase ouvre `0.1.1-pre.001` à partir du prompt final `prompts/001-V0_1_1_START_PROMPT.md`.
|
||||
`0.1.1-rel.001` publie la surface Core stable après validation complète de `pre.005`. Le commit de release reçoit le tag `v0.1.1`. La release suivante s'ouvre avec :
|
||||
|
||||
```text
|
||||
0.1.2 — Logging foundation
|
||||
prompts/002-V0_1_2_START_PROMPT.md
|
||||
```
|
||||
|
||||
699
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
Normal file
699
docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md
Normal file
@@ -0,0 +1,699 @@
|
||||
<!-- file: docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Plan KSP 0.1.1 — Core foundation
|
||||
|
||||
## Statut
|
||||
|
||||
Plan historique clôturé de `0.1.1`, établi par `0.1.1-pre.001`, consolidé jusqu'à `0.1.1-pre.005` puis fermé par `0.1.1-rel.001`.
|
||||
|
||||
La surface fonctionnelle Core prévue par ce plan est implémentée et validée. Le delta `rel.001` publie `workspace.package.version = "0.1.1"`; son commit doit recevoir le tag stable `v0.1.1` avant ouverture de `0.1.2`.
|
||||
|
||||
## Base auditée
|
||||
|
||||
Base fournie :
|
||||
|
||||
```text
|
||||
0.0.3 stable
|
||||
```
|
||||
|
||||
Constats sur l'archive reçue :
|
||||
|
||||
- `workspace.package.version = "0.0.3"` ;
|
||||
- seul `crates/ksp-core-lib` est membre du workspace ;
|
||||
- `ksp-core-lib` ne possède encore aucune dépendance ;
|
||||
- `crates/ksp-core-lib/src/lib.rs` contient uniquement la façade/squelette minimal et les lints de crate ;
|
||||
- le delta final `deltas/0.0.3/rel.001.md` indique que les validations Cargo de publication ont été exécutées avec succès par le user avant stabilisation ;
|
||||
- le prompt `prompts/001-V0_1_1_START_PROMPT.md` présent dans l'archive correspond au prompt final attendu pour cette session.
|
||||
|
||||
Dans le workflow KSP, l'archive `khadhroony-solana-project-v0.0.3.zip` est produite directement par Gitea depuis le tag correspondant. Cette provenance suffit à considérer la base fournie comme la release stable/taguée `v0.0.3` attendue ; l'absence normale de `.git` dans l'archive n'ajoute pas de vérification Git supplémentaire à cette session.
|
||||
|
||||
## Mission bornée
|
||||
|
||||
`0.1.1` doit rendre `ksp-core-lib` immédiatement consommable par les prochaines fondations N1 sans lui faire absorber leurs responsabilités.
|
||||
|
||||
La surface retenue pour cette release est limitée à :
|
||||
|
||||
1. un contrat commun `Error` / `Result` ouvert aux domaines supérieurs ;
|
||||
2. la primitive Solana d'adresse publique nécessaire aux Program IDs ;
|
||||
3. les Program IDs fondamentaux du runtime Solana, leurs deux représentations canonique texte/`Pubkey` et un petit registre descriptif enumerable ;
|
||||
4. les réexports crate-root, rustdocs et tests nécessaires à ces contrats.
|
||||
|
||||
Aucune autre primitive n'est ajoutée sans usage concret découvert pendant la release.
|
||||
|
||||
## Inventaire des contrats Core nécessaires maintenant
|
||||
|
||||
### `Error` / `Result`
|
||||
|
||||
Besoin concret immédiat : `0.1.2` Logging puis les autres crates KSP doivent pouvoir retourner une erreur KSP commune sans obliger leurs consommateurs à adopter une erreur propre à chaque couche.
|
||||
|
||||
Le modèle historique de bot3 fondé sur une enum centrale comportant des variantes telles que Config, Tracing, Tauri, HTTP ou DB n'est pas repris : une telle enum fait connaître à Core les domaines supérieurs et doit être modifiée à chaque nouveau domaine.
|
||||
|
||||
### `Pubkey`
|
||||
|
||||
Les Program IDs ont besoin d'une représentation Solana typée et commune. Core doit posséder cette dépendance fondamentale et peut réexporter le type afin que les crates KSP supérieures n'aient pas à importer directement la crate Solana correspondante uniquement pour manipuler un Program ID.
|
||||
|
||||
### Program IDs fondamentaux
|
||||
|
||||
Core doit posséder les identifiants fondamentaux du runtime Solana qui ne relèvent d'aucun protocole supérieur.
|
||||
|
||||
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 retenu
|
||||
|
||||
### Forme générale
|
||||
|
||||
`pre.002` stabilise un type structuré et extensible plutôt qu'une enum fermée.
|
||||
|
||||
Surface conceptuelle :
|
||||
|
||||
```text
|
||||
ErrorCode
|
||||
domain: &'static str
|
||||
code: &'static str
|
||||
|
||||
ErrorContext
|
||||
key: &'static str
|
||||
value: String
|
||||
|
||||
Error
|
||||
code: ErrorCode
|
||||
message: String
|
||||
context: Vec<ErrorContext>
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>
|
||||
|
||||
Result<T> = std::result::Result<T, Error>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- `Error` est le seul type d'erreur KSP commun exposé par Core.
|
||||
- `Result<T>` est un alias public vers `std::result::Result<T, Error>`.
|
||||
- `ErrorCode` sépare explicitement un domaine stable et un code stable sans enum centrale des domaines.
|
||||
- Les domaines supérieurs définissent leurs propres constantes `ErrorCode` ; Core ne connaît pas Config, Logging, Wallet, Transport, Store, Tauri ou les protocoles.
|
||||
- Les identifiants de domaine/code sont des chaînes statiques afin de favoriser des codes définis au code source et stables, pas des catégories dynamiques construites au runtime.
|
||||
- `message` reste un diagnostic lisible par un humain.
|
||||
- `ErrorContext` permet d'ajouter du contexte structuré sans étendre le type `Error` à chaque besoin métier.
|
||||
- Les clés de contexte sont statiques ; les valeurs sont possédées.
|
||||
- Le contexte d'erreur ne doit pas contenir de secret. Logging décidera plus tard quels champs sont effectivement émis.
|
||||
- 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` utilise exactement la forme `<domain>.<code>: <message>` ; il ne concatène pas automatiquement le contexte ou la chaîne de causes.
|
||||
- Aucun `From<ExternalError>` 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.
|
||||
|
||||
Exemple conceptuel de code qualifié :
|
||||
|
||||
```text
|
||||
config.profile_missing
|
||||
logging.initialization_failed
|
||||
transport.http_request_failed
|
||||
```
|
||||
|
||||
Ces exemples illustrent le contrat ouvert ; ils ne créent aucune connaissance de ces domaines dans Core.
|
||||
|
||||
## API publique Core prévue
|
||||
|
||||
La façade crate-root doit exposer directement les contrats consommables :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorContext
|
||||
ksp_core_lib::Result
|
||||
ksp_core_lib::Pubkey
|
||||
ksp_core_lib::ProgramIdEntry
|
||||
ksp_core_lib::ProgramIdFilter
|
||||
ksp_core_lib::ProgramIdKind
|
||||
ksp_core_lib::entries
|
||||
ksp_core_lib::program_ids
|
||||
ksp_core_lib::program_ids_by_domain
|
||||
ksp_core_lib::program_ids_by_family
|
||||
ksp_core_lib::program_ids_by_protocol
|
||||
ksp_core_lib::native_program_ids
|
||||
ksp_core_lib::find_program_id
|
||||
ksp_core_lib::find_program_pubkey
|
||||
ksp_core_lib::declare_program_id!
|
||||
ksp_core_lib::PRGID_*
|
||||
ksp_core_lib::PRGIDPK_*
|
||||
```
|
||||
|
||||
Les modules d'implémentation restent privés conformément aux règles Rust du dépôt.
|
||||
|
||||
Arborescence candidate après développement :
|
||||
|
||||
```text
|
||||
crates/ksp-core-lib/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── lib.rs
|
||||
│ ├── error.rs
|
||||
│ └── program_ids.rs
|
||||
├── unit_tests/
|
||||
│ ├── error.rs
|
||||
│ └── program_ids.rs
|
||||
└── tests/
|
||||
└── public_api.rs
|
||||
```
|
||||
|
||||
Cette arborescence peut être ajustée si l'implémentation révèle une séparation plus simple, sans introduire `mod.rs` ni `pub mod`.
|
||||
|
||||
## Audit Solana/Anza actuel
|
||||
|
||||
Audit effectué le 2026-08-14 sur les sources officielles actuelles du dépôt `anza-xyz/solana-sdk` et les métadonnées de publication correspondantes.
|
||||
|
||||
### `solana-pubkey`
|
||||
|
||||
État observé :
|
||||
|
||||
```text
|
||||
solana-pubkey 4.3.0
|
||||
MSRV officiel du workspace Solana SDK : Rust 1.89.0
|
||||
```
|
||||
|
||||
La génération actuelle de `solana-pubkey` est une façade de compatibilité officielle sur `solana-address` : elle réexporte notamment `solana_address::Address` sous le nom `Pubkey`.
|
||||
|
||||
Décision pour `0.1.1` :
|
||||
|
||||
- conserver le vocabulaire/API KSP `Pubkey` déjà prévu par l'architecture ;
|
||||
- utiliser directement `solana-pubkey` comme dépendance propriétaire de `ksp-core-lib` ;
|
||||
- déclarer la génération retenue sous `[workspace.dependencies]` avec la contrainte explicite `^4.3`, la résolution auditée actuelle étant `4.3.0` ;
|
||||
- définir `default-features = false` au niveau workspace puis consommer la dépendance dans `ksp-core-lib` avec `solana-pubkey.workspace = true` ;
|
||||
- n'activer ni Borsh, ni Wincode, ni Serde, ni Rand, ni feature cryptographique par anticipation ;
|
||||
- réexporter le type `Pubkey` depuis `ksp_core_lib` afin que son utilisation fasse partie intentionnellement du contrat Core.
|
||||
|
||||
L'audit final `pre.005` confirme qu'aucune feature optionnelle de `solana-pubkey` ne doit être activée dans `0.1.1`. La surface Core actuelle utilise uniquement l'identité `Pubkey`, la construction compile-time des Program IDs et les opérations déjà disponibles avec la dépendance retenue. Les features `alloc`, `borsh`, `bytemuck`, `curve25519`, `rand`, `serde`, `sha2`, `std` et `wincode` restent need-driven et seront activées uniquement dans la crate propriétaire lorsqu'un contrat réel l'exigera.
|
||||
|
||||
Le `cargo tree -p ksp-core-lib -e features` exécuté par le user montre des features de `solana-address` telles que `copy`, `decode`, `error`, `sanitize`, `syscalls` et `default`. Elles proviennent de la composition interne résolue de `solana-pubkey`/`solana-address` et ne justifient pas d'activer une feature optionnelle KSP supplémentaire sur `solana-pubkey`.
|
||||
|
||||
Depuis `pre.003-fix.001`, la règle générale KSP impose la centralisation des dépendances externes sous `[workspace.dependencies]` et l'héritage `.workspace = true` dans les crates membres. La résolution Cargo observée pour la contrainte `^4.3` reste `solana-pubkey 4.3.0` au moment de cette tranche.
|
||||
|
||||
### `solana-address`
|
||||
|
||||
`solana-address 2.7.0` est la primitive interne actuelle derrière `solana-pubkey`.
|
||||
|
||||
Elle n'est pas ajoutée directement à KSP en `0.1.1` : l'ajouter en parallèle n'apporte aucun besoin Core supplémentaire et multiplierait les points d'entrée publics pour la même identité Solana.
|
||||
|
||||
### `solana-sdk-ids`
|
||||
|
||||
La source officielle `solana-sdk-ids` reste utile comme référence d'audit des identifiants publiés par Anza/Solana.
|
||||
|
||||
État du package observé pendant `pre.001` :
|
||||
|
||||
```text
|
||||
solana-sdk-ids 3.1.0
|
||||
```
|
||||
|
||||
Décision acquise : **ne pas ajouter `solana-sdk-ids` à KSP**, ni comme dépendance runtime, ni comme dev-dependency.
|
||||
|
||||
KSP possède ses propres constantes et registres de Program IDs. Les sources officielles Anza/Solana sont consultées pour vérifier les valeurs et l'évolution de la surface, mais cette vérification ne doit pas créer une dépendance Cargo à leur crate d'IDs.
|
||||
|
||||
### Crates explicitement non nécessaires à `0.1.1`
|
||||
|
||||
Ne pas ajouter :
|
||||
|
||||
- `solana-sdk` umbrella ;
|
||||
- `solana-keypair` ;
|
||||
- `solana-signer` ;
|
||||
- `solana-hash` ;
|
||||
- `solana-nonce` ;
|
||||
- crates transaction/message/instruction ;
|
||||
- crates RPC/client ;
|
||||
- Borsh/Wincode/Serde pour une hypothétique future surface wire.
|
||||
|
||||
La génération retenue de `solana-pubkey` requiert Rust 1.89.0. La validation `pre.002-fix.001` a été exécutée avec une génération Clippy Rust 1.94.0, donc la toolchain observée satisfait ce MSRV. Une future révision ne devra pas revenir silencieusement à une vieille génération Solana uniquement pour contourner une exigence de toolchain.
|
||||
|
||||
## Program IDs retenus pour la première surface Core
|
||||
|
||||
`pre.003` fixe la première surface Core à **18 Program IDs**.
|
||||
|
||||
Les 17 identifiants fondamentaux exposés par la surface officielle actuelle `solana-sdk-ids` sont recopiés comme valeurs KSP sans créer de dépendance Cargo vers cette crate :
|
||||
|
||||
- System ;
|
||||
- Stake ;
|
||||
- Vote ;
|
||||
- Config ;
|
||||
- Feature ;
|
||||
- Compute Budget ;
|
||||
- Address Lookup Table ;
|
||||
- BPF Loader historique v1 ;
|
||||
- BPF Loader v2 ;
|
||||
- BPF Loader Upgradeable ;
|
||||
- Loader v4 ;
|
||||
- Native Loader ;
|
||||
- précompiles Ed25519, Secp256k1 et Secp256r1 ;
|
||||
- ZK ElGamal Proof ;
|
||||
- ZK Token Proof.
|
||||
|
||||
Le dix-huitième identifiant est le Slashing Program `S1ashing11111111111111111111111111111111111`. Il n'est pas encore publié dans `solana-sdk-ids`, mais son statut de programme enshrined et son adresse sont confirmés par SIMD-0204 et la documentation Anza. L'ancien `ks-program-ids` de bot3 avait déjà cette entrée ; `pre.003` ne la conserve toutefois qu'après cette revérification officielle indépendante.
|
||||
|
||||
### Exclusions volontaires de `0.1.1`
|
||||
|
||||
Ne pas ajouter fonctionnellement pendant cette release :
|
||||
|
||||
- SPL Token, Token-2022, ATA, Memo ou tout autre protocole SPL ;
|
||||
- Metaplex et autres protocoles ;
|
||||
- `incinerator`, qui est une adresse spéciale et non un Program ID exécutable ;
|
||||
- les sysvar account IDs ;
|
||||
- les autres well-known accounts non exécutables uniquement pour agrandir la première surface.
|
||||
|
||||
Les exemples SPL/protocoles utilisés pour définir la nomenclature ci-dessous illustrent la convention future et ne modifient pas le hors-scope de `0.1.1`.
|
||||
|
||||
Les well-known account IDs doivent rester explicitement séparés des Program IDs. L'ancien `native_well_known_account_ids()` constitue une bonne direction conceptuelle ; aucune API vide n'est toutefois créée en `0.1.1` tant qu'aucun well-known account n'est réellement retenu dans la surface de la release.
|
||||
|
||||
## Nomenclature des Program IDs
|
||||
|
||||
La nomenclature des symboles et la taxonomie du registre sont deux contrats liés mais distincts.
|
||||
|
||||
Le nom Rust d'une constante doit privilégier une **identité stable** et ne doit pas embarquer toute la classification fonctionnelle, car une reclassification future ne doit pas obliger à renommer un symbole public.
|
||||
|
||||
La forme générale devient :
|
||||
|
||||
```text
|
||||
PRGID_<NAMESPACE>_<PROGRAM_OR_FAMILY>_<VARIANT?>_<VERSION?> -> &'static str Base58
|
||||
PRGIDPK_<NAMESPACE>_<PROGRAM_OR_FAMILY>_<VARIANT?>_<VERSION?> -> Pubkey
|
||||
```
|
||||
|
||||
Règles :
|
||||
|
||||
- `PRGID_` identifie toujours la représentation texte Base58 ;
|
||||
- `PRGIDPK_` identifie toujours la représentation `Pubkey` ;
|
||||
- le suffixe après le préfixe doit être identique entre les deux formes ;
|
||||
- `NAMESPACE` identifie le namespace/propriétaire stable de l'identité, par exemple `SOLANA`, `SPL`, `METAPLEX`, `RAYDIUM`, `METEORA`, `GOOSEFX` ou `JUPITER` ;
|
||||
- `PROGRAM_OR_FAMILY` et `VARIANT` décrivent l'identité publique utile du programme sans tenter de recopier mécaniquement tous les champs de `ProgramIdEntry` ;
|
||||
- `VERSION` n'est ajoutée que lorsque le projet/protocole distingue réellement plusieurs générations d'une même lignée de programme ;
|
||||
- un suffixe ressemblant à une année ou une version dans un nom officiel n'est pas automatiquement interprété comme `program_version` : `Token-2022` reste par exemple une identité de programme distincte et non une déduction automatique de version ;
|
||||
- la valeur de version est un label KSP normalisé à partir de la génération publiquement reconnue (`V1`, `V2`, `V3`, `V4`, `V6`, `V0_5`, etc.), pas la version d'une crate ou d'une IDL.
|
||||
|
||||
Exemples de convention :
|
||||
|
||||
```text
|
||||
PRGID_SOLANA_SYSTEM
|
||||
PRGIDPK_SOLANA_SYSTEM
|
||||
|
||||
PRGID_SOLANA_LOADER_BPF_V2
|
||||
PRGIDPK_SOLANA_LOADER_BPF_V2
|
||||
|
||||
PRGID_SOLANA_PRECOMPILE_ED25519
|
||||
PRGIDPK_SOLANA_PRECOMPILE_ED25519
|
||||
|
||||
PRGID_SPL_MEMO_V1
|
||||
PRGIDPK_SPL_MEMO_V1
|
||||
PRGID_SPL_MEMO_V3
|
||||
PRGIDPK_SPL_MEMO_V3
|
||||
PRGID_SPL_MEMO_V4
|
||||
PRGIDPK_SPL_MEMO_V4
|
||||
|
||||
PRGID_METEORA_DAMM_V2
|
||||
PRGIDPK_METEORA_DAMM_V2
|
||||
|
||||
PRGID_GOOSEFX_GAMMA
|
||||
PRGIDPK_GOOSEFX_GAMMA
|
||||
PRGID_GOOSEFX_SSL_V2
|
||||
PRGIDPK_GOOSEFX_SSL_V2
|
||||
```
|
||||
|
||||
Les exemples SPL/DEX définissent uniquement la convention future pendant `0.1.1` ; ces protocoles restent hors scope fonctionnel de cette release.
|
||||
|
||||
### Pourquoi `NAMESPACE` et non `DOMAIN` dans le symbole
|
||||
|
||||
Le mot `domain` est réservé à la taxonomie fonctionnelle du registre décrite plus bas. Une constante doit rester stable si la classification fonctionnelle d'un programme est affinée.
|
||||
|
||||
Par exemple, `PRGID_GOOSEFX_GAMMA` reste un bon identifiant public même si KSP affine plus tard sa classification AMM. Le nom de symbole ne doit donc pas être une sérialisation complète de `domain/family/protocol/subfamily`.
|
||||
|
||||
## Construction et ownership des constantes
|
||||
|
||||
KSP possède la chaîne Base58 canonique de chaque Program ID et ne dépend pas d'un registre runtime externe pour la fournir.
|
||||
|
||||
La chaîne Base58 ne doit être écrite qu'une seule fois dans la déclaration KSP. Une macro publique KSP, nommée initialement `declare_program_id!`, doit produire les deux représentations à partir d'une déclaration unique, selon une forme conceptuelle de ce type :
|
||||
|
||||
```text
|
||||
declare_program_id!(
|
||||
PRGID_SOLANA_SYSTEM,
|
||||
PRGIDPK_SOLANA_SYSTEM,
|
||||
"11111111111111111111111111111111"
|
||||
);
|
||||
```
|
||||
|
||||
Le résultat conceptuel est :
|
||||
|
||||
```text
|
||||
PRGID_SOLANA_SYSTEM : &'static str
|
||||
PRGIDPK_SOLANA_SYSTEM : Pubkey
|
||||
```
|
||||
|
||||
La macro doit s'inspirer de la mécanique compile-time de `solana_address::declare_id!`/des primitives correspondantes exposées via la génération `solana-pubkey`, mais KSP possède son API et sa nomenclature. L'implémentation exacte sera vérifiée en `pre.003` contre la version réellement retenue de `solana-pubkey`.
|
||||
|
||||
Invariants :
|
||||
|
||||
- aucune seconde copie manuelle de la valeur Base58 ;
|
||||
- aucune conversion runtime inutile ;
|
||||
- aucun `unwrap`, `expect`, `panic` ou opérateur `?` ;
|
||||
- les deux constantes sont disponibles au crate-root ;
|
||||
- la macro n'impose pas les symboles génériques `ID`, `id()` ou `check_id()` qui entreraient en collision lorsque plusieurs Program IDs sont déclarés dans Core.
|
||||
|
||||
Les sources officielles Solana/Anza servent :
|
||||
|
||||
1. de source de vérité externe pour vérifier la valeur Base58 ;
|
||||
2. de contrôle de l'évolution des IDs/runtime ;
|
||||
3. de référence d'audit ponctuelle sans dépendance Cargo.
|
||||
|
||||
## Registre descriptif des Program IDs
|
||||
|
||||
L'ancien `ks-program-ids` fournissait notamment :
|
||||
|
||||
```text
|
||||
ProgramIdEntry
|
||||
entries()
|
||||
registered_program_ids()
|
||||
native_program_ids()
|
||||
native_well_known_account_ids()
|
||||
find_registered_program_id()
|
||||
```
|
||||
|
||||
Cette fonctionnalité doit être reprise et améliorée autour d'un **registre canonique unique**. Les vues spécialisées ne doivent pas maintenir des listes indépendantes et dupliquer les mêmes Program IDs.
|
||||
|
||||
### Axes de classification retenus
|
||||
|
||||
L'audit de l'ancien registre bot3 et des IDLs archivées montre qu'un seul axe hiérarchique ne suffit pas. La taxonomie KSP doit séparer au minimum :
|
||||
|
||||
- `domain` : domaine fonctionnel large ;
|
||||
- `family` : famille fonctionnelle dans ce domaine ;
|
||||
- `protocol` : protocole/projet auquel appartient le programme ;
|
||||
- `subfamily` : branche, architecture ou produit interne optionnel dans une même famille/protocole ;
|
||||
- `program_version` : génération publique optionnelle de la **lignée du programme on-chain** ;
|
||||
- `kind` : classification technique nécessaire aux vues Core telles que les programmes natifs/loaders/précompiles ;
|
||||
- le code KSP unique, la chaîne Base58 `PRGID_*` et le `Pubkey` `PRGIDPK_*` restent les identités de l'entrée.
|
||||
|
||||
Les vocabulaires de `domain`, `family`, `protocol`, `subfamily` et `program_version` restent des chaînes extensibles : Core ne crée aucune enum fermée des futurs protocoles Solana. `kind` est volontairement une petite enum technique `ProgramIdKind` (`Program`, `Loader`, `Precompile`, `EnshrinedProgram`) parce qu'elle décrit la nature de l'entrée plutôt qu'un catalogue de protocoles.
|
||||
|
||||
`family = amm` est retenu comme famille agrégatrice future pour les modèles AMM. Les variantes `cpmm`, `clmm`, `dlmm`, `damm`, `stable_swap`, `weighted_swap`, `gamma`, `ssl` ou équivalentes appartiennent au niveau `subfamily` lorsqu'elles représentent réellement une branche architecturale du protocole. Cela permettra à une future vue `amm_program_ids()` de retrouver l'ensemble de ces programmes au lieu de limiter la recherche à l'ancien préfixe bot3 `AMM_*`.
|
||||
|
||||
`subfamily` reste optionnelle : un programme unique peut supporter plusieurs courbes/mécanismes et ne doit pas être forcé artificiellement dans une seule sous-famille. L'AMM Aldrin constitue notamment un cas où le programme peut couvrir plusieurs types de courbes ; la famille `amm` suffit alors si aucune sous-famille unique n'est normative.
|
||||
|
||||
### `program_version` est distinct de `subfamily`
|
||||
|
||||
La version est un axe indépendant. Elle ne doit jamais être encodée comme une `subfamily` uniquement pour distinguer deux Program IDs.
|
||||
|
||||
Cas représentatifs audités :
|
||||
|
||||
| Cas | `domain`/`family`/`protocol` | `subfamily` | `program_version` |
|
||||
|----------------------------|-------------------------------------------------------------------|-------------------|-------------------------------------------------------------|
|
||||
| SPL Memo v1 / v3 / v4 | identiques entre les trois entrées | identique/absente | `v1` / `v3` / `v4` |
|
||||
| Aldrin AMM v1 / v2 | identiques | identique/absente | `v1` / `v2` |
|
||||
| Meteora DAMM v1 / v2 | identiques | `damm` | `v1` / `v2` |
|
||||
| Meteora DLMM | même domaine/protocole AMM | `dlmm` | aucune si aucune génération normative n'est attachée à l'ID |
|
||||
| GooseFX GAMMA | même domaine/famille/protocole GooseFX que les autres AMM GooseFX | `gamma` | aucune génération `v1` ne doit être inventée |
|
||||
| GooseFX SSL v2 | même domaine/famille/protocole GooseFX | `ssl` | `v2` |
|
||||
| Jupiter Aggregator v4 / v6 | identiques pour la lignée Aggregator | `aggregator` | `v4` / `v6` |
|
||||
|
||||
Cette séparation évite notamment de traiter GooseFX `GAMMA` comme « V1 » de GooseFX `SSL V2`, ce que les sources/IDLs ne justifient pas.
|
||||
|
||||
### Version du programme versus version d'IDL
|
||||
|
||||
`program_version` ne représente **jamais** la version de l'IDL, de la crate cliente, du SDK ou du schéma Anchor.
|
||||
|
||||
L'audit des IDLs archivées de bot3 démontre que ces nombres évoluent indépendamment du Program ID :
|
||||
|
||||
- l'IDL `jupiter_v6` porte une version d'IDL `0.1.0` alors que la génération publique du programme est `v6` ;
|
||||
- l'IDL `goosefx_v2` porte une version d'IDL `0.3.0` alors que la génération publique est `v2` ;
|
||||
- l'IDL GooseFX `gamma` porte une version de schéma `0.2.0` sans faire de GAMMA une hypothétique « v0.2 » du protocole ;
|
||||
- l'IDL Meteora DAMM v2 peut porter une version de schéma différente de `v2`.
|
||||
|
||||
Une éventuelle provenance/version d'IDL appartient plus tard à la couche d'interface/decoder ou à ses métadonnées, pas à l'identité `ProgramIdEntry` de Core.
|
||||
|
||||
Le nom `protocol_version` n'est pas retenu : un même protocole peut posséder simultanément plusieurs composants/lignées versionnés indépendamment (par exemple un aggregator, un limit-order program, un vault ou un lending program). `program_version` borne correctement la version à l'entrée/lignée concernée.
|
||||
|
||||
### Recherches et vues
|
||||
|
||||
Direction de l'API :
|
||||
|
||||
```text
|
||||
ProgramIdEntry
|
||||
ProgramIdFilter
|
||||
entries()
|
||||
program_ids(filter)
|
||||
native_program_ids()
|
||||
find_program_id()
|
||||
```
|
||||
|
||||
Le filtre générique doit pouvoir combiner les axes, au minimum :
|
||||
|
||||
```text
|
||||
domain
|
||||
family
|
||||
protocol
|
||||
subfamily
|
||||
program_version
|
||||
kind
|
||||
```
|
||||
|
||||
Des helpers lisibles peuvent être exposés lorsque leur usage est réel :
|
||||
|
||||
```text
|
||||
program_ids_by_domain(...)
|
||||
program_ids_by_family(...)
|
||||
program_ids_by_protocol(...)
|
||||
native_program_ids()
|
||||
```
|
||||
|
||||
Une future surface possédant des Program IDs AMM pourra ajouter :
|
||||
|
||||
```text
|
||||
amm_program_ids()
|
||||
```
|
||||
|
||||
Cette fonction devra être une vue de la classification canonique (`family = amm`) et non un second registre manuel. `0.1.1` ne crée pas un helper AMM vide puisque les Program IDs AMM restent hors scope de la release, mais son ajout futur ne doit nécessiter aucune refonte de `ProgramIdEntry`.
|
||||
|
||||
Les vues filtrées retournent des iterators paresseux sur le registre canonique et n'allouent pas de collection intermédiaire. `entries()` reste la vue exhaustive sous forme de slice statique. `native_program_ids()` et les helpers `program_ids_by_domain(...)`, `program_ids_by_family(...)` et `program_ids_by_protocol(...)` sont des vues de cette même source.
|
||||
|
||||
`registered_program_ids()` de bot3 reste considéré comme un alias redondant de `entries()` et n'est pas repris automatiquement. Les well-known accounts suivent un registre/naming distinct lorsqu'ils deviennent nécessaires.
|
||||
|
||||
### Invariants du registre
|
||||
|
||||
`ProgramIdEntry` doit permettre :
|
||||
|
||||
- recherche exacte par chaîne Base58 et, si utile, par `Pubkey` ;
|
||||
- recherche par domaine ;
|
||||
- recherche par famille ;
|
||||
- recherche par protocole ;
|
||||
- recherche par sous-famille lorsqu'elle existe ;
|
||||
- recherche par génération de programme lorsqu'elle existe ;
|
||||
- intersections de plusieurs critères sans créer un registre secondaire par combinaison ;
|
||||
- production des vues spécialisées telles que `native_program_ids()` et, plus tard, `amm_program_ids()` ;
|
||||
- unicité des codes et Program IDs canoniques.
|
||||
|
||||
La classification est descriptive. Elle ne porte aucun decoder, executor, IDL ou capability de dispatch et ne devient pas le registry fonctionnel de `ksp-program-lib`.
|
||||
|
||||
### Validation externe de la taxonomie pendant `pre.001-fix.002`
|
||||
|
||||
Le réaudit a confronté l'inventaire bot3 et ses IDLs à plusieurs sources externes actuelles :
|
||||
|
||||
- la source officielle Agave de chargement SPL distingue explicitement les Memo `1.0.0`, `3.0.0` et `4.0.0` avec trois Program IDs distincts ;
|
||||
- l'interface SPL Memo actuelle expose séparément les modules `v1`, `v3` et `v4` ;
|
||||
- Solana Explorer/Solscan exposent encore les Program IDs correspondants ;
|
||||
- les sources GooseFX distinguent le programme GAMMA et la lignée SSL/V2 ;
|
||||
- les sources Meteora distinguent DAMM v1, DAMM v2 et DLMM ;
|
||||
- les sources Jupiter distinguent les générations de son Swap Aggregator, notamment v4 et v6.
|
||||
|
||||
Ces cas valident la séparation `family` / `subfamily` / `program_version` et invalident l'utilisation de `subfamily` comme simple conteneur de version.
|
||||
|
||||
## Primitives communes supplémentaires
|
||||
|
||||
Aucune primitive supplémentaire n'est démontrée nécessaire pendant `pre.001`.
|
||||
|
||||
Sont donc explicitement reportés :
|
||||
|
||||
- identité/version de module générique ;
|
||||
- provenance/processor version ;
|
||||
- `Hash` ;
|
||||
- `Nonce` ;
|
||||
- temps/slot ;
|
||||
- identifiants de transport/provider ;
|
||||
- types de transaction/instruction/account ;
|
||||
- traits de module ou de composant.
|
||||
|
||||
L'ancien `ks-core::ModuleKind` / `ModuleName` / `ModuleVersion` de bot3 n'est pas migré par défaut. Une convention de provenance ne sera ajoutée que lorsqu'un consommateur réel l'exigera.
|
||||
|
||||
## Stratégie de tests
|
||||
|
||||
### `pre.002` — Error
|
||||
|
||||
Tests unitaires externes sous `unit_tests/` pour vérifier au minimum :
|
||||
|
||||
- conservation du domaine/code ;
|
||||
- message ;
|
||||
- ajout et ordre du contexte ;
|
||||
- format `Display` ;
|
||||
- absence du contexte/source dans le rendu si ce choix est conservé ;
|
||||
- conservation de `source()` ;
|
||||
- `Send + Sync` du type commun au moyen d'un test de compilation approprié.
|
||||
|
||||
Test d'intégration sous `tests/` pour vérifier que `Error`, `ErrorCode`, `ErrorContext` et `Result` sont réellement consommables depuis le crate-root.
|
||||
|
||||
### `pre.003` — Pubkey et Program IDs
|
||||
|
||||
Tests unitaires externes pour vérifier les invariants internes éventuels.
|
||||
|
||||
Tests d'intégration pour vérifier :
|
||||
|
||||
- `ksp_core_lib::Pubkey` consommable depuis la façade ;
|
||||
- type exact des constantes `PRGID_*` et `PRGIDPK_*` ;
|
||||
- égalité entre chaque chaîne Base58 possédée par KSP et sa représentation `Pubkey` compile-time ;
|
||||
- unicité des codes, chaînes Base58 et `Pubkey` du registre ;
|
||||
- cohérence de `entries()`, `program_ids(...)`, `native_program_ids()` et `find_program_id()` ;
|
||||
- filtres par `domain`, `family`, `protocol`, `subfamily`, `program_version` et `kind`, y compris leurs intersections ;
|
||||
- absence de duplication des entrées entre registre canonique et vues spécialisées ;
|
||||
- séparation entre Program IDs et well-known accounts ;
|
||||
- conformité des valeurs avec les sources officielles Anza/Solana consultées par l'audit, sans dépendance `solana-sdk-ids`.
|
||||
|
||||
L'ordre de `entries()` ne devient un contrat public que s'il est explicitement documenté comme tel ; sinon les tests doivent vérifier les invariants sans imposer arbitrairement un ordre.
|
||||
|
||||
## Validations prévues
|
||||
|
||||
Après chaque modification Rust applicable :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Audits complémentaires prévus lorsque la dépendance Solana existe :
|
||||
|
||||
```bash
|
||||
cargo tree -p ksp-core-lib
|
||||
cargo tree -p ksp-core-lib -d
|
||||
cargo tree -p ksp-core-lib -e features
|
||||
```
|
||||
|
||||
Objectifs :
|
||||
|
||||
- confirmer que Core ne tire aucune couche KSP supérieure ;
|
||||
- confirmer l'absence de codecs wire ajoutés par KSP ;
|
||||
- examiner toute duplication de génération fondamentale ;
|
||||
- vérifier que seules les features Solana nécessaires sont actives.
|
||||
|
||||
Les scripts/audits propres au dépôt seront exécutés s'ils existent réellement au moment de chaque tranche. Aucun script de ce type n'est présent dans l'archive `0.0.3` auditée.
|
||||
|
||||
## Découpage des prereleases
|
||||
|
||||
### `0.1.1-pre.001` — audit + brainstorming + plan
|
||||
|
||||
Objectifs :
|
||||
|
||||
- inventorier Core ;
|
||||
- fixer la direction Error/Result ;
|
||||
- borner les Program IDs ;
|
||||
- vérifier la génération Solana actuelle ;
|
||||
- décider les dépendances réellement candidates ;
|
||||
- fixer tests, API et hors-scope ;
|
||||
- ouvrir le plan `003-V0_1_1_CORE_FOUNDATION_PLAN.md`.
|
||||
|
||||
Pas de développement fonctionnel Core.
|
||||
|
||||
### `0.1.1-pre.002` — Error/Result
|
||||
|
||||
Objectifs :
|
||||
|
||||
- implémenter `ErrorCode`, `ErrorContext`, `Error` et `Result<T>` ;
|
||||
- mettre en place les modules privés et réexports crate-root associés ;
|
||||
- ajouter les tests unitaires externes et tests d'intégration de cette surface ;
|
||||
- valider l'absence de connaissance des domaines supérieurs.
|
||||
|
||||
Aucune dépendance Solana n'est nécessaire à cette tranche.
|
||||
|
||||
### `0.1.1-pre.003` — Pubkey + Program IDs
|
||||
|
||||
Objectifs :
|
||||
|
||||
- revérifier les versions Solana/Anza au jour de l'implémentation ;
|
||||
- vérifier la toolchain observée par rapport au MSRV de la génération retenue ;
|
||||
- déclarer `solana-pubkey = { version = "^4.3", default-features = false }` sous `[workspace.dependencies]` et la consommer uniquement dans le propriétaire `ksp-core-lib` avec `solana-pubkey.workspace = true` ;
|
||||
- réexporter `Pubkey` ;
|
||||
- implémenter `declare_program_id!` et la paire `PRGID_*` / `PRGIDPK_*` ;
|
||||
- finaliser à 18 l'inventaire des Program IDs fondamentaux à partir des sources officielles actuelles ;
|
||||
- implémenter `ProgramIdEntry`, `ProgramIdFilter`, `ProgramIdKind`, `entries()`, `program_ids(...)`, les vues domain/family/protocol, `native_program_ids()`, `find_program_id()` et `find_program_pubkey()` ;
|
||||
- implémenter la taxonomie extensible `domain` / `family` / `protocol` / `subfamily` / `program_version` / `kind` sans enum centrale fermée des protocoles ;
|
||||
- garantir que les futurs helpers spécialisés comme `amm_program_ids()` puissent être des vues du registre canonique sans duplication ;
|
||||
- ajouter les tests de conformité, d'unicité, de filtrage et de façade publique ;
|
||||
- confirmer l'absence totale de dépendance `solana-sdk-ids` ;
|
||||
- auditer le graphe/features réels après validation Cargo par le user.
|
||||
|
||||
### `0.1.1-pre.004` — intégration Core + audits
|
||||
|
||||
Résultat de l'audit d'intégration :
|
||||
|
||||
- `Error` / `Result`, `Pubkey` et le registre Program IDs composent une façade Core cohérente sans dépendance vers une couche KSP supérieure ;
|
||||
- aucune primitive N1 supplémentaire n'est démontrée nécessaire ;
|
||||
- les modules d'implémentation restent privés et les contrats consommables sont réexportés explicitement au crate-root ;
|
||||
- le test d'intégration public vérifie aussi qu'un consommateur peut déclarer un `const ErrorCode`, usage requis par les futures crates propriétaires de domaines ;
|
||||
- la rustdoc crate-level est complétée pour expliciter la frontière Core.
|
||||
|
||||
Validations `pre.004` exécutées avec succès par le user le 2026-08-14 :
|
||||
|
||||
- `cargo fmt --all` ;
|
||||
- `cargo check --workspace` ;
|
||||
- `cargo test --workspace` : 14 tests unitaires et 3 tests d'intégration publics réussis ;
|
||||
- `cargo clippy --workspace --all-targets` : succès sans warning communiqué ;
|
||||
- `cargo tree -p ksp-core-lib` : dépendance directe unique `solana-pubkey 4.3.0`, résolvant `solana-address 2.7.0` ;
|
||||
- `cargo tree -p ksp-core-lib -d` : aucun doublon ;
|
||||
- `cargo tree -p ksp-core-lib -e features` : graphe des features inspecté, sans besoin d'activer une feature optionnelle `solana-pubkey` supplémentaire pour le contrat Core actuel.
|
||||
|
||||
La tranche ne change ni le contrat Error, ni la taxonomie, ni l'inventaire des 18 Program IDs.
|
||||
|
||||
### `0.1.1-pre.005` — clôture
|
||||
|
||||
Résultat de clôture validé :
|
||||
|
||||
- aucune nouvelle primitive Core et aucune nouvelle feature `solana-pubkey` ne sont ajoutées ;
|
||||
- les documents de référence de `0.1.1` sont réalignés avec la surface effectivement livrée ;
|
||||
- aucun `README.md`/`USAGE.md` spécifique à la crate n'est ajouté : la rustdoc crate-level et les tests publics couvrent suffisamment la petite surface actuelle ;
|
||||
- aucun changelog général n'existe dans la base actuelle, donc aucun fichier de changelog artificiel n'est créé uniquement pour cette release ;
|
||||
- le prompt final `prompts/002-V0_1_2_START_PROMPT.md` est créé pour ouvrir Logging après publication stable de `0.1.1` ;
|
||||
- la publication stable est réalisée séparément par `0.1.1-rel.001`, conformément au workflow de versionnement.
|
||||
|
||||
### `0.1.1-rel.001` — publication stable
|
||||
|
||||
La publication stable :
|
||||
|
||||
- passe `workspace.package.version` de `0.1.1-pre.5` à `0.1.1` ;
|
||||
- enregistre les validations finales de `pre.005` exécutées avec succès par le user le 2026-08-14 ;
|
||||
- confirme 14 tests unitaires et 3 tests d'intégration publics réussis ;
|
||||
- confirme Clippy sans warning communiqué ;
|
||||
- confirme `solana-pubkey 4.3.0` comme unique dépendance externe directe de `ksp-core-lib` ;
|
||||
- confirme l'absence de doublons dans `cargo tree -d` ;
|
||||
- confirme que le graphe de features ne justifie aucune feature optionnelle `solana-pubkey` supplémentaire ;
|
||||
- marque `0.1.1` comme réalisée dans le roadmap et conserve le présent plan comme historique clôturé ;
|
||||
- prépare le commit de release puis le tag stable `v0.1.1`.
|
||||
|
||||
Aucun contrat public, Program ID, dépendance ou feature n'est modifié par `rel.001`.
|
||||
|
||||
Un `pre.NNN-fix.NNN` corrige la tranche correspondante sans réécrire son historique.
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
`0.1.1` n'ouvre pas :
|
||||
|
||||
- `ksp-logging-lib` ;
|
||||
- `ksp-config-lib` ;
|
||||
- Tauri ;
|
||||
- wallet/keypair/signer ;
|
||||
- Borsh/Wincode et autres codecs wire ;
|
||||
- `ksp-interface-lib` ;
|
||||
- Program decoder/registry/`ProgramExecutionPreparer` ;
|
||||
- execution policy/orchestration ;
|
||||
- transport RPC/WS/Helius/Yellowstone ;
|
||||
- Store/PostgreSQL ;
|
||||
- materializers ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML.
|
||||
|
||||
## Questions ouvertes non bloquantes
|
||||
|
||||
Aucune question ouverte ne bloque la publication de `0.1.1`. Les capacités volontairement différées restent soumises à la règle need-driven des releases futures plutôt qu'à des placeholders Core.
|
||||
1130
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
Normal file
1130
docs/plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Règles des dépendances KSP
|
||||
|
||||
@@ -23,6 +23,14 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
||||
- **DEP-KSP-004** — Aucun `ksp-data-api` global n'est introduit uniquement pour éviter des conversions explicites entre modèles appartenant à des responsabilités différentes.
|
||||
- **DEP-KSP-005** — Une dépendance autorisée par le graphe n'est ajoutée au manifeste que lorsqu'un usage réel la justifie.
|
||||
|
||||
## Déclaration Cargo et centralisation workspace
|
||||
|
||||
- **DEP-CARGO-001** — Toute dépendance externe utilisée par une crate membre du workspace est déclarée une seule fois dans le `Cargo.toml` racine sous `[workspace.dependencies]`.
|
||||
- **DEP-CARGO-002** — Une crate membre consomme une dépendance centralisée avec `<dependency>.workspace = true` et ne redéclare pas localement sa version.
|
||||
- **DEP-CARGO-003** — Les options communes de résolution telles que `default-features` et la contrainte de version sont définies au niveau `[workspace.dependencies]`. Une crate membre n'ajoute localement que des features réellement propres à son usage lorsqu'elles sont nécessaires et compatibles avec l'héritage Cargo.
|
||||
- **DEP-CARGO-004** — Lorsqu'une génération majeure/mineure compatible est retenue, KSP exprime explicitement l'intention sous forme caret `^M.m` (par exemple `^4.3`) plutôt qu'avec une écriture patch telle que `4.3.0`. Même si Cargo interprète aussi par défaut cette dernière comme une contrainte compatible caret, KSP normalise la syntaxe pour rendre l'intention manifeste. Un pin exact `=M.m.p` ou un bornage différent requiert une justification explicite.
|
||||
- **DEP-CARGO-005** — Le `Cargo.lock` résout la version patch concrète à l'intérieur de la contrainte du workspace ; cette résolution ne remplace pas la politique de version déclarée dans le manifeste racine.
|
||||
|
||||
## Codecs wire et cohérence des versions
|
||||
|
||||
- **DEP-WIRE-001** — Pour les surfaces wire officielles KSP, les dépendances directes vers `borsh`, `wincode` ou codecs équivalents appartiennent normalement à `ksp-interface-lib`.
|
||||
@@ -35,12 +43,15 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
||||
|
||||
## Logging
|
||||
|
||||
- **DEP-LOG-001** — `ksp-logging-lib` est le propriétaire KSP direct de `tracing`, `tracing-appender`, `tracing-subscriber` et de l'initialisation/configuration logging.
|
||||
- **DEP-LOG-001** — `ksp-logging-lib` est le propriétaire KSP direct de `tracing`, `tracing-appender`, `tracing-subscriber` et de l'initialisation/configuration runtime logging.
|
||||
- **DEP-LOG-002** — `ksp-logging-lib` peut dépendre de `ksp-core-lib` pour le contrat commun `Error` / `Result`.
|
||||
- **DEP-LOG-003** — `ksp-core-lib` ne dépend pas de `ksp-logging-lib` dans l'architecture actuelle.
|
||||
- **DEP-LOG-004** — Les crates KSP comportant du runtime peuvent dépendre directement de `ksp-logging-lib` et ne dépendent normalement pas directement de `tracing`.
|
||||
- **DEP-LOG-004** — Les crates KSP comportant du runtime dépendent de `ksp-logging-lib` lorsqu'elles instrumentent leur comportement et n'utilisent pas directement `tracing`, `tracing-subscriber` ou `tracing-appender` pour émettre leurs propres événements/spans.
|
||||
- **DEP-LOG-005** — Les crates `*-api` purement déclaratives n'ajoutent pas une dépendance logging sans comportement réel à instrumenter.
|
||||
- **DEP-LOG-006** — Une application/framework peut exceptionnellement intégrer directement un plugin/dépendance tracing imposé par son framework, notamment Tauri, sans créer une seconde politique de logging parallèle à `ksp-logging-lib`.
|
||||
- **DEP-LOG-006** — Le subscriber KSP rend silencieux par défaut les targets externes ; une information tierce utile est réémise explicitement par la crate KSP propriétaire sous son propre target au lieu de renommer/réécrire l'événement tiers.
|
||||
- **DEP-LOG-007** — Une application/framework peut exceptionnellement intégrer directement un plugin/dépendance tracing imposé par son framework, notamment Tauri, sans créer une seconde politique de logging parallèle à `ksp-logging-lib`; les événements KSP restent émis via la façade KSP.
|
||||
- **DEP-LOG-008** — `ksp-logging-lib` possède ses settings runtime et son hot reload ; `ksp-config-lib` peut plus tard construire ces settings et demander une reconfiguration sans créer de dépendance inverse Logging -> Config.
|
||||
- **DEP-LOG-009** — Tout bridge `tracing` public mais caché de la documentation rendu techniquement nécessaire par l’expansion des macros de `ksp-logging-lib` est un détail d’implémentation réservé à ces macros ; une crate consommatrice ne l’utilise jamais directement et reste limitée à la façade KSP documentée.
|
||||
|
||||
## Program / Execution
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: prompts/000-README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Prompts KSP
|
||||
|
||||
@@ -21,4 +21,6 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
|
||||
|
||||
## Documents
|
||||
|
||||
- [`001-V0_1_1_START_PROMPT.md`](001-V0_1_1_START_PROMPT.md) — prompt final ouvrant la première release fonctionnelle `0.1.1` après publication stable de `0.0.3`.
|
||||
- [`001-V0_1_1_START_PROMPT.md`](001-V0_1_1_START_PROMPT.md) — prompt historique ouvrant la première release fonctionnelle `0.1.1` après publication stable de `0.0.3` ;
|
||||
- [`002-V0_1_2_START_PROMPT.md`](002-V0_1_2_START_PROMPT.md) — prompt final destiné à ouvrir `0.1.2 — Logging foundation` après publication stable de `0.1.1`.
|
||||
- [`003-V0_1_3_START_PROMPT.md`](003-V0_1_3_START_PROMPT.md) — prompt final destiné à ouvrir `0.1.3 — Configuration foundation` après publication stable de `0.1.2`.
|
||||
|
||||
335
prompts/002-V0_1_2_START_PROMPT.md
Normal file
335
prompts/002-V0_1_2_START_PROMPT.md
Normal file
@@ -0,0 +1,335 @@
|
||||
<!-- file: prompts/002-V0_1_2_START_PROMPT.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Prompt de démarrage KSP 0.1.2
|
||||
|
||||
**Statut : Final — à utiliser après validation et publication stable de `0.1.1`.**
|
||||
|
||||
## 1. Identité
|
||||
|
||||
Release fonctionnelle :
|
||||
|
||||
```text
|
||||
0.1.2 — Logging foundation
|
||||
```
|
||||
|
||||
Deuxième release fonctionnelle de Khadhroony Solana Project.
|
||||
|
||||
## 2. Mission
|
||||
|
||||
Introduire et stabiliser `ksp-logging-lib` comme façade KSP commune et propriétaire du logging/tracing runtime.
|
||||
|
||||
Cette crate doit être la seule crate KSP qui importe directement et configure la stack `tracing` nécessaire à la politique générale de logs. Les autres crates comportementales KSP doivent consommer la façade de `ksp-logging-lib` plutôt que définir chacune leur propre initialisation ou leur propre politique de tracing.
|
||||
|
||||
La release doit établir une surface assez générale pour Logging lui-même et pour les prochaines crates N1, sans ouvrir `ksp-config-lib`, Tauri, Transport, Wallet, Program, Store ou les autres couches supérieures.
|
||||
|
||||
## 3. Base requise
|
||||
|
||||
Base attendue :
|
||||
|
||||
```text
|
||||
0.1.1 stable
|
||||
```
|
||||
|
||||
La session commence uniquement après validation de la dernière prerelease de `0.1.1`, publication du delta final `rel.001` et tag stable :
|
||||
|
||||
```text
|
||||
v0.1.1
|
||||
```
|
||||
|
||||
Dans le workflow KSP, une archive Gitea nommée `khadhroony-solana-project-v0.1.1.zip` provient directement du tag correspondant et constitue une base stable suffisante pour la session.
|
||||
|
||||
## 4. État validé à préserver
|
||||
|
||||
`ksp-core-lib` fournit désormais la fondation N1 commune, notamment :
|
||||
|
||||
```text
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorContext
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::Result<T>
|
||||
ksp_core_lib::Pubkey
|
||||
```
|
||||
|
||||
ainsi que la propriété KSP des Program IDs fondamentaux et leur registre descriptif.
|
||||
|
||||
Logging peut dépendre de `ksp-core-lib` pour `Error` / `Result`. La relation inverse reste interdite : Core ne dépend pas de Logging.
|
||||
|
||||
La politique Cargo établie dans `0.1.1` doit être conservée :
|
||||
|
||||
- toute dépendance externe est déclarée au `Cargo.toml` racine sous `[workspace.dependencies]` ;
|
||||
- une crate membre consomme ces dépendances avec `<crate>.workspace = true` ;
|
||||
- les versions sont exprimées avec une génération compatible explicite telle que `^M.m`, sauf pin justifié ;
|
||||
- les features et `default-features` sont activées uniquement lorsqu'un besoin concret le démontre.
|
||||
|
||||
## 5. Sources de vérité internes
|
||||
|
||||
Relire en priorité les fichiers réellement présents dans la base, notamment :
|
||||
|
||||
- `ROADMAP.md` ;
|
||||
- `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` ;
|
||||
- `docs/plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md` ;
|
||||
- `docs/architecture/002-LAYERS_AND_DEPENDENCIES.md` ;
|
||||
- `docs/architecture/003-COMPONENT_CONTRACTS.md` ;
|
||||
- `docs/architecture/004-COMPONENT_INVENTORY.md` ;
|
||||
- `docs/architecture/005-DEPENDENCY_GRAPH.md` ;
|
||||
- `docs/rules/RULES_DEPENDENCIES.md` ;
|
||||
- `docs/rules/RULES_KSP.md` ;
|
||||
- `docs/rules/RULES_RUST.md` ;
|
||||
- `docs/rules/VERSION_WORKFLOW.md` ;
|
||||
- `docs/IDEAS.md` ;
|
||||
- les deltas `deltas/0.1.1/*`.
|
||||
|
||||
Relire aussi les règles/index racine supplémentaires présents au moment de la session. Ne jamais inventer un document absent de la base.
|
||||
|
||||
## 6. Sources externes normatives
|
||||
|
||||
Avant d'ajouter `tracing`, `tracing-subscriber`, `tracing-appender` ou toute crate liée :
|
||||
|
||||
- vérifier les versions publiées actuelles depuis les sources officielles Tokio/tracing et crates.io/docs.rs ;
|
||||
- examiner leurs features et dépendances réelles ;
|
||||
- identifier le MSRV/toolchain pertinent ;
|
||||
- éviter d'activer les default features ou features optionnelles uniquement par commodité ;
|
||||
- examiner `cargo tree` et `cargo tree -e features` après intégration.
|
||||
|
||||
La première prerelease doit choisir les dépendances réellement nécessaires à l'API retenue ; la présence architecturale d'une crate candidate n'oblige pas à l'ajouter si la surface finale n'en a pas besoin.
|
||||
|
||||
## 7. Première prerelease obligatoire : `0.1.2-pre.001`
|
||||
|
||||
`pre.001` est d'abord une prerelease de **brainstorming, audit et planification**.
|
||||
|
||||
Elle doit au minimum :
|
||||
|
||||
1. inventorier l'état réel du workspace et confirmer la création/absence actuelle de `ksp-logging-lib` ;
|
||||
2. auditer la stack `tracing` officielle actuelle, ses versions, features et dépendances ;
|
||||
3. définir précisément la frontière entre façade KSP, initialisation et backend/subscriber ;
|
||||
4. déterminer si l'API publique principale doit utiliser des macros, des fonctions ou une combinaison ;
|
||||
5. préserver les callsites/source locations réels pour les événements de logging ;
|
||||
6. définir les niveaux KSP `error`, `warn`, `info`, `debug`, `trace` ;
|
||||
7. définir les champs structurés communs utiles maintenant, notamment `target`, `domain`, `component` ou équivalents sans inventer une taxonomie trop rigide ;
|
||||
8. définir le contrat de settings runtime de Logging sans dépendre de Config ;
|
||||
9. définir le lifecycle d'initialisation, les erreurs d'initialisation et le comportement d'une initialisation répétée ;
|
||||
10. étudier console, fichiers, filtering, appender non bloquant et rotation uniquement selon les besoins de la première surface ;
|
||||
11. traiter explicitement la durée de vie/ownership des guards nécessaires aux writers non bloquants si cette voie est retenue ;
|
||||
12. définir la stratégie de prévention des secrets dans les logs ;
|
||||
13. proposer l'API publique et les crate-root reexports ;
|
||||
14. proposer les tests unitaires/intégration et audits de dépendances ;
|
||||
15. dimensionner les prereleases suivantes ;
|
||||
16. confirmer les hors-scope.
|
||||
|
||||
Ne pas transformer `pre.001` en une grosse phase de développement avant validation du plan.
|
||||
|
||||
## 8. Responsabilité de `ksp-logging-lib`
|
||||
|
||||
La direction acquise est :
|
||||
|
||||
```text
|
||||
ksp-logging-lib
|
||||
-> ksp-core-lib
|
||||
-> tracing stack réellement retenue
|
||||
```
|
||||
|
||||
`ksp-logging-lib` doit être la façade commune de logging/tracing du projet et le propriétaire de la politique runtime correspondante.
|
||||
|
||||
Les crates comportementales KSP pourront dépendre directement de `ksp-logging-lib` et utiliser sa surface KSP pour :
|
||||
|
||||
```text
|
||||
error
|
||||
warn
|
||||
info
|
||||
debug
|
||||
trace
|
||||
```
|
||||
|
||||
avec les champs structurés réellement retenus par `pre.001`.
|
||||
|
||||
Les crates `*-api` purement déclaratives restent sans dépendance logging par défaut lorsqu'elles n'ont aucun comportement réel à tracer.
|
||||
|
||||
## 9. Callsite et macros
|
||||
|
||||
L'audit doit porter une attention particulière à la préservation du callsite réel.
|
||||
|
||||
Une simple fonction wrapper autour d'une macro `tracing::*` peut enregistrer le fichier/module/ligne du wrapper plutôt que ceux de l'appelant. La surface KSP doit donc être conçue pour préserver correctement les métadonnées de callsite, quitte à exposer des macros KSP qui délèguent aux macros `tracing` au point d'appel.
|
||||
|
||||
Le design exact des macros/fonctions et leurs noms sont décidés dans `pre.001`, puis testés avant stabilisation.
|
||||
|
||||
## 10. Settings et Config
|
||||
|
||||
`ksp-logging-lib` ne dépend pas de `ksp-config-lib`.
|
||||
|
||||
Logging possède les settings runtime strictement nécessaires à son initialisation. `ksp-config-lib`, lorsqu'il sera développé en `0.1.3`, pourra lire/résoudre ses documents puis convertir explicitement la configuration obtenue vers les settings publics de Logging.
|
||||
|
||||
Ne pas introduire de document JSON/TOML de configuration global dans Logging uniquement pour anticiper Config.
|
||||
|
||||
## 11. Sorties et lifecycle
|
||||
|
||||
Le `pre.001` doit déterminer la première surface réellement utile parmi :
|
||||
|
||||
- console/stdout/stderr ;
|
||||
- fichiers ;
|
||||
- filtrage global et/ou par target/domain ;
|
||||
- format lisible et/ou structuré ;
|
||||
- rotation ;
|
||||
- writer non bloquant ;
|
||||
- flush/shutdown propre.
|
||||
|
||||
Lorsque `tracing-appender::non_blocking` ou un mécanisme équivalent est retenu, la durée de vie du guard doit être possédée par un objet/lifecycle KSP explicite afin d'éviter une perte silencieuse de logs à la fin du scope d'initialisation.
|
||||
|
||||
Une capacité non nécessaire à la première validation n'est pas ajoutée par anticipation.
|
||||
|
||||
## 12. Erreurs
|
||||
|
||||
Les erreurs Logging utilisent le contrat Core :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::Result<T>
|
||||
```
|
||||
|
||||
`ksp-logging-lib` définit ses propres constantes `ErrorCode` dans son domaine sans ajouter de variante ou de connaissance Logging à `ksp-core-lib`.
|
||||
|
||||
Les causes externes utiles sont conservées via le contrat `source` Core lorsqu'elles satisfont les bornes prévues.
|
||||
|
||||
Aucun `unwrap`, `expect`, `panic` production ou opérateur `?` n'est utilisé.
|
||||
|
||||
## 13. Secrets et données sensibles
|
||||
|
||||
Logging ne doit pas devenir un canal de fuite de secrets.
|
||||
|
||||
`pre.001` doit au minimum définir :
|
||||
|
||||
- quels champs sont interdits par politique ;
|
||||
- comment les settings sensibles sont exclus ;
|
||||
- quelles données doivent être explicitement redacted/omises par les appelants ;
|
||||
- si des helpers de redaction génériques sont réellement nécessaires maintenant.
|
||||
|
||||
Ne pas logger automatiquement les contenus de clés privées, seeds, passwords, tokens d'API ou autres secrets.
|
||||
|
||||
## 14. Dépendances et propriété
|
||||
|
||||
Interdictions :
|
||||
|
||||
```text
|
||||
ksp-core-lib -X-> ksp-logging-lib
|
||||
ksp-logging-lib -X-> ksp-config-lib
|
||||
ksp-logging-lib -X-> wallet/transport/program/store/workers/jobs/apps
|
||||
```
|
||||
|
||||
`ksp-logging-lib` est la seule crate KSP qui doit normalement importer directement `tracing` et réaliser la configuration du subscriber/appender retenu.
|
||||
|
||||
Une application Tauri future peut exceptionnellement devoir intégrer une crate/plugin tracing imposée par son framework. Cette exception reste au niveau adaptateur/application et ne crée pas une seconde politique de logging parallèle à `ksp-logging-lib`.
|
||||
|
||||
## 15. Hors scope strict de `0.1.2`
|
||||
|
||||
- `ksp-config-lib` et documents/profils Config ;
|
||||
- application Tauri ;
|
||||
- wallet/keypair/signer ;
|
||||
- RPC/WS/providers ;
|
||||
- Program decoding/execution ;
|
||||
- Store/PostgreSQL ;
|
||||
- materializers ;
|
||||
- workers/jobs/pipelines ;
|
||||
- scenarios ;
|
||||
- trading/ML ;
|
||||
- OpenTelemetry ou export réseau de traces, sauf besoin concret explicitement revalidé et borné ;
|
||||
- observability distribuée complète.
|
||||
|
||||
## 16. Règles Rust et Cargo
|
||||
|
||||
Préserver les règles workspace, notamment :
|
||||
|
||||
- Rust 2024 ;
|
||||
- async-first pour les I/O futures lorsque pertinent, sans rendre artificiellement async les appels de logging synchrones ;
|
||||
- `unsafe` interdit ;
|
||||
- `unwrap` / `expect` interdits ;
|
||||
- `panic` interdit en production ;
|
||||
- opérateur `?` interdit ;
|
||||
- returns explicites, y compris dans les closures lorsque Clippy l'exige ;
|
||||
- `unreachable_pub = deny` ;
|
||||
- `missing_docs = warn` ;
|
||||
- imports de traits seulement lorsque nécessaire ;
|
||||
- réexports crate-root explicites ;
|
||||
- pas de `mod.rs` ;
|
||||
- pas de `pub(super)` / `pub(in ...)` ;
|
||||
- code/Rustdoc en anglais ;
|
||||
- tests unitaires externes au `src` selon la convention du dépôt ;
|
||||
- dépendances externes centralisées sous `[workspace.dependencies]`.
|
||||
|
||||
## 17. Git et deltas
|
||||
|
||||
À partir de `0.1.x`, chaque delta est commité :
|
||||
|
||||
```text
|
||||
pre.NNN
|
||||
pre.NNN-fix.NNN
|
||||
rel.NNN
|
||||
```
|
||||
|
||||
Une erreur est corrigée par le delta suivant ; l'historique n'est pas réécrit.
|
||||
|
||||
Seul le commit final validé comme stable reçoit :
|
||||
|
||||
```text
|
||||
v0.1.2
|
||||
```
|
||||
|
||||
## 18. Dimensionnement indicatif
|
||||
|
||||
Le nombre réel de prereleases est décidé dans `pre.001`.
|
||||
|
||||
Trajectoire candidate uniquement :
|
||||
|
||||
```text
|
||||
pre.001 audit + brainstorming + plan
|
||||
pre.002 crate/settings + façade levels/callsites
|
||||
pre.003 initialisation + console/filtering
|
||||
pre.004 fichiers/appender/rotation/lifecycle si retenus
|
||||
pre.005 intégration/tests/audits
|
||||
pre.006 validation finale/docs/cleanup/prompt 0.1.3
|
||||
```
|
||||
|
||||
Scinder une tranche si son périmètre devient trop large. Supprimer/réorganiser une tranche si le `pre.001` démontre qu'une capacité candidate n'est pas nécessaire.
|
||||
|
||||
## 19. Validations attendues
|
||||
|
||||
Lorsque les commandes sont applicables :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo tree -p ksp-logging-lib
|
||||
cargo tree -p ksp-logging-lib -d
|
||||
cargo tree -p ksp-logging-lib -e features
|
||||
```
|
||||
|
||||
Exécuter également les scripts/audits réellement présents dans le dépôt.
|
||||
|
||||
Aucune validation non exécutée ne doit être déclarée réussie.
|
||||
|
||||
## 20. Critères de sortie
|
||||
|
||||
`0.1.2` peut être publiée stable lorsque :
|
||||
|
||||
- la façade Logging et son ownership sont clairs ;
|
||||
- les callsites sont préservés par tests ;
|
||||
- les cinq niveaux KSP nécessaires sont utilisables ;
|
||||
- l'initialisation choisie est déterministe et testée ;
|
||||
- console/fichiers/filtering/lifecycle retenus sont validés ;
|
||||
- les erreurs utilisent Core sans dépendance inverse ;
|
||||
- aucune dépendance Config ou domaine supérieur n'a été introduite ;
|
||||
- les secrets sont protégés par une politique documentée/testable ;
|
||||
- le graphe de dépendances/features est audité ;
|
||||
- les validations workspace sont propres ;
|
||||
- la documentation finale et le prompt de la release suivante sont prêts.
|
||||
|
||||
## 21. Release suivante
|
||||
|
||||
La release suivante prévue est :
|
||||
|
||||
```text
|
||||
0.1.3 — ksp-config-lib
|
||||
```
|
||||
|
||||
Son périmètre concret reste soumis au `pre.001` correspondant et peut être scindé si Config s'avère trop large pour une seule release.
|
||||
370
prompts/003-V0_1_3_START_PROMPT.md
Normal file
370
prompts/003-V0_1_3_START_PROMPT.md
Normal file
@@ -0,0 +1,370 @@
|
||||
<!-- file: prompts/003-V0_1_3_START_PROMPT.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Prompt de démarrage KSP 0.1.3
|
||||
|
||||
## 1. Contexte de reprise
|
||||
|
||||
Projet : `khadhroony-solana-project` (KSP).
|
||||
|
||||
Base requise avant ouverture de cette session :
|
||||
|
||||
```text
|
||||
v0.1.2 stable
|
||||
```
|
||||
|
||||
La release `0.1.2` a introduit `ksp-logging-lib` comme façade KSP unique de logging/tracing runtime. `0.1.3` ne doit pas rouvrir cette architecture ; Config doit consommer ses contrats publics.
|
||||
|
||||
Release à développer :
|
||||
|
||||
```text
|
||||
0.1.3 — Configuration foundation
|
||||
```
|
||||
|
||||
## 2. Mission
|
||||
|
||||
Introduire `ksp-config-lib` comme propriétaire unique KSP de la configuration applicative : documents de configuration unitaires et composites, résolution des profils, variables d'environnement, validation, lecture et modifications/persistences explicitement autorisées.
|
||||
|
||||
Les autres crates et applications KSP ne doivent pas lire, résoudre ou modifier directement les fichiers de configuration ni les variables d'environnement. Elles consomment les contrats de `ksp-config-lib`. Une application dédiée au management de configuration utilise donc Config comme unique frontière, y compris lorsqu'elle doit afficher ou modifier des valeurs sensibles explicitement autorisées.
|
||||
|
||||
La première prerelease est obligatoirement une tranche de brainstorming, audit et planification. Ne pas commencer directement par une implémentation large de Config.
|
||||
|
||||
## 3. Base architecturale à préserver
|
||||
|
||||
Dépendances candidates de la nouvelle crate :
|
||||
|
||||
```text
|
||||
ksp-config-lib
|
||||
-> ksp-core-lib
|
||||
-> ksp-logging-lib
|
||||
```
|
||||
|
||||
Relations interdites :
|
||||
|
||||
```text
|
||||
ksp-core-lib -X-> ksp-config-lib
|
||||
ksp-logging-lib -X-> ksp-config-lib
|
||||
```
|
||||
|
||||
`ksp-logging-lib` possède toujours ses propres `LoggingSettings` et son lifecycle `initialize` / `reinitialize`. Config lit/résout ses documents puis construit explicitement les settings publics Logging ; Logging ne lit aucun document Config et ne connaît aucun profil.
|
||||
|
||||
## 4. Première prerelease obligatoire : `0.1.3-pre.001`
|
||||
|
||||
Cette tranche doit produire un plan détaillé avant développement fonctionnel.
|
||||
|
||||
Elle doit notamment :
|
||||
|
||||
1. auditer l'état réel du workspace stable `0.1.2` ;
|
||||
2. réauditer les règles Config déjà présentes dans le dépôt et les archives historiques pertinentes sans les copier aveuglément ;
|
||||
3. inventorier les documents de configuration unitaires nécessaires à court terme et les fichiers composites qui les assemblent pour un exécutable/application ;
|
||||
4. distinguer valeurs globales, valeurs profilées, sélection du profil, documents unitaires réutilisables et composition propre aux exécutables ;
|
||||
5. fixer la politique de résolution document unitaire -> composition -> profil -> env override -> valeur effective ;
|
||||
6. fixer la validation, les diagnostics et les erreurs Core nécessaires ;
|
||||
7. fixer les opérations de modification/sauvegarde autorisées et leurs garanties d'atomicité ;
|
||||
8. fixer la frontière secrets/public/debug et les droits explicites permettant à une application de management Config d'accéder aux secrets lorsqu'elle doit les consulter ou les modifier ;
|
||||
9. définir la relation exacte avec `LoggingSettings` et le hot reload Logging ;
|
||||
10. décider si le périmètre Config tient proprement dans une seule release `0.1.3` ou doit être scindé ;
|
||||
11. produire `docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md` et le plan souple des prereleases suivantes.
|
||||
|
||||
Le `pre.001` reste une tranche de planification : ne pas ajouter de dépendance fonctionnelle inutilisée et ne pas créer une large surface de code avant validation du plan.
|
||||
|
||||
## 5. Documents spécialisés
|
||||
|
||||
La configuration KSP doit être pensée comme plusieurs documents spécialisés plutôt qu'un unique fichier global monolithique lorsque les responsabilités sont distinctes.
|
||||
|
||||
Le point déjà fixé est notamment :
|
||||
|
||||
```text
|
||||
logging.config.json
|
||||
```
|
||||
|
||||
séparé de la configuration applicative générale.
|
||||
|
||||
Le `pre.001` doit inventorier les autres documents réellement nécessaires au premier cycle et éviter de créer prématurément des fichiers pour des composants non développés.
|
||||
|
||||
La structure doit conserver la séparation déjà utile dans khadhroony-bot3 entre **documents unitaires** et **fichiers composites** :
|
||||
|
||||
- un document unitaire possède une responsabilité spécialisée et reste réutilisable indépendamment des exécutables ;
|
||||
- un fichier composite assemble les documents requis par un exécutable/application et peut sélectionner ou remplacer les profils nécessaires sans dupliquer les documents spécialisés.
|
||||
|
||||
Les vrais fichiers de configuration runtime appartiennent sous :
|
||||
|
||||
```text
|
||||
config/
|
||||
```
|
||||
|
||||
Les schemas appartiennent sous :
|
||||
|
||||
```text
|
||||
config/schemas/
|
||||
```
|
||||
|
||||
Les exemples ne doivent pas être mélangés aux fichiers runtime réels et appartiennent sous :
|
||||
|
||||
```text
|
||||
config/examples/
|
||||
```
|
||||
|
||||
Les noms exacts, la nomenclature des fichiers composites et leur format restent à valider dans le plan Config `pre.001`.
|
||||
|
||||
## 6. Valeurs globales et profils
|
||||
|
||||
Une valeur qui ne varie pas selon le profil reste hors profils.
|
||||
|
||||
Exemples historiques déjà retenus comme principe :
|
||||
|
||||
```text
|
||||
logging.logs_directory
|
||||
wallets_directory
|
||||
```
|
||||
|
||||
Le `default_profile` est autonome : il sélectionne un profil par défaut mais ne doit pas être artificiellement imbriqué dans chacun des profils.
|
||||
|
||||
Les fichiers composites propres à un binaire/application sélectionnent les documents unitaires utilisés et peuvent remplacer les profils choisis lorsqu'un besoin concret l'exige. Cette composition doit rester possédée et résolue par `ksp-config-lib`, pas par chaque exécutable séparément.
|
||||
|
||||
## 7. Variables d'environnement
|
||||
|
||||
Toutes les variables d'environnement applicatives doivent être namespacées selon leur propriétaire fonctionnel.
|
||||
|
||||
Pour les composants génériques KSP / `ksp-*` :
|
||||
|
||||
```text
|
||||
KSP_*
|
||||
KSP_PUBLIC_*
|
||||
KSP_SECRET_*
|
||||
```
|
||||
|
||||
Pour les futures applications/composants bot du projet :
|
||||
|
||||
```text
|
||||
KSPB_*
|
||||
KSPB_PUBLIC_*
|
||||
KSPB_SECRET_*
|
||||
```
|
||||
|
||||
`ksp-config-lib` est le propriétaire unique de la lecture, de la résolution, de la validation et de l'écriture éventuelle des variables d'environnement KSP. Les autres crates/applications ne doivent pas contourner cette frontière par des lectures directes de l'environnement applicatif.
|
||||
|
||||
Le `pre.001` doit formaliser précisément la correspondance entre document, clé de configuration et override d'environnement.
|
||||
|
||||
Aucune variable applicative sans préfixe propriétaire ne doit être introduite.
|
||||
|
||||
## 8. Secrets, public et debug
|
||||
|
||||
La classification Config doit distinguer les valeurs publiques, ordinaires et secrètes, mais **secret ne signifie pas interdiction absolue de lecture**.
|
||||
|
||||
Direction déjà retenue :
|
||||
|
||||
- `KSP_SECRET_*` / `KSPB_SECRET_*` : valeurs sensibles, jamais exposées accidentellement dans les logs, diagnostics ordinaires ou surfaces publiques génériques ;
|
||||
- `KSP_PUBLIC_*` / `KSPB_PUBLIC_*` : valeurs explicitement exposables ;
|
||||
- autres valeurs : politique d'exposition à fixer selon le contrat applicatif et le contexte debug ;
|
||||
- les composants runtime qui ont légitimement besoin d'un secret doivent pouvoir l'obtenir via un contrat Config explicite ;
|
||||
- une application possédant une fonctionnalité de **management de configuration** doit pouvoir, via une surface Config explicitement prévue et contrôlée, consulter et modifier les secrets nécessaires. Cela couvre notamment la future `ksp-app-config-desk` et, plus tard, la partie Config d'une éventuelle application générale.
|
||||
|
||||
La surface Config doit donc formaliser une **politique d'accès** aux secrets, et non une règle simpliste « jamais exposé ». Le `pre.001` doit décider les contrats distincts de lecture runtime, consultation de management, mutation et exposition Tauri/DTO afin d'éviter toute fuite implicite tout en permettant l'administration légitime.
|
||||
|
||||
Cela ne change pas la responsabilité de Logging concernant le contenu des messages : `ksp-logging-lib` ne scanne ni ne redacte automatiquement les secrets fournis par ses callers.
|
||||
|
||||
## 9. Relation Config -> Logging
|
||||
|
||||
Config peut produire un `ksp_logging_lib::LoggingSettings` à partir de sa configuration effective puis appeler le lifecycle Logging au niveau d'orchestration approprié.
|
||||
|
||||
Séquence conceptuelle :
|
||||
|
||||
```text
|
||||
documents Config
|
||||
-> résolution/profil/env
|
||||
-> configuration Logging effective
|
||||
-> LoggingSettings
|
||||
-> ksp_logging_lib::initialize(...) ou reinitialize(...)
|
||||
```
|
||||
|
||||
Le mécanisme qui détecte un changement de fichier, s'il est introduit plus tard, appartient à Config/application/orchestration ; Logging fournit uniquement sa reconfiguration runtime.
|
||||
|
||||
`0.1.3-pre.001` doit préciser qui possède le `LoggingGuard` dans les premières compositions concrètes sans créer de singleton global Config inutile.
|
||||
|
||||
## 10. Validation et erreurs
|
||||
|
||||
`ksp-config-lib` doit réutiliser :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorContext
|
||||
ksp_core_lib::Result<T>
|
||||
```
|
||||
|
||||
Les codes propres à Config appartiennent au domaine Config et ne doivent pas être ajoutés comme connaissance métier à Core.
|
||||
|
||||
Les diagnostics doivent distinguer autant que nécessaire :
|
||||
|
||||
- document absent lorsque obligatoire ;
|
||||
- syntaxe invalide ;
|
||||
- schema/contrainte invalide ;
|
||||
- profil demandé absent ;
|
||||
- valeur effective invalide ;
|
||||
- override d'environnement invalide ;
|
||||
- opération de sauvegarde/modification impossible.
|
||||
|
||||
La nomenclature exacte des ErrorCode est décidée dans le plan puis ajoutée seulement quand chaque erreur devient nécessaire.
|
||||
|
||||
## 11. Mutation et persistence
|
||||
|
||||
La configuration n'est pas uniquement un lecteur statique : le périmètre candidat de `0.1.3` comprend les modifications/sauvegardes explicitement autorisées.
|
||||
|
||||
Le `pre.001` doit décider :
|
||||
|
||||
- quels documents peuvent être modifiés par API ;
|
||||
- quelles valeurs sont read-only ;
|
||||
- comment préserver format/version/schema ;
|
||||
- comment éviter un fichier partiellement écrit ;
|
||||
- comment représenter une modification rejetée ;
|
||||
- comment distinguer configuration souhaitée et configuration effective lorsqu'un composant runtime ne peut pas appliquer immédiatement une valeur.
|
||||
|
||||
Si cette surface rend la release trop large, elle doit être scindée plutôt que comprimée artificiellement.
|
||||
|
||||
## 12. Frontière application/Tauri
|
||||
|
||||
`0.1.3` reste une release de bibliothèque Config.
|
||||
|
||||
La validation desktop complète est prévue ensuite, par défaut dans :
|
||||
|
||||
```text
|
||||
0.1.4 — ksp-app-config-desk
|
||||
```
|
||||
|
||||
Les DTO/bindings Tauri n'appartiennent donc pas automatiquement à `ksp-config-lib`. TS-RS reste principalement une frontière des applications Tauri et ne doit être dérivé dans une crate générique que pour un contrat externe réellement générique et indépendant de Tauri. Une application Config desktop pourra toutefois exposer, par des commandes/DTO applicatifs dédiés, les opérations privilégiées de consultation/modification de secrets que `ksp-config-lib` autorise explicitement ; elle ne doit jamais contourner Config en lisant les fichiers ou l'environnement directement.
|
||||
|
||||
## 13. Règles Rust et Cargo à conserver
|
||||
|
||||
Conserver les règles normatives du dépôt, notamment :
|
||||
|
||||
- Rust 2024 ;
|
||||
- `unsafe` interdit ;
|
||||
- pas de `unwrap`, `expect`, `panic` dans le code production ;
|
||||
- pas d'opérateur `?` ;
|
||||
- retours explicites selon les règles Clippy du workspace ;
|
||||
- imports de traits seulement lorsque nécessaire, chemins pleinement qualifiés sinon ;
|
||||
- pas de `mod.rs` ;
|
||||
- pas de `pub(super)` / `pub(in ...)` ;
|
||||
- code et Rustdoc en anglais ;
|
||||
- documentation Markdown en français ;
|
||||
- tests unitaires hors `src` selon la convention du dépôt ;
|
||||
- dépendances externes communes déclarées uniquement sous `[workspace.dependencies]` puis consommées avec `.workspace = true` ;
|
||||
- versions externes sous contraintes caret de génération compatibles, après vérification de la version actuelle au moment de l'ajout ;
|
||||
- ne pas versionner `Cargo.lock`.
|
||||
|
||||
## 14. Logging dans Config
|
||||
|
||||
`ksp-config-lib` contient du comportement runtime et doit normalement dépendre de `ksp-logging-lib` pour ses propres événements utiles.
|
||||
|
||||
Elle ne doit jamais importer directement :
|
||||
|
||||
```text
|
||||
tracing
|
||||
tracing-subscriber
|
||||
tracing-appender
|
||||
```
|
||||
|
||||
Ses targets KSP explicites utilisent le nom Cargo :
|
||||
|
||||
```text
|
||||
ksp-config-lib
|
||||
```
|
||||
|
||||
Les détails d'une bibliothèque tierce utilisés par Config sont silencieux par défaut ; Config réémet sous son propre target les informations réellement utiles au diagnostic KSP.
|
||||
|
||||
## 15. Hors scope initial
|
||||
|
||||
Sauf décision explicite du `pre.001`, ne pas ouvrir dans `0.1.3` :
|
||||
|
||||
- application desktop Config ;
|
||||
- Tauri comme dépendance de `ksp-config-lib` ;
|
||||
- Wallet ;
|
||||
- Store/PostgreSQL ;
|
||||
- RPC/WS/provider ;
|
||||
- Program/decoder/execution ;
|
||||
- workers/jobs/pipelines ;
|
||||
- trading/ML ;
|
||||
- watcher générique de tous les fichiers du projet ;
|
||||
- service distribué de configuration ;
|
||||
- secrets manager distant ;
|
||||
- configuration spécifique à des composants qui n'existent pas encore.
|
||||
|
||||
## 16. Git, versions et deltas
|
||||
|
||||
Chaque livraison est un delta commité :
|
||||
|
||||
```text
|
||||
pre.NNN
|
||||
pre.NNN-fix.NNN
|
||||
rel.NNN
|
||||
```
|
||||
|
||||
Cargo utilise les identifiants SemVer sans zéros de tête :
|
||||
|
||||
```text
|
||||
0.1.3-pre.1
|
||||
0.1.3-pre.1.fix.1
|
||||
```
|
||||
|
||||
Les noms de deltas/documents peuvent conserver la représentation `pre.001` / `fix.001`.
|
||||
|
||||
Une correction ultérieure ne réécrit pas un delta déjà livré.
|
||||
|
||||
Seul le commit final stable reçoit :
|
||||
|
||||
```text
|
||||
v0.1.3
|
||||
```
|
||||
|
||||
## 17. Première action de la session
|
||||
|
||||
Commencer par `0.1.3-pre.001` : audit, brainstorming et plan de travail.
|
||||
|
||||
Ne pas traiter ce `pre.001` comme un simple audit passif. Il doit produire les décisions nécessaires au développement des tranches suivantes et une matrice claire des responsabilités Config.
|
||||
|
||||
## 18. Dernière prerelease
|
||||
|
||||
La dernière prerelease de `0.1.3` devra :
|
||||
|
||||
- exécuter les validations finales ;
|
||||
- consolider la documentation durable ;
|
||||
- nettoyer/archiver uniquement ce que les règles exigent ;
|
||||
- préparer le prompt de la release suivante ;
|
||||
- vérifier le graphe de dépendances/features ;
|
||||
- fermer les TODO de release ou les reporter explicitement ;
|
||||
- préparer la livraison `rel.001` et le tag stable après validation utilisateur.
|
||||
|
||||
## 19. Validations minimales attendues
|
||||
|
||||
Lorsque les commandes sont applicables :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-config-lib
|
||||
cargo tree -p ksp-config-lib -d
|
||||
cargo tree -p ksp-config-lib -e features
|
||||
```
|
||||
|
||||
Exécuter également tout script d'audit réellement présent dans le dépôt au moment de la validation.
|
||||
|
||||
Aucune commande non exécutée ne doit être déclarée réussie.
|
||||
|
||||
## 20. Critère de succès du `pre.001`
|
||||
|
||||
Le `pre.001` est terminé lorsque nous savons précisément :
|
||||
|
||||
- quels documents existent dans la première surface Config ;
|
||||
- ce qui est global et ce qui est profilé ;
|
||||
- comment fonctionne `default_profile` ;
|
||||
- comment se résolvent les overrides `KSP_*`/`KSPB_*` ;
|
||||
- comment documents unitaires et fichiers composites sont séparés puis résolus ;
|
||||
- comment les secrets/public/debug sont classifiés et quels contrats autorisent leur lecture/mutation ;
|
||||
- quels contrats de lecture/résolution/validation/mutation sont publics ou privilégiés, et comment `ksp-config-lib` reste l'unique manager des fichiers Config et variables d'environnement ;
|
||||
- comment Config construit `LoggingSettings` sans dépendance inverse ;
|
||||
- quelles dépendances externes sont réellement nécessaires ;
|
||||
- si `0.1.3` reste une seule release ou doit être scindée ;
|
||||
- quel est le découpage des prereleases de développement.
|
||||
Reference in New Issue
Block a user