From 8360f25f59c29e6a580a94c7100212ddd9dd8281 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 15 Aug 2026 08:40:11 +0200 Subject: [PATCH] v0.1.3-pre.002 --- Cargo.toml | 6 +- crates/ksp-config-lib/Cargo.toml | 14 ++ crates/ksp-config-lib/src/bootstrap.rs | 205 +++++++++++++++ crates/ksp-config-lib/src/error.rs | 8 + crates/ksp-config-lib/src/lib.rs | 29 +++ crates/ksp-config-lib/tests/public_api.rs | 40 +++ crates/ksp-config-lib/unit_tests/bootstrap.rs | 142 +++++++++++ deltas/0.1.3/pre.002.md | 233 ++++++++++++++++++ .../005-V0_1_3_CONFIG_FOUNDATION_PLAN.md | 89 ++++--- 9 files changed, 725 insertions(+), 41 deletions(-) create mode 100644 crates/ksp-config-lib/Cargo.toml create mode 100644 crates/ksp-config-lib/src/bootstrap.rs create mode 100644 crates/ksp-config-lib/src/error.rs create mode 100644 crates/ksp-config-lib/src/lib.rs create mode 100644 crates/ksp-config-lib/tests/public_api.rs create mode 100644 crates/ksp-config-lib/unit_tests/bootstrap.rs create mode 100644 deltas/0.1.3/pre.002.md diff --git a/Cargo.toml b/Cargo.toml index f48976e..75aeba5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 40 +# version: 41 [workspace] resolver = "3" -members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"] +members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.3-pre.1" +version = "0.1.3-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-config-lib/Cargo.toml b/crates/ksp-config-lib/Cargo.toml new file mode 100644 index 0000000..dbd58f6 --- /dev/null +++ b/crates/ksp-config-lib/Cargo.toml @@ -0,0 +1,14 @@ +# file: crates/ksp-config-lib/Cargo.toml +# version: 1 + +[package] +name = "ksp-config-lib" +version.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +ksp-core-lib = { path = "../ksp-core-lib" } + +[lints] +workspace = true diff --git a/crates/ksp-config-lib/src/bootstrap.rs b/crates/ksp-config-lib/src/bootstrap.rs new file mode 100644 index 0000000..6ccb1c0 --- /dev/null +++ b/crates/ksp-config-lib/src/bootstrap.rs @@ -0,0 +1,205 @@ +// file: crates/ksp-config-lib/src/bootstrap.rs +// version: 1 + +/// Default root containing KSP runtime configuration documents. +pub const DEFAULT_CFG_PATH: &str = "config"; +/// Default root containing KSP JSON schemas. +pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas"; +/// Bootstrap argument used to replace the configuration document root. +pub const ARG_CFG_PATH: &str = "--cfgpath"; +/// Bootstrap argument used to replace the schema root. +pub const ARG_SCHEMA_PATH: &str = "--schemapath"; + +/// Non-recursive bootstrap options required before Config can resolve any managed document. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ConfigBootstrapOptions { + cfg_path: std::path::PathBuf, + schema_path: std::path::PathBuf, +} + +impl ConfigBootstrapOptions { + /// Creates bootstrap options using the KSP hardcoded configuration and schema roots. + pub fn defaults() -> ksp_core_lib::Result { + return Self::from_paths(crate::DEFAULT_CFG_PATH, crate::DEFAULT_SCHEMA_PATH); + } + + /// Creates bootstrap options from explicit programmatic configuration and schema roots. + pub fn from_paths( + cfg_path: impl std::convert::Into, + schema_path: impl std::convert::Into, + ) -> ksp_core_lib::Result { + let cfg_path = validate_bootstrap_path(crate::ARG_CFG_PATH, cfg_path.into()); + let cfg_path = match cfg_path { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let schema_path = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, schema_path.into()); + return match schema_path { + std::result::Result::Ok(value) => std::result::Result::Ok(Self { cfg_path, schema_path: value }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Parses the KSP-owned bootstrap path arguments from a raw process argument slice. + /// + /// Both `--cfgpath=value` / `--cfgpath value` and `--schemapath=value` / `--schemapath value` are accepted. Unrelated arguments are ignored so an + /// application can pass its complete argument vector. When the same bootstrap path is specified more than once, the last explicit value wins. + pub fn from_args(args: &[std::ffi::OsString]) -> ksp_core_lib::Result { + let mut options = Self::defaults_unchecked(); + let mut index: usize = 0; + while index < args.len() { + let argument = &args[index]; + if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_CFG_PATH) { + let parsed = parse_separate_path_argument(args, index, crate::ARG_CFG_PATH); + match parsed { + std::result::Result::Ok((path, next_index)) => { + options.cfg_path = path; + index = next_index; + }, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } else if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_SCHEMA_PATH) { + let parsed = parse_separate_path_argument(args, index, crate::ARG_SCHEMA_PATH); + match parsed { + std::result::Result::Ok((path, next_index)) => { + options.schema_path = path; + index = next_index; + }, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } else { + let inline = parse_inline_path_argument(argument); + match inline { + std::option::Option::Some((kind, path)) => match kind { + BootstrapPathKind::Config => options.cfg_path = path, + BootstrapPathKind::Schema => options.schema_path = path, + }, + std::option::Option::None => {}, + } + } + index += 1; + } + return Self::from_paths(options.cfg_path, options.schema_path); + } + + /// Returns the root used for managed runtime configuration documents. + #[must_use] + pub fn cfg_path(&self) -> &std::path::Path { + return self.cfg_path.as_path(); + } + + /// Returns the root used for managed JSON schemas. + #[must_use] + pub fn schema_path(&self) -> &std::path::Path { + return self.schema_path.as_path(); + } + + /// Replaces the configuration document root after applying bootstrap path validation. + pub fn with_cfg_path(self, path: impl std::convert::Into) -> ksp_core_lib::Result { + let validated = validate_bootstrap_path(crate::ARG_CFG_PATH, path.into()); + return match validated { + std::result::Result::Ok(cfg_path) => std::result::Result::Ok(Self { cfg_path, schema_path: self.schema_path }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + /// Replaces the schema root after applying bootstrap path validation. + pub fn with_schema_path(self, path: impl std::convert::Into) -> ksp_core_lib::Result { + let validated = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, path.into()); + return match validated { + std::result::Result::Ok(schema_path) => std::result::Result::Ok(Self { cfg_path: self.cfg_path, schema_path }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } + + fn defaults_unchecked() -> Self { + return Self { + cfg_path: std::path::PathBuf::from(crate::DEFAULT_CFG_PATH), + schema_path: std::path::PathBuf::from(crate::DEFAULT_SCHEMA_PATH), + }; + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BootstrapPathKind { + Config, + Schema, +} + +fn parse_inline_path_argument(argument: &std::ffi::OsStr) -> std::option::Option<(BootstrapPathKind, std::path::PathBuf)> { + let text = match argument.to_str() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let cfg_prefix = "--cfgpath="; + let schema_prefix = "--schemapath="; + if let std::option::Option::Some(value) = text.strip_prefix(cfg_prefix) { + return std::option::Option::Some((BootstrapPathKind::Config, std::path::PathBuf::from(value))); + } + if let std::option::Option::Some(value) = text.strip_prefix(schema_prefix) { + return std::option::Option::Some((BootstrapPathKind::Schema, std::path::PathBuf::from(value))); + } + return std::option::Option::None; +} + +fn parse_separate_path_argument(args: &[std::ffi::OsString], index: usize, argument_name: &'static str) -> ksp_core_lib::Result<(std::path::PathBuf, usize)> { + let value_index = index + 1; + if value_index >= args.len() { + return std::result::Result::Err(missing_argument_value_error(argument_name)); + } + let value = &args[value_index]; + let option_like = match value.to_str() { + std::option::Option::Some(text) => text.starts_with("--"), + std::option::Option::None => false, + }; + if option_like { + return std::result::Result::Err(missing_argument_value_error(argument_name)); + } + return std::result::Result::Ok((std::path::PathBuf::from(value.as_os_str()), value_index)); +} + +fn validate_bootstrap_path(argument_name: &'static str, path: std::path::PathBuf) -> ksp_core_lib::Result { + if path.as_os_str().is_empty() { + return std::result::Result::Err(invalid_path_error(argument_name, &path, "path is empty")); + } + let metadata = std::fs::metadata(path.as_path()); + return match metadata { + std::result::Result::Ok(value) => { + if value.is_dir() { + std::result::Result::Ok(path) + } else { + std::result::Result::Err(invalid_path_error(argument_name, &path, "existing path is not a directory")) + } + }, + std::result::Result::Err(error) => { + if error.kind() == std::io::ErrorKind::NotFound { + std::result::Result::Ok(path) + } else { + std::result::Result::Err(invalid_path_source_error(argument_name, &path, error)) + } + }, + }; +} + +fn missing_argument_value_error(argument_name: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE, "Config bootstrap argument requires a path value") + .with_context("argument", argument_name); +} + +fn invalid_path_error(argument_name: &'static str, path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path is invalid") + .with_context("argument", argument_name) + .with_context("path", path.to_string_lossy().into_owned()) + .with_context("reason", reason); +} + +fn invalid_path_source_error(argument_name: &'static str, path: &std::path::Path, source: std::io::Error) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path cannot be inspected") + .with_context("argument", argument_name) + .with_context("path", path.to_string_lossy().into_owned()) + .with_source(source); +} + +#[cfg(test)] +#[path = "../unit_tests/bootstrap.rs"] +mod tests; diff --git a/crates/ksp-config-lib/src/error.rs b/crates/ksp-config-lib/src/error.rs new file mode 100644 index 0000000..473408f --- /dev/null +++ b/crates/ksp-config-lib/src/error.rs @@ -0,0 +1,8 @@ +// file: crates/ksp-config-lib/src/error.rs +// version: 1 + +/// 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"); + +/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path. +pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path"); diff --git a/crates/ksp-config-lib/src/lib.rs b/crates/ksp-config-lib/src/lib.rs new file mode 100644 index 0000000..cafb789 --- /dev/null +++ b/crates/ksp-config-lib/src/lib.rs @@ -0,0 +1,29 @@ +// file: crates/ksp-config-lib/src/lib.rs +// version: 1 +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! KSP-owned application configuration facade. +//! +//! `0.1.3-pre.002` establishes the non-recursive bootstrap boundary used before any managed configuration document can be read. Configuration and schema +//! roots have hardcoded KSP defaults and can only be replaced through explicit bootstrap arguments or the programmatic [`ConfigBootstrapOptions`] API. +//! Document registries, JSON/schema loading, profiles, environment resolution and persistence are introduced by later bounded prereleases. + +mod bootstrap; +mod error; + +/// Bootstrap argument used to replace the configuration document root. +pub use self::bootstrap::ARG_CFG_PATH; +/// Bootstrap argument used to replace the schema root. +pub use self::bootstrap::ARG_SCHEMA_PATH; +/// Non-recursive bootstrap options required before Config can resolve any managed document. +pub use self::bootstrap::ConfigBootstrapOptions; +/// Default root containing KSP runtime configuration documents. +pub use self::bootstrap::DEFAULT_CFG_PATH; +/// Default root containing KSP JSON schemas. +pub use self::bootstrap::DEFAULT_SCHEMA_PATH; +/// Error code used when a Config bootstrap argument is missing its value. +pub use self::error::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE; +/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path. +pub use self::error::ERROR_CODE_BOOTSTRAP_INVALID_PATH; diff --git a/crates/ksp-config-lib/tests/public_api.rs b/crates/ksp-config-lib/tests/public_api.rs new file mode 100644 index 0000000..c72c17c --- /dev/null +++ b/crates/ksp-config-lib/tests/public_api.rs @@ -0,0 +1,40 @@ +// file: crates/ksp-config-lib/tests/public_api.rs +// version: 1 + +#[test] +fn bootstrap_contract_is_available_from_crate_root() { + let result = ksp_config_lib::ConfigBootstrapOptions::defaults(); + assert!(result.is_ok(), "default bootstrap options should be available: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new(ksp_config_lib::DEFAULT_CFG_PATH)); + assert_eq!(options.schema_path(), std::path::Path::new(ksp_config_lib::DEFAULT_SCHEMA_PATH)); + assert_eq!(ksp_config_lib::ARG_CFG_PATH, "--cfgpath"); + assert_eq!(ksp_config_lib::ARG_SCHEMA_PATH, "--schemapath"); + } +} + +#[test] +fn programmatic_bootstrap_paths_are_independent() { + let result = ksp_config_lib::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas"); + assert!(result.is_ok(), "programmatic bootstrap paths should be valid: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("runtime-config")); + assert_eq!(options.schema_path(), std::path::Path::new("runtime-schemas")); + } +} + +#[test] +fn cli_bootstrap_parser_is_available_from_crate_root() { + let args = [ + std::ffi::OsString::from("consumer"), + std::ffi::OsString::from("--cfgpath=consumer-config"), + std::ffi::OsString::from("--schemapath"), + std::ffi::OsString::from("consumer-schemas"), + ]; + let result = ksp_config_lib::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_ok(), "public bootstrap parser should accept KSP path arguments: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("consumer-config")); + assert_eq!(options.schema_path(), std::path::Path::new("consumer-schemas")); + } +} diff --git a/crates/ksp-config-lib/unit_tests/bootstrap.rs b/crates/ksp-config-lib/unit_tests/bootstrap.rs new file mode 100644 index 0000000..d5d4d3c --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/bootstrap.rs @@ -0,0 +1,142 @@ +// file: crates/ksp-config-lib/unit_tests/bootstrap.rs +// version: 1 + +#[test] +fn defaults_use_hardcoded_ksp_roots() { + let result = super::ConfigBootstrapOptions::defaults(); + assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); + assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); + } +} + +#[test] +fn cfg_path_override_keeps_schema_default() { + let defaults = super::ConfigBootstrapOptions::defaults(); + assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); + if let std::result::Result::Ok(options) = defaults { + let result = options.with_cfg_path("custom-config"); + assert!(result.is_ok(), "cfg path override should be valid: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("custom-config")); + assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); + } + } +} + +#[test] +fn schema_path_override_keeps_cfg_default() { + let defaults = super::ConfigBootstrapOptions::defaults(); + assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); + if let std::result::Result::Ok(options) = defaults { + let result = options.with_schema_path("custom-schemas"); + assert!(result.is_ok(), "schema path override should be valid: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); + assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas")); + } + } +} + +#[test] +fn cfg_cli_override_keeps_schema_default() { + let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_ok(), "cfg CLI override should parse: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("cli-config")); + assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); + } +} + +#[test] +fn schema_cli_override_keeps_cfg_default() { + let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_ok(), "schema CLI override should parse: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); + assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas")); + } +} + +#[test] +fn parser_accepts_inline_and_separate_forms_and_last_value_wins() { + let args = [ + std::ffi::OsString::from("ksp-app"), + std::ffi::OsString::from("--cfgpath=first-config"), + std::ffi::OsString::from("--unrelated"), + std::ffi::OsString::from("--cfgpath"), + std::ffi::OsString::from("second-config"), + std::ffi::OsString::from("--schemapath=first-schemas"), + std::ffi::OsString::from("--schemapath"), + std::ffi::OsString::from("second-schemas"), + ]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("second-config")); + assert_eq!(options.schema_path(), std::path::Path::new("second-schemas")); + } +} + +#[test] +fn parser_reports_missing_separate_value() { + let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_err(), "missing value must be rejected"); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); + } +} + +#[test] +fn parser_reports_another_option_as_missing_separate_value() { + let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_err(), "another option must not become a bootstrap path value"); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); + } +} + +#[test] +fn empty_inline_path_is_rejected() { + let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")]; + let result = super::ConfigBootstrapOptions::from_args(&args); + assert!(result.is_err(), "empty path must be rejected"); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH); + } +} + +#[test] +fn explicit_programmatic_paths_do_not_depend_on_default_roots() { + let result = super::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas"); + assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}"); + if let std::result::Result::Ok(options) = result { + assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config")); + assert_eq!(options.schema_path(), std::path::Path::new("programmatic-schemas")); + } +} + +#[test] +fn existing_non_directory_path_is_rejected() { + let fixture = unique_fixture_path("existing-file"); + let create = std::fs::write(fixture.as_path(), b"fixture"); + assert!(create.is_ok(), "fixture file should be creatable: {create:?}"); + let result = super::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas"); + let remove = std::fs::remove_file(fixture.as_path()); + assert!(remove.is_ok(), "fixture file should be removable: {remove:?}"); + assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory"); + if let std::result::Result::Err(error) = result { + assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH); + } +} + +fn unique_fixture_path(name: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!("ksp-config-lib-{name}-{}", std::process::id())); + return path; +} diff --git a/deltas/0.1.3/pre.002.md b/deltas/0.1.3/pre.002.md new file mode 100644 index 0000000..4d3f33f --- /dev/null +++ b/deltas/0.1.3/pre.002.md @@ -0,0 +1,233 @@ + + + +# Delta 0.1.3-pre.002 + +## Base requise + +Livraison documentaire précédente validée : + +```text +0.1.3-pre.001-fix.003 +``` + +La base porte : + +```text +workspace.package.version = "0.1.3-pre.1" +Cargo.toml header version = 40 +``` + +Le plan `docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md` est en version `4` et borne `pre.002` à la création de `ksp-config-lib` et au bootstrap `cfgpath` / `schemapath` uniquement. + +## Objet de pre.002 + +Cette tranche ouvre le développement fonctionnel de Config sans anticiper les tranches suivantes. + +Elle : + +- crée `ksp-config-lib` ; +- ajoute la crate au workspace ; +- fixe les deux racines bootstrap non récursives ; +- expose leur équivalent programmatique ; +- possède le parsing des deux arguments CLI correspondants ; +- valide les chemins bootstrap ; +- introduit uniquement les erreurs Config nécessaires à cette surface ; +- ajoute les tests unitaires et d'intégration de cette API publique. + +Elle n'introduit pas encore : + +- le registre `file_id -> filename` ; +- `--filemap` ; +- `serde`, `serde_json` ou `jsonschema` ; +- les documents JSON runtime ; +- les profils/composites ; +- `.env` ou `std::env::var` ; +- l'interpolation `${...}` ; +- les secrets ; +- la persistence ; +- une dépendance directe à `ksp-logging-lib` tant qu'aucun événement Config ne l'utilise réellement. + +## Crate `ksp-config-lib` + +La nouvelle crate hérite de la version, de l'édition, du repository et des lints du workspace. + +Sa seule dépendance est actuellement : + +```toml +ksp-core-lib = { path = "../ksp-core-lib" } +``` + +Cela respecte la règle d'ajout des dépendances uniquement lorsqu'elles sont réellement utilisées. La direction architecturale future reste `ksp-config-lib -> ksp-logging-lib`, mais cette dépendance n'est pas ajoutée prématurément dans `pre.002`. + +## Bootstrap non récursif + +Les défauts KSP sont codés dans Config : + +```text +DEFAULT_CFG_PATH = "config" +DEFAULT_SCHEMA_PATH = "config/schemas" +``` + +Ils ne dépendent d'aucun document Config, `.env` ou variable applicative. + +La surface publique introduite est : + +```text +ConfigBootstrapOptions::defaults() +ConfigBootstrapOptions::from_paths(...) +ConfigBootstrapOptions::from_args(...) +ConfigBootstrapOptions::cfg_path() +ConfigBootstrapOptions::schema_path() +ConfigBootstrapOptions::with_cfg_path(...) +ConfigBootstrapOptions::with_schema_path(...) +``` + +Les arguments possédés par Config sont : + +```text +--cfgpath +--schemapath +``` + +Le parser accepte les deux formes : + +```text +--cfgpath=/path/to/configs +--cfgpath /path/to/configs + +--schemapath=/path/to/schemas +--schemapath /path/to/schemas +``` + +Les arguments étrangers sont ignorés afin qu'une application puisse transmettre son vecteur d'arguments complet à Config. Si un même path est fourni plusieurs fois, le dernier override explicite gagne. + +Les deux roots restent indépendants : remplacer `cfgpath` ne modifie pas `schemapath`, et inversement. + +## Validation des chemins + +`pre.002` applique seulement les garanties qui sont valides avant la création des premiers documents runtime : + +- path vide : refusé ; +- path existant et répertoire : accepté ; +- path existant mais non répertoire : refusé ; +- path inexistant : accepté, car `config/` et `config/schemas/` ne sont créés que dans une tranche ultérieure ; +- path relatif ou absolu : accepté. + +Un argument séparé sans valeur, ou immédiatement suivi d'une autre option `--...`, produit une erreur dédiée. + +## Erreurs Config initiales + +Les codes restent possédés par `ksp-config-lib` avec le domaine `config` : + +```text +config.bootstrap_argument_missing_value +config.bootstrap_invalid_path +``` + +Ils utilisent les contrats existants de Core : + +```text +ksp_core_lib::Error +ksp_core_lib::ErrorCode +ksp_core_lib::Result +``` + +Core ne reçoit aucune connaissance métier Config. + +## Tests ajoutés + +Les tests unitaires couvrent notamment : + +- les deux défauts hardcodés ; +- l'indépendance des overrides cfg/schema ; +- les formes CLI inline et séparées ; +- la règle du dernier override ; +- l'ignorance des arguments étrangers ; +- l'absence de valeur ; +- le refus d'un path vide ; +- l'acceptation d'un path programmatique inexistant ; +- le refus d'un path existant qui est un fichier. + +Les tests d'intégration vérifient la façade publique au crate-root et le parsing consommable par une crate externe. + +## Version technique + +La prerelease devient : + +```text +workspace.package.version = "0.1.3-pre.2" +``` + +Le manifest racine devient : + +```text +# version: 41 +``` + +Le plan Config devient : + +```text + +``` + +## Fichiers ajoutés + +```text +crates/ksp-config-lib/Cargo.toml +crates/ksp-config-lib/src/bootstrap.rs +crates/ksp-config-lib/src/error.rs +crates/ksp-config-lib/src/lib.rs +crates/ksp-config-lib/unit_tests/bootstrap.rs +crates/ksp-config-lib/tests/public_api.rs +deltas/0.1.3/pre.002.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md +``` + +## Fichiers supprimés + +Aucun. + +## Validations non exécutées à faire dans l'environnement utilisateur + +```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 +``` + +Aucun script d'audit Rust/KSP exécutable n'est présent dans la base reconstruite de cette tranche. + +L'environnement de génération du delta ne fournit pas `cargo`, `rustc` ni `rustfmt`. Aucune commande Cargo ci-dessus n'est donc déclarée réussie avant la validation dans l'environnement utilisateur. + +## Validations exécutées avant livraison + +Des contrôles statiques ont vérifié avant livraison : + +- headers `file:` / `version:` présents sur les nouveaux fichiers ; +- aucune ligne Rust supérieure à 160 colonnes avant formatage ; +- aucun `unsafe`, `unwrap`, `expect`, `panic!` ou opérateur `?` dans `src/` ; +- aucun `use` de non-trait ; +- aucune lecture de variable applicative par `std::env` dans `src/` ; +- aucune dépendance externe nouvelle ; +- aucun `Cargo.lock` ajouté au delta. + +## Suite après validation + +Si `pre.002` est validée, la prochaine tranche prévue est : + +```text +0.1.3-pre.003 — registre logique file_id -> filename + --filemap +``` + +Elle ne doit pas encore lire les documents JSON ou leurs schemas. 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 4441916..e547dff 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`, complété par `0.1.3-pre.001-fix.002`, puis regranularisé par `0.1.3-pre.001-fix.003` avant tout développement fonctionnel de Config. +Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001`, complété par `0.1.3-pre.001-fix.002`, puis regranularisé par `0.1.3-pre.001-fix.003`. `0.1.3-pre.002` ouvre maintenant le développement fonctionnel avec la seule fondation bootstrap de `ksp-config-lib`. La base auditée reste la release stable `v0.1.2`. @@ -230,25 +230,25 @@ La complétion Logging n'ouvre ni une nouvelle façade ni une dépendance invers ## 5. Matrice de responsabilités -| Responsabilité | Propriétaire | Consommateurs | Interdit | -|---|---|---|---| -| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers | -| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename | -| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire | -| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer | -| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate | -| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire | -| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config | -| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config | -| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers | -| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate | -| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application | -| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application | -| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent | -| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config | -| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global | -| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config | -| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config | +| Responsabilité | Propriétaire | Consommateurs | Interdit | +|---------------------------------------------------------------------|--------------------------------------|--------------------------------------------|-----------------------------------------------------------| +| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers | +| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename | +| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire | +| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer | +| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate | +| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire | +| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config | +| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config | +| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers | +| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate | +| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application | +| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application | +| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent | +| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config | +| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global | +| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config | +| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config | ## 6. Registre de fichiers, bootstrap, arborescence et nomenclature @@ -580,23 +580,23 @@ Décisions : ### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer -| Capacité | `0.1.2` | Requise par `std.logging.json` | -|---|---:|---:| -| filtre global | oui | oui | -| overrides par target | oui | oui | -| lifecycle spans | oui | oui | -| console stdout/stderr | oui | oui | -| console enabled | via `Option` | oui explicite | -| console ANSI configurable | non | oui | -| format console configurable | non | oui | -| plusieurs fichiers | non | oui | -| rotation par fichier | un seul fichier | oui par sink | -| format par fichier | non | oui | -| filtre par sink/target | non | oui | -| filtre par sink/domain | non | oui | -| filtre par sink/niveau | non indépendant | oui | -| hot reload transactionnel | oui | à conserver | -| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks | +| Capacité | `0.1.2` | Requise par `std.logging.json` | +|-----------------------------------|----------------:|-------------------------------------:| +| filtre global | oui | oui | +| overrides par target | oui | oui | +| lifecycle spans | oui | oui | +| console stdout/stderr | oui | oui | +| console enabled | via `Option` | oui explicite | +| console ANSI configurable | non | oui | +| format console configurable | non | oui | +| plusieurs fichiers | non | oui | +| rotation par fichier | un seul fichier | oui par sink | +| format par fichier | non | oui | +| filtre par sink/target | non | oui | +| filtre par sink/domain | non | oui | +| filtre par sink/niveau | non indépendant | oui | +| hot reload transactionnel | oui | à conserver | +| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks | Ce tableau est un **gap identifié**, pas une invitation à déplacer Logging dans Config. La tranche qui le ferme modifie `ksp-logging-lib` uniquement dans son domaine propriétaire. @@ -1569,6 +1569,19 @@ Objectif unique : créer la crate et fixer comment Config trouve ses deux racine - tests unitaires du bootstrap ; - **aucun registre `file_id` dans cette tranche**. +Implémentation candidate livrée par `pre.002` : + +- `ksp-config-lib` entre dans le workspace avec `ksp-core-lib` comme seule dépendance actuellement utilisée ; +- `ksp-logging-lib` reste une dépendance architecturale future de Config mais n'est pas ajoutée tant qu'aucun événement Config ne l'utilise réellement ; +- `DEFAULT_CFG_PATH = "config"` et `DEFAULT_SCHEMA_PATH = "config/schemas"` sont codés dans Config ; +- `ConfigBootstrapOptions::defaults()` valide les deux défauts ; +- `ConfigBootstrapOptions::from_paths(...)` fournit l'équivalent programmatique explicite sans dépendre des chemins par défaut ; +- `ConfigBootstrapOptions::from_args(...)` accepte `--cfgpath=value`, `--cfgpath value`, `--schemapath=value` et `--schemapath value`, ignore les arguments étrangers et applique le dernier override explicite d'un même path ; +- un path inexistant est accepté à ce stade, un path vide ou un path existant non répertoire est refusé ; +- le registre `file_id`, `--filemap`, JSON, schemas, profils et environnement restent strictement hors de `pre.002`. + +La validation utilisateur de cette implémentation reste requise avant l'ouverture de `pre.003`. + ### `0.1.3-pre.003` — registre logique `file_id -> filename` Objectif unique : rendre les noms physiques remplaçables sans modifier l'identité logique des fichiers.