v0.1.3-pre.012
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 55
|
# version: 56
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.3-pre.11.fix.1"
|
version = "0.1.3-pre.12"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/environment.rs
|
// file: crates/ksp-config-lib/src/environment.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Default local environment file read by Config from the process launch directory.
|
/// Default local environment file read by Config from the process launch directory.
|
||||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||||
@@ -246,7 +246,7 @@ impl ConfigEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
|
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
|
||||||
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
|
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/error.rs
|
// file: crates/ksp-config-lib/src/error.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
/// Error code used when a Config bootstrap argument is missing its value.
|
/// 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");
|
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.
|
/// 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");
|
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");
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
// file: crates/ksp-config-lib/src/lib.rs
|
// file: crates/ksp-config-lib/src/lib.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
//! KSP-owned application configuration facade.
|
//! 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
|
//! 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
|
//! runtime document. Environment-derived values preserve real/safe representations, sensitivity and provenance; the standard Logging profile can now be
|
||||||
//! prerelease.
|
//! mapped explicitly to `ksp_logging_lib::LoggingSettings`. Persistence remains in a later bounded prerelease.
|
||||||
|
|
||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
mod composite;
|
mod composite;
|
||||||
mod document;
|
mod document;
|
||||||
mod environment;
|
mod environment;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod logging;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod registry;
|
mod registry;
|
||||||
mod sensitivity;
|
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;
|
pub use self::error::ERROR_CODE_DOTENV_FILE_READ_FAILED;
|
||||||
/// Error code used when the local `.env` file contains invalid syntax.
|
/// Error code used when the local `.env` file contains invalid syntax.
|
||||||
pub use self::error::ERROR_CODE_DOTENV_SYNTAX_INVALID;
|
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.
|
/// Error code used when a Config environment placeholder is malformed.
|
||||||
pub use self::error::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID;
|
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.
|
/// 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;
|
pub use self::error::ERROR_CODE_SCHEMA_INVALID;
|
||||||
/// Error code used when a Config document fails its registered JSON Schema validation.
|
/// Error code used when a Config document fails its registered JSON Schema validation.
|
||||||
pub use self::error::ERROR_CODE_SCHEMA_VALIDATION_FAILED;
|
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.
|
/// Source that selected an effective standard Config profile.
|
||||||
pub use self::profile::ConfigProfileSelectionSource;
|
pub use self::profile::ConfigProfileSelectionSource;
|
||||||
/// Origin of one top-level value in a resolved standard Config profile.
|
/// Origin of one top-level value in a resolved standard Config profile.
|
||||||
|
|||||||
437
crates/ksp-config-lib/src/logging.rs
Normal file
437
crates/ksp-config-lib/src/logging.rs
Normal file
@@ -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<ResolvedLoggingConfig> {
|
||||||
|
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<EffectiveFileSource>,
|
||||||
|
target_filters: std::vec::Vec<EffectiveTargetFilterSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
domains: std::vec::Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ResolvedLoggingConfig> {
|
||||||
|
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::<EffectiveLoggingSource>(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<ksp_logging_lib::ConsoleSettings> {
|
||||||
|
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<EffectiveFileSource>,
|
||||||
|
logs_directory: &std::path::Path,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_logging_lib::FileSettings>> {
|
||||||
|
let mut files = std::vec::Vec::<ksp_logging_lib::FileSettings>::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<ksp_logging_lib::OutputFilter> {
|
||||||
|
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<ksp_logging_lib::LogFilterLevel> {
|
||||||
|
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<ksp_logging_lib::SpanEvents> {
|
||||||
|
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<ksp_logging_lib::LogFormat> {
|
||||||
|
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<ksp_logging_lib::FileRotation> {
|
||||||
|
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<std::path::PathBuf> {
|
||||||
|
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 => "<unavailable>".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;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
// 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]
|
#[test]
|
||||||
fn bootstrap_contract_is_available_from_crate_root() {
|
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.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||||
assert_eq!(provenance.environment_source(), std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Fallback));
|
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::<ksp_config_lib::ResolvedLoggingConfig>() > 0);
|
||||||
|
}
|
||||||
|
|||||||
329
crates/ksp-config-lib/unit_tests/logging.rs
Normal file
329
crates/ksp-config-lib/unit_tests/logging.rs
Normal file
@@ -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::<String, String>::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::<std::vec::Vec<&str>>(), targets.to_vec());
|
||||||
|
assert_eq!(file.filter().domains().iter().map(String::as_str).collect::<std::vec::Vec<&str>>(), domains.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
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<crate::ResolvedConfigProfile> {
|
||||||
|
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::<String, String>::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());
|
||||||
|
}
|
||||||
|
}
|
||||||
315
deltas/0.1.3/pre.012.md
Normal file
315
deltas/0.1.3/pre.012.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<!-- file: deltas/0.1.3/pre.012.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/000-README.md -->
|
<!-- file: docs/plans/000-README.md -->
|
||||||
<!-- version: 18 -->
|
<!-- version: 19 -->
|
||||||
|
|
||||||
# Plans KSP
|
# 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 ;
|
- [`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`.
|
- [`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`.
|
- [`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.
|
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 -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 16 -->
|
<!-- version: 17 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# 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.
|
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
|
## `0.1.4` — Config desktop par défaut
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 15 -->
|
||||||
|
|
||||||
# Plan `0.1.3` — Configuration foundation
|
# Plan `0.1.3` — Configuration foundation
|
||||||
|
|
||||||
## 1. Statut et objectif
|
## 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`.
|
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
|
### `0.1.3-pre.012` — adapter Config -> Logging
|
||||||
|
|
||||||
- `ResolvedLoggingConfig` ;
|
Tranche livrée :
|
||||||
- conversion explicite vers les contrats publics `ksp_logging_lib::*` ;
|
|
||||||
- aucune dépendance inverse ;
|
- `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` ;
|
||||||
- démonstration `initialize/reinitialize` ;
|
- `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 ;
|
||||||
- `LoggingGuard` orchestration-owned ;
|
- mapping complet de `LogFilterLevel`, `SpanEvents`, `ConsoleOutput`, `LogFormat`, `FileRotation`, `OutputFilter`, `TargetFilter`, console et multi-fichiers ;
|
||||||
- tests de mapping multi-output/filter/domain ;
|
- `files[].path` est séparé en directory relatif + file-name prefix pour `FileSettings`, sous un `logs_directory` commun ;
|
||||||
- vérification qu'aucune valeur secrète n'est utilisée dans les diagnostics Logging.
|
- `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
|
### `0.1.3-pre.013` — management + persistence JSON/.env
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# Contrats des fichiers
|
# 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.
|
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.
|
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/`
|
## Répertoire `prompts/`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_KSP.md -->
|
<!-- file: docs/rules/RULES_KSP.md -->
|
||||||
<!-- version: 16 -->
|
<!-- version: 17 -->
|
||||||
|
|
||||||
# Règles spécifiques à KSP
|
# 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-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-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-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
|
## Programmes et exécution
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user