From a9405ec7ffc238ab1fee0f2fc15089ffe372a0b3 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 16 Aug 2026 05:01:33 +0200 Subject: [PATCH] v0.1.3-pre.012 --- Cargo.toml | 4 +- crates/ksp-config-lib/src/environment.rs | 4 +- crates/ksp-config-lib/src/error.rs | 5 +- crates/ksp-config-lib/src/lib.rs | 13 +- crates/ksp-config-lib/src/logging.rs | 437 ++++++++++++++++++ crates/ksp-config-lib/tests/public_api.rs | 14 +- crates/ksp-config-lib/unit_tests/logging.rs | 329 +++++++++++++ deltas/0.1.3/pre.012.md | 315 +++++++++++++ docs/plans/000-README.md | 4 +- docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md | 4 +- .../005-V0_1_3_CONFIG_FOUNDATION_PLAN.md | 28 +- docs/rules/FILE_CONTRACTS.md | 4 +- docs/rules/RULES_KSP.md | 4 +- 13 files changed, 1139 insertions(+), 26 deletions(-) create mode 100644 crates/ksp-config-lib/src/logging.rs create mode 100644 crates/ksp-config-lib/unit_tests/logging.rs create mode 100644 deltas/0.1.3/pre.012.md diff --git a/Cargo.toml b/Cargo.toml index 2d29914..2e30394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 55 +# version: 56 [workspace] resolver = "3" members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.3-pre.11.fix.1" +version = "0.1.3-pre.12" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-config-lib/src/environment.rs b/crates/ksp-config-lib/src/environment.rs index 2ee6904..1476ce7 100644 --- a/crates/ksp-config-lib/src/environment.rs +++ b/crates/ksp-config-lib/src/environment.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/environment.rs -// version: 2 +// version: 3 /// Default local environment file read by Config from the process launch directory. pub const DEFAULT_DOTENV_PATH: &str = ".env"; @@ -246,7 +246,7 @@ impl ConfigEnvironment { } #[cfg(test)] - fn from_maps(process: std::collections::BTreeMap, dotenv: std::collections::BTreeMap) -> Self { + pub(crate) fn from_maps(process: std::collections::BTreeMap, dotenv: std::collections::BTreeMap) -> Self { return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) }; } } diff --git a/crates/ksp-config-lib/src/error.rs b/crates/ksp-config-lib/src/error.rs index 310d5ed..de5e455 100644 --- a/crates/ksp-config-lib/src/error.rs +++ b/crates/ksp-config-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/error.rs -// version: 6 +// version: 7 /// Error code used when a Config bootstrap argument is missing its value. pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value"); @@ -57,3 +57,6 @@ pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_co /// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed. pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid"); + +/// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract. +pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid"); diff --git a/crates/ksp-config-lib/src/lib.rs b/crates/ksp-config-lib/src/lib.rs index 1f76447..b588fa0 100644 --- a/crates/ksp-config-lib/src/lib.rs +++ b/crates/ksp-config-lib/src/lib.rs @@ -1,21 +1,22 @@ // file: crates/ksp-config-lib/src/lib.rs -// version: 7 +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] //! KSP-owned application configuration facade. //! -//! `0.1.3-pre.011` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite +//! `0.1.3-pre.012` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite //! resolution and KSP/KSPB environment resolution through process + `.env` + fallback precedence. The standard Logging document remains the first registered -//! runtime document. Environment-derived values now preserve real/safe representations, sensitivity and provenance; persistence remains in a later bounded -//! prerelease. +//! runtime document. Environment-derived values preserve real/safe representations, sensitivity and provenance; the standard Logging profile can now be +//! mapped explicitly to `ksp_logging_lib::LoggingSettings`. Persistence remains in a later bounded prerelease. mod bootstrap; mod composite; mod document; mod environment; mod error; +mod logging; mod profile; mod registry; mod sensitivity; @@ -60,6 +61,8 @@ pub use self::error::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID; pub use self::error::ERROR_CODE_DOTENV_FILE_READ_FAILED; /// Error code used when the local `.env` file contains invalid syntax. pub use self::error::ERROR_CODE_DOTENV_SYNTAX_INVALID; +/// Error code used when an environment-resolved Config cannot map safely to a runtime consumer contract. +pub use self::error::ERROR_CODE_EFFECTIVE_CONFIG_INVALID; /// Error code used when a Config environment placeholder is malformed. pub use self::error::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID; /// Error code used when a supported Config environment variable has a non-UTF-8 process value. @@ -86,6 +89,8 @@ pub use self::error::ERROR_CODE_PROFILE_NOT_FOUND; pub use self::error::ERROR_CODE_SCHEMA_INVALID; /// Error code used when a Config document fails its registered JSON Schema validation. pub use self::error::ERROR_CODE_SCHEMA_VALIDATION_FAILED; +/// Effective standard Logging configuration mapped to `ksp_logging_lib::LoggingSettings`. +pub use self::logging::ResolvedLoggingConfig; /// Source that selected an effective standard Config profile. pub use self::profile::ConfigProfileSelectionSource; /// Origin of one top-level value in a resolved standard Config profile. diff --git a/crates/ksp-config-lib/src/logging.rs b/crates/ksp-config-lib/src/logging.rs new file mode 100644 index 0000000..d5fe22d --- /dev/null +++ b/crates/ksp-config-lib/src/logging.rs @@ -0,0 +1,437 @@ +// file: crates/ksp-config-lib/src/logging.rs +// version: 1 + +/// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract. +#[derive(Clone, Eq, PartialEq)] +pub struct ResolvedLoggingConfig { + file_id: crate::ConfigFileId, + source_path: std::path::PathBuf, + profile_id: String, + selection_source: crate::ConfigProfileSelectionSource, + effective: crate::ResolvedConfigJson, + logs_directory: std::path::PathBuf, + settings: ksp_logging_lib::LoggingSettings, +} + +impl ResolvedLoggingConfig { + /// Returns the logical Config document identifier used by this runtime configuration. + #[must_use] + pub const fn file_id(&self) -> &crate::ConfigFileId { + return &self.file_id; + } + + /// Returns the physical source Config document path. + #[must_use] + pub fn source_path(&self) -> &std::path::Path { + return self.source_path.as_path(); + } + + /// Returns the selected standard Logging profile identifier. + #[must_use] + pub fn profile_id(&self) -> &str { + return self.profile_id.as_str(); + } + + /// Returns the source that selected the standard Logging profile. + #[must_use] + pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { + return self.selection_source; + } + + /// Returns the detailed environment-resolved effective Config view. + /// + /// The real tree is available to legitimate runtime consumers and the safe tree is suitable for ordinary diagnostics. + #[must_use] + pub const fn effective(&self) -> &crate::ResolvedConfigJson { + return &self.effective; + } + + /// Returns the validated real Logging root directory. + /// + /// Relative Config values are anchored to the process current working directory when this adapter runs. Absolute Config values are preserved. + #[must_use] + pub fn logs_directory(&self) -> &std::path::Path { + return self.logs_directory.as_path(); + } + + /// Returns the mapped runtime Logging settings. + #[must_use] + pub const fn settings(&self) -> &ksp_logging_lib::LoggingSettings { + return &self.settings; + } + + /// Consumes this resolved Config and returns the mapped runtime Logging settings. + #[must_use] + pub fn into_settings(self) -> ksp_logging_lib::LoggingSettings { + return self.settings; + } +} + +impl std::fmt::Debug for ResolvedLoggingConfig { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("ResolvedLoggingConfig") + .field("file_id", &self.file_id) + .field("source_path", &self.source_path) + .field("profile_id", &self.profile_id) + .field("selection_source", &self.selection_source) + .field("effective", &self.effective) + .finish_non_exhaustive(); + } +} + +impl crate::ConfigDocumentEngine { + /// Loads the standard Logging document, selects a profile, resolves environment placeholders and maps the effective result to `LoggingSettings`. + /// + /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Source JSON validation remains distinct + /// from effective runtime validation: an environment value that resolves to an invalid Logging setting returns + /// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] and does not silently fall back to the placeholder fallback. + pub fn load_resolved_logging_config( + &self, + requested_profile: std::option::Option<&str>, + environment: &crate::ConfigEnvironment, + ) -> ksp_core_lib::Result { + let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); + let file_id = match file_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let profile = self.load_resolved_profile(&file_id, requested_profile); + let profile = match profile { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return resolve_logging_profile(&profile, environment); + } +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveLoggingSource { + format_version: u32, + logs_directory: String, + profile_id: String, + default_filter: String, + span_events: String, + console: EffectiveConsoleSource, + files: std::vec::Vec, + target_filters: std::vec::Vec, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveConsoleSource { + enabled: bool, + output: String, + ansi: bool, + format: String, + filter: EffectiveOutputFilterSource, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveFileSource { + output_id: String, + enabled: bool, + path: String, + rotation: String, + format: String, + ansi: bool, + filter: EffectiveOutputFilterSource, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveOutputFilterSource { + level: String, + targets: std::vec::Vec, + domains: std::vec::Vec, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveTargetFilterSource { + target_prefix: String, + level: String, +} + +fn resolve_logging_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { + let effective = profile.resolve_effective_environment_detailed(environment); + let effective = match effective { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let sensitivity_validation = validate_logging_sensitivity(profile, &effective); + if let std::result::Result::Err(error) = sensitivity_validation { + return std::result::Result::Err(error); + } + let source = serde_json::from_value::(effective.value().clone()); + let source = match source { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + effective_error(profile, "effective Logging Config cannot be decoded into the runtime adapter contract").with_source(error), + ); + }, + }; + if source.format_version != 1 { + return std::result::Result::Err(effective_error(profile, "effective Logging format_version is unsupported")); + } + if source.profile_id != profile.profile_id() { + return std::result::Result::Err(effective_error(profile, "effective Logging profile_id differs from the selected source profile")); + } + let safe_logs_directory = safe_string_at(effective.safe_value(), "/logs_directory"); + let logs_directory = resolve_logs_directory(source.logs_directory.as_str(), safe_logs_directory.as_str(), profile); + let logs_directory = match logs_directory { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let default_filter = map_level(source.default_filter.as_str(), "default_filter", profile); + let default_filter = match default_filter { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let span_events = map_span_events(source.span_events.as_str(), profile); + let span_events = match span_events { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let console = map_console(source.console, profile); + let console = match console { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let files = map_files(source.files, logs_directory.as_path(), profile); + let files = match files { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut settings = ksp_logging_lib::LoggingSettings::new(default_filter, span_events, std::option::Option::Some(console), files); + for target_filter in source.target_filters { + let level = map_level(target_filter.level.as_str(), "target_filters.level", profile); + let level = match level { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + settings = settings.with_target_filter(ksp_logging_lib::TargetFilter::new(target_filter.target_prefix, level)); + } + let validation = settings.validate(); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err( + effective_error(profile, "effective Logging settings fail the Logging runtime contract") + .with_context("logging_error_domain", error.code().domain()) + .with_context("logging_error_code", error.code().code()), + ); + } + return std::result::Result::Ok(ResolvedLoggingConfig { + file_id: profile.file_id().clone(), + source_path: profile.path().to_path_buf(), + profile_id: profile.profile_id().to_owned(), + selection_source: profile.selection_source(), + effective, + logs_directory, + settings, + }); +} + +fn validate_logging_sensitivity(profile: &crate::ResolvedConfigProfile, effective: &crate::ResolvedConfigJson) -> ksp_core_lib::Result<()> { + if effective.sensitivity().is_secret() { + return std::result::Result::Err(effective_error(profile, "standard Logging configuration must not consume Secret environment values")); + } + return std::result::Result::Ok(()); +} + +fn map_console(source: EffectiveConsoleSource, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + let output = match source.output.as_str() { + "stdout" => ksp_logging_lib::ConsoleOutput::Stdout, + "stderr" => ksp_logging_lib::ConsoleOutput::Stderr, + _ => return std::result::Result::Err(effective_field_error(profile, "console.output", "effective Logging console output is unsupported")), + }; + let format = map_format(source.format.as_str(), "console.format", profile); + let format = match format { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let filter = map_output_filter(source.filter, "console.filter", profile); + let filter = match filter { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(ksp_logging_lib::ConsoleSettings::new(source.enabled, output, source.ansi, format, filter)); +} + +fn map_files( + sources: std::vec::Vec, + logs_directory: &std::path::Path, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + let mut files = std::vec::Vec::::with_capacity(sources.len()); + for source in sources { + if !relative_file_path_is_valid(source.path.as_str()) { + return std::result::Result::Err( + effective_field_error(profile, "files.path", "effective Logging file path must stay relative to logs_directory without traversal") + .with_context("output_id", source.output_id.as_str()), + ); + } + let file_path = std::path::Path::new(source.path.as_str()); + let file_name = match file_path.file_name().and_then(std::ffi::OsStr::to_str) { + std::option::Option::Some(value) if !value.is_empty() => value.to_owned(), + _ => return std::result::Result::Err(effective_field_error(profile, "files.path", "effective Logging file path has no UTF-8 file name")), + }; + let relative_directory = match file_path.parent() { + std::option::Option::Some(value) => value, + std::option::Option::None => std::path::Path::new(""), + }; + let directory = logs_directory.join(relative_directory); + let rotation = map_rotation(source.rotation.as_str(), profile); + let rotation = match rotation { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let format = map_format(source.format.as_str(), "files.format", profile); + let format = match format { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let filter = map_output_filter(source.filter, "files.filter", profile); + let filter = match filter { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let file = ksp_logging_lib::FileSettings::new(source.output_id, source.enabled, directory, file_name, rotation, format, filter).with_ansi(source.ansi); + files.push(file); + } + return std::result::Result::Ok(files); +} + +fn map_output_filter( + source: EffectiveOutputFilterSource, + field: &'static str, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result { + let level = map_level(source.level.as_str(), field, profile); + let level = match level { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(ksp_logging_lib::OutputFilter::new(level, source.targets, source.domains)); +} + +fn map_level(value: &str, field: &'static str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + return match value { + "off" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Off), + "error" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Error), + "warn" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Warn), + "info" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Info), + "debug" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Debug), + "trace" => std::result::Result::Ok(ksp_logging_lib::LogFilterLevel::Trace), + _ => std::result::Result::Err(effective_field_error(profile, field, "effective Logging level is unsupported")), + }; +} + +fn map_span_events(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + return match value { + "off" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::Off), + "new_and_close" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::NewAndClose), + "full" => std::result::Result::Ok(ksp_logging_lib::SpanEvents::Full), + _ => std::result::Result::Err(effective_field_error(profile, "span_events", "effective Logging span_events value is unsupported")), + }; +} + +fn map_format(value: &str, field: &'static str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + return match value { + "human" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Human), + "compact" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Compact), + "pretty" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Pretty), + "json" => std::result::Result::Ok(ksp_logging_lib::LogFormat::Json), + _ => std::result::Result::Err(effective_field_error(profile, field, "effective Logging format is unsupported")), + }; +} + +fn map_rotation(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + return match value { + "never" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Never), + "hourly" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Hourly), + "daily" => std::result::Result::Ok(ksp_logging_lib::FileRotation::Daily), + _ => std::result::Result::Err(effective_field_error(profile, "files.rotation", "effective Logging rotation is unsupported")), + }; +} + +fn resolve_logs_directory(value: &str, safe_value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { + if value.trim().is_empty() { + return std::result::Result::Err( + effective_field_error(profile, "logs_directory", "effective Logging logs_directory must not be empty").with_context("safe_value", safe_value), + ); + } + let configured = std::path::PathBuf::from(value); + let resolved = if configured.is_absolute() { + configured + } else { + let current_directory = std::env::current_dir(); + let current_directory = match current_directory { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + effective_field_error(profile, "logs_directory", "process current working directory cannot be resolved").with_source(error), + ); + }, + }; + current_directory.join(configured) + }; + let metadata = std::fs::metadata(resolved.as_path()); + match metadata { + std::result::Result::Ok(value) if !value.is_dir() => { + return std::result::Result::Err( + effective_field_error(profile, "logs_directory", "effective Logging logs_directory resolves to an existing non-directory path") + .with_context("safe_value", safe_value), + ); + }, + std::result::Result::Ok(_) => {}, + std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}, + std::result::Result::Err(error) => { + return std::result::Result::Err( + effective_field_error(profile, "logs_directory", "effective Logging logs_directory cannot be inspected") + .with_context("safe_value", safe_value) + .with_source(error), + ); + }, + } + return std::result::Result::Ok(resolved); +} + +fn relative_file_path_is_valid(value: &str) -> bool { + let path = std::path::Path::new(value); + if path.is_absolute() { + return false; + } + let mut has_normal_component = false; + for component in path.components() { + match component { + std::path::Component::Normal(_) => has_normal_component = true, + std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => return false, + } + } + return has_normal_component; +} + +fn safe_string_at(value: &serde_json::Value, pointer: &str) -> String { + return match value.pointer(pointer).and_then(serde_json::Value::as_str) { + std::option::Option::Some(value) => value.to_owned(), + std::option::Option::None => "".to_owned(), + }; +} + +fn effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error { + return effective_error(profile, reason).with_context("field", field); +} + +fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract") + .with_context("file_id", profile.file_id().as_str()) + .with_context("profile_id", profile.profile_id()) + .with_context("reason", reason); +} + +#[cfg(test)] +#[path = "../unit_tests/logging.rs"] +mod tests; diff --git a/crates/ksp-config-lib/tests/public_api.rs b/crates/ksp-config-lib/tests/public_api.rs index fabc2e5..fcdf020 100644 --- a/crates/ksp-config-lib/tests/public_api.rs +++ b/crates/ksp-config-lib/tests/public_api.rs @@ -1,7 +1,8 @@ // file: crates/ksp-config-lib/tests/public_api.rs -// version: 8 +// version: 9 -//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution and sensitivity contracts. +//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity and +//! Logging-adapter contracts. #[test] fn bootstrap_contract_is_available_from_crate_root() { @@ -168,3 +169,12 @@ fn sensitivity_and_safe_resolution_contracts_are_available_from_crate_root() { assert_eq!(provenance.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN")); assert_eq!(provenance.environment_source(), std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Fallback)); } + +#[test] +fn logging_adapter_contract_is_available_from_crate_root() { + let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_logging_config; + let _ = adapter; + assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.domain(), "config"); + assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.code(), "effective_config_invalid"); + assert!(std::mem::size_of::() > 0); +} diff --git a/crates/ksp-config-lib/unit_tests/logging.rs b/crates/ksp-config-lib/unit_tests/logging.rs new file mode 100644 index 0000000..d25ac5c --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/logging.rs @@ -0,0 +1,329 @@ +// file: crates/ksp-config-lib/unit_tests/logging.rs +// version: 1 + +#[test] +fn committed_logging_profile_maps_complete_runtime_contract() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "committed Logging Config should map"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_LOGGING); + assert_eq!(resolved.profile_id(), "local_dev"); + assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile); + assert_eq!(resolved.logs_directory(), current_directory().join("logs").as_path()); + assert_eq!(resolved.effective().value()["logs_directory"], serde_json::Value::String("logs".to_owned())); + let settings = resolved.settings(); + assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Warn); + assert_eq!(settings.span_events(), ksp_logging_lib::SpanEvents::NewAndClose); + assert_eq!(settings.target_filters().len(), 2); + assert_eq!(settings.target_filters()[0].target_prefix(), "ksp-config-lib"); + assert_eq!(settings.target_filters()[0].level(), ksp_logging_lib::LogFilterLevel::Trace); + assert_eq!(settings.target_filters()[1].target_prefix(), "ksp-logging-lib"); + assert_eq!(settings.target_filters()[1].level(), ksp_logging_lib::LogFilterLevel::Debug); + let console = settings.console(); + assert!(console.is_some(), "committed Logging Config declares console settings"); + let console = match console { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + assert!(console.enabled()); + assert_eq!(console.output(), ksp_logging_lib::ConsoleOutput::Stderr); + assert!(console.ansi()); + assert_eq!(console.format(), ksp_logging_lib::LogFormat::Compact); + assert_eq!(console.filter().level(), ksp_logging_lib::LogFilterLevel::Debug); + assert_eq!(console.filter().targets(), &["*".to_owned()]); + assert_eq!(console.filter().domains(), &["*".to_owned()]); + assert_eq!(settings.files().len(), 2); + assert_file( + &settings.files()[0], + "file.all.debug", + current_directory().join("logs/debug").as_path(), + "ksp-debug.log", + ksp_logging_lib::FileRotation::Daily, + ksp_logging_lib::LogFormat::Human, + ksp_logging_lib::LogFilterLevel::Debug, + &["*"], + &["*"], + ); + assert_file( + &settings.files()[1], + "file.config.error", + current_directory().join("logs/config").as_path(), + "ksp-config-errors.jsonl", + ksp_logging_lib::FileRotation::Daily, + ksp_logging_lib::LogFormat::Json, + ksp_logging_lib::LogFilterLevel::Error, + &["ksp-config-lib"], + &["config"], + ); +} + +#[test] +fn relative_logs_directory_is_anchored_to_process_current_directory() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = environment_with_logs_directory("relative-ksp-logs"); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "relative Logging root should map"); + if let std::result::Result::Ok(resolved) = resolved { + assert_eq!(resolved.logs_directory(), current_directory().join("relative-ksp-logs").as_path()); + assert_eq!(resolved.settings().files()[0].directory(), current_directory().join("relative-ksp-logs/debug").as_path()); + } +} + +#[test] +fn absolute_logs_directory_is_preserved() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let absolute = std::env::temp_dir().join(format!("ksp-pre012-absolute-{}", std::process::id())); + let environment = environment_with_logs_directory(absolute.to_string_lossy().as_ref()); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "absolute Logging root should map even when it does not exist yet"); + if let std::result::Result::Ok(resolved) = resolved { + assert_eq!(resolved.logs_directory(), absolute.as_path()); + } +} + +#[test] +fn effective_file_paths_cannot_escape_logging_root() { + assert!(super::relative_file_path_is_valid("debug/ksp.log")); + assert!(super::relative_file_path_is_valid("ksp.log")); + assert!(!super::relative_file_path_is_valid("../ksp.log")); + assert!(!super::relative_file_path_is_valid("./ksp.log")); + assert!(!super::relative_file_path_is_valid("/var/log/ksp.log")); +} + +#[test] +fn explicit_empty_logs_directory_is_invalid_instead_of_using_fallback() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = environment_with_logs_directory(""); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + let error = match resolved { + std::result::Result::Ok(_) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); + assert!(error.context().iter().any(|item| -> bool { + return item.key() == "field" && item.value() == "logs_directory"; + })); +} + +#[test] +fn existing_non_directory_logging_root_is_rejected_without_secret_leak() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let secret = format!("ksp-secret-path-canary-{}", std::process::id()); + let path = std::env::temp_dir().join(secret.as_str()); + let write = std::fs::write(path.as_path(), b"not a directory"); + assert!(write.is_ok(), "secret canary file should be created"); + let profile = load_committed_profile(&engine); + let profile = match profile { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + cleanup_file(path.as_path()); + return; + }, + }; + let resolved = super::resolve_logs_directory(path.to_string_lossy().as_ref(), crate::REDACTED_CONFIG_VALUE, &profile); + cleanup_file(path.as_path()); + let error = match resolved { + std::result::Result::Ok(_) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); + let debug = format!("{error:?}"); + assert!(!debug.contains(secret.as_str()), "effective Config diagnostics must not reveal real secret-derived paths"); + assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); +} + +#[test] +fn logging_adapter_rejects_secret_effective_values_without_exposing_canary() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let profile = load_committed_profile(&engine); + let profile = match profile { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let canary = "ksp-pre012-secret-canary-c3e4"; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_LOGGING_CANARY".to_owned(), canary.to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); + let effective = environment.resolve_json_detailed(&serde_json::json!({"canary": "${KSP_SECRET_LOGGING_CANARY}"})); + assert!(effective.is_ok(), "secret canary fixture should resolve"); + let effective = match effective { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let validation = super::validate_logging_sensitivity(&profile, &effective); + let error = match validation { + std::result::Result::Ok(()) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); + let debug = format!("{error:?}"); + assert!(!debug.contains(canary), "Logging adapter diagnostics must not reveal secret canaries"); +} + +#[test] +fn mapped_logging_settings_can_initialize_and_reinitialize_runtime() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let root = std::env::temp_dir().join(format!("ksp-pre012-runtime-{}", std::process::id())); + cleanup_directory(root.as_path()); + let environment = environment_with_logs_directory(root.to_string_lossy().as_ref()); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "runtime Logging Config should map"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let guard = ksp_logging_lib::initialize(resolved.settings()); + assert!(guard.is_ok(), "mapped Logging settings should initialize the Logging runtime"); + let mut guard = match guard { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(root.join("debug").is_dir(), "Logging initialization should create the first configured file directory"); + assert!(root.join("config").is_dir(), "Logging initialization should create the second configured file directory"); + let reload = ksp_logging_lib::reinitialize(&mut guard, resolved.settings()); + assert!(reload.is_ok(), "mapped Logging settings should support hot reload"); + let disabled = ksp_logging_lib::LoggingSettings::new( + ksp_logging_lib::LogFilterLevel::Off, + ksp_logging_lib::SpanEvents::Off, + std::option::Option::None, + std::vec::Vec::new(), + ); + let disable = ksp_logging_lib::reinitialize(&mut guard, &disabled); + assert!(disable.is_ok(), "test Logging runtime should disable outputs before cleanup"); + drop(guard); + cleanup_directory(root.as_path()); +} + +#[test] +fn resolved_logging_debug_uses_safe_effective_view() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let root = std::env::temp_dir().join(format!("ksp-pre012-debug-{}", std::process::id())); + let environment = environment_with_logs_directory(root.to_string_lossy().as_ref()); + let resolved = engine.load_resolved_logging_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "Logging Config should map for Debug contract"); + if let std::result::Result::Ok(resolved) = resolved { + let debug = format!("{resolved:?}"); + assert!(debug.contains("ResolvedLoggingConfig")); + assert!(debug.contains("effective")); + } +} + +fn assert_file( + file: &ksp_logging_lib::FileSettings, + output_id: &str, + directory: &std::path::Path, + file_name: &str, + rotation: ksp_logging_lib::FileRotation, + format: ksp_logging_lib::LogFormat, + level: ksp_logging_lib::LogFilterLevel, + targets: &[&str], + domains: &[&str], +) { + assert_eq!(file.output_id(), output_id); + assert!(file.enabled()); + assert_eq!(file.directory(), directory); + assert_eq!(file.file_name_prefix(), file_name); + assert_eq!(file.rotation(), rotation); + assert_eq!(file.format(), format); + assert!(!file.ansi()); + assert_eq!(file.filter().level(), level); + assert_eq!(file.filter().targets().iter().map(String::as_str).collect::>(), targets.to_vec()); + assert_eq!(file.filter().domains().iter().map(String::as_str).collect::>(), domains.to_vec()); +} + +fn committed_engine() -> ksp_core_lib::Result { + let workspace = workspace_root(); + let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); + let bootstrap = match bootstrap { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let registry = crate::ConfigFileRegistry::defaults(); + let registry = match registry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); +} + +fn load_committed_profile(engine: &crate::ConfigDocumentEngine) -> ksp_core_lib::Result { + let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); + let file_id = match file_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return engine.load_resolved_profile(&file_id, std::option::Option::None); +} + +fn environment_with_logs_directory(value: &str) -> crate::ConfigEnvironment { + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_LOGS_DIRECTORY".to_owned(), value.to_owned()); + return crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); +} + +fn workspace_root() -> std::path::PathBuf { + return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); +} + +fn current_directory() -> std::path::PathBuf { + let current = std::env::current_dir(); + return match current { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => std::path::PathBuf::new(), + }; +} + +fn cleanup_file(path: &std::path::Path) { + let removal = std::fs::remove_file(path); + if let std::result::Result::Err(error) = removal + && error.kind() != std::io::ErrorKind::NotFound + { + eprintln!("unable to cleanup Config Logging adapter file {}: {error}", path.display()); + } +} + +fn cleanup_directory(path: &std::path::Path) { + let removal = std::fs::remove_dir_all(path); + if let std::result::Result::Err(error) = removal + && error.kind() != std::io::ErrorKind::NotFound + { + eprintln!("unable to cleanup Config Logging adapter directory {}: {error}", path.display()); + } +} diff --git a/deltas/0.1.3/pre.012.md b/deltas/0.1.3/pre.012.md new file mode 100644 index 0000000..f26b8f3 --- /dev/null +++ b/deltas/0.1.3/pre.012.md @@ -0,0 +1,315 @@ + + + +# Delta 0.1.3-pre.012 + +## Base requise + +Livraison précédente validée : + +```text +0.1.3-pre.011-fix.001 +``` + +Version technique de cette base : + +```text +workspace.package.version = "0.1.3-pre.11.fix.1" +Cargo.toml header version = 55 +``` + +Validations utilisateur exécutées le 2026-08-16 : + +```text +cargo fmt --all OK +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test --workspace OK +cargo tree -p ksp-config-lib OK +cargo tree -p ksp-config-lib -d OK, doublon transitif syn 2/3 déjà connu +cargo tree -p ksp-logging-lib -d OK, aucun doublon +``` + +`cargo test --workspace` confirme notamment 61 tests unitaires + 9 tests publics pour `ksp-config-lib`. + +## Objet de pre.012 + +Construire la première frontière runtime complète possédée par Config : + +```text +cfg.std.logging + -> JSON + schema + invariants source + -> profil sélectionné + -> process > .env > fallback + -> real/safe/sensitivity/provenance + -> validation effective + -> ksp_logging_lib::LoggingSettings +``` + +Cette tranche ne modifie pas encore les documents Config et ne persiste rien. + +## `ResolvedLoggingConfig` + +Nouveau contrat public : + +```text +ResolvedLoggingConfig +``` + +Il conserve : + +```text +file_id +source_path +profile_id +selection_source +effective ResolvedConfigJson +logs_directory résolu +LoggingSettings +``` + +Son `Debug` manuel n'affiche pas directement `LoggingSettings` ni le root réel. Il s'appuie sur `ResolvedConfigJson::Debug`, donc sur l'arbre sûr/redacted. + +Les consumers légitimes peuvent obtenir : + +```text +settings() +into_settings() +logs_directory() +effective() +``` + +Le `LoggingGuard` n'est jamais stocké par Config : il reste détenu par l'orchestration qui appelle `ksp_logging_lib::initialize/reinitialize`. + +## Entrée de l'adapter + +`ConfigDocumentEngine` ajoute : + +```text +load_resolved_logging_config(requested_profile, environment) +``` + +La méthode : + +1. charge `cfg.std.logging` ; +2. applique le profil par défaut ou le profil explicite ; +3. résout les placeholders avec le snapshot `ConfigEnvironment` ; +4. conserve la vue réelle/sûre et la provenance ; +5. valide la configuration effective ; +6. mappe les valeurs vers les contrats publics `ksp_logging_lib::*` ; +7. exécute enfin `LoggingSettings::validate()`. + +## Mapping Logging + +Le mapping couvre explicitement : + +```text +LogFilterLevel +SpanEvents +ConsoleOutput +LogFormat +FileRotation +OutputFilter +TargetFilter +ConsoleSettings +FileSettings[] +LoggingSettings +``` + +Le document commité `local_dev` doit donc produire les deux sinks fichier déjà déclarés et leurs filtres `level/target/domain`, ainsi que les deux overrides globaux de target. + +## `logs_directory` + +Après interpolation : + +- un chemin absolu est conservé ; +- un chemin relatif est ancré sur le current working directory du processus au moment de l'adaptation ; +- un root inexistant est accepté : Logging créera les répertoires requis lors de l'initialisation des appenders ; +- un root existant qui n'est pas un directory est rejeté ; +- une erreur filesystem autre que `NotFound` pendant l'inspection est rejetée. + +Le fallback `${KSP_LOGS_DIRECTORY:-logs}` s'applique uniquement si `KSP_LOGS_DIRECTORY` est absent. + +Une valeur explicitement présente mais vide : + +```text +KSP_LOGS_DIRECTORY= +``` + +reste une valeur présente et produit : + +```text +config.effective_config_invalid +``` + +Elle ne retombe jamais silencieusement sur `logs`. + +## Chemins des sinks + +Les `files[].path` restent relatifs sous `logs_directory`. + +L'invariant est désormais contrôlé deux fois : + +1. sur le document source ; +2. après interpolation environnementale. + +La seconde validation empêche par exemple une valeur environnementale de transformer dynamiquement un path relatif en : + +```text +../outside.log +/var/log/outside.log +``` + +L'adapter sépare ensuite le path relatif en : + +```text +directory relatif +file-name prefix +``` + +puis construit `FileSettings` sous le root Logging résolu. + +## Frontière secrets + +Le document standard Logging n'a aucun besoin fonctionnel de secret. + +L'adapter refuse donc toute configuration effective dont `ResolvedConfigJson::sensitivity()` est `Secret`. + +Cette règle évite de transmettre une vraie valeur secrète à `LoggingSettings`, puis potentiellement à des diagnostics filesystem de `ksp-logging-lib`. + +Les erreurs finales de `LoggingSettings::validate()` sont encapsulées dans `config.effective_config_invalid` avec seulement : + +```text +logging_error_domain +logging_error_code +``` + +Les contextes internes de l'erreur Logging ne sont pas recopiés par Config. + +## Démonstration runtime + +Un test Config utilise le document commité, un root temporaire et le vrai runtime Logging afin de démontrer : + +```text +Config -> LoggingSettings -> initialize -> reinitialize +``` + +Le test vérifie aussi que l'initialisation des appenders crée les sous-répertoires configurés lorsqu'ils n'existent pas, puis recharge une configuration sans outputs avant cleanup. + +## Diagnostics et erreurs + +Nouveau code stable : + +```text +config.effective_config_invalid +``` + +Il distingue une source JSON/schema valide d'une configuration devenue invalide après environnement/adaptation runtime. + +Les diagnostics Config de paths utilisent une représentation sûre lorsque la valeur pourrait provenir d'un resolver détaillé. + +## Règles documentaires + +Deux règles KSP deviennent durables : + +```text +KSP-CONFIG-012 +KSP-CONFIG-013 +``` + +Elles fixent respectivement : + +- la sémantique absolu/relatif/CWD/fallback de `logs_directory` ; +- l'interdiction de secrets dans le standard Logging effectif. + +`FILE_CONTRACTS.md` enregistre également la revalidation post-interpolation de `files[].path`. + +## `.env.example` + +Aucune nouvelle variable runtime n'est introduite. + +`.env.example` reste inchangé : + +```text +KSP_LOGS_DIRECTORY=logs +``` + +## Dépendances + +Aucune nouvelle dépendance Cargo. + +La direction reste : + +```text +ksp-config-lib -> ksp-logging-lib +ksp-logging-lib -X-> ksp-config-lib +``` + +## Tests ajoutés + +Le nouveau module `unit_tests/logging.rs` couvre notamment : + +- mapping complet du profil `local_dev` ; +- root relatif ancré au CWD ; +- root absolu conservé ; +- valeur explicite vide rejetée sans fallback ; +- root existant non-directory rejeté ; +- paths fichier effectifs sans escape ; +- frontière Secret de Logging et canary sans fuite ; +- `Debug` de `ResolvedLoggingConfig` ; +- `initialize/reinitialize` réel et création des répertoires de sinks. + +La surface publique ajoute un test d'adressabilité de `ResolvedLoggingConfig`, de la méthode adapter et du nouveau code d'erreur. + +Après ajout, la crate contient 70 tests unitaires Config et 10 tests publics à exécuter chez l'utilisateur. + +## Fichiers ajoutés + +```text +crates/ksp-config-lib/src/logging.rs +crates/ksp-config-lib/unit_tests/logging.rs +deltas/0.1.3/pre.012.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-config-lib/src/environment.rs +crates/ksp-config-lib/src/error.rs +crates/ksp-config-lib/src/lib.rs +crates/ksp-config-lib/tests/public_api.rs +docs/rules/RULES_KSP.md +docs/rules/FILE_CONTRACTS.md +docs/plans/000-README.md +docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md +``` + +## Hors scope + +Toujours hors `pre.012` : + +- mutation/persistence JSON ; +- create/update/remove `.env` ; +- reveal secret de management ; +- application desktop Config ; +- watcher/reload automatique de Config ; +- autres documents standard Store/Wallet/Transport. + +Ces sujets commencent avec `pre.013` ou les releases prévues ultérieurement. + +## Validation demandée + +```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 +cargo tree -p ksp-logging-lib -d +``` + +Aucune validation Cargo locale n'est revendiquée dans l'environnement de génération de ce delta. diff --git a/docs/plans/000-README.md b/docs/plans/000-README.md index b46b4c5..5f9304f 100644 --- a/docs/plans/000-README.md +++ b/docs/plans/000-README.md @@ -1,5 +1,5 @@ - + # Plans KSP @@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou - [`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`. -- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, `pre.008` la résolution globals/profils/`default_profile`, puis `pre.009` les compositions génériques par `file_id`; `pre.010` a livré `.env`, process env, `.env.example` et le resolver `${...}`; `pre.011` ajoute sensibilité, valeur réelle/sûre, redaction et provenance; `pre.012` poursuivra avec l'adapter Config -> Logging. +- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, `pre.008` la résolution globals/profils/`default_profile`, puis `pre.009` les compositions génériques par `file_id`; `pre.010` a livré `.env`, process env, `.env.example` et le resolver `${...}`; `pre.011` ajoute sensibilité, valeur réelle/sûre, redaction et provenance; `pre.012` livre l'adapter Config -> Logging et la validation effective des chemins Logging; `pre.013` poursuivra avec management/persistence. Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre. diff --git a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md index c762818..8fd6c88 100644 --- a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +++ b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md @@ -1,5 +1,5 @@ - + # Séquence des releases fonctionnelles KSP @@ -219,7 +219,7 @@ pre.015 clôture Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 minutes de travail effectif. -`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` a ajouté la résolution générique globals/profils/`default_profile`; `pre.009` a ajouté les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif; `pre.010` a ajouté le snapshot process + `.env`, `.env.example` et le resolver `${...}`; `pre.011` ajoute sensibilité, valeurs réelle/sûre, redaction et provenance enrichie. Après validation utilisateur, `pre.012` construira l'adapter Config -> Logging. +`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` a ajouté la résolution générique globals/profils/`default_profile`; `pre.009` a ajouté les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif; `pre.010` a ajouté le snapshot process + `.env`, `.env.example` et le resolver `${...}`; `pre.011` a ajouté sensibilité, valeurs réelle/sûre, redaction et provenance enrichie; `pre.012` livre l'adapter Config -> Logging, la validation effective de `logs_directory`/`files[].path` et le contrat de non-usage des secrets par Logging. Après validation utilisateur, `pre.013` ouvrira management + persistence JSON/.env. ## `0.1.4` — Config desktop par défaut diff --git a/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md b/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md index 0c7adaf..feaf418 100644 --- a/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md +++ b/docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.1.3` — Configuration foundation ## 1. Statut et objectif -Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema, `pre.008` la résolution des globals/profils/`default_profile`, `pre.009` les compositions génériques par `file_id`, `pre.010` le snapshot process + `.env` et le resolver `${...}`, puis `pre.011` la sensibilité et les représentations real/safe/provenance. La prochaine tranche est `pre.012` pour l'adapter Config -> Logging. +Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema, `pre.008` la résolution des globals/profils/`default_profile`, `pre.009` les compositions génériques par `file_id`, `pre.010` le snapshot process + `.env` et le resolver `${...}`, puis `pre.011` la sensibilité et les représentations real/safe/provenance. `pre.012` livre maintenant l'adapter Config -> Logging, avec validation effective des paths et frontière anti-secret. La prochaine tranche est `pre.013` pour management + persistence JSON/.env. La base auditée reste la release stable `v0.1.2`. @@ -1824,13 +1824,23 @@ Tranche livrée : ### `0.1.3-pre.012` — adapter Config -> Logging -- `ResolvedLoggingConfig` ; -- conversion explicite vers les contrats publics `ksp_logging_lib::*` ; -- aucune dépendance inverse ; -- démonstration `initialize/reinitialize` ; -- `LoggingGuard` orchestration-owned ; -- tests de mapping multi-output/filter/domain ; -- vérification qu'aucune valeur secrète n'est utilisée dans les diagnostics Logging. +Tranche livrée : + +- `ResolvedLoggingConfig` conserve `file_id`, source path, profil sélectionné, source de sélection, arbre effectif détaillé real/safe/provenance, root Logging résolu et `ksp_logging_lib::LoggingSettings` ; +- `ConfigDocumentEngine::load_resolved_logging_config(requested_profile, environment)` charge `cfg.std.logging`, sélectionne le profil, applique process > `.env` > fallback puis convertit explicitement tous les contrats Logging ; +- mapping complet de `LogFilterLevel`, `SpanEvents`, `ConsoleOutput`, `LogFormat`, `FileRotation`, `OutputFilter`, `TargetFilter`, console et multi-fichiers ; +- `files[].path` est séparé en directory relatif + file-name prefix pour `FileSettings`, sous un `logs_directory` commun ; +- `logs_directory` absolu est conservé ; relatif, il est ancré sur `std::env::current_dir()` au moment de l'adaptation ; un root inexistant est accepté pour permettre à Logging de le créer ; un path existant non-directory ou impossible à inspecter est rejeté ; +- une valeur explicite `KSP_LOGS_DIRECTORY=` vide est une valeur présente et produit `config.effective_config_invalid` : aucun fallback silencieux vers `logs` ; +- les `files[].path` sont revalidés après interpolation et doivent rester relatifs sans `.`/`..`/root/prefix, empêchant une variable d'environnement de faire sortir un sink du root Logging ; +- l'adapter rejette toute configuration Logging effective dont la sensibilité agrégée est `Secret`; Logging n'a aucun besoin fonctionnel de secrets et ses diagnostics filesystem ne doivent jamais recevoir de valeur secrète ; +- les erreurs de validation finales de `LoggingSettings` sont encapsulées par Config avec uniquement le code Logging stable, sans recopier les contextes runtime susceptibles de contenir des valeurs effectives ; +- `ResolvedLoggingConfig` possède un `Debug` manuel qui expose l'arbre effectif via sa représentation sûre et n'affiche ni les settings réels ni le root réel séparément ; +- un test démontre que les settings issus de Config peuvent réellement `initialize` puis `reinitialize` `ksp-logging-lib`; le `LoggingGuard` reste détenu par l'orchestration/test, jamais stocké comme singleton Config ; +- l'initialisation Logging crée les sous-répertoires de sinks configurés lorsqu'ils n'existent pas ; +- aucune nouvelle variable d'environnement, aucune modification de `.env.example`, aucune nouvelle dépendance Cargo. + +La validation utilisateur de `pre.011-fix.001` est acquise le 2026-08-16 : `fmt/check/clippy/test` passent, `ksp-config-lib` compte 61 tests unitaires + 9 tests publics, et le graphe conserve uniquement le doublon transitif `syn 2`/`syn 3` déjà connu via `jsonschema`; `ksp-logging-lib -d` reste sans doublon. ### `0.1.3-pre.013` — management + persistence JSON/.env diff --git a/docs/rules/FILE_CONTRACTS.md b/docs/rules/FILE_CONTRACTS.md index 6239148..0f5fa2c 100644 --- a/docs/rules/FILE_CONTRACTS.md +++ b/docs/rules/FILE_CONTRACTS.md @@ -1,5 +1,5 @@ - + # Contrats des fichiers @@ -51,6 +51,8 @@ Les noms physiques sont remplaçables via le registre Config lorsque le contrat Le fichier runtime d'environnement est toujours `./.env` pour `0.1.3`. Il n'est ni un document `config/` ni une source de bootstrap de `cfgpath`/`schemapath`. Le template `.env.example` est la référence versionnée permettant de créer localement `.env` et d'identifier par diff les nouvelles clés attendues. +Pour `config/std.logging.json`, `logs_directory` accepte un chemin absolu ou relatif. Après interpolation, un chemin relatif est ancré sur le current working directory du processus au moment où Config construit `LoggingSettings`. Une valeur explicitement définie mais vide/invalide ne retombe jamais sur le fallback `logs`. Les `files[].path` restent relatifs sous ce root et sont revalidés après interpolation afin d'interdire un chemin absolu ou un traversal introduit dynamiquement. + Pour un document standard profilé, `default_profile` et `profiles` sont des clés structurelles réservées. Les autres propriétés top-level sont des valeurs globales. Chaque entrée de `profiles` possède un `profile_id` unique ; `default_profile` référence obligatoirement l'un de ces identifiants. La résolution Config peut sélectionner le profil par défaut ou un profil explicite et conserve séparément la provenance `Global` / `Profile` de la vue effective. Les consumers ne reconstituent jamais eux-mêmes cette fusion. ## Répertoire `prompts/` diff --git a/docs/rules/RULES_KSP.md b/docs/rules/RULES_KSP.md index 3e778d7..651ad80 100644 --- a/docs/rules/RULES_KSP.md +++ b/docs/rules/RULES_KSP.md @@ -1,5 +1,5 @@ - + # Règles spécifiques à KSP @@ -40,6 +40,8 @@ - **KSP-CONFIG-009** — Toute valeur Config contenant un segment `Secret` conserve une valeur réelle pour le runtime légitime et une représentation sûre où chaque segment secret est remplacé par `********`; les `Debug` génériques ne révèlent jamais la valeur réelle secrète. - **KSP-CONFIG-010** — La sensibilité d'une chaîne composée est la plus forte des placeholders utilisés. Un fallback d'une variable `Secret` reste `Secret` et doit être redacted même si la valeur provient du fallback. - **KSP-CONFIG-011** — La provenance de résolution n'embarque jamais la valeur d'environnement elle-même ; elle distingue document literal, process, `.env` et fallback avec le nom de variable concerné. +- **KSP-CONFIG-012** — Pour `std.logging`, `logs_directory` peut être absolu ou relatif ; un chemin relatif est ancré sur le current working directory du processus lors de la construction des settings. Le fallback du placeholder ne s'applique que si la variable est absente ; une valeur explicitement présente mais invalide provoque une erreur de configuration effective. +- **KSP-CONFIG-013** — Le document standard Logging ne consomme pas de variable classée `Secret`. L'adapter Config -> Logging rejette une configuration effective `Secret` afin qu'aucune valeur secrète ne soit transmise aux diagnostics runtime/filesystem de Logging. ## Programmes et exécution