v0.1.3-pre.009

This commit is contained in:
2026-08-15 21:25:08 +02:00
parent d1196c03e5
commit a5b4c748ea
17 changed files with 1121 additions and 42 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 49
# version: 50
[workspace]
resolver = "3"
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package]
version = "0.1.3-pre.8"
version = "0.1.3-pre.9"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,25 @@
{
"format_version": 1,
"default_profile": "local_default",
"profiles": [
{
"profile_id": "local_default",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging"
}
]
},
{
"profile_id": "local_explicit",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
}
]
}
]
}

View File

@@ -0,0 +1,62 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "KSP composite configuration",
"type": "object",
"additionalProperties": false,
"required": ["format_version", "default_profile", "profiles"],
"properties": {
"format_version": {
"const": 1
},
"default_profile": {
"type": "string",
"minLength": 1
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/composite_profile"
}
}
},
"$defs": {
"composite_profile": {
"type": "object",
"additionalProperties": false,
"required": ["profile_id", "documents"],
"properties": {
"profile_id": {
"type": "string",
"minLength": 1
},
"documents": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/document_reference"
}
}
}
},
"document_reference": {
"type": "object",
"additionalProperties": false,
"required": ["component_id", "file_id"],
"properties": {
"component_id": {
"type": "string",
"minLength": 1
},
"file_id": {
"type": "string",
"pattern": "^cfg\\.std\\.[a-z0-9][a-z0-9._-]*$"
},
"profile_id": {
"type": "string",
"minLength": 1
}
}
}
}
}

View File

@@ -0,0 +1,309 @@
// file: crates/ksp-config-lib/src/composite.rs
// version: 1
/// One resolved document component selected by a composite profile.
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedCompositeComponent {
component_id: String,
resolved: crate::ResolvedConfigProfile,
}
impl ResolvedCompositeComponent {
/// Returns the component identifier unique inside the selected composite profile.
#[must_use]
pub fn component_id(&self) -> &str {
return self.component_id.as_str();
}
/// Returns the referenced standard document after global/profile resolution.
#[must_use]
pub const fn resolved(&self) -> &crate::ResolvedConfigProfile {
return &self.resolved;
}
}
/// Validated composite document resolved to one composite profile and all of its referenced standard documents.
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedConfigComposite {
file_id: crate::ConfigFileId,
path: std::path::PathBuf,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
components: std::collections::BTreeMap<String, ResolvedCompositeComponent>,
}
impl ResolvedConfigComposite {
/// Returns the logical composite file identifier.
#[must_use]
pub fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the physical path of the validated composite source.
#[must_use]
pub fn path(&self) -> &std::path::Path {
return self.path.as_path();
}
/// Returns the selected composite profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns whether the composite profile came from `default_profile` or an explicit caller selection.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns all resolved components keyed by their composite-local `component_id`.
#[must_use]
pub const fn components(&self) -> &std::collections::BTreeMap<String, ResolvedCompositeComponent> {
return &self.components;
}
/// Returns one resolved component by its composite-local identifier.
#[must_use]
pub fn component(&self, component_id: &str) -> std::option::Option<&ResolvedCompositeComponent> {
return self.components.get(component_id);
}
}
impl crate::ConfigDocumentEngine {
/// Loads, validates and resolves one registered composite Config document.
///
/// Composite descriptors are introduced only when a concrete consumer exists. The generic contract and schema are available from `0.1.3-pre.009` onward.
/// Passing `None` selects the composite's `default_profile`; passing `Some(profile_id)` selects a composite profile explicitly.
pub fn load_resolved_composite(
&self,
file_id: &crate::ConfigFileId,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ResolvedConfigComposite> {
let document = self.load_validated_document(file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !document.file_id().as_str().starts_with("cfg.composite.") {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "requested Config document is not a composite")
.with_context("file_id", document.file_id().as_str()),
);
}
return resolve_composite_document(self, &document, requested_profile);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct CompositeDocumentSource {
format_version: u32,
default_profile: String,
profiles: std::vec::Vec<CompositeProfileSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct CompositeProfileSource {
profile_id: String,
documents: std::vec::Vec<CompositeDocumentReferenceSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct CompositeDocumentReferenceSource {
component_id: String,
file_id: String,
profile_id: std::option::Option<String>,
}
pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if !document.file_id().as_str().starts_with("cfg.composite.") {
return std::result::Result::Ok(());
}
let source = parse_composite_source(document);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if source.format_version != 1 {
return std::result::Result::Err(composite_semantic_error(document, "unsupported composite format_version"));
}
if source.default_profile.trim().is_empty() {
return std::result::Result::Err(composite_semantic_error(document, "composite default_profile must not be empty"));
}
for (profile_index, profile) in source.profiles.iter().enumerate() {
if profile.profile_id.trim().is_empty() {
return std::result::Result::Err(
composite_semantic_error(document, "composite profile_id must not be empty").with_context("profile_index", profile_index.to_string()),
);
}
let mut component_ids = std::collections::BTreeSet::<String>::new();
for (component_index, reference) in profile.documents.iter().enumerate() {
if component_ids.contains(reference.component_id.as_str()) {
return std::result::Result::Err(
composite_semantic_error(document, "component_id values must be unique inside one composite profile")
.with_context("profile_id", profile.profile_id.as_str())
.with_context("component_index", component_index.to_string())
.with_context("component_id", reference.component_id.as_str()),
);
}
component_ids.insert(reference.component_id.clone());
let validation = validate_reference(engine, document, profile, reference, component_index);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
}
}
return std::result::Result::Ok(());
}
fn resolve_composite_document(
engine: &crate::ConfigDocumentEngine,
document: &crate::ConfigJsonDocument,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ResolvedConfigComposite> {
let source = parse_composite_source(document);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (selected_profile_id, selection_source) = match requested_profile {
std::option::Option::Some(value) => (value, crate::ConfigProfileSelectionSource::Explicit),
std::option::Option::None => (source.default_profile.as_str(), crate::ConfigProfileSelectionSource::DefaultProfile),
};
let mut selected: std::option::Option<&CompositeProfileSource> = std::option::Option::None;
for profile in &source.profiles {
if profile.profile_id == selected_profile_id {
selected = std::option::Option::Some(profile);
break;
}
}
let selected = match selected {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_NOT_FOUND, "requested composite Config profile does not exist")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("profile_id", selected_profile_id),
);
},
};
let mut components = std::collections::BTreeMap::<String, ResolvedCompositeComponent>::new();
for reference in &selected.documents {
let file_id = crate::ConfigFileId::new(reference.file_id.as_str());
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = crate::profile::load_resolved_profile_with_source(
engine,
&file_id,
reference.profile_id.as_deref(),
crate::ConfigProfileSelectionSource::Composite,
);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let component = ResolvedCompositeComponent { component_id: reference.component_id.clone(), resolved };
components.insert(reference.component_id.clone(), component);
}
return std::result::Result::Ok(ResolvedConfigComposite {
file_id: document.file_id().clone(),
path: document.path().to_path_buf(),
profile_id: selected_profile_id.to_owned(),
selection_source,
components,
});
}
fn validate_reference(
engine: &crate::ConfigDocumentEngine,
document: &crate::ConfigJsonDocument,
profile: &CompositeProfileSource,
reference: &CompositeDocumentReferenceSource,
component_index: usize,
) -> ksp_core_lib::Result<()> {
if reference.component_id.trim().is_empty() {
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "component_id must not be empty"));
}
let file_id = crate::ConfigFileId::new(reference.file_id.as_str());
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is invalid"));
},
};
if !file_id.as_str().starts_with("cfg.std.") {
return std::result::Result::Err(composite_reference_error(
document,
profile,
reference,
component_index,
"composite references must target standard Config document file_ids",
));
}
let descriptor = engine.registry().descriptor(&file_id);
let descriptor = match descriptor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is not registered"));
},
};
if descriptor.kind() != crate::ConfigFileKind::Config {
return std::result::Result::Err(composite_reference_error(
document,
profile,
reference,
component_index,
"referenced file_id is not a Config document",
));
}
let resolved =
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
if let std::result::Result::Err(error) = resolved {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
fn parse_composite_source(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<CompositeDocumentSource> {
let source = serde_json::from_value::<CompositeDocumentSource>(document.value().clone());
return match source {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
composite_semantic_error(document, "schema-valid composite document cannot be decoded into the KSP source contract").with_source(error),
),
};
}
fn composite_semantic_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "Config composite violates KSP semantic invariants")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn composite_reference_error(
document: &crate::ConfigJsonDocument,
profile: &CompositeProfileSource,
reference: &CompositeDocumentReferenceSource,
component_index: usize,
reason: &'static str,
) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "Config composite contains an invalid document reference")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("profile_id", profile.profile_id.as_str())
.with_context("component_index", component_index.to_string())
.with_context("component_id", reference.component_id.as_str())
.with_context("referenced_file_id", reference.file_id.as_str())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/composite.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 2
// version: 3
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -97,7 +97,7 @@ impl ConfigDocumentEngine {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let semantic_validation = validate_document_semantics(&document);
let semantic_validation = validate_document_semantics(self, &document);
return match semantic_validation {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -207,11 +207,15 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
};
}
fn validate_document_semantics(document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let profile_validation = crate::profile::validate_document_profile_contract(document);
if let std::result::Result::Err(error) = profile_validation {
return std::result::Result::Err(error);
}
let composite_validation = crate::composite::validate_composite_document_contract(engine, document);
if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error);
}
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/error.rs
// version: 4
// version: 5
/// 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");
@@ -36,3 +36,6 @@ pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_co
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");

View File

@@ -1,16 +1,17 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 4
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! KSP-owned application configuration facade.
//!
//! `0.1.3-pre.008` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading and standard-document profile resolution. The standard
//! Logging document is the first registered runtime document. Composite resolution, environment substitution, sensitivity and persistence remain in later bounded
//! prereleases.
//! `0.1.3-pre.009` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution and generic composite
//! resolution by stable `file_id`. The standard Logging document remains the first registered runtime document. Environment substitution, sensitivity and persistence
//! remain in later bounded prereleases.
mod bootstrap;
mod composite;
mod document;
mod error;
mod profile;
@@ -26,6 +27,10 @@ pub use self::bootstrap::ConfigBootstrapOptions;
pub use self::bootstrap::DEFAULT_CFG_PATH;
/// Default root containing KSP JSON schemas.
pub use self::bootstrap::DEFAULT_SCHEMA_PATH;
/// One resolved document component selected by a composite profile.
pub use self::composite::ResolvedCompositeComponent;
/// Validated composite document resolved to one profile and its referenced standard documents.
pub use self::composite::ResolvedConfigComposite;
/// Generic JSON/JSON Schema engine owned by Config.
pub use self::document::ConfigDocumentEngine;
/// A Config-managed JSON document after syntax, schema and current semantic validation.
@@ -34,6 +39,8 @@ pub use self::document::ConfigJsonDocument;
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;
/// Error code used when a composite document contains an invalid or unsupported document reference.
pub use self::error::ERROR_CODE_COMPOSITE_REFERENCE_INVALID;
/// Error code used when a schema-valid Config document violates KSP semantic invariants.
pub use self::error::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID;
/// Error code used when the same logical Config file identifier is registered more than once.
@@ -70,10 +77,14 @@ pub use self::registry::ConfigFileId;
pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the generic composite JSON Schema document.
pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
/// Default physical filename for the standard Logging configuration document.
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
/// Default physical filename for the standard Logging JSON Schema document.
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
/// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard Logging configuration document.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/profile.rs
// version: 1
// version: 2
/// Origin of one top-level value in a resolved standard Config profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -17,6 +17,8 @@ pub enum ConfigProfileSelectionSource {
DefaultProfile,
/// A caller explicitly requested the profile by `profile_id`.
Explicit,
/// A composite document selected the referenced standard document profile.
Composite,
}
/// Validated standard document resolved to one profile while retaining global/profile provenance.
@@ -86,7 +88,7 @@ impl crate::ConfigDocumentEngine {
/// Loads, validates and resolves one standard Config document to its default or explicitly requested profile.
///
/// Passing `None` selects the autonomous `default_profile` declared by the document. Passing `Some(profile_id)` selects that profile explicitly.
/// Environment interpolation and composite overrides are intentionally not applied by this prerelease.
/// Environment interpolation is intentionally not applied by this prerelease.
pub fn load_resolved_profile(
&self,
file_id: &crate::ConfigFileId,
@@ -97,10 +99,32 @@ impl crate::ConfigDocumentEngine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_document_profile(&document, requested_profile);
let source = match requested_profile {
std::option::Option::Some(_) => ConfigProfileSelectionSource::Explicit,
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
};
return resolve_document_profile(&document, requested_profile, source);
}
}
pub(crate) fn load_resolved_profile_with_source(
engine: &crate::ConfigDocumentEngine,
file_id: &crate::ConfigFileId,
requested_profile: std::option::Option<&str>,
explicit_source: ConfigProfileSelectionSource,
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
let document = engine.load_validated_document(file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = match requested_profile {
std::option::Option::Some(_) => explicit_source,
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
};
return resolve_document_profile(&document, requested_profile, source);
}
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,
@@ -158,7 +182,11 @@ pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDoc
return std::result::Result::Ok(());
}
fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ResolvedConfigProfile> {
fn resolve_document_profile(
document: &crate::ConfigJsonDocument,
requested_profile: std::option::Option<&str>,
explicit_source: ConfigProfileSelectionSource,
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires an object document")),
@@ -168,7 +196,7 @@ fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_prof
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires default_profile")),
};
let (selected_profile, selection_source) = match requested_profile {
std::option::Option::Some(value) => (value, ConfigProfileSelectionSource::Explicit),
std::option::Option::Some(value) => (value, explicit_source),
std::option::Option::None => (default_profile, ConfigProfileSelectionSource::DefaultProfile),
};
let profiles = match root.get("profiles").and_then(serde_json::Value::as_array) {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 2
// version: 3
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -7,10 +7,14 @@ pub const ARG_FILE_MAP: &str = "--filemap";
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document.
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -77,7 +81,7 @@ impl ConfigFileDescriptor {
return self.schema_file_id.as_ref();
}
fn new(
pub(crate) fn new(
file_id: &'static str,
kind: ConfigFileKind,
filename: &'static str,
@@ -131,7 +135,13 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema]);
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema, composite_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
@@ -208,7 +218,7 @@ impl ConfigFileRegistry {
}
}
fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors {
let file_id = descriptor.file_id.clone();

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 5
// version: 6
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document and profile-resolution contracts.
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document, profile-resolution and composite contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -116,3 +116,20 @@ fn resolved_profile_contract_is_available_from_crate_root() {
}
}
}
#[test]
fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
let schema_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
assert!(schema_id.is_ok(), "public composite schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id)) = (registry, schema_id) {
let descriptor = registry.descriptor(&schema_id);
assert!(descriptor.is_ok(), "public composite schema descriptor should exist: {descriptor:?}");
if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.filename(), std::path::Path::new(ksp_config_lib::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
assert_eq!(descriptor.kind(), ksp_config_lib::ConfigFileKind::Schema);
}
}
assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);
}

View File

@@ -0,0 +1,247 @@
// file: crates/ksp-config-lib/unit_tests/composite.rs
// version: 1
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
#[test]
fn committed_composite_example_resolves_default_document_profile() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::None);
assert!(resolved.is_ok(), "committed composite example should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "local_default");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
let logging = resolved.component("logging");
assert!(logging.is_some(), "logging component should be resolved");
if let std::option::Option::Some(logging) = logging {
assert_eq!(logging.resolved().file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(logging.resolved().profile_id(), "local_dev");
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(logging.resolved().origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
}
}
}
}
#[test]
fn composite_profile_override_marks_referenced_profile_selection_as_composite() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::Some("local_explicit"));
assert!(resolved.is_ok(), "explicit composite profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
let logging = resolved.component("logging");
assert!(logging.is_some(), "logging component should be resolved");
if let std::option::Option::Some(logging) = logging {
assert_eq!(logging.resolved().profile_id(), "local_dev");
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::Composite);
}
}
}
}
#[test]
fn unknown_composite_profile_has_profile_not_found_error() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_resolved_composite(&file_id, std::option::Option::Some("missing"));
assert!(result.is_err(), "unknown composite profile must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_PROFILE_NOT_FOUND);
}
}
}
#[test]
fn unknown_referenced_file_id_is_rejected_during_composite_validation() {
let fixture = fixture_roots("unknown-reference");
let composite = r#"{
"format_version": 1,
"default_profile": "test",
"profiles": [{
"profile_id": "test",
"documents": [{"component_id": "missing", "file_id": "cfg.std.missing"}]
}]
}"#;
let prepared = prepare_fixture(&fixture, composite);
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_validated_document(&file_id);
assert!(result.is_err(), "unknown composite reference must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
#[test]
fn duplicate_component_ids_are_rejected_inside_one_composite_profile() {
let fixture = fixture_roots("duplicate-component");
let composite = r#"{
"format_version": 1,
"default_profile": "test",
"profiles": [{
"profile_id": "test",
"documents": [
{"component_id": "logging", "file_id": "cfg.std.logging"},
{"component_id": "logging", "file_id": "cfg.std.logging"}
]
}]
}"#;
let prepared = prepare_fixture(&fixture, composite);
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_validated_document(&file_id);
assert!(result.is_err(), "duplicate component_id must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
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 = test_registry(TEST_COMPOSITE_FILENAME);
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn fixture_engine(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = test_registry("composite.test.json");
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate::ConfigFileRegistry> {
let logging = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_STD_LOGGING,
crate::ConfigFileKind::Config,
crate::DEFAULT_STD_LOGGING_FILENAME,
std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_LOGGING),
);
let logging = match logging {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_schema = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_SCHEMA_STD_LOGGING,
crate::ConfigFileKind::Schema,
crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME,
std::option::Option::None,
);
let logging_schema = match logging_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_SCHEMA_COMPOSITE,
crate::ConfigFileKind::Schema,
crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME,
std::option::Option::None,
);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::registry::ConfigFileDescriptor::new(
TEST_COMPOSITE_FILE_ID,
crate::ConfigFileKind::Config,
composite_filename,
std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE),
);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]);
}
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {
cleanup_fixture(fixture);
let config = std::fs::create_dir_all(fixture.config.as_path());
if let std::result::Result::Err(error) = config {
return std::result::Result::Err(error);
}
let schemas = std::fs::create_dir_all(fixture.schemas.as_path());
if let std::result::Result::Err(error) = schemas {
return std::result::Result::Err(error);
}
let workspace = workspace_root();
let logging = std::fs::copy(workspace.join("config/std.logging.json"), fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME));
if let std::result::Result::Err(error) = logging {
return std::result::Result::Err(error);
}
let logging_schema =
std::fs::copy(workspace.join("config/schemas/std.logging.schema.json"), fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = logging_schema {
return std::result::Result::Err(error);
}
let composite_schema =
std::fs::copy(workspace.join("config/schemas/composite.schema.json"), fixture.schemas.join(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = composite_schema {
return std::result::Result::Err(error);
}
return std::fs::write(fixture.config.join("composite.test.json"), composite);
}
fn workspace_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
}
struct FixtureRoots {
root: std::path::PathBuf,
config: std::path::PathBuf,
schemas: std::path::PathBuf,
}
fn fixture_roots(name: &str) -> FixtureRoots {
let root = std::env::temp_dir().join(format!("ksp-config-composite-{name}-{}", std::process::id()));
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
}
fn cleanup_fixture(fixture: &FixtureRoots) {
let removal = std::fs::remove_dir_all(fixture.root.as_path());
if let std::result::Result::Err(error) = removal {
if error.kind() != std::io::ErrorKind::NotFound {
eprintln!("unable to cleanup Config composite fixture {}: {error}", fixture.root.display());
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 2
// version: 3
#[test]
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
@@ -195,3 +195,26 @@ fn absolute_fixture_path() -> std::path::PathBuf {
path.push("ksp-config-lib-absolute-mapping.json");
return path;
}
#[test]
fn defaults_register_generic_composite_schema_without_runtime_composite() {
let registry = super::ConfigFileRegistry::defaults();
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE);
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id), std::result::Result::Ok(runtime_id)) = (registry, schema_id, runtime_id) {
let schema = registry.descriptor(&schema_id);
let runtime = registry.descriptor(&runtime_id);
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
}
if let std::result::Result::Err(error) = runtime {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
}
}
}

307
deltas/0.1.3/pre.009.md Normal file
View File

@@ -0,0 +1,307 @@
<!-- file: deltas/0.1.3/pre.009.md -->
<!-- version: 1 -->
# Delta 0.1.3-pre.009
## Base requise
Livraison précédente validée :
```text
0.1.3-pre.008
```
Version technique de cette base :
```text
workspace.package.version = "0.1.3-pre.8"
Cargo.toml header version = 49
```
Validations utilisateur exécutées le 2026-08-15 :
```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 -e features OK
```
`cargo test --workspace` confirme notamment 33 tests unitaires + 6 tests publics pour `ksp-config-lib`. `cargo tree -p ksp-config-lib -d` ne signale que la coexistence transitive déjà connue `syn 2.0.119` / `syn 3.0.3` portée par l'écosystème `jsonschema`; aucune correction de dépendance KSP n'est justifiée dans cette tranche.
## Objet de pre.009
Introduire le contrat de composition générique avant la résolution environnement :
```text
composite file_id
-> schema.composite
-> composite profile
-> document references by file_id
-> default or composite-selected document profile
-> ResolvedConfigProfile per component
-> preserve Global/Profile provenance
```
Aucun composite runtime d'application fictif n'est créé.
## `schema.composite`
Le registre Config connaît désormais :
```text
schema.composite -> composite.schema.json
```
Le mapping est résolu sous `schemapath` et peut être remplacé comme les autres fichiers connus :
```text
--filemap=schema.composite=my.composite.schema.json
```
En revanche, le registre par défaut n'introduit aucun :
```text
cfg.composite.<consumer>
```
Un tel descriptor sera ajouté seulement lorsqu'un consumer concret existera.
## Contrat composite
Le schema générique versionné est :
```text
config/schemas/composite.schema.json
```
Un exemple non runtime est fourni sous :
```text
config/examples/composite.example.json
```
Structure :
```text
format_version
default_profile
profiles[]
profile_id
documents[]
component_id
file_id
profile_id? # optionnel
```
Règles :
- le composite utilise le même modèle `default_profile` / `profiles` que les documents standards ;
- `component_id` est unique à l'intérieur d'un profil composite ;
- une référence cible exclusivement un `cfg.std.*` connu du registre ;
- une référence ne contient jamais de filename physique ;
- l'imbrication de composites n'est pas ouverte dans cette première surface ;
- `profile_id` absent conserve le `default_profile` autonome du document référencé ;
- `profile_id` présent impose ce profil depuis la composition.
## Résolution publique
`ConfigDocumentEngine` expose maintenant :
```text
load_resolved_composite(file_id, requested_profile)
```
Le caller peut sélectionner :
```text
None -> default_profile du composite
Some(profile_id) -> profil composite explicite
```
Le résultat `ResolvedConfigComposite` conserve :
```text
file_id du composite
path source
profile_id composite sélectionné
selection_source
components par component_id
```
Chaque `ResolvedCompositeComponent` conserve le `ResolvedConfigProfile` du document standard référencé.
Ainsi les informations déjà stabilisées en `pre.008` restent disponibles :
```text
globals
profile
effective
origin(key) = Global | Profile
```
## Provenance de sélection
`ConfigProfileSelectionSource` possède désormais :
```text
DefaultProfile
Explicit
Composite
```
Lorsqu'un composite déclare explicitement :
```json
{
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
}
```
le `ResolvedConfigProfile` correspondant porte :
```text
selection_source = Composite
```
Si `profile_id` est absent, le document référencé conserve :
```text
selection_source = DefaultProfile
```
La provenance `Global/Profile` de chaque valeur top-level n'est pas remplacée par cette provenance de sélection.
## Validation sémantique
Un composite enregistré est soumis à :
1. JSON syntax ;
2. `schema.composite` ;
3. contrat générique `default_profile` / `profiles` ;
4. unicité des `component_id` par profil ;
5. validité et enregistrement des `file_id` référencés ;
6. restriction aux documents standards `cfg.std.*` ;
7. validation/résolution du profil référencé, y compris le `default_profile` lorsqu'aucun override n'est fourni.
Un document référencé invalide ou inconnu utilise le nouveau diagnostic :
```text
config.composite_reference_invalid
```
Un profil document ou composite demandé mais absent conserve :
```text
config.profile_not_found
```
## Tests
Les nouveaux tests couvrent notamment :
- enregistrement de `schema.composite` sans composite runtime fictif ;
- validation du schema/example composite versionné ;
- résolution du profil composite par défaut ;
- utilisation du `default_profile` du document référencé ;
- sélection explicite d'un profil composite ;
- provenance `Composite` lorsqu'un profil documentaire est imposé par le composite ;
- profil composite inconnu ;
- référence vers un `file_id` standard inconnu ;
- duplication de `component_id` dans un profil composite ;
- disponibilité des constantes/schema/provenance depuis le crate-root.
Les tests de résolution utilisent un descriptor composite privé à la crate pointant vers l'exemple versionné ; il ne fait pas partie de `ConfigFileRegistry::defaults()`.
## Hors scope
Cette tranche n'introduit pas encore :
- composite runtime d'une application réelle ;
- `.env` ;
- lecture de l'environnement du processus ;
- interpolation `${NAME}` / `${NAME:-fallback}` ;
- `Public/Internal/Secret` ;
- redaction ;
- adaptation vers `ksp_logging_lib::LoggingSettings` ;
- mutation/persistence.
## Dépendances
Aucune dépendance externe ou feature Cargo supplémentaire n'est ajoutée.
`ksp-config-lib` ne dépend toujours pas de `ksp-logging-lib` dans cette tranche.
## Version technique
La prerelease devient :
```text
workspace.package.version = "0.1.3-pre.9"
Cargo.toml header version = 50
```
## Fichiers ajoutés
```text
config/examples/composite.example.json
config/schemas/composite.schema.json
crates/ksp-config-lib/src/composite.rs
crates/ksp-config-lib/unit_tests/composite.rs
deltas/0.1.3/pre.009.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-config-lib/src/document.rs
crates/ksp-config-lib/src/error.rs
crates/ksp-config-lib/src/lib.rs
crates/ksp-config-lib/src/profile.rs
crates/ksp-config-lib/src/registry.rs
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/unit_tests/registry.rs
docs/plans/000-README.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
docs/rules/FILE_CONTRACTS.md
```
## Fichiers supprimés
Aucun.
## Contrôles exécutés dans l'environnement de génération
- parsing JSON du schema et de l'exemple composite ;
- validation du schema Draft 2020-12 et de l'exemple avec l'implémentation JSON Schema disponible dans l'environnement ;
- comparaison du delta avec la base `pre.008` ;
- contrôle des headers/version modifiés ;
- contrôle de l'absence de nouvelle dépendance Cargo ;
- contrôle statique des interdictions `unsafe`, `unwrap`, `expect`, `panic`, opérateur `?` et accès environnement applicatif dans les nouveaux sources de production ;
- contrôle de la composition exacte de l'archive delta ;
- réapplication du delta sur une copie de `pre.008` pour vérifier la reproduction de l'arbre livré.
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Aucune validation Rust n'est déclarée réussie ici.
## Validations utilisateur à exécuter
```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
```
Après validation de `pre.009`, la prochaine tranche planifiée est :
```text
0.1.3-pre.010 — .env + process env + resolver ${...}
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Plans KSP
@@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ;
- [`003-V0_1_1_CORE_FOUNDATION_PLAN.md`](003-V0_1_1_CORE_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.1`, établi par `0.1.1-pre.001` puis consolidé jusqu'à `0.1.1-rel.001`.
- [`004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](004-V0_1_2_LOGGING_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.2`, établi par `0.1.2-pre.001` puis consolidé jusqu'à `0.1.2-rel.001`.
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, puis `pre.008` la résolution globals/profils/`default_profile`; `pre.009` poursuivra avec les compositions génériques par `file_id`.
- [`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` poursuivra avec `.env`, process env et le resolver `${...}`.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# 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 1520 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` ajoute la résolution générique globals/profils/`default_profile` avec provenance top-level. Après validation utilisateur, `pre.009` introduira les compositions génériques par `file_id`.
`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` ajoute les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif. Après validation utilisateur, `pre.010` introduira `.env`, process env et le resolver `${...}`.
## `0.1.4` — Config desktop par défaut

View File

@@ -1,11 +1,11 @@
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Plan `0.1.3` — Configuration foundation
## 1. Statut et objectif
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et `pre.008` la résolution des globals/profils/`default_profile`. La prochaine tranche est `pre.009` pour les compositions génériques par `file_id`.
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` et `pre.009` les compositions génériques par `file_id`. La prochaine tranche est `pre.010` pour `.env`, process env et le resolver `${...}`.
La base auditée reste la release stable `v0.1.2`.
@@ -409,7 +409,30 @@ config/composite.ksp-app-wallet-desk.json
Leur identité logique reste `cfg.composite.<consumer>`; le filename peut être remplacé par `--filemap` sans modifier leur contenu logique.
Aucun composite runtime fictif n'est créé avant l'existence d'un consumer concret. Le moteur et le schéma génériques peuvent néanmoins être testés dans `0.1.3` par fixtures/examples.
Aucun composite runtime fictif n'est créé avant l'existence d'un consumer concret. Le moteur et le schéma génériques sont exercés par `config/examples/composite.example.json` et par un descriptor de test privé à `ksp-config-lib`.
Contrat générique livré en `pre.009` :
```json
{
"format_version": 1,
"default_profile": "local",
"profiles": [
{
"profile_id": "local",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
}
]
}
]
}
```
`component_id` est local au profil composite et unique dans celui-ci. `file_id` référence exclusivement un document standard `cfg.std.*` connu du registre. `profile_id` est optionnel : absent, il conserve le `default_profile` autonome du document référencé ; présent, il sélectionne explicitement ce profil au nom du composite. Les composites ne référencent jamais les filenames physiques et ne s'imbriquent pas dans cette première surface.
### 6.6 Schémas
@@ -419,7 +442,7 @@ Les schémas appartiennent par défaut sous :
config/schemas/
```
Première surface candidate :
Surface actuelle :
```text
config/schemas/std.logging.schema.json
@@ -1740,12 +1763,21 @@ La validation utilisateur de `pre.007` est acquise : `fmt/check/clippy/test` pas
### `0.1.3-pre.009` — compositions génériques par `file_id`
- contrat composite générique ;
- schema/example composite ;
- références de documents exclusivement par `file_id` ;
- sélection/remplacement de profils documentaires ;
- résolution composition -> document -> profil/global ;
- aucun composite runtime fictif obligatoire.
Tranche livrée :
- `schema.composite` est enregistré par défaut vers `config/schemas/composite.schema.json` et reste surchargeable par `--filemap` ;
- aucun `cfg.composite.<consumer>` nest enregistré tant quun consumer concret nexiste pas ;
- `config/examples/composite.example.json` exerce le contrat sans devenir une source runtime implicite ;
- un composite utilise `format_version`, `default_profile` et `profiles[]` comme les documents standards profilés ;
- chaque profil composite contient `documents[]` avec `component_id`, `file_id` et `profile_id` optionnel ;
- `component_id` est unique dans un profil composite ;
- les références ciblent exclusivement des `cfg.std.*` déjà enregistrés, jamais des filenames ni dautres composites ;
- `profile_id` absent laisse le document référencé choisir son `default_profile` autonome ;
- `profile_id` présent impose ce profil et marque sa source de sélection `ConfigProfileSelectionSource::Composite` ;
- `ResolvedConfigComposite` conserve le composite sélectionné et ses `ResolvedCompositeComponent`, chacun portant le `ResolvedConfigProfile` complet du document référencé ;
- les provenances `Global` / `Profile` du document spécialisé sont donc conservées sous la composition ;
- `ConfigDocumentEngine::load_resolved_composite(file_id, requested_profile)` est prêt pour les futurs descriptors `cfg.composite.<consumer>` ;
- les tests utilisent un descriptor composite privé à la crate pointant vers lexemple versionné afin de valider la résolution complète sans créer un composite runtime fictif.
### `0.1.3-pre.010` — `.env` + process env + resolver `${...}`

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/FILE_CONTRACTS.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Contrats des fichiers
@@ -38,11 +38,12 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
## Répertoire `config/`
| Fichier/famille | Responsabilité | Règle de modification |
|--------------------------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `config/std.<domain>.json` | Document runtime spécialisé Config possédé par `ksp-config-lib`. | Identifié par un `file_id` stable, validé par son schema enregistré et lu/modifié uniquement via Config. Comme JSON ne porte pas de commentaires de header, la version du format appartient au champ JSON `format_version`. |
| `config/schemas/*.schema.json` | JSON Schema des documents Config gérés. | Identifié par un `file_id` `schema.*`; le schema doit être valide pour le draft déclaré avant validation d'une instance. Aucun secret/runtime local ne doit y apparaître. |
| `config/examples/*.json` | Exemples versionnés séparés des vrais fichiers runtime. | Doivent rester schema-valides et illustratifs ; ils ne constituent jamais une source runtime implicite. |
| Fichier/famille | Responsabilité | Règle de modification |
|------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `config/std.<domain>.json` | Document runtime spécialisé Config possédé par `ksp-config-lib`. | Identifié par un `file_id` stable, validé par son schema enregistré et lu/modifié uniquement via Config. Comme JSON ne porte pas de commentaires de header, la version du format appartient au champ JSON `format_version`. |
| `config/composite.<consumer>.json` | Composition runtime propre à un consumer concret. | Référence uniquement des documents standards par `file_id`; son descriptor référence `schema.composite` tant quun schema spécialisé nest pas requis. Aucun composite runtime fictif nest créé avant lexistence du consumer. |
| `config/schemas/*.schema.json` | JSON Schema des documents Config gérés. | Identifié par un `file_id` `schema.*`; le schema doit être valide pour le draft déclaré avant validation d'une instance. Aucun secret/runtime local ne doit y apparaître. |
| `config/examples/*.json` | Exemples versionnés séparés des vrais fichiers runtime. | Doivent rester schema-valides et illustratifs ; ils ne constituent jamais une source runtime implicite. |
Les noms physiques sont remplaçables via le registre Config lorsque le contrat le permet ; les consumers référencent les documents par `file_id`, pas par filename.