diff --git a/Cargo.toml b/Cargo.toml index aefed78..fd0598b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 333 +# version: 334 [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.2.fix.1" +version = "0.3.2-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-lib/src/error.rs b/crates/ksp-store-lib/src/error.rs new file mode 100644 index 0000000..2a50c05 --- /dev/null +++ b/crates/ksp-store-lib/src/error.rs @@ -0,0 +1,13 @@ +// file: crates/ksp-store-lib/src/error.rs +// version: 1 + +/// Error code reserved for operations attempted after a Store backend has entered its closed state. +pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed"); +/// Error code used when a known Store backend was selected but its Cargo feature is not compiled. +pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled"); +/// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle. +pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed"); +/// Error code used when backend-neutral Store settings violate runtime bounds or invariants. +pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid"); +/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound. +pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout"); diff --git a/crates/ksp-store-lib/src/lib.rs b/crates/ksp-store-lib/src/lib.rs index bd9660c..1712715 100644 --- a/crates/ksp-store-lib/src/lib.rs +++ b/crates/ksp-store-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/lib.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,19 +7,170 @@ //! Common backend-neutral Store runtime facade for KSP. //! -//! `0.3.2-pre.002` establishes only the crate and Cargo feature graph. Runtime -//! settings, backend selection, lifecycle, error mapping and API reexports are -//! introduced by later prereleases of `0.3.2`. +//! `0.3.2-pre.003` materializes Config-independent settings, stable backend +//! identity, bounded validation and the opaque async Store lifecycle contract. +//! Physical PostgreSQL connection, pool, TLS and migrations remain private +//! future slices of this release. //! //! The default `postgres` feature compiles the official PostgreSQL backend as //! an optional implementation dependency. No backend implementation type is //! part of this crate's public surface. mod constants; +mod error; +mod settings; +mod store; -/// Crate-owned tracing target reserved for later Store runtime behavior. +/// Error code reserved for operations attempted after a Store backend is closed. +pub use self::error::ERROR_CODE_BACKEND_CLOSED; +/// Error code used when a known Store backend is selected without its compiled feature. +pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED; +/// Error code used when a compiled Store backend cannot complete opening. +pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED; +/// Error code used when Store settings violate backend-neutral bounds or invariants. +pub use self::error::ERROR_CODE_SETTINGS_INVALID; +/// Error code used when explicit Store shutdown exceeds its configured deadline. +pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT; +/// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade. +pub use self::settings::PostgresBootstrapSettings; +/// Bounded PostgreSQL connection-pool settings owned by the Store facade. +pub use self::settings::PostgresPoolSettings; +/// PostgreSQL settings owned by the Store facade without exposing backend implementation types. +pub use self::settings::PostgresStoreSettings; +/// TLS policy accepted by the backend-neutral PostgreSQL settings surface. +pub use self::settings::PostgresTlsMode; +/// Backend identity understood independently from compiled Cargo features. +pub use self::settings::StoreBackendKind; +/// Backend-specific settings selected through the common Store facade. +pub use self::settings::StoreBackendSettings; +/// Complete backend-neutral settings consumed by the common Store runtime facade. +pub use self::settings::StoreSettings; +/// Opaque common Store runtime facade with consuming async shutdown. +pub use self::store::Store; +/// Error code used when a RAW write collides with divergent content for the same logical identity. +pub use ksp_store_api::ERROR_CODE_RAW_CONFLICT; +/// Error code used when a RAW Store model violates one of its backend-agnostic invariants. +pub use ksp_store_api::ERROR_CODE_RAW_MODEL_INVALID; +/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract. +pub use ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID; +/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. +pub use ksp_store_api::ERROR_CODE_RAW_PROVENANCE_INVALID; +/// Error code used when one RAW query or cursor violates backend-agnostic query invariants. +pub use ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID; +/// Error code used when a RAW retention transition violates the logical lifecycle contract. +pub use ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID; +/// Common KSP error type used by Store-facing contracts. +pub use ksp_store_api::Error; +/// Stable structured code identifying a KSP error category and condition. +pub use ksp_store_api::ErrorCode; +/// Structured contextual field attached to a KSP error. +pub use ksp_store_api::ErrorContext; +/// Maximum complete RAW account-data length admitted by the Store API. +pub use ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES; +/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code. +pub use ksp_store_api::MAX_RAW_CODE_BYTES; +/// Maximum opaque query cursor length admitted by the Store API. +pub use ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES; +/// Maximum KSP-owned canonical RAW payload admitted by the Store API. +pub use ksp_store_api::MAX_RAW_PAYLOAD_BYTES; +/// Maximum source-wire payload size recorded as acquisition metadata. +pub use ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES; +/// Maximum supported Unix millisecond timestamp. +pub use ksp_store_api::MAX_RAW_UNIX_MILLIS; +/// Canonical Solana account address primitive shared by persistent models. +pub use ksp_store_api::Pubkey; +/// Persistable acquisition observation linked to one complete canonical RAW account state. +pub use ksp_store_api::RawAccountObservation; +/// Read capability for persisted RAW account-state observations. +pub use ksp_store_api::RawAccountObservationRead; +/// Write capability for additional observations of already persisted RAW account states. +pub use ksp_store_api::RawAccountObservationWrite; +/// Canonical complete N1 RAW account state independent from acquisition transport. +pub use ksp_store_api::RawAccountState; +/// Backend-independent list query for complete canonical RAW account states. +pub use ksp_store_api::RawAccountStateQuery; +/// Read capability for complete canonical RAW account states. +pub use ksp_store_api::RawAccountStateRead; +/// Durable backend-independent identity of one canonical RAW account state. +pub use ksp_store_api::RawAccountStateReference; +/// Write capability for complete canonical RAW account-state acquisitions. +pub use ksp_store_api::RawAccountStateWrite; +/// Origin category describing why one acquisition was performed. +pub use ksp_store_api::RawAcquisitionOrigin; +/// Safe source-independent acquisition provenance attached to one persisted observation. +pub use ksp_store_api::RawAcquisitionProvenance; +/// Combined outcome of one atomic canonical RAW entity plus observation acquisition. +pub use ksp_store_api::RawAcquisitionWriteOutcome; +/// Fixed-size digest identifying canonical or source bytes without retaining them. +pub use ksp_store_api::RawContentHash; +/// Outcome for one canonical RAW entity in an idempotent persistence operation. +pub use ksp_store_api::RawEntityWriteOutcome; +/// Bounded identifier of one KSP-owned source-independent RAW persistence format. +pub use ksp_store_api::RawFormatId; +/// Bounded logical network/cluster identifier used in backend-independent Store identities. +pub use ksp_store_api::RawNetworkId; +/// Stable deterministic idempotence key for one persisted acquisition observation. +pub use ksp_store_api::RawObservationKey; +/// Outcome for one deterministic acquisition observation write. +pub use ksp_store_api::RawObservationWriteOutcome; +/// One deterministic page of backend-independent Store results. +pub use ksp_store_api::RawPage; +/// Opaque backend-owned cursor returned by one deterministic Store query. +pub use ksp_store_api::RawPageCursor; +/// Caller-requested page size without an arbitrary KSP policy ceiling. +pub use ksp_store_api::RawPageLimit; +/// Opaque-cursor page request used by backend-independent list operations. +pub use ksp_store_api::RawPageRequest; +/// Bounded source-independent KSP RAW persistence payload. +pub use ksp_store_api::RawPayload; +/// Bounded logical code used by acquisition provenance fields. +pub use ksp_store_api::RawProvenanceCode; +/// Logical availability state of one canonical RAW payload. +pub use ksp_store_api::RawRetentionState; +/// Outcome of one atomic RAW retention transition. +pub use ksp_store_api::RawRetentionWriteOutcome; +/// Optional inclusive Solana slot bounds for one Store query. +pub use ksp_store_api::RawSlotRange; +/// Deterministic traversal direction for Store list queries. +pub use ksp_store_api::RawSortDirection; +/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch. +pub use ksp_store_api::RawTimestamp; +/// Canonical source-independent N1 RAW transaction persisted by Store backends. +pub use ksp_store_api::RawTransaction; +/// Explicit write mode for canonical RAW transaction acquisitions. +pub use ksp_store_api::RawTransactionAcquisitionMode; +/// Persistable acquisition observation linked to one canonical RAW transaction. +pub use ksp_store_api::RawTransactionObservation; +/// Read capability for persisted RAW transaction observations. +pub use ksp_store_api::RawTransactionObservationRead; +/// Write capability for additional observations of already persisted RAW transactions. +pub use ksp_store_api::RawTransactionObservationWrite; +/// Backend-independent list query for canonical RAW transactions. +pub use ksp_store_api::RawTransactionQuery; +/// Read capability for canonical RAW transactions. +pub use ksp_store_api::RawTransactionRead; +/// Durable backend-independent identity of one canonical RAW transaction. +pub use ksp_store_api::RawTransactionReference; +/// Read capability for canonical RAW transaction retention metadata. +pub use ksp_store_api::RawTransactionRetentionRead; +/// Requested compare-and-transition operation for one RAW transaction retention state. +pub use ksp_store_api::RawTransactionRetentionTransition; +/// Write capability for policy-authorized RAW transaction retention transitions. +pub use ksp_store_api::RawTransactionRetentionWrite; +/// Canonical 64-byte Solana transaction signature used by Store identities. +pub use ksp_store_api::RawTransactionSignature; +/// Minimal durable identity retained after a canonical RAW transaction payload is purged. +pub use ksp_store_api::RawTransactionTombstone; +/// Write capability for canonical RAW transaction acquisitions. +pub use ksp_store_api::RawTransactionWrite; +/// Common KSP result alias using [`Error`]. +pub use ksp_store_api::Result; +/// Boxed async operation returned by object-safe Store capability contracts. +pub use ksp_store_api::StoreApiFuture; + +/// Crate-owned tracing target reserved for Store runtime behavior. pub(crate) use self::constants::TRACING_TARGET; // Keep the mandatory crate-owned tracing target part of the compiled scaffold -// without inventing runtime logging before the first behavioral tranche. -const _: &str = TRACING_TARGET; +// without inventing runtime logging before the first behavioral log site. +const _: &str = crate::TRACING_TARGET; diff --git a/crates/ksp-store-lib/src/settings.rs b/crates/ksp-store-lib/src/settings.rs new file mode 100644 index 0000000..50db947 --- /dev/null +++ b/crates/ksp-store-lib/src/settings.rs @@ -0,0 +1,381 @@ +// file: crates/ksp-store-lib/src/settings.rs +// version: 1 + +const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; +const DEFAULT_MAX_CONNECTIONS: u32 = 8; +const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS: u64 = 10_000; +const DEFAULT_MIGRATION_TIMEOUT_MS: u64 = 30_000; +const DEFAULT_POOL_CREATE_TIMEOUT_MS: u64 = 10_000; +const DEFAULT_POOL_RECYCLE_TIMEOUT_MS: u64 = 5_000; +const DEFAULT_POOL_WAIT_TIMEOUT_MS: u64 = 5_000; +const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 5_000; +const MAX_CONNECTIONS: u32 = 64; +const MAX_CONNECT_TIMEOUT_MS: u64 = 60_000; +const MAX_MIGRATION_LOCK_TIMEOUT_MS: u64 = 120_000; +const MAX_MIGRATION_TIMEOUT_MS: u64 = 300_000; +const MAX_POOL_CREATE_TIMEOUT_MS: u64 = 60_000; +const MAX_POOL_RECYCLE_TIMEOUT_MS: u64 = 60_000; +const MAX_POOL_WAIT_TIMEOUT_MS: u64 = 60_000; +const MAX_SHUTDOWN_TIMEOUT_MS: u64 = 30_000; +const MIN_CONNECTIONS: u32 = 1; +const MIN_CONNECT_TIMEOUT_MS: u64 = 100; +const MIN_MIGRATION_LOCK_TIMEOUT_MS: u64 = 100; +const MIN_MIGRATION_TIMEOUT_MS: u64 = 1_000; +const MIN_POOL_CREATE_TIMEOUT_MS: u64 = 100; +const MIN_POOL_RECYCLE_TIMEOUT_MS: u64 = 100; +const MIN_POOL_WAIT_TIMEOUT_MS: u64 = 100; +const MIN_SHUTDOWN_TIMEOUT_MS: u64 = 100; + +/// Backend identity understood by the common Store runtime independently from compiled Cargo features. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum StoreBackendKind { + /// Official PostgreSQL Store backend. + Postgres, +} + +impl StoreBackendKind { + /// Returns the stable safe backend code used in diagnostics and configuration mapping. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Postgres => "postgres", + }; + } +} + +/// TLS policy accepted by the backend-neutral PostgreSQL settings surface. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum PostgresTlsMode { + /// Connect without TLS. + Disabled, + /// Require TLS and verify both the certificate chain and requested server identity. + VerifyFull, +} + +/// Bounded PostgreSQL connection-pool settings owned by the Store facade. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PostgresPoolSettings { + connect_timeout: std::time::Duration, + create_timeout: std::time::Duration, + max_connections: u32, + recycle_timeout: std::time::Duration, + wait_timeout: std::time::Duration, +} + +impl PostgresPoolSettings { + /// Creates explicit PostgreSQL pool bounds without performing any I/O. + #[must_use] + pub const fn new( + max_connections: u32, + connect_timeout: std::time::Duration, + wait_timeout: std::time::Duration, + create_timeout: std::time::Duration, + recycle_timeout: std::time::Duration, + ) -> Self { + return Self { connect_timeout, create_timeout, max_connections, recycle_timeout, wait_timeout }; + } + + /// Returns the timeout for establishing one physical PostgreSQL connection. + #[must_use] + pub const fn connect_timeout(&self) -> std::time::Duration { + return self.connect_timeout; + } + + /// Returns the timeout for creating one pooled PostgreSQL object. + #[must_use] + pub const fn create_timeout(&self) -> std::time::Duration { + return self.create_timeout; + } + + /// Returns the maximum number of physical PostgreSQL connections owned by the pool. + #[must_use] + pub const fn max_connections(&self) -> u32 { + return self.max_connections; + } + + /// Returns the timeout for recycling one pooled PostgreSQL object. + #[must_use] + pub const fn recycle_timeout(&self) -> std::time::Duration { + return self.recycle_timeout; + } + + /// Returns the maximum time one acquisition can wait for pool capacity. + #[must_use] + pub const fn wait_timeout(&self) -> std::time::Duration { + return self.wait_timeout; + } + + /// Validates all pool bounds without opening a connection. + pub fn validate(&self) -> ksp_store_api::Result<()> { + if self.max_connections < MIN_CONNECTIONS || self.max_connections > MAX_CONNECTIONS { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL pool connection bound is invalid") + .with_context("field", "postgres.pool.max_connections") + .with_context("minimum", MIN_CONNECTIONS.to_string()) + .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); + if let std::result::Result::Err(error) = connect_validation { + return std::result::Result::Err(error); + } + let wait_validation = validate_duration("postgres.pool.wait_timeout", self.wait_timeout, MIN_POOL_WAIT_TIMEOUT_MS, MAX_POOL_WAIT_TIMEOUT_MS); + 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); + 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); + if let std::result::Result::Err(error) = recycle_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(()); + } +} + +impl std::default::Default for PostgresPoolSettings { + fn default() -> Self { + return Self::new( + DEFAULT_MAX_CONNECTIONS, + std::time::Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS), + std::time::Duration::from_millis(DEFAULT_POOL_WAIT_TIMEOUT_MS), + std::time::Duration::from_millis(DEFAULT_POOL_CREATE_TIMEOUT_MS), + std::time::Duration::from_millis(DEFAULT_POOL_RECYCLE_TIMEOUT_MS), + ); + } +} + +/// Bounded PostgreSQL bootstrap settings owned by the Store facade. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PostgresBootstrapSettings { + auto_migrate: bool, + migration_lock_timeout: std::time::Duration, + migration_timeout: std::time::Duration, +} + +impl PostgresBootstrapSettings { + /// Creates explicit bootstrap behavior and migration deadlines. + #[must_use] + pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self { + return Self { auto_migrate, migration_lock_timeout, migration_timeout }; + } + + /// Returns whether pending KSP-owned migrations may be applied during Store opening. + #[must_use] + pub const fn auto_migrate(&self) -> bool { + return self.auto_migrate; + } + + /// Returns the bounded wait allowed for the private PostgreSQL migration lock. + #[must_use] + pub const fn migration_lock_timeout(&self) -> std::time::Duration { + return self.migration_lock_timeout; + } + + /// Returns the bounded duration allowed for one migration/bootstrap run. + #[must_use] + pub const fn migration_timeout(&self) -> std::time::Duration { + return self.migration_timeout; + } + + /// 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); + if let std::result::Result::Err(error) = migration_validation { + return std::result::Result::Err(error); + } + let lock_validation = validate_duration( + "postgres.bootstrap.migration_lock_timeout", + self.migration_lock_timeout, + MIN_MIGRATION_LOCK_TIMEOUT_MS, + MAX_MIGRATION_LOCK_TIMEOUT_MS, + ); + if let std::result::Result::Err(error) = lock_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(()); + } +} + +impl std::default::Default for PostgresBootstrapSettings { + fn default() -> Self { + return Self::new( + true, + std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS), + std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS), + ); + } +} + +/// PostgreSQL settings owned by the Store facade and independent from Config or backend implementation types. +pub struct PostgresStoreSettings { + bootstrap: PostgresBootstrapSettings, + connection_uri: std::string::String, + pool: PostgresPoolSettings, + tls_mode: PostgresTlsMode, +} + +impl PostgresStoreSettings { + /// Creates PostgreSQL Store settings from an explicitly supplied sensitive connection URI and typed runtime bounds. + #[must_use] + pub fn new( + connection_uri: impl std::convert::Into, + pool: PostgresPoolSettings, + tls_mode: PostgresTlsMode, + bootstrap: PostgresBootstrapSettings, + ) -> Self { + return Self { bootstrap, connection_uri: connection_uri.into(), pool, tls_mode }; + } + + /// Returns the PostgreSQL bootstrap settings without exposing the sensitive connection URI. + #[must_use] + pub const fn bootstrap(&self) -> PostgresBootstrapSettings { + return self.bootstrap; + } + + /// Returns the PostgreSQL pool settings without exposing the sensitive connection URI. + #[must_use] + pub const fn pool(&self) -> PostgresPoolSettings { + return self.pool; + } + + /// Returns the selected PostgreSQL TLS policy without exposing the sensitive connection URI. + #[must_use] + pub const fn tls_mode(&self) -> PostgresTlsMode { + return self.tls_mode; + } + + /// Validates backend-neutral PostgreSQL settings without parsing the URI or performing I/O. + pub fn validate(&self) -> ksp_store_api::Result<()> { + if self.connection_uri.is_empty() { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL connection URI is required") + .with_context("field", "postgres.connection_uri"), + ); + } + let pool_validation = self.pool.validate(); + if let std::result::Result::Err(error) = pool_validation { + return std::result::Result::Err(error); + } + let bootstrap_validation = self.bootstrap.validate(); + if let std::result::Result::Err(error) = bootstrap_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(()); + } +} + +impl std::fmt::Debug for PostgresStoreSettings { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("PostgresStoreSettings") + .field("connection_uri", &"") + .field("pool", &self.pool) + .field("tls_mode", &self.tls_mode) + .field("bootstrap", &self.bootstrap) + .finish(); + } +} + +/// Backend-specific settings selected through the common Store facade. +#[derive(Debug)] +#[non_exhaustive] +pub enum StoreBackendSettings { + /// Settings for the known PostgreSQL backend, whether or not its Cargo feature is compiled. + Postgres(PostgresStoreSettings), +} + +impl StoreBackendSettings { + /// Returns the stable backend identity represented by these settings. + #[must_use] + pub const fn kind(&self) -> StoreBackendKind { + return match self { + Self::Postgres(_) => StoreBackendKind::Postgres, + }; + } + + /// Validates backend-specific settings without performing I/O. + pub fn validate(&self) -> ksp_store_api::Result<()> { + return match self { + Self::Postgres(settings) => settings.validate(), + }; + } +} + +/// Complete backend-neutral settings consumed by the common Store runtime facade. +#[derive(Debug)] +pub struct StoreSettings { + backend: StoreBackendSettings, + shutdown_timeout: std::time::Duration, +} + +impl StoreSettings { + /// Creates complete Store runtime settings from one explicit backend and shutdown bound. + #[must_use] + pub const fn new(backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self { + return Self { backend, shutdown_timeout }; + } + + /// Returns the selected backend settings. + #[must_use] + pub const fn backend(&self) -> &StoreBackendSettings { + return &self.backend; + } + + /// Returns the selected stable backend identity. + #[must_use] + pub const fn backend_kind(&self) -> StoreBackendKind { + return self.backend.kind(); + } + + /// Returns the maximum duration allowed for explicit Store shutdown. + #[must_use] + pub const fn shutdown_timeout(&self) -> std::time::Duration { + return self.shutdown_timeout; + } + + /// Validates all backend-neutral Store settings before any backend I/O can start. + pub fn validate(&self) -> ksp_store_api::Result<()> { + let shutdown_validation = validate_duration("shutdown_timeout", self.shutdown_timeout, MIN_SHUTDOWN_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS); + if let std::result::Result::Err(error) = shutdown_validation { + return std::result::Result::Err(error); + } + let backend_validation = self.backend.validate(); + if let std::result::Result::Err(error) = backend_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(()); + } +} + +impl StoreSettings { + /// Creates settings using the common default shutdown bound while keeping 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)); + } +} + +fn validate_duration(field: &'static str, value: std::time::Duration, minimum_ms: u64, maximum_ms: u64) -> ksp_store_api::Result<()> { + let minimum = std::time::Duration::from_millis(minimum_ms); + let maximum = std::time::Duration::from_millis(maximum_ms); + if value < minimum || value > maximum { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "Store runtime duration is outside the supported resource bound") + .with_context("field", field) + .with_context("minimum_ms", minimum_ms.to_string()) + .with_context("maximum_ms", maximum_ms.to_string()), + ); + } + return std::result::Result::Ok(()); +} + +#[cfg(test)] +#[path = "../unit_tests/settings.rs"] +mod tests; diff --git a/crates/ksp-store-lib/src/store.rs b/crates/ksp-store-lib/src/store.rs new file mode 100644 index 0000000..652d216 --- /dev/null +++ b/crates/ksp-store-lib/src/store.rs @@ -0,0 +1,56 @@ +// file: crates/ksp-store-lib/src/store.rs +// version: 1 + +/// Opaque common Store runtime facade. +/// +/// A successful value is returned only after the selected backend has completed its bounded readiness path. `0.3.2-pre.003` establishes the lifecycle +/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases. +#[derive(Debug)] +#[non_exhaustive] +pub struct Store; + +impl Store { + /// Validates settings, selects the requested backend and opens a ready Store runtime. + /// + /// A known backend whose Cargo feature is absent is rejected before any I/O. During `0.3.2-pre.003`, the compiled PostgreSQL path also stops before I/O + /// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`. + pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result { + let validation = settings.validate(); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(error); + } + let backend_kind = settings.backend_kind(); + return match backend_kind { + crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await, + }; + } + + /// Explicitly closes the Store runtime and consumes its facade handle. + /// + /// `pre.003` cannot yet produce a successful Store instance, so the physical bounded shutdown path remains reserved for backend composition. The consuming + /// async signature is fixed here so no pool or backend handle needs to escape later. + pub async fn close(self) -> ksp_store_api::Result<()> { + return std::result::Result::Ok(()); + } +} + +#[cfg(feature = "postgres")] +async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_OPEN_FAILED, "PostgreSQL Store runtime opening is not materialized in this prerelease") + .with_context("backend", backend_kind.code()) + .with_context("stage", "runtime_foundation_pending"), + ); +} + +#[cfg(not(feature = "postgres"))] +async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result { + return std::result::Result::Err( + ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled") + .with_context("backend", backend_kind.code()), + ); +} + +#[cfg(test)] +#[path = "../unit_tests/store.rs"] +mod tests; diff --git a/crates/ksp-store-lib/tests/dependency_boundary.rs b/crates/ksp-store-lib/tests/dependency_boundary.rs index ef6326c..cef831e 100644 --- a/crates/ksp-store-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-lib/tests/dependency_boundary.rs @@ -1,11 +1,11 @@ // file: crates/ksp-store-lib/tests/dependency_boundary.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] -//! Cargo-feature and dependency-boundary canaries for the common Store runtime scaffold. +//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade. #[test] fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() { @@ -24,30 +24,27 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() { "deadpool-postgres", "rustls", ] { - assert!(!manifest.contains(forbidden), "forbidden pre.002 Store facade dependency detected: {forbidden}"); + assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}"); } return; } #[test] -fn pre_002_scaffold_keeps_backend_types_and_future_runtime_surface_private() { +fn pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports() { let crate_root = include_str!("../src/lib.rs"); - assert!(crate_root.contains("mod constants;")); - assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;")); - assert!(crate_root.contains("const _: &str = TRACING_TARGET;")); - for forbidden in [ - "pub mod ", - "pub use ksp_store_postgres_lib", - "StoreSettings", - "StoreBackendSettings", - "PostgresStoreSettings", - "pub struct Store", - "tokio_postgres", - "deadpool_postgres", - ] { - assert!(!crate_root.contains(forbidden), "forbidden pre.002 Store facade surface detected: {forbidden}"); + assert!(crate_root.contains("mod error;")); + assert!(crate_root.contains("mod settings;")); + assert!(crate_root.contains("mod store;")); + assert!(crate_root.contains("pub use self::settings::StoreSettings;")); + assert!(crate_root.contains("pub use self::store::Store;")); + assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;")); + assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;")); + for forbidden in ["pub mod ", "pub use ksp_store_postgres_lib", "tokio_postgres", "deadpool_postgres", "rustls::", "Pool", "Client", "Row", "Statement"] { + assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}"); + } + let production = format!("{}\n{}", include_str!("../src/settings.rs"), include_str!("../src/store.rs")); + for forbidden in ["ksp_config_lib", "std::env", "dotenv", "PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", ".pgpass", "tokio_postgres", "deadpool_postgres"] { + assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}"); } - let constants = include_str!("../src/constants.rs"); - assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-lib\";")); return; } diff --git a/crates/ksp-store-lib/tests/feature_mismatch.rs b/crates/ksp-store-lib/tests/feature_mismatch.rs new file mode 100644 index 0000000..7659488 --- /dev/null +++ b/crates/ksp-store-lib/tests/feature_mismatch.rs @@ -0,0 +1,46 @@ +// file: crates/ksp-store-lib/tests/feature_mismatch.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Feature-selection canaries for known Store backends. + +fn poll_ready(future: impl std::future::Future) -> T { + let mut future = std::boxed::Box::pin(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + return match std::future::Future::poll(future.as_mut(), &mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("pre.003 Store feature-dispatch future unexpectedly became pending before any I/O exists"), + }; +} + +fn settings() -> ksp_store_lib::StoreSettings { + let postgres = ksp_store_lib::PostgresStoreSettings::new( + "postgresql://operator-supplied-sensitive-value", + ksp_store_lib::PostgresPoolSettings::default(), + ksp_store_lib::PostgresTlsMode::Disabled, + ksp_store_lib::PostgresBootstrapSettings::default(), + ); + return ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres)); +} + +#[cfg(not(feature = "postgres"))] +#[test] +fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() { + let result = poll_ready(ksp_store_lib::Store::open(settings())); + let error = result.err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED)); + return; +} + +#[cfg(feature = "postgres")] +#[test] +fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() { + let result = poll_ready(ksp_store_lib::Store::open(settings())); + let error = result.err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED)); + return; +} diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs new file mode 100644 index 0000000..308bd12 --- /dev/null +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -0,0 +1,45 @@ +// file: crates/ksp-store-lib/tests/public_api.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Public API canaries for Store settings, lifecycle and Store API reexports. + +#[test] +fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() { + let postgres = ksp_store_lib::PostgresStoreSettings::new( + "postgresql://operator-supplied-sensitive-value", + ksp_store_lib::PostgresPoolSettings::default(), + 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)); + assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres); + assert!(settings.validate().is_ok()); + let _open = ksp_store_lib::Store::open; + let _close = ksp_store_lib::Store::close; + return; +} + +#[test] +fn pre_003_common_error_codes_are_stable_and_store_owned() { + assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store"); + assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid"); + assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled"); + assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed"); + assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed"); + assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout"); + return; +} + +#[test] +fn pre_003_facade_reexports_backend_agnostic_store_api_types() { + let _raw_transaction = std::mem::size_of::>(); + let _raw_account_state = std::mem::size_of::>(); + let _query = std::mem::size_of::>(); + let _capability = std::mem::size_of::>(); + let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(()); + return; +} diff --git a/crates/ksp-store-lib/unit_tests/settings.rs b/crates/ksp-store-lib/unit_tests/settings.rs new file mode 100644 index 0000000..f55525f --- /dev/null +++ b/crates/ksp-store-lib/unit_tests/settings.rs @@ -0,0 +1,139 @@ +// file: crates/ksp-store-lib/unit_tests/settings.rs +// version: 1 + +fn valid_postgres_settings() -> crate::PostgresStoreSettings { + return crate::PostgresStoreSettings::new( + "postgresql://secret-user:secret-password@db.internal/ksp", + crate::PostgresPoolSettings::default(), + crate::PostgresTlsMode::VerifyFull, + crate::PostgresBootstrapSettings::default(), + ); +} + +#[test] +fn defaults_match_the_pre_001_runtime_bounds() { + let pool = crate::PostgresPoolSettings::default(); + assert_eq!(pool.max_connections(), 8); + assert_eq!(pool.connect_timeout(), std::time::Duration::from_millis(10_000)); + assert_eq!(pool.wait_timeout(), std::time::Duration::from_millis(5_000)); + assert_eq!(pool.create_timeout(), std::time::Duration::from_millis(10_000)); + assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000)); + let bootstrap = crate::PostgresBootstrapSettings::default(); + 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())); + assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000)); + assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres); + return; +} + +#[test] +fn exact_runtime_boundaries_validate_and_adjacent_values_are_rejected() { + let minimum_pool = crate::PostgresPoolSettings::new( + 1, + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + ); + assert!(minimum_pool.validate().is_ok()); + let maximum_pool = crate::PostgresPoolSettings::new( + 64, + std::time::Duration::from_millis(60_000), + std::time::Duration::from_millis(60_000), + std::time::Duration::from_millis(60_000), + std::time::Duration::from_millis(60_000), + ); + assert!(maximum_pool.validate().is_ok()); + assert!( + crate::PostgresPoolSettings::new( + 0, + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + ) + .validate() + .is_err() + ); + assert!( + crate::PostgresPoolSettings::new( + 65, + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + ) + .validate() + .is_err() + ); + assert!( + crate::PostgresPoolSettings::new( + 1, + std::time::Duration::from_millis(99), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + ) + .validate() + .is_err() + ); + assert!( + crate::PostgresPoolSettings::new( + 1, + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(60_001), + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100), + ) + .validate() + .is_err() + ); + let minimum_bootstrap = crate::PostgresBootstrapSettings::new(false, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(100)); + assert!(minimum_bootstrap.validate().is_ok()); + let maximum_bootstrap = crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(300_000), std::time::Duration::from_millis(120_000)); + assert!(maximum_bootstrap.validate().is_ok()); + assert!( + crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(999), std::time::Duration::from_millis(100),) + .validate() + .is_err() + ); + assert!( + crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(120_001),) + .validate() + .is_err() + ); + return; +} + +#[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)); + assert!(valid.validate().is_ok()); + let invalid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001)); + let error = invalid.validate().err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); + return; +} + +#[test] +fn connection_uri_is_required_but_never_rendered_by_debug_or_validation_error() { + let secret = "postgresql://secret-user:secret-password@db.internal/ksp"; + let settings = valid_postgres_settings(); + let debug = format!("{settings:?}"); + assert!(!debug.contains(secret)); + assert!(!debug.contains("secret-user")); + assert!(!debug.contains("secret-password")); + assert!(debug.contains("")); + let empty = crate::PostgresStoreSettings::new( + std::string::String::new(), + crate::PostgresPoolSettings::default(), + crate::PostgresTlsMode::Disabled, + crate::PostgresBootstrapSettings::default(), + ); + let error = empty.validate().err(); + assert_eq!(error.as_ref().map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); + assert!(!format!("{:?}", error).contains(secret)); + return; +} diff --git a/crates/ksp-store-lib/unit_tests/store.rs b/crates/ksp-store-lib/unit_tests/store.rs new file mode 100644 index 0000000..5d743a1 --- /dev/null +++ b/crates/ksp-store-lib/unit_tests/store.rs @@ -0,0 +1,55 @@ +// file: crates/ksp-store-lib/unit_tests/store.rs +// version: 1 + +fn poll_ready(future: impl std::future::Future) -> T { + let mut future = std::boxed::Box::pin(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + return match std::future::Future::poll(future.as_mut(), &mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("pre.003 Store contract future unexpectedly became pending before any I/O exists"), + }; +} + +fn valid_store_settings() -> crate::StoreSettings { + let postgres = crate::PostgresStoreSettings::new( + "postgresql://secret-user:secret-password@db.internal/ksp", + crate::PostgresPoolSettings::default(), + crate::PostgresTlsMode::VerifyFull, + crate::PostgresBootstrapSettings::default(), + ); + return crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres)); +} + +#[test] +fn invalid_settings_are_rejected_before_backend_dispatch() { + let postgres = crate::PostgresStoreSettings::new( + std::string::String::new(), + crate::PostgresPoolSettings::default(), + crate::PostgresTlsMode::Disabled, + crate::PostgresBootstrapSettings::default(), + ); + let settings = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres)); + let result = poll_ready(crate::Store::open(settings)); + let error = result.err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); + return; +} + +#[cfg(feature = "postgres")] +#[test] +fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() { + let result = poll_ready(crate::Store::open(valid_store_settings())); + let error = result.err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_OPEN_FAILED)); + return; +} + +#[cfg(not(feature = "postgres"))] +#[test] +fn known_postgres_without_feature_is_rejected_before_io() { + let result = poll_ready(crate::Store::open(valid_store_settings())); + let error = result.err(); + assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED)); + return; +} diff --git a/deltas/0.3.2/pre.003.md b/deltas/0.3.2/pre.003.md new file mode 100644 index 0000000..8cb9632 --- /dev/null +++ b/deltas/0.3.2/pre.003.md @@ -0,0 +1,214 @@ +# Delta `0.3.2-pre.003` — Store settings, backend selection et lifecycle contract + +## 1. Base requise + +Base exacte : + +```text +0.3.2-pre.002-fix.001 +workspace.package.version = 0.3.2-pre.2.fix.1 +``` + +Le gate opérateur fourni le 29 août 2026 est entièrement vert et sans warning : + +```text +cargo fmt --all PASS +audit Rust général / exports / workspace PASS +audit Markdown PASS — 186 tables / 126 files +cargo check --workspace PASS +cargo clippy --workspace --all-targets PASS +cargo test -p ksp-store-lib PASS +cargo test -p ksp-store-postgres-lib PASS +cargo check -p ksp-store-lib --no-default-features PASS +``` + +Le correctif `pre.002-fix.001` est déjà commité. `pre.003` ne réécrit pas ce delta. Le crate-root `ksp-store-lib` étant réellement modifié dans cette tranche, son ancrage `TRACING_TARGET` adopte désormais la forme canonique `const _: &str = crate::TRACING_TARGET;`. `crates/ksp-store-lib/src/constants.rs` n'est pas modifié et conserve donc son header/version existant. + +## 2. Objectif + +Matérialiser uniquement le contrat runtime backend-neutral prévu par le plan 023 : + +```text +StoreBackendKind +StoreBackendSettings +PostgresStoreSettings +PostgresPoolSettings +PostgresBootstrapSettings +PostgresTlsMode +StoreSettings +Store::open(settings).await +Store::close(self).await +codes d'erreur Store stables +réexports ksp-store-api depuis la façade +``` + +Aucune connexion, pool physique, TLS connector, migration, Config `std.store` ou SQL n'est introduit. + +## 3. Settings et bornes + +`PostgresStoreSettings` possède une URI explicitement fournie par le caller. Il n'existe aucun `Default` qui invente une URI ou un backend. + +La chaîne URI : + +```text +est possédée par le settings +est intégralement redacted dans Debug +n'a aucun getter public +n'entre dans aucun ErrorContext +n'est pas parsée avant pre.005 +``` + +Les settings typés matérialisent les bornes décidées en `pre.001` : + +```text +max_connections défaut 8 plage 1..64 +connect_timeout_ms défaut 10_000 plage 100..60_000 +pool_wait_timeout_ms défaut 5_000 plage 100..60_000 +pool_create_timeout_ms défaut 10_000 plage 100..60_000 +pool_recycle_timeout_ms défaut 5_000 plage 100..60_000 +shutdown_timeout_ms défaut 5_000 plage 100..30_000 +auto_migrate défaut true +migration_timeout_ms défaut 30_000 plage 1_000..300_000 +migration_lock_ms défaut 10_000 plage 100..120_000 +``` + +Ces bornes restent des garde-fous de ressources/lifecycle et non une policy worker/job. + +## 4. Backend selection et lifecycle + +Backend connu : + +```text +StoreBackendKind::Postgres +``` + +Le dispatch `Store::open` valide d'abord tous les settings puis : + +```text +feature postgres absente -> store.backend_not_compiled avant I/O +feature postgres présente -> store.backend_open_failed / runtime_foundation_pending avant I/O +``` + +Le second résultat est volontairement transitoire : `pre.003` ne fabrique jamais un faux `Store` prêt avant la connexion réelle de `pre.005`. La signature finale `Store::close(self).await` est fixée maintenant, mais aucune ressource physique n'existe encore à fermer. + +## 5. Erreurs Store communes + +Codes crate-root : + +```text +ERROR_CODE_BACKEND_CLOSED -> store.backend_closed +ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled +ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed +ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid +ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout +``` + +Aucune erreur externe ou chaîne distante n'est attachée à cette tranche. + +## 6. Réexports API + +`ksp-store-lib` réexporte explicitement les 60 éléments crate-root de `ksp-store-api` nécessaires à la consommation de la façade. + +La façade compte donc à cette tranche : + +```text +60 reexports ksp-store-api +13 éléments runtime Store propres +73 exports crate-root au total +``` + +Aucun type, handle ou symbole de `ksp-store-postgres-lib` n'est réexporté. + +## 7. Fichiers + +Modifiés : + +```text +Cargo.toml +crates/ksp-store-lib/src/lib.rs +crates/ksp-store-lib/tests/dependency_boundary.rs +docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +``` + +Ajoutés : + +```text +crates/ksp-store-lib/src/error.rs +crates/ksp-store-lib/src/settings.rs +crates/ksp-store-lib/src/store.rs +crates/ksp-store-lib/unit_tests/settings.rs +crates/ksp-store-lib/unit_tests/store.rs +crates/ksp-store-lib/tests/feature_mismatch.rs +crates/ksp-store-lib/tests/public_api.rs +deltas/0.3.2/pre.003.md +``` + +Non modifiés intentionnellement : + +```text +crates/ksp-store-lib/src/constants.rs +crates/ksp-store-postgres-lib/** +ksp-store-api/** +config/** +CHANGELOG.md +ROADMAP.md +``` + +## 8. Version + +La tranche touche le runtime/API Rust ; la version workspace devient : + +```text +0.3.2-pre.3 +``` + +Identifiant de livraison : + +```text +0.3.2-pre.003 +``` + +## 9. Validations exécutées pendant la préparation + +```text +python3 scripts/audit_rust_workspace_rules.py PASS +``` + +Les validations Cargo ne sont pas disponibles dans l'environnement de génération et ne sont jamais déclarées PASS. + +## 10. Gate opérateur requis + +```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-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo test -p ksp-store-postgres-lib +cargo check -p ksp-store-lib --no-default-features +cargo tree -p ksp-store-lib --edges normal +cargo tree -p ksp-store-lib -e features +cargo tree -p ksp-store-postgres-lib --edges normal +``` + +## 11. Hors scope et suite + +Toujours absents : + +```text +Config std.store +tokio-postgres +Deadpool +Rustls +connexion PostgreSQL +migrations/bootstrap physique +health/readiness physique +RawTransaction PostgreSQL +RawAccountState PostgreSQL +``` + +Si le gate est vert, `0.3.2-pre.004` ouvre exclusivement Config `std.store`, son schema/example/registry/adaptor et les impacts de packaging strictement requis. 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 ebc7d10..827a6d0 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 @@ -366,7 +366,26 @@ migration_lock_ms défaut 10_000 plage 100..120_000 Il n'existe pas de `Default` qui invente une URI de production. Config et les callers programmatiques doivent construire explicitement le backend settings. -### 8.3 URI et secrets +### 8.3 Surface exacte matérialisée en `pre.003` + +Les noms publics sont figés comme suit : + +```text +StoreBackendKind::Postgres +StoreBackendSettings::Postgres(PostgresStoreSettings) +PostgresTlsMode::{Disabled, VerifyFull} +PostgresPoolSettings +PostgresBootstrapSettings +PostgresStoreSettings +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. + +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`. + +### 8.4 URI et secrets La connexion URI est considérée sensible intégralement, même lorsqu'elle ne contient pas de mot de passe visible. @@ -421,16 +440,16 @@ La fermeture consomme le Store, ferme le pool, interdit de nouvelles acquisition Cette forme limite structurellement le risque de close concurrent avec une opération qui emprunte encore le Store. -### 9.3 Erreurs stables candidates +### 9.3 Erreurs stables matérialisées -Domaine Store commun : +Domaine Store commun, figé en `pre.003` : ```text -STORE_SETTINGS_INVALID -STORE_BACKEND_NOT_COMPILED -STORE_BACKEND_OPEN_FAILED -STORE_BACKEND_CLOSED -STORE_SHUTDOWN_TIMEOUT +ERROR_CODE_BACKEND_CLOSED -> store.backend_closed +ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled +ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed +ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid +ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout ``` Backend PostgreSQL : @@ -899,6 +918,23 @@ Les deux crates possèdent déjà leur `src/constants.rs` et leur `TRACING_TARGE Matérialiser `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, erreurs stable, façade `Store` sans connexion lourde et réexports API. Tester la feature mismatch. +**Statut : matérialisé par `0.3.2-pre.003`, gate opérateur requis.** + +La tranche fixe : + +```text +73 exports crate-root de ksp-store-lib : 60 reexports ksp-store-api + 13 éléments runtime Store +settings Config-independent sans serde/env +URI PostgreSQL intégralement redacted et sans getter public +pool/bootstrap/shutdown bornés selon pre.001 +Store::open(settings).await +Store::close(self).await +postgres absent de la build -> store.backend_not_compiled avant I/O +postgres compilé avant pre.005 -> store.backend_open_failed/runtime_foundation_pending avant I/O +``` + +Le dernier comportement est volontairement transitoire : `pre.003` ne retourne jamais un faux `Store` prêt. `pre.005` remplacera uniquement ce stop de staging par la construction physique, puis `pre.007` fermera le shutdown/health end-to-end. Aucun type PostgreSQL physique n'est public. + ### `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. @@ -993,8 +1029,6 @@ Aucune question architecturale ne bloque `pre.002`. Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate : ```text -nom exact des structs/methods settings en pre.003 -codes numériques/strings finaux des ErrorCode en pre.003 mapping exact deadpool timeouts en pre.005 construction exacte du rustls RootCertStore en pre.005 DDL précis de ksp_store_schema_migrations en pre.006 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 af9a98e..b41e82f 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 @@ -70,6 +70,8 @@ Le gate opérateur après application de `pre.001`, fourni le 29 août 2026, est Le gate opérateur de `pre.002`, fourni le 29 août 2026, confirme également les tests ciblés, `--no-default-features` et les `cargo tree`. `cargo check`/Clippy terminent avec succès mais signalent deux warnings par nouvelle crate (`TRACING_TARGET` importé mais inutilisé et constant `dead_code`) ; `pre.002-fix.001` traite cet écart par un canari compile-time privé, tout en conservant les invariants KSP-TRACE-102/103 et sans ajouter de logging runtime. +Le gate opérateur de `pre.002-fix.001`, fourni le 29 août 2026, est entièrement vert et sans warning : audits Rust/Markdown, workspace check/Clippy, tests des deux crates et compilation `ksp-store-lib --no-default-features` passent. Cette base est l'entrée effective de `pre.003`. + ## 3. Frontières Cargo ### V32-DEP-001 — Façade -> API @@ -150,13 +152,17 @@ Statut : `TODO gate final`, baseline `v0.3.1` déjà verte. Critère : `StoreSettings` est constructible sans `ksp-config-lib`, sans env et sans serde requis par la façade. -Statut : `TODO pre.003`. +Matérialisé par `pre.003` : `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, pool/bootstrap/TLS typés, sans dépendance Config/serde/env. + +Statut : `TODO gate opérateur pre.003`. ### V32-API-002 — Backend connu non compilé Critère : `Postgres` reste un backend connu sans feature et `Store::open` échoue avant I/O avec un code stable. -Statut : `TODO pre.003`. +Matérialisé par `pre.003` : le test `feature_mismatch` appelle réellement `Store::open` sous `--no-default-features` et exige `store.backend_not_compiled`. + +Statut : `TODO gate opérateur pre.003 / TODO pre.009 final`. ### V32-API-003 — Aucun type backend physique public @@ -175,7 +181,9 @@ Statut : `TODO pre.009`. Critère : un consumer de `ksp-store-lib` accède aux contrats Store API utiles sans dépendre directement de la crate backend. -Statut : `TODO pre.003`. +`pre.003` réexporte explicitement les 60 symboles crate-root acquis de `ksp-store-api` depuis `ksp-store-lib`, sans glob et sans réexport backend. + +Statut : `TODO gate opérateur pre.003 / TODO pre.009 exact exports`. ### V32-API-005 — Lifecycle @@ -189,7 +197,9 @@ close borné Drop best-effort seulement ``` -Statut : `TODO pre.003/pre.007`. +`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await` et garde `Store` opaque/non constructible par un consumer. Aucun succès d'ouverture n'est simulé avant la connexion réelle : le backend compilé s'arrête avec `store.backend_open_failed` et le contexte sûr `runtime_foundation_pending`. + +Statut : `TODO gate opérateur pre.003` pour les signatures et le staging ; `TODO pre.007` pour le shutdown physique borné. ## 5. Config ownership @@ -485,11 +495,15 @@ Statut : `TODO pre.009`. Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close. -Statut : `TODO pre.003/pre.005/pre.009`. +Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. Le parsing PostgreSQL et les timeouts physiques restent à `pre.005`. + +Statut : `TODO gate opérateur pre.003 / TODO pre.005 / TODO pre.009`. ### V32-SEC-004 — Feature mismatch avant I/O -Statut : `TODO pre.003/pre.009`. +`pre.003` matérialise un canari d'intégration compilé avec et sans `postgres`. Sans feature, `Store::open` retourne le code stable `store.backend_not_compiled` avant tout chemin backend physique. + +Statut : `TODO gate opérateur pre.003 / TODO pre.009`. ### V32-SEC-005 — Server error sanitization @@ -514,6 +528,7 @@ Après création des crates : ```bash cargo test -p ksp-store-api cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features cargo test -p ksp-store-postgres-lib cargo test -p ksp-config-lib cargo check -p ksp-store-lib --no-default-features