v0.1.3-pre.008
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 48
|
||||
# version: 49
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.7"
|
||||
version = "0.1.3-pre.8"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/document.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -208,6 +208,10 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
|
||||
}
|
||||
|
||||
fn validate_document_semantics(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);
|
||||
}
|
||||
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// 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");
|
||||
@@ -33,3 +33,6 @@ pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_cor
|
||||
|
||||
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
|
||||
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
|
||||
|
||||
/// 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");
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.007` owns the non-recursive bootstrap roots, stable logical file registry and first generic JSON/JSON Schema loading pipeline. The standard
|
||||
//! Logging document is the first registered runtime document. Profile resolution, environment substitution, sensitivity and persistence remain in later bounded
|
||||
//! `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.
|
||||
|
||||
mod bootstrap;
|
||||
mod document;
|
||||
mod error;
|
||||
mod profile;
|
||||
mod registry;
|
||||
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
@@ -47,10 +48,18 @@ pub use self::error::ERROR_CODE_FILE_MAPPING_INVALID;
|
||||
pub use self::error::ERROR_CODE_JSON_FILE_READ_FAILED;
|
||||
/// Error code used when a Config-managed file contains invalid JSON syntax.
|
||||
pub use self::error::ERROR_CODE_JSON_SYNTAX_INVALID;
|
||||
/// Error code used when an explicitly requested Config profile does not exist.
|
||||
pub use self::error::ERROR_CODE_PROFILE_NOT_FOUND;
|
||||
/// Error code used when a JSON Schema document is itself invalid.
|
||||
pub use self::error::ERROR_CODE_SCHEMA_INVALID;
|
||||
/// Error code used when a Config document fails its registered JSON Schema validation.
|
||||
pub use self::error::ERROR_CODE_SCHEMA_VALIDATION_FAILED;
|
||||
/// Source that selected an effective standard Config profile.
|
||||
pub use self::profile::ConfigProfileSelectionSource;
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
pub use self::profile::ConfigValueOrigin;
|
||||
/// Validated standard Config document resolved to one profile with global/profile provenance.
|
||||
pub use self::profile::ResolvedConfigProfile;
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub use self::registry::ARG_FILE_MAP;
|
||||
/// Logical descriptor associating a stable file identifier with its physical filename and validation schema.
|
||||
|
||||
236
crates/ksp-config-lib/src/profile.rs
Normal file
236
crates/ksp-config-lib/src/profile.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
// file: crates/ksp-config-lib/src/profile.rs
|
||||
// version: 1
|
||||
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ConfigValueOrigin {
|
||||
/// Value comes from the global section of the specialized document.
|
||||
Global,
|
||||
/// Value comes from the selected profile object.
|
||||
Profile,
|
||||
}
|
||||
|
||||
/// Source that selected the effective profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ConfigProfileSelectionSource {
|
||||
/// The document's autonomous `default_profile` selected the profile.
|
||||
DefaultProfile,
|
||||
/// A caller explicitly requested the profile by `profile_id`.
|
||||
Explicit,
|
||||
}
|
||||
|
||||
/// Validated standard document resolved to one profile while retaining global/profile provenance.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ResolvedConfigProfile {
|
||||
file_id: crate::ConfigFileId,
|
||||
path: std::path::PathBuf,
|
||||
profile_id: String,
|
||||
selection_source: ConfigProfileSelectionSource,
|
||||
globals: serde_json::Map<String, serde_json::Value>,
|
||||
profile: serde_json::Map<String, serde_json::Value>,
|
||||
effective: serde_json::Map<String, serde_json::Value>,
|
||||
origins: std::collections::BTreeMap<String, ConfigValueOrigin>,
|
||||
}
|
||||
|
||||
impl ResolvedConfigProfile {
|
||||
/// Returns the logical document identifier from which this profile was resolved.
|
||||
#[must_use]
|
||||
pub fn file_id(&self) -> &crate::ConfigFileId {
|
||||
return &self.file_id;
|
||||
}
|
||||
|
||||
/// Returns the physical path of the validated source document.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &std::path::Path {
|
||||
return self.path.as_path();
|
||||
}
|
||||
|
||||
/// Returns the selected unique profile identifier.
|
||||
#[must_use]
|
||||
pub fn profile_id(&self) -> &str {
|
||||
return self.profile_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether selection came from `default_profile` or an explicit caller request.
|
||||
#[must_use]
|
||||
pub const fn selection_source(&self) -> ConfigProfileSelectionSource {
|
||||
return self.selection_source;
|
||||
}
|
||||
|
||||
/// Returns document-global values, excluding the reserved `default_profile` and `profiles` keys.
|
||||
#[must_use]
|
||||
pub fn globals(&self) -> &serde_json::Map<String, serde_json::Value> {
|
||||
return &self.globals;
|
||||
}
|
||||
|
||||
/// Returns the selected profile object including its `profile_id`.
|
||||
#[must_use]
|
||||
pub fn profile(&self) -> &serde_json::Map<String, serde_json::Value> {
|
||||
return &self.profile;
|
||||
}
|
||||
|
||||
/// Returns a deterministic top-level effective view in which selected profile keys override same-named global keys.
|
||||
#[must_use]
|
||||
pub fn effective(&self) -> &serde_json::Map<String, serde_json::Value> {
|
||||
return &self.effective;
|
||||
}
|
||||
|
||||
/// Returns the top-level provenance for an effective key.
|
||||
#[must_use]
|
||||
pub fn origin(&self, key: &str) -> std::option::Option<ConfigValueOrigin> {
|
||||
return self.origins.get(key).copied();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn load_resolved_profile(
|
||||
&self,
|
||||
file_id: &crate::ConfigFileId,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
||||
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),
|
||||
};
|
||||
return resolve_document_profile(&document, requested_profile);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
std::option::Option::None => return std::result::Result::Ok(()),
|
||||
};
|
||||
let default_profile = root.get("default_profile");
|
||||
let profiles = root.get("profiles");
|
||||
if default_profile.is_none() && profiles.is_none() {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let default_profile = match default_profile.and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => return std::result::Result::Err(profile_semantic_error(document, "default_profile must identify a non-empty profile_id")),
|
||||
};
|
||||
let profiles = match profiles.and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return std::result::Result::Err(profile_semantic_error(document, "profiles must contain at least one profile object")),
|
||||
};
|
||||
let mut ids = std::collections::BTreeSet::<String>::new();
|
||||
let mut default_found = false;
|
||||
for (index, profile) in profiles.iter().enumerate() {
|
||||
let object = match profile.as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
profile_semantic_error(document, "profile entry must be an object").with_context("profile_index", index.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let profile_id = match object.get("profile_id").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
profile_semantic_error(document, "profile entry must declare a non-empty profile_id").with_context("profile_index", index.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
if ids.contains(profile_id) {
|
||||
return std::result::Result::Err(
|
||||
profile_semantic_error(document, "profile_id values must be unique")
|
||||
.with_context("profile_index", index.to_string())
|
||||
.with_context("profile_id", profile_id),
|
||||
);
|
||||
}
|
||||
ids.insert(profile_id.to_owned());
|
||||
if profile_id == default_profile {
|
||||
default_found = true;
|
||||
}
|
||||
}
|
||||
if !default_found {
|
||||
return std::result::Result::Err(
|
||||
profile_semantic_error(document, "default_profile must reference an existing profile_id").with_context("default_profile", default_profile),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>) -> 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")),
|
||||
};
|
||||
let default_profile = match root.get("default_profile").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
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::None => (default_profile, ConfigProfileSelectionSource::DefaultProfile),
|
||||
};
|
||||
let profiles = match root.get("profiles").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires profiles")),
|
||||
};
|
||||
let mut selected: std::option::Option<&serde_json::Map<String, serde_json::Value>> = std::option::Option::None;
|
||||
for profile in profiles {
|
||||
let object = match profile.as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if object.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some(selected_profile) {
|
||||
selected = std::option::Option::Some(object);
|
||||
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 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),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut globals = serde_json::Map::<String, serde_json::Value>::new();
|
||||
let mut effective = serde_json::Map::<String, serde_json::Value>::new();
|
||||
let mut origins = std::collections::BTreeMap::<String, ConfigValueOrigin>::new();
|
||||
for (key, value) in root {
|
||||
if key != "default_profile" && key != "profiles" {
|
||||
globals.insert(key.clone(), value.clone());
|
||||
effective.insert(key.clone(), value.clone());
|
||||
origins.insert(key.clone(), ConfigValueOrigin::Global);
|
||||
}
|
||||
}
|
||||
let profile = (*selected).clone();
|
||||
for (key, value) in &profile {
|
||||
effective.insert(key.clone(), value.clone());
|
||||
origins.insert(key.clone(), ConfigValueOrigin::Profile);
|
||||
}
|
||||
return std::result::Result::Ok(ResolvedConfigProfile {
|
||||
file_id: document.file_id().clone(),
|
||||
path: document.path().to_path_buf(),
|
||||
profile_id: selected_profile.to_owned(),
|
||||
selection_source,
|
||||
globals,
|
||||
profile,
|
||||
effective,
|
||||
origins,
|
||||
});
|
||||
}
|
||||
|
||||
fn profile_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 document violates KSP profile invariants")
|
||||
.with_context("file_id", document.file_id().as_str())
|
||||
.with_context("path", document.path().to_string_lossy().into_owned())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/profile.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, logical file registry and validated JSON document contracts.
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document and profile-resolution contracts.
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
@@ -92,3 +92,27 @@ fn validated_json_document_engine_is_available_from_crate_root() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_profile_contract_is_available_from_crate_root() {
|
||||
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert!(bootstrap.is_ok(), "public bootstrap should accept committed Config roots: {bootstrap:?}");
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
assert!(file_id.is_ok(), "public Logging file_id should remain constructible: {file_id:?}");
|
||||
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
|
||||
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let resolved = engine.load_resolved_profile(&file_id, std::option::Option::None);
|
||||
assert!(resolved.is_ok(), "public profile resolver should resolve committed default profile: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "local_dev");
|
||||
assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Global));
|
||||
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Profile));
|
||||
assert!(!resolved.effective().contains_key("default_profile"));
|
||||
assert!(!resolved.effective().contains_key("profiles"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/document.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
|
||||
@@ -143,6 +143,85 @@ fn schema_valid_logging_document_can_still_fail_ksp_semantics() {
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_profile_ids_are_semantically_invalid() {
|
||||
let fixture = fixture_roots("duplicate-profile-id");
|
||||
let workspace = workspace_root();
|
||||
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
|
||||
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
|
||||
if let std::result::Result::Ok(schema_source) = schema_source {
|
||||
let source = valid_logging_source_with_profiles("first", "first", "first");
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
|
||||
assert!(prepared.is_ok(), "duplicate profile fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
||||
}
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_default_profile_target_is_semantically_invalid() {
|
||||
let fixture = fixture_roots("missing-default-profile");
|
||||
let workspace = workspace_root();
|
||||
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
|
||||
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
|
||||
if let std::result::Result::Ok(schema_source) = schema_source {
|
||||
let source = valid_logging_source_with_profiles("missing", "first", "second");
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
|
||||
assert!(prepared.is_ok(), "missing default profile fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
||||
}
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
fn valid_logging_source_with_profiles(default_profile: &str, first_profile: &str, second_profile: &str) -> String {
|
||||
return format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"logs_directory": "logs",
|
||||
"default_profile": "{default_profile}",
|
||||
"profiles": [
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}}"#,
|
||||
valid_logging_profile(first_profile, "file.first"),
|
||||
valid_logging_profile(second_profile, "file.second")
|
||||
);
|
||||
}
|
||||
|
||||
fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
|
||||
return format!(
|
||||
r#"{{
|
||||
"profile_id": "{profile_id}",
|
||||
"default_filter": "info",
|
||||
"span_events": "off",
|
||||
"console": {{
|
||||
"enabled": false,
|
||||
"output": "stderr",
|
||||
"ansi": false,
|
||||
"format": "human",
|
||||
"filter": {{"level": "trace", "targets": ["*"], "domains": ["*"]}}
|
||||
}},
|
||||
"files": [{{
|
||||
"output_id": "{output_id}",
|
||||
"enabled": true,
|
||||
"path": "output.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"filter": {{"level": "info", "targets": ["*"], "domains": ["*"]}}
|
||||
}}],
|
||||
"target_filters": []
|
||||
}}"#
|
||||
);
|
||||
}
|
||||
|
||||
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJsonDocument> {
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
|
||||
let bootstrap = match bootstrap {
|
||||
@@ -212,7 +291,7 @@ struct FixtureRoots {
|
||||
|
||||
fn fixture_roots(name: &str) -> FixtureRoots {
|
||||
let mut root = std::env::temp_dir();
|
||||
root.push(format!("ksp-config-lib-pre007-{name}-{}", std::process::id()));
|
||||
root.push(format!("ksp-config-lib-pre008-{name}-{}", std::process::id()));
|
||||
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
|
||||
}
|
||||
|
||||
|
||||
70
crates/ksp-config-lib/unit_tests/profile.rs
Normal file
70
crates/ksp-config-lib/unit_tests/profile.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/profile.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn committed_default_profile_resolves_globals_profile_and_provenance() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::None);
|
||||
assert!(resolved.is_ok(), "default profile should resolve: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "local_dev");
|
||||
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(resolved.globals().get("logs_directory").and_then(serde_json::Value::as_str), std::option::Option::Some("${KSP_LOGS_DIRECTORY:-logs}"));
|
||||
assert_eq!(resolved.profile().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("warn"));
|
||||
assert_eq!(resolved.effective().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("warn"));
|
||||
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(super::ConfigValueOrigin::Global));
|
||||
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(super::ConfigValueOrigin::Profile));
|
||||
assert!(!resolved.effective().contains_key("default_profile"));
|
||||
assert!(!resolved.effective().contains_key("profiles"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_profile_selection_is_distinct_from_default_selection() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::Some("local_dev"));
|
||||
assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "local_dev");
|
||||
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::Explicit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_explicit_profile_has_distinct_error_code() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::Some("does-not-exist"));
|
||||
assert!(result.is_err(), "unknown explicit profile must be rejected: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_PROFILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
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),
|
||||
};
|
||||
}
|
||||
268
deltas/0.1.3/pre.008.md
Normal file
268
deltas/0.1.3/pre.008.md
Normal file
@@ -0,0 +1,268 @@
|
||||
<!-- file: deltas/0.1.3/pre.008.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.008
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.007
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.7"
|
||||
Cargo.toml header version = 48
|
||||
```
|
||||
|
||||
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 tree -p ksp-config-lib -d` ne signale qu'une coexistence transitive `syn 2.0.119` / `syn 3.0.3`, portée par l'écosystème `jsonschema`/ICU/proc-macros. Le graphe observé ne contient pas de stack HTTP/TLS de résolution distante avec `jsonschema default-features = false`; aucun changement de dépendance n'est donc justifié dans cette tranche.
|
||||
|
||||
## Objet de pre.008
|
||||
|
||||
Fermer la résolution des paramètres globaux, profils et `default_profile` avant d'introduire les composites :
|
||||
|
||||
```text
|
||||
validated standard document
|
||||
-> validate profile identities/default_profile
|
||||
-> select default or explicit profile
|
||||
-> retain globals
|
||||
-> retain selected profile
|
||||
-> build effective top-level view
|
||||
-> preserve Global/Profile provenance
|
||||
```
|
||||
|
||||
Cette tranche reste générique : le moteur de profils n'est pas un type Logging. `std.logging.json` est uniquement le premier document standard qui l'exerce.
|
||||
|
||||
## Contrat générique des profils
|
||||
|
||||
Un document standard profilé possède deux clés structurelles réservées :
|
||||
|
||||
```text
|
||||
default_profile
|
||||
profiles
|
||||
```
|
||||
|
||||
Toutes les autres propriétés top-level sont considérées comme des paramètres globaux du document.
|
||||
|
||||
Garanties désormais validées après JSON Schema :
|
||||
|
||||
- `profiles` contient au moins un objet ;
|
||||
- chaque profil possède un `profile_id` non vide ;
|
||||
- les `profile_id` sont uniques dans le document ;
|
||||
- `default_profile` est autonome au niveau global ;
|
||||
- `default_profile` référence obligatoirement un `profile_id` existant.
|
||||
|
||||
Un document qui viole ces invariants retourne :
|
||||
|
||||
```text
|
||||
config.document_semantic_invalid
|
||||
```
|
||||
|
||||
## Résolution publique
|
||||
|
||||
`ConfigDocumentEngine` expose maintenant :
|
||||
|
||||
```text
|
||||
load_resolved_profile(file_id, None)
|
||||
-> sélectionne default_profile
|
||||
|
||||
load_resolved_profile(file_id, Some(profile_id))
|
||||
-> sélection explicite
|
||||
```
|
||||
|
||||
Un profil explicitement demandé mais absent retourne un diagnostic distinct :
|
||||
|
||||
```text
|
||||
config.profile_not_found
|
||||
```
|
||||
|
||||
Cette erreur ne signifie pas que le document source est invalide : le document peut être parfaitement valide tout en ne contenant pas le profil demandé par le caller.
|
||||
|
||||
## `ResolvedConfigProfile`
|
||||
|
||||
La nouvelle surface publique conserve :
|
||||
|
||||
```text
|
||||
file_id
|
||||
source path
|
||||
profile_id sélectionné
|
||||
selection_source = DefaultProfile | Explicit
|
||||
globals
|
||||
profile
|
||||
effective
|
||||
origin(key) = Global | Profile
|
||||
```
|
||||
|
||||
`globals` exclut volontairement :
|
||||
|
||||
```text
|
||||
default_profile
|
||||
profiles
|
||||
```
|
||||
|
||||
`profile` conserve l'objet du profil sélectionné, y compris son `profile_id`.
|
||||
|
||||
`effective` est une vue top-level déterministe :
|
||||
|
||||
1. insertion des globals ;
|
||||
2. insertion des propriétés du profil sélectionné.
|
||||
|
||||
Si un futur schema permet une même clé aux deux niveaux, la valeur du profil est prioritaire et sa provenance devient `Profile`. Les schemas spécialisés restent libres d'interdire ce cas lorsqu'une propriété doit rester strictement globale.
|
||||
|
||||
La provenance documentaire commune reste disponible par :
|
||||
|
||||
```text
|
||||
file_id
|
||||
path
|
||||
```
|
||||
|
||||
et la provenance de chaque valeur top-level par :
|
||||
|
||||
```text
|
||||
ConfigValueOrigin::Global
|
||||
ConfigValueOrigin::Profile
|
||||
```
|
||||
|
||||
Les futures couches composite/env/sensibilité devront enrichir cette provenance sans la perdre.
|
||||
|
||||
## `std.logging.json`
|
||||
|
||||
Le fichier runtime et son schema ne nécessitent aucun changement dans cette tranche.
|
||||
|
||||
Le document commité :
|
||||
|
||||
```text
|
||||
default_profile = local_dev
|
||||
```
|
||||
|
||||
résout maintenant réellement le profil `local_dev`.
|
||||
|
||||
Exemples de provenance :
|
||||
|
||||
```text
|
||||
logs_directory -> Global
|
||||
default_filter -> Profile
|
||||
```
|
||||
|
||||
Le placeholder :
|
||||
|
||||
```text
|
||||
${KSP_LOGS_DIRECTORY:-logs}
|
||||
```
|
||||
|
||||
reste non interprété en `pre.008`; il sera résolu par la tranche environnement prévue plus tard.
|
||||
|
||||
## Tests
|
||||
|
||||
Les tests ajoutés couvrent :
|
||||
|
||||
- résolution du `default_profile` commité ;
|
||||
- sélection explicite d'un profil existant ;
|
||||
- distinction `DefaultProfile` / `Explicit` ;
|
||||
- conservation séparée des globals et du profil ;
|
||||
- construction de la vue effective ;
|
||||
- provenance `Global` / `Profile` ;
|
||||
- profil explicite inconnu -> `config.profile_not_found` ;
|
||||
- `profile_id` dupliqués -> document sémantiquement invalide ;
|
||||
- `default_profile` ne référençant aucun profil -> document sémantiquement invalide ;
|
||||
- consommation de la nouvelle surface depuis le crate-root dans le test d'intégration public.
|
||||
|
||||
## Hors scope
|
||||
|
||||
Cette tranche n'introduit pas encore :
|
||||
|
||||
- document composite ;
|
||||
- `schema.composite` ;
|
||||
- override de profil depuis un composite ;
|
||||
- `.env` ;
|
||||
- lecture `std::env` applicative ;
|
||||
- interpolation `${...}` ;
|
||||
- `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.8"
|
||||
Cargo.toml header version = 49
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-config-lib/src/profile.rs
|
||||
crates/ksp-config-lib/unit_tests/profile.rs
|
||||
deltas/0.1.3/pre.008.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/tests/public_api.rs
|
||||
crates/ksp-config-lib/unit_tests/document.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
|
||||
|
||||
- comparaison du delta avec la base `pre.007` ;
|
||||
- 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`, opérateur `?` 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.007` 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.008`, la prochaine tranche planifiée est :
|
||||
|
||||
```text
|
||||
0.1.3-pre.009 — compositions génériques par file_id
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# 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` et `pre.007` livre maintenant le moteur JSON/JSON Schema et le premier `std.logging.json`; `pre.008` poursuit avec globals/profils/`default_profile`.
|
||||
- [`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`.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 13 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -219,7 +219,7 @@ pre.015 clôture
|
||||
|
||||
Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 minutes de travail effectif.
|
||||
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` livre le moteur JSON/JSON Schema et le premier document `std.logging.json`. Après validation utilisateur de `pre.007`, la prochaine tranche est `pre.008` pour globals, profils et `default_profile`.
|
||||
`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`.
|
||||
|
||||
## `0.1.4` — Config desktop par défaut
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Plan `0.1.3` — Configuration foundation
|
||||
|
||||
## 1. Statut et objectif
|
||||
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain` et `pre.007` introduit le moteur JSON/JSON Schema ainsi que le premier document runtime `std.logging.json`. La prochaine tranche est `pre.008` pour la résolution des globals/profils/`default_profile`.
|
||||
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`.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -1722,13 +1722,21 @@ Aucune dépendance à `ksp-logging-lib` n'est encore nécessaire dans Config : l
|
||||
|
||||
### `0.1.3-pre.008` — globals + profils + `default_profile`
|
||||
|
||||
- paramètres globaux hors profils ;
|
||||
- `profile_id` unique ;
|
||||
- `default_profile` autonome ;
|
||||
- sélection/résolution d'un profil spécialisé ;
|
||||
- héritage/usage des paramètres globaux par les profils selon le contrat retenu ;
|
||||
- provenance document/global/profile ;
|
||||
- tests sur `std.logging.json`.
|
||||
Tranche livrée :
|
||||
|
||||
- tout document standard possédant `default_profile` + `profiles` est soumis au contrat générique de profils Config ;
|
||||
- les `profile_id` doivent être uniques dans le document ;
|
||||
- `default_profile` reste une propriété globale autonome et doit référencer exactement un `profile_id` existant ;
|
||||
- `ConfigDocumentEngine::load_resolved_profile(file_id, requested_profile)` sélectionne soit le profil par défaut (`None`), soit un profil explicite (`Some(profile_id)`) ;
|
||||
- une sélection explicite inconnue retourne `config.profile_not_found` sans rendre le document source invalide ;
|
||||
- `ResolvedConfigProfile` conserve le `file_id`, le path source, le `profile_id` sélectionné, la source de sélection, les globals, le profil et une vue effective ;
|
||||
- les globals sont toutes les propriétés top-level hors clés réservées `default_profile` et `profiles` ;
|
||||
- la vue effective fusionne les globals puis les propriétés du profil sélectionné ; une clé du profil, si elle existe aussi globalement dans un futur schema, est déterministement prioritaire ;
|
||||
- `ConfigValueOrigin::{Global, Profile}` conserve la provenance top-level de chaque clé effective ;
|
||||
- aucune interpolation `${...}`, aucun override composite et aucune sensibilité ne sont encore appliqués à cette vue ; ces couches seront ajoutées sans faire perdre la provenance déjà portée ;
|
||||
- les tests couvrent le profil par défaut commité, la sélection explicite, la provenance global/profile, un profil demandé absent, les `profile_id` dupliqués et un `default_profile` orphelin.
|
||||
|
||||
La validation utilisateur de `pre.007` est acquise : `fmt/check/clippy/test` passent, le moteur Config compte 28 tests unitaires + 5 tests publics et `jsonschema` avec `default-features = false` n'introduit pas de stack HTTP/TLS. Le seul doublon signalé par `cargo tree -d` est `syn 2`/`syn 3`, transitif via l'écosystème `jsonschema` et sans duplication de crate runtime KSP à corriger dans cette tranche.
|
||||
|
||||
### `0.1.3-pre.009` — compositions génériques par `file_id`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Contrats des fichiers
|
||||
|
||||
@@ -46,6 +46,8 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
|
||||
|
||||
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.
|
||||
|
||||
Pour un document standard profilé, `default_profile` et `profiles` sont des clés structurelles réservées. Les autres propriétés top-level sont des valeurs globales. Chaque entrée de `profiles` possède un `profile_id` unique ; `default_profile` référence obligatoirement l'un de ces identifiants. La résolution Config peut sélectionner le profil par défaut ou un profil explicite et conserve séparément la provenance `Global` / `Profile` de la vue effective. Les consumers ne reconstituent jamais eux-mêmes cette fusion.
|
||||
|
||||
## Répertoire `prompts/`
|
||||
|
||||
| Fichier/famille | Responsabilité | Règle de modification |
|
||||
|
||||
Reference in New Issue
Block a user