From 0a4cddafc4f6489539f91dae3a526a2fddd3fea0 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 29 Aug 2026 19:04:47 +0200 Subject: [PATCH] v0.3.2-pre.004-fix.001 --- .env.example | 13 +++- Cargo.toml | 4 +- config/examples/std.store.example.json | 55 ++++++++++++- config/schemas/std.store.schema.json | 10 +++ config/std.store.json | 55 ++++++++++++- .../unit_tests/profiles.rs | 5 +- crates/ksp-config-lib/src/store.rs | 27 +++++-- .../unit_tests/fixtures/std.store.json | 78 +++++++++++++++++++ crates/ksp-config-lib/unit_tests/store.rs | 70 ++++++++++++----- crates/ksp-store-lib/src/settings.rs | 51 ++++++++---- .../ksp-store-lib/tests/feature_mismatch.rs | 11 ++- crates/ksp-store-lib/tests/public_api.rs | 9 ++- crates/ksp-store-lib/unit_tests/settings.rs | 18 ++++- crates/ksp-store-lib/unit_tests/store.rs | 13 +++- deltas/0.3.2/pre.004-fix.001.md | 77 ++++++++++++++++++ ...3-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md | 70 +++++++++++------ .../019-V0_3_2_STORE_POSTGRES_FOUNDATION.md | 42 +++++++--- 17 files changed, 509 insertions(+), 99 deletions(-) create mode 100644 crates/ksp-config-lib/unit_tests/fixtures/std.store.json create mode 100644 deltas/0.3.2/pre.004-fix.001.md diff --git a/.env.example b/.env.example index bdcdffe..b033eb4 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,18 @@ # file: .env.example -# version: 11 +# version: 13 # KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. KSP_LOGS_DIRECTORY=logs -# PostgreSQL connection URI used by Config std.store profiles. -# Keep credentials only in the process environment or local .env; the committed fallback remains secret-classified and is redacted in safe projections. -# KSP_SECRET_STORE_POSTGRES_URI=postgresql://user:password@localhost/ksp +# PostgreSQL URI for the Devnet Store target. +KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://... + +# PostgreSQL URI for the Mainnet Store target. +KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://... + +# PostgreSQL URI for the Testnet Store target. +KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://... # KSP Wallet root directory. Used by config/std.wallet.json before an optional profile subdirectory is appended. # The committed Wallet document falls back to "wallets" when neither the process environment nor .env defines this variable. diff --git a/Cargo.toml b/Cargo.toml index d4b3165..b49acf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 335 +# version: 336 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.2-pre.4" +version = "0.3.2-pre.4.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/config/examples/std.store.example.json b/config/examples/std.store.example.json index 6bc56ac..1c62849 100644 --- a/config/examples/std.store.example.json +++ b/config/examples/std.store.example.json @@ -1,12 +1,61 @@ { "format_version": 1, - "default_profile": "postgres_default", + "default_profile": "devnet", "profiles": [ { - "profile_id": "postgres_default", + "profile_id": "devnet", + "network": "devnet", "backend": "postgres", "postgres": { - "connection_uri": "${KSP_SECRET_STORE_POSTGRES_URI:-postgresql://localhost/ksp}", + "connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "mainnet", + "network": "mainnet-beta", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "testnet", + "network": "testnet", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}", "pool": { "max_connections": 8, "connect_timeout_ms": 10000, diff --git a/config/schemas/std.store.schema.json b/config/schemas/std.store.schema.json index 6def4e8..427d7ab 100644 --- a/config/schemas/std.store.schema.json +++ b/config/schemas/std.store.schema.json @@ -29,6 +29,12 @@ "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "networkId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_.:-]+$" + }, "duration100To60000": { "type": "integer", "minimum": 100, @@ -39,6 +45,7 @@ "additionalProperties": false, "required": [ "profile_id", + "network", "backend", "postgres" ], @@ -46,6 +53,9 @@ "profile_id": { "$ref": "#/$defs/profileId" }, + "network": { + "$ref": "#/$defs/networkId" + }, "backend": { "const": "postgres" }, diff --git a/config/std.store.json b/config/std.store.json index 6bc56ac..1c62849 100644 --- a/config/std.store.json +++ b/config/std.store.json @@ -1,12 +1,61 @@ { "format_version": 1, - "default_profile": "postgres_default", + "default_profile": "devnet", "profiles": [ { - "profile_id": "postgres_default", + "profile_id": "devnet", + "network": "devnet", "backend": "postgres", "postgres": { - "connection_uri": "${KSP_SECRET_STORE_POSTGRES_URI:-postgresql://localhost/ksp}", + "connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "mainnet", + "network": "mainnet-beta", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "testnet", + "network": "testnet", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}", "pool": { "max_connections": 8, "connect_timeout_ms": 10000, diff --git a/crates/ksp-app-config-desk/unit_tests/profiles.rs b/crates/ksp-app-config-desk/unit_tests/profiles.rs index f10d7d0..b2458fa 100644 --- a/crates/ksp-app-config-desk/unit_tests/profiles.rs +++ b/crates/ksp-app-config-desk/unit_tests/profiles.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/unit_tests/profiles.rs -// version: 7 +// version: 8 #[test] fn profile_inventory_exposes_registered_profile_documents() { @@ -15,6 +15,9 @@ fn profile_inventory_exposes_registered_profile_documents() { assert!(inventory.iter().any(|document| -> bool { return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT; })); + assert!(inventory.iter().any(|document| -> bool { + return document.file_id == ksp_config_lib::FILE_ID_STD_STORE; + })); assert!(inventory.iter().any(|document| -> bool { return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET; })); diff --git a/crates/ksp-config-lib/src/store.rs b/crates/ksp-config-lib/src/store.rs index 880d49c..8490ac8 100644 --- a/crates/ksp-config-lib/src/store.rs +++ b/crates/ksp-config-lib/src/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/store.rs -// version: 1 +// version: 2 /// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`. pub struct ResolvedStoreConfig { @@ -25,11 +25,19 @@ impl ResolvedStoreConfig { } /// Returns the selected standard Store profile identifier. + /// + /// For `std.store`, the profile identifier is also the stable named Store target identifier. #[must_use] pub fn profile_id(&self) -> &str { return self.profile_id.as_str(); } + /// Returns the selected named Store target identifier. + #[must_use] + pub fn target_id(&self) -> &str { + return self.profile_id.as_str(); + } + /// Returns the source that selected the standard Store profile. #[must_use] pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { @@ -111,6 +119,7 @@ impl crate::ConfigDocumentEngine { struct EffectiveStoreSource { backend: String, format_version: u32, + network: String, postgres: EffectivePostgresSource, profile_id: String, } @@ -164,9 +173,7 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c let source = match source { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { - return std::result::Result::Err( - effective_error(profile, "effective Store Config cannot be decoded into the runtime adapter contract").with_source(error), - ); + return std::result::Result::Err(effective_error(profile, "effective Store Config cannot be decoded into the runtime adapter contract").with_source(error)); }, }; if source.format_version != 1 { @@ -195,8 +202,14 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms), std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms), ); + let network = ksp_store_lib::RawNetworkId::new(source.network); + let network = match network { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(effective_error(profile, "effective Store network identifier is invalid")), + }; let postgres = ksp_store_lib::PostgresStoreSettings::new(source.postgres.connection_uri, pool, tls_mode, bootstrap); let settings = ksp_store_lib::StoreSettings::new( + network, ksp_store_lib::StoreBackendSettings::Postgres(postgres), std::time::Duration::from_millis(source.postgres.shutdown_timeout_ms), ); @@ -206,6 +219,7 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c ksp_logging_lib::debug!( target: crate::TRACING_TARGET, profile_id = profile.profile_id(), + network = settings.network().as_str(), backend = settings.backend_kind().code(), "mapped standard Store Config to Store settings" ); @@ -219,7 +233,10 @@ fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &c }); } -fn validate_connection_uri_provenance(effective: &crate::ResolvedConfigJson, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<()> { +fn validate_connection_uri_provenance( + effective: &crate::ResolvedConfigJson, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result<()> { let provenance = match effective.provenance_at("/postgres/connection_uri") { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI provenance is unavailable")), diff --git a/crates/ksp-config-lib/unit_tests/fixtures/std.store.json b/crates/ksp-config-lib/unit_tests/fixtures/std.store.json new file mode 100644 index 0000000..1c62849 --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/fixtures/std.store.json @@ -0,0 +1,78 @@ +{ + "format_version": 1, + "default_profile": "devnet", + "profiles": [ + { + "profile_id": "devnet", + "network": "devnet", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "mainnet", + "network": "mainnet-beta", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "testnet", + "network": "testnet", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}", + "pool": { + "max_connections": 8, + "connect_timeout_ms": 10000, + "wait_timeout_ms": 5000, + "create_timeout_ms": 10000, + "recycle_timeout_ms": 5000 + }, + "tls": { + "mode": "verify_full" + }, + "bootstrap": { + "auto_migrate": true, + "migration_timeout_ms": 30000, + "migration_lock_timeout_ms": 10000 + }, + "shutdown_timeout_ms": 5000 + } + } + ] +} diff --git a/crates/ksp-config-lib/unit_tests/store.rs b/crates/ksp-config-lib/unit_tests/store.rs index ce0da6f..392482e 100644 --- a/crates/ksp-config-lib/unit_tests/store.rs +++ b/crates/ksp-config-lib/unit_tests/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/store.rs -// version: 1 +// version: 2 #[test] fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() { @@ -13,9 +13,11 @@ fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() { assert!(resolved.is_ok(), "committed Store profile should map without opening PostgreSQL: {resolved:?}"); if let std::result::Result::Ok(resolved) = resolved { assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_STORE); - assert_eq!(resolved.profile_id(), "postgres_default"); + assert_eq!(resolved.profile_id(), "devnet"); + assert_eq!(resolved.target_id(), "devnet"); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile); assert_eq!(resolved.settings().backend_kind(), ksp_store_lib::StoreBackendKind::Postgres); + assert_eq!(resolved.settings().network().as_str(), "devnet"); assert_eq!(resolved.settings().shutdown_timeout(), std::time::Duration::from_millis(5_000)); let postgres = match resolved.settings().backend() { ksp_store_lib::StoreBackendSettings::Postgres(postgres) => std::option::Option::Some(postgres), @@ -35,10 +37,10 @@ fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() { } assert!(resolved.effective().sensitivity().is_secret()); let safe = resolved.effective().safe_value().to_string(); - assert!(!safe.contains("postgresql://localhost/ksp")); + assert!(!safe.contains("postgresql://localhost/ksp_devnet")); assert!(safe.contains(crate::REDACTED_CONFIG_VALUE)); let debug = format!("{resolved:?}"); - assert!(!debug.contains("postgresql://localhost/ksp")); + assert!(!debug.contains("postgresql://localhost/ksp_devnet")); } } @@ -49,9 +51,9 @@ fn process_store_uri_wins_and_remains_redacted_in_safe_views() { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; - let canary = "postgresql://secret-user:secret-pass@db.example/ksp"; + let canary = "postgresql://secret-user:secret-pass@db.example/ksp_devnet"; let mut process = std::collections::BTreeMap::::new(); - process.insert("KSP_SECRET_STORE_POSTGRES_URI".to_owned(), canary.to_owned()); + process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), canary.to_owned()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment); assert!(resolved.is_ok(), "secret process Store URI should map: {resolved:?}"); @@ -82,10 +84,10 @@ fn literal_or_nonsecret_store_uri_is_rejected_by_effective_adapter() { std::result::Result::Err(_) => return, }; let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut); - if let std::option::Option::Some(profiles) = profiles { - if let std::option::Option::Some(profile) = profiles.first_mut() { - profile["postgres"]["connection_uri"] = serde_json::Value::String(value.to_owned()); - } + if let std::option::Option::Some(profiles) = profiles + && let std::option::Option::Some(profile) = profiles.first_mut() + { + profile["postgres"]["connection_uri"] = serde_json::Value::String(value.to_owned()); } let engine = fixture_engine_with_document(fixture.path(), &source); assert!(engine.is_ok()); @@ -104,6 +106,40 @@ fn literal_or_nonsecret_store_uri_is_rejected_by_effective_adapter() { } } +#[test] +fn named_store_targets_select_one_network_and_database_without_runtime_multiplexing() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), "postgresql://devnet.invalid/ksp_devnet".to_owned()); + process.insert("KSP_SECRET_STORE_MAINNET_POSTGRES_URI".to_owned(), "postgresql://mainnet.invalid/ksp_mainnet".to_owned()); + process.insert("KSP_SECRET_STORE_TESTNET_POSTGRES_URI".to_owned(), "postgresql://testnet.invalid/ksp_testnet".to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); + for (target_id, network, expected_uri) in [ + ("devnet", "devnet", "postgresql://devnet.invalid/ksp_devnet"), + ("mainnet", "mainnet-beta", "postgresql://mainnet.invalid/ksp_mainnet"), + ("testnet", "testnet", "postgresql://testnet.invalid/ksp_testnet"), + ] { + let resolved = engine.load_resolved_store_config(std::option::Option::Some(target_id), &environment); + assert!(resolved.is_ok(), "named Store target should resolve independently: {target_id}: {resolved:?}"); + if let std::result::Result::Ok(resolved) = resolved { + assert_eq!(resolved.target_id(), target_id); + assert_eq!(resolved.profile_id(), target_id); + assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit); + assert_eq!(resolved.settings().network().as_str(), network); + assert_eq!( + resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str), + std::option::Option::Some(expected_uri), + ); + assert!(!resolved.effective().safe_value().to_string().contains(expected_uri)); + } + } + return; +} + fn committed_engine() -> ksp_core_lib::Result { let workspace = workspace_root(); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); @@ -122,24 +158,16 @@ fn committed_engine() -> ksp_core_lib::Result { fn fixture_engine_with_document(root: &std::path::Path, document: &serde_json::Value) -> ksp_core_lib::Result { let config_root = root.join("config"); if let std::result::Result::Err(error) = std::fs::create_dir_all(config_root.as_path()) { - return std::result::Result::Err( - ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error), - ); + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error)); } let bytes = serde_json::to_vec_pretty(document); let bytes = match bytes { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - return std::result::Result::Err( - ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Store Config cannot be encoded").with_source(error), - ); - }, + std::result::Result::Err(error) => return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Store Config cannot be encoded").with_source(error)), }; let path = config_root.join(crate::DEFAULT_STD_STORE_FILENAME); if let std::result::Result::Err(error) = std::fs::write(path.as_path(), bytes) { - return std::result::Result::Err( - ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Store Config cannot be written").with_source(error), - ); + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Store Config cannot be written").with_source(error)); } let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root, workspace_root().join("config/schemas")); let bootstrap = match bootstrap { diff --git a/crates/ksp-store-lib/src/settings.rs b/crates/ksp-store-lib/src/settings.rs index 50db947..6b73576 100644 --- a/crates/ksp-store-lib/src/settings.rs +++ b/crates/ksp-store-lib/src/settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/settings.rs -// version: 1 +// version: 2 const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; const DEFAULT_MAX_CONNECTIONS: u32 = 8; @@ -117,7 +117,12 @@ impl PostgresPoolSettings { .with_context("maximum", MAX_CONNECTIONS.to_string()), ); } - let connect_validation = validate_duration("postgres.pool.connect_timeout", self.connect_timeout, MIN_CONNECT_TIMEOUT_MS, MAX_CONNECT_TIMEOUT_MS); + let connect_validation = validate_duration( + "postgres.pool.connect_timeout", + self.connect_timeout, + MIN_CONNECT_TIMEOUT_MS, + MAX_CONNECT_TIMEOUT_MS, + ); if let std::result::Result::Err(error) = connect_validation { return std::result::Result::Err(error); } @@ -125,12 +130,21 @@ impl PostgresPoolSettings { if let std::result::Result::Err(error) = wait_validation { return std::result::Result::Err(error); } - let create_validation = validate_duration("postgres.pool.create_timeout", self.create_timeout, MIN_POOL_CREATE_TIMEOUT_MS, MAX_POOL_CREATE_TIMEOUT_MS); + let create_validation = validate_duration( + "postgres.pool.create_timeout", + self.create_timeout, + MIN_POOL_CREATE_TIMEOUT_MS, + MAX_POOL_CREATE_TIMEOUT_MS, + ); if let std::result::Result::Err(error) = create_validation { return std::result::Result::Err(error); } - let recycle_validation = - validate_duration("postgres.pool.recycle_timeout", self.recycle_timeout, MIN_POOL_RECYCLE_TIMEOUT_MS, MAX_POOL_RECYCLE_TIMEOUT_MS); + let recycle_validation = validate_duration( + "postgres.pool.recycle_timeout", + self.recycle_timeout, + MIN_POOL_RECYCLE_TIMEOUT_MS, + MAX_POOL_RECYCLE_TIMEOUT_MS, + ); if let std::result::Result::Err(error) = recycle_validation { return std::result::Result::Err(error); } @@ -185,8 +199,12 @@ impl PostgresBootstrapSettings { /// Validates bootstrap and migration deadlines without contacting PostgreSQL. pub fn validate(&self) -> ksp_store_api::Result<()> { - let migration_validation = - validate_duration("postgres.bootstrap.migration_timeout", self.migration_timeout, MIN_MIGRATION_TIMEOUT_MS, MAX_MIGRATION_TIMEOUT_MS); + let migration_validation = validate_duration( + "postgres.bootstrap.migration_timeout", + self.migration_timeout, + MIN_MIGRATION_TIMEOUT_MS, + MAX_MIGRATION_TIMEOUT_MS, + ); if let std::result::Result::Err(error) = migration_validation { return std::result::Result::Err(error); } @@ -312,14 +330,15 @@ impl StoreBackendSettings { #[derive(Debug)] pub struct StoreSettings { backend: StoreBackendSettings, + network: ksp_store_api::RawNetworkId, shutdown_timeout: std::time::Duration, } impl StoreSettings { - /// Creates complete Store runtime settings from one explicit backend and shutdown bound. + /// Creates complete Store runtime settings for one explicit logical network, backend and shutdown bound. #[must_use] - pub const fn new(backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self { - return Self { backend, shutdown_timeout }; + pub fn new(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self { + return Self { backend, network, shutdown_timeout }; } /// Returns the selected backend settings. @@ -334,6 +353,12 @@ impl StoreSettings { return self.backend.kind(); } + /// Returns the single logical network bound to this Store instance. + #[must_use] + pub const fn network(&self) -> &ksp_store_api::RawNetworkId { + return &self.network; + } + /// Returns the maximum duration allowed for explicit Store shutdown. #[must_use] pub const fn shutdown_timeout(&self) -> std::time::Duration { @@ -355,10 +380,10 @@ impl StoreSettings { } impl StoreSettings { - /// Creates settings using the common default shutdown bound while keeping backend construction explicit. + /// Creates settings using the common default shutdown bound while keeping network and backend construction explicit. #[must_use] - pub fn with_default_shutdown(backend: StoreBackendSettings) -> Self { - return Self::new(backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS)); + pub fn with_default_shutdown(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings) -> Self { + return Self::new(network, backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS)); } } diff --git a/crates/ksp-store-lib/tests/feature_mismatch.rs b/crates/ksp-store-lib/tests/feature_mismatch.rs index 801407e..1b8b6f7 100644 --- a/crates/ksp-store-lib/tests/feature_mismatch.rs +++ b/crates/ksp-store-lib/tests/feature_mismatch.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/feature_mismatch.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -17,6 +17,13 @@ fn poll_ready(future: impl std::future::Future) -> T { }; } +fn valid_network() -> ksp_store_lib::RawNetworkId { + return match ksp_store_lib::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"), + }; +} + fn settings() -> ksp_store_lib::StoreSettings { let postgres = ksp_store_lib::PostgresStoreSettings::new( "postgresql://operator-supplied-sensitive-value", @@ -24,7 +31,7 @@ fn settings() -> ksp_store_lib::StoreSettings { ksp_store_lib::PostgresTlsMode::Disabled, ksp_store_lib::PostgresBootstrapSettings::default(), ); - return ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres)); + return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres)); } #[cfg(not(feature = "postgres"))] diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs index 308bd12..86ac309 100644 --- a/crates/ksp-store-lib/tests/public_api.rs +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/public_api.rs -// version: 1 +// version: 2 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -15,8 +15,13 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() { ksp_store_lib::PostgresTlsMode::VerifyFull, ksp_store_lib::PostgresBootstrapSettings::default(), ); - let settings = ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres)); + let network = match ksp_store_lib::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid public API network rejected: {error:?}"), + }; + let settings = ksp_store_lib::StoreSettings::with_default_shutdown(network, ksp_store_lib::StoreBackendSettings::Postgres(postgres)); assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres); + assert_eq!(settings.network().as_str(), "devnet"); assert!(settings.validate().is_ok()); let _open = ksp_store_lib::Store::open; let _close = ksp_store_lib::Store::close; diff --git a/crates/ksp-store-lib/unit_tests/settings.rs b/crates/ksp-store-lib/unit_tests/settings.rs index 9e98868..70a4b3d 100644 --- a/crates/ksp-store-lib/unit_tests/settings.rs +++ b/crates/ksp-store-lib/unit_tests/settings.rs @@ -1,5 +1,12 @@ // file: crates/ksp-store-lib/unit_tests/settings.rs -// version: 2 +// version: 3 + +fn valid_network() -> crate::RawNetworkId { + return match crate::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"), + }; +} fn valid_postgres_settings() -> crate::PostgresStoreSettings { return crate::PostgresStoreSettings::new( @@ -22,9 +29,10 @@ fn defaults_match_the_pre_001_runtime_bounds() { assert!(bootstrap.auto_migrate()); assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000)); assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000)); - let store = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(valid_postgres_settings())); + let store = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings())); assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000)); assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres); + assert_eq!(store.network().as_str(), "devnet"); return; } @@ -109,9 +117,11 @@ fn exact_runtime_boundaries_validate_and_adjacent_values_are_rejected() { #[test] fn store_shutdown_bound_is_independent_from_backend_and_rejects_outside_values() { - let valid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100)); + let valid = + crate::StoreSettings::new(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100)); assert!(valid.validate().is_ok()); - let invalid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001)); + let invalid = + crate::StoreSettings::new(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001)); let error = invalid.validate().err(); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); return; diff --git a/crates/ksp-store-lib/unit_tests/store.rs b/crates/ksp-store-lib/unit_tests/store.rs index 2fbec14..01a186b 100644 --- a/crates/ksp-store-lib/unit_tests/store.rs +++ b/crates/ksp-store-lib/unit_tests/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/unit_tests/store.rs -// version: 2 +// version: 3 fn poll_ready(future: impl std::future::Future) -> T { let mut future = std::boxed::Box::pin(future); @@ -11,6 +11,13 @@ fn poll_ready(future: impl std::future::Future) -> T { }; } +fn valid_network() -> crate::RawNetworkId { + return match crate::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"), + }; +} + fn valid_store_settings() -> crate::StoreSettings { let postgres = crate::PostgresStoreSettings::new( "postgresql://secret-user:secret-password@db.internal/ksp", @@ -18,7 +25,7 @@ fn valid_store_settings() -> crate::StoreSettings { crate::PostgresTlsMode::VerifyFull, crate::PostgresBootstrapSettings::default(), ); - return crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres)); + return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres)); } #[test] @@ -29,7 +36,7 @@ fn invalid_settings_are_rejected_before_backend_dispatch() { crate::PostgresTlsMode::Disabled, crate::PostgresBootstrapSettings::default(), ); - let settings = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres)); + let settings = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres)); let result = poll_ready(crate::Store::open(settings)); let error = result.err(); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); diff --git a/deltas/0.3.2/pre.004-fix.001.md b/deltas/0.3.2/pre.004-fix.001.md new file mode 100644 index 0000000..bda6eaa --- /dev/null +++ b/deltas/0.3.2/pre.004-fix.001.md @@ -0,0 +1,77 @@ + + + +# Delta `0.3.2-pre.004-fix.001` — Store targets réseau-spécifiques + gate fixes + +## 1. Base + +Base exacte : `0.3.2-pre.004`. Le gate opérateur du 29 août 2026 confirme `cargo check`, les tests Config/Store principaux et les trois builds Tauri, mais révèle : + +```text +1 warning Clippy collapsible_if dans ksp-config-lib/unit_tests/store.rs +1 échec Config Desk : fixture cfg.std.store absente +``` + +La discussion de design menée pendant ce gate confirme aussi qu'une instance `Store` ne doit jamais multiplexer plusieurs réseaux. + +## 2. Décision target/réseau + +Le mécanisme de profiles Config existant devient directement le mécanisme de targets Store nommés : + +```text +profile_id = target Store nommé +default_profile = target autonome +profile forcé = target explicitement imposé par job/worker/app +1 target = 1 network + 1 backend + 1 URI/base +1 StoreSettings = 1 RawNetworkId + 1 backend +``` + +`ksp-store-lib` ne contient aucun registry ni routeur multi-target. Pour changer de réseau, le consumer résout un autre profile `std.store` puis ouvre une autre instance Store. + +## 3. Targets committed + +```text +devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI -> ksp_devnet +mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI -> ksp_mainnet +testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI -> ksp_testnet +``` + +`default_profile = devnet`, cohérent avec le défaut Transport et plus sûr pour les workflows de développement. Le schema reste extensible à d'autres targets/réseaux sûrs. + +## 4. Deuxième protection réseau + +`StoreSettings` exige désormais un `RawNetworkId`. Les futures opérations RAW devront rejeter avant I/O toute identité/query dont le réseau ne correspond pas au réseau de l'instance Store. Cette vérification ne route jamais automatiquement vers un autre target. + +## 5. Correctifs de gate + +- collapse du `if let` imbriqué signalé par Clippy ; +- ajout de `crates/ksp-config-lib/unit_tests/fixtures/std.store.json` ; +- Config Desk vérifie désormais explicitement que `cfg.std.store` appartient à l'inventaire profilé ; +- tests Config ajoutés pour la sélection explicite et indépendante de `devnet`, `mainnet` et `testnet`. + +## 6. Version + +```text +workspace.package.version = 0.3.2-pre.4.fix.1 +``` + +## 7. Gate opérateur + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-config-lib +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo check -p ksp-store-lib --no-default-features +cargo test -p ksp-app-config-desk +``` + +Les trois builds Tauri `pre.004` sont déjà verts et les resources packagées ne changent pas dans ce fix. + +## 8. Suite + +Après gate propre, `pre.005` peut ouvrir la connexion PostgreSQL physique. Elle consommera un seul target/réseau déjà résolu ; aucun multiplexage réseau n'est à introduire. diff --git a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md index 0f36d0c..d6752ac 100644 --- a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +++ b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.2` — Store/PostgreSQL runtime foundation @@ -349,6 +349,7 @@ Lorsque la feature est active, `ksp-store-lib` convertit en privé ces settings Les groupes de settings sont : ```text +network RawNetworkId explicite, un seul réseau logique par instance Store connection_uri String possédée, Debug redacted, aucun getter de diagnostic pool bornes de section 6 tls Disabled ou VerifyFull @@ -381,7 +382,7 @@ StoreSettings Store ``` -`PostgresPoolSettings::default()` et `PostgresBootstrapSettings::default()` portent exactement les bornes décidées par le gate `pre.001`. `StoreSettings::with_default_shutdown(...)` fournit uniquement la borne commune de shutdown à 5 s. Il n'existe toujours aucun `Default` pour `PostgresStoreSettings` ou `StoreSettings`, afin de ne jamais inventer une URI ou un backend de production. +`PostgresPoolSettings::default()` et `PostgresBootstrapSettings::default()` portent exactement les bornes décidées par le gate `pre.001`. À partir de `pre.004-fix.001`, `StoreSettings` exige aussi un `RawNetworkId` explicite et `StoreSettings::with_default_shutdown(network, ...)` fournit uniquement la borne commune de shutdown à 5 s. Il n'existe toujours aucun `Default` pour `PostgresStoreSettings` ou `StoreSettings`, afin de ne jamais inventer une URI ou un backend de production. Les validations `pre.003` restent backend-neutral et sans parsing PostgreSQL : URI non vide, pool 1..64, timeouts pool/connect 100..60 000 ms, migration 1 000..300 000 ms, lock 100..120 000 ms et shutdown 100..30 000 ms. Le parsing/normalisation de l'URI et les options réellement honorées restent propriétaires de `pre.005`. @@ -611,28 +612,41 @@ Le document V1 est profilé comme les autres documents standards : ```json { "format_version": 1, - "default_profile": "postgres_default", + "default_profile": "devnet", "profiles": [ { - "profile_id": "postgres_default", + "profile_id": "devnet", + "network": "devnet", "backend": "postgres", "postgres": { - "connection_uri": "${KSP_SECRET_STORE_POSTGRES_URI:-postgresql://localhost/ksp}", - "pool": { - "max_connections": 8, - "connect_timeout_ms": 10000, - "wait_timeout_ms": 5000, - "create_timeout_ms": 10000, - "recycle_timeout_ms": 5000 - }, - "tls": { - "mode": "verify_full" - }, - "bootstrap": { - "auto_migrate": true, - "migration_timeout_ms": 30000, - "migration_lock_timeout_ms": 10000 - }, + "connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}", + "pool": { "max_connections": 8, "connect_timeout_ms": 10000, "wait_timeout_ms": 5000, "create_timeout_ms": 10000, "recycle_timeout_ms": 5000 }, + "tls": { "mode": "verify_full" }, + "bootstrap": { "auto_migrate": true, "migration_timeout_ms": 30000, "migration_lock_timeout_ms": 10000 }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "mainnet", + "network": "mainnet-beta", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", + "pool": { "max_connections": 8, "connect_timeout_ms": 10000, "wait_timeout_ms": 5000, "create_timeout_ms": 10000, "recycle_timeout_ms": 5000 }, + "tls": { "mode": "verify_full" }, + "bootstrap": { "auto_migrate": true, "migration_timeout_ms": 30000, "migration_lock_timeout_ms": 10000 }, + "shutdown_timeout_ms": 5000 + } + }, + { + "profile_id": "testnet", + "network": "testnet", + "backend": "postgres", + "postgres": { + "connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}", + "pool": { "max_connections": 8, "connect_timeout_ms": 10000, "wait_timeout_ms": 5000, "create_timeout_ms": 10000, "recycle_timeout_ms": 5000 }, + "tls": { "mode": "verify_full" }, + "bootstrap": { "auto_migrate": true, "migration_timeout_ms": 30000, "migration_lock_timeout_ms": 10000 }, "shutdown_timeout_ms": 5000 } } @@ -640,13 +654,17 @@ Le document V1 est profilé comme les autres documents standards : } ``` -Le fallback localhost est classé `Secret` car il appartient à un placeholder `KSP_SECRET_*`; sa safe projection reste redacted. Il ne provoque aucune connexion automatique. +Chaque profile `std.store` est un **target Store nommé** : son `profile_id` est l'identifiant de target, son champ `network` fixe exactement un réseau logique et son backend désigne exactement une base/configuration physique. `default_profile` choisit le target autonome ; un job, worker ou app peut imposer un autre `profile_id` via le mécanisme Config existant. `ksp-store-lib` ne route jamais entre plusieurs targets. + +Le target par défaut committed est `devnet`, cohérent avec le défaut Transport et plus sûr pour les workflows de développement. Les targets `mainnet` et `testnet` utilisent des URI distinctes. Le fallback localhost de chaque target est classé `Secret` car il appartient à un placeholder `KSP_SECRET_*`; sa safe projection reste redacted. Il ne provoque aucune connexion automatique. `.env.example` ajoutera dans la même tranche : ```text -# PostgreSQL connection URI used by Config std.store profiles. -# KSP_SECRET_STORE_POSTGRES_URI=postgresql://user:password@localhost/ksp +# PostgreSQL connection URIs used by named Config std.store targets. +# KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_devnet +# KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_mainnet +# KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_testnet ``` La valeur d'exemple commentée n'est jamais un vrai credential. @@ -657,6 +675,10 @@ Config reconnaît `backend = "postgres"` indépendamment de la feature du consum Ainsi Config ne doit pas connaître les `cfg(feature = "postgres")` de chaque consumer. +### 12.4 Invariant target/réseau + +Une instance `Store` ouverte reçoit exactement un `StoreSettings.network`. Ce réseau provient du target Config sélectionné, mais reste un contrat Store backend-neutral. Les futures opérations RAW doivent comparer ce réseau à celui de l'identité/query avant I/O : une transaction/account/query `devnet` soumise à un Store `mainnet-beta` doit être rejetée, jamais routée vers un autre Store. La sélection d'un autre réseau se fait en résolvant un autre profile/target puis en ouvrant une autre instance `Store`. + ## 13. Logging et redaction Les deux crates runtime auront un `constants.rs` privé avec un target explicite : @@ -937,7 +959,7 @@ Le dernier comportement est volontairement transitoire : `pre.003` ne retourne j ### `pre.004` — Config `std.store` -Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity/redaction, packaging resources strictement nécessaires. Aucun env dans Store/backend. `pre.004` matérialise les noms canoniques `std.store.json`, `std.store.schema.json` et `std.store.example.json`, conformément au prompt de démarrage et aux conventions Config existantes. +Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity/redaction, targets nommés réseau-spécifiques, packaging resources strictement nécessaires. Aucun env dans Store/backend. `pre.004` matérialise les noms canoniques `std.store.json`, `std.store.schema.json` et `std.store.example.json`, conformément au prompt de démarrage et aux conventions Config existantes. ### `pre.005` — PostgreSQL connection + deadpool + Rustls diff --git a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md index d8fc63e..af92d7a 100644 --- a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +++ b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.2` — Store/PostgreSQL runtime foundation @@ -213,28 +213,46 @@ Critères : std.store enregistré schema V1 valide example valide -profiles typés +profiles/targets nommés typés +un réseau explicite par target backend postgres explicite ``` -Matérialisé par `pre.004` avec `cfg.std.store` / `schema.std.store`, V1 profilée, backend PostgreSQL typé et example canonique. +Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées. -Statut : `TODO gate opérateur pre.004`. +Statut : `TODO gate opérateur pre.004-fix.001`. ### V32-CONFIG-002 — Secrets/provenance Critères : ```text -KSP_SECRET_STORE_POSTGRES_URI classé Secret +KSP_SECRET_STORE_{DEVNET,MAINNET,TESTNET}_POSTGRES_URI classés Secret safe projection redacted provenance sans valeur dotenv inventory à jour ``` -Matérialisé par `pre.004` : `KSP_SECRET_STORE_POSTGRES_URI` est inventorié, le fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur. +Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur. -Statut : `TODO gate opérateur pre.004`. +Statut : `TODO gate opérateur pre.004-fix.001`. + +### V32-CONFIG-002B — Target/réseau sans multiplexage + +Critères : + +```text +profile_id std.store = identifiant de target Store +default_profile choisit exactement un target +StoreSettings porte exactement un RawNetworkId +résolution explicite mainnet/devnet/testnet retourne URI + network du target choisi +ksp-store-lib ne possède aucun routeur multi-target/multi-réseau +future RAW mismatch Store.network != entity/query.network rejeté avant I/O +``` + +Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`. + +Statut : `TODO gate opérateur pre.004-fix.001 / TODO 0.3.3-0.3.4`. ### V32-CONFIG-003 — No-env Store/backend @@ -249,17 +267,17 @@ PG* .pgpass ``` -`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; Store/backend restent sans dépendance Config et sans lecture KSP/KSPB. +`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB. -Statut : `TODO gate opérateur pre.004 / TODO pre.009`. +Statut : `TODO gate opérateur pre.004-fix.001 / TODO pre.009`. ### V32-CONFIG-004 — Adapter Config -> Store -Critère : `ksp-config-lib` seul transforme un profil résolu en `StoreSettings` et ne transmet aucun secret dans diagnostics. +Critère : `ksp-config-lib` seul transforme un profil/target résolu en `StoreSettings` réseau-spécifique et ne transmet aucun secret dans diagnostics. -Matérialisé par `pre.004` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false` et construit `StoreSettings` sans forcer la feature backend. +Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend. -Statut : `TODO gate opérateur pre.004`. +Statut : `TODO gate opérateur pre.004-fix.001`. ## 6. Pool et lifecycle PostgreSQL