v0.3.2-pre.003

This commit is contained in:
2026-08-29 17:43:52 +02:00
parent ced653bfc0
commit a8c90107b5
13 changed files with 1193 additions and 47 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 333 # version: 334
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.2-pre.2.fix.1" version = "0.3.2-pre.3"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -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");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs // file: crates/ksp-store-lib/src/lib.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -7,19 +7,170 @@
//! Common backend-neutral Store runtime facade for KSP. //! Common backend-neutral Store runtime facade for KSP.
//! //!
//! `0.3.2-pre.002` establishes only the crate and Cargo feature graph. Runtime //! `0.3.2-pre.003` materializes Config-independent settings, stable backend
//! settings, backend selection, lifecycle, error mapping and API reexports are //! identity, bounded validation and the opaque async Store lifecycle contract.
//! introduced by later prereleases of `0.3.2`. //! Physical PostgreSQL connection, pool, TLS and migrations remain private
//! future slices of this release.
//! //!
//! The default `postgres` feature compiles the official PostgreSQL backend as //! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is //! an optional implementation dependency. No backend implementation type is
//! part of this crate's public surface. //! part of this crate's public surface.
mod constants; 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; pub(crate) use self::constants::TRACING_TARGET;
// Keep the mandatory crate-owned tracing target part of the compiled scaffold // Keep the mandatory crate-owned tracing target part of the compiled scaffold
// without inventing runtime logging before the first behavioral tranche. // without inventing runtime logging before the first behavioral log site.
const _: &str = TRACING_TARGET; const _: &str = crate::TRACING_TARGET;

View File

@@ -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<std::string::String>,
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", &"<redacted>")
.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;

View File

@@ -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<Self> {
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<Store> {
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<Store> {
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;

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs // file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![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] #[test]
fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() { 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", "deadpool-postgres",
"rustls", "rustls",
] { ] {
assert!(!manifest.contains(forbidden), "forbidden pre.002 Store facade dependency detected: {forbidden}"); assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
} }
return; return;
} }
#[test] #[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"); let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod constants;")); assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;")); assert!(crate_root.contains("mod settings;"));
assert!(crate_root.contains("const _: &str = TRACING_TARGET;")); assert!(crate_root.contains("mod store;"));
for forbidden in [ assert!(crate_root.contains("pub use self::settings::StoreSettings;"));
"pub mod ", assert!(crate_root.contains("pub use self::store::Store;"));
"pub use ksp_store_postgres_lib", assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
"StoreSettings", assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
"StoreBackendSettings", for forbidden in ["pub mod ", "pub use ksp_store_postgres_lib", "tokio_postgres", "deadpool_postgres", "rustls::", "Pool", "Client", "Row", "Statement"] {
"PostgresStoreSettings", assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}");
"pub struct Store", }
"tokio_postgres", let production = format!("{}\n{}", include_str!("../src/settings.rs"), include_str!("../src/store.rs"));
"deadpool_postgres", 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}");
assert!(!crate_root.contains(forbidden), "forbidden pre.002 Store facade surface detected: {forbidden}");
} }
let constants = include_str!("../src/constants.rs");
assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-lib\";"));
return; return;
} }

View File

@@ -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<T>(future: impl std::future::Future<Output = T>) -> 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;
}

View File

@@ -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::<std::option::Option<ksp_store_lib::RawTransaction>>();
let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>();
let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>();
let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>();
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
return;
}

View File

@@ -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("<redacted>"));
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;
}

View File

@@ -0,0 +1,55 @@
// file: crates/ksp-store-lib/unit_tests/store.rs
// version: 1
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> 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;
}

214
deltas/0.3.2/pre.003.md Normal file
View File

@@ -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.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md --> <!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
<!-- version: 3 --> <!-- version: 4 -->
# Plan `0.3.2` — Store/PostgreSQL runtime foundation # 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. 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. 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. 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 ```text
STORE_SETTINGS_INVALID ERROR_CODE_BACKEND_CLOSED -> store.backend_closed
STORE_BACKEND_NOT_COMPILED ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled
STORE_BACKEND_OPEN_FAILED ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed
STORE_BACKEND_CLOSED ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid
STORE_SHUTDOWN_TIMEOUT ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout
``` ```
Backend PostgreSQL : 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. 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` ### `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. 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 : Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :
```text ```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 mapping exact deadpool timeouts en pre.005
construction exacte du rustls RootCertStore en pre.005 construction exacte du rustls RootCertStore en pre.005
DDL précis de ksp_store_schema_migrations en pre.006 DDL précis de ksp_store_schema_migrations en pre.006

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md --> <!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
<!-- version: 3 --> <!-- version: 4 -->
# Validation `0.3.2` — Store/PostgreSQL runtime foundation # 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`, 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 ## 3. Frontières Cargo
### V32-DEP-001 — Façade -> API ### 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. 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é ### 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. 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 ### 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. 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 ### V32-API-005 — Lifecycle
@@ -189,7 +197,9 @@ close borné
Drop best-effort seulement 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 ## 5. Config ownership
@@ -485,11 +495,15 @@ Statut : `TODO pre.009`.
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close. 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 ### 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 ### V32-SEC-005 — Server error sanitization
@@ -514,6 +528,7 @@ Après création des crates :
```bash ```bash
cargo test -p ksp-store-api cargo test -p ksp-store-api
cargo test -p ksp-store-lib 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-store-postgres-lib
cargo test -p ksp-config-lib cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features cargo check -p ksp-store-lib --no-default-features