18 Commits

Author SHA1 Message Date
bb56574824 v0.3.2-rel.001 2026-08-30 06:13:07 +02:00
8363c8bdc9 v0.3.2-pre.012 2026-08-30 06:08:18 +02:00
b375f263dc v0.3.2-pre.011 2026-08-30 05:55:29 +02:00
e1fe419028 v0.3.2-pre.010 2026-08-29 23:44:19 +02:00
9ec4f26874 v0.3.2-pre.009 2026-08-29 22:00:20 +02:00
80eea82398 v0.3.2-pre.008 2026-08-29 21:54:00 +02:00
e39cf656b2 v0.3.2-pre.007 2026-08-29 21:40:57 +02:00
c4f56d9e85 v0.3.2-pre.006 2026-08-29 19:53:00 +02:00
c1dbaad88d v0.3.2-pre.005-fix.001 2026-08-29 19:35:35 +02:00
d93184d41d v0.3.2-pre.005 2026-08-29 19:33:43 +02:00
de3cec6a23 v0.3.2-pre.004-fix.001 2026-08-29 19:04:58 +02:00
0a4cddafc4 v0.3.2-pre.004-fix.001 2026-08-29 19:04:47 +02:00
6d2b1401aa v0.3.2-pre.004 2026-08-29 18:42:37 +02:00
0a8fb5dd1b v0.3.2-pre.003-fix.001 2026-08-29 17:47:22 +02:00
a8c90107b5 v0.3.2-pre.003 2026-08-29 17:43:52 +02:00
ced653bfc0 v0.3.2-pre.002-fix.001 2026-08-29 17:42:14 +02:00
153014be9d v0.3.2-pre.002 2026-08-29 16:48:16 +02:00
8d4b3b67fb v0.3.2-pre.001 2026-08-29 16:45:14 +02:00
85 changed files with 11778 additions and 84 deletions

View File

@@ -1,10 +1,19 @@
# file: .env.example
# version: 10
# version: 13
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
KSP_LOGS_DIRECTORY=logs
# PostgreSQL URI for the Devnet Store target.
KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://...
# PostgreSQL URI for the Mainnet Store target.
KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://...
# PostgreSQL URI for the Testnet Store target.
KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://...
# KSP Wallet root directory. Used by config/std.wallet.json before an optional profile subdirectory is appended.
# The committed Wallet document falls back to "wallets" when neither the process environment nor .env defines this variable.
KSP_WALLETS_DIRECTORY=wallets

View File

@@ -1,8 +1,18 @@
<!-- file: CHANGELOG.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# Changelog KSP
## 0.3.2 — Store/PostgreSQL runtime foundation — 2026-08-30
`0.3.2` introduit ensemble `ksp-store-lib` et `ksp-store-postgres-lib` comme fondation runtime/backend PostgreSQL au-dessus des contrats backend-agnostic de `ksp-store-api`, sans encore implémenter de capability RAW métier. La façade `Store` conserve un backend connu `Postgres` activé par défaut via Cargo feature, reste compilable avec `--no-default-features`, lie chaque instance à exactement un `RawNetworkId` et n'expose aucun `Pool`, `Client`, `Row`, `Statement`, driver ou SQL physique. `ksp-config-lib` possède désormais `std.store` avec trois targets nommés `devnet`, `mainnet` et `testnet`, chacun associé à un réseau explicite et à une URI PostgreSQL Secret indépendante ; Store/backend ne lisent directement ni `.env`, ni `KSP_*`, ni `PG*`, ni `.pgpass`.
Le backend de référence utilise `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2`, `tokio-postgres-rustls 0.14.0`, Rustls 0.23 avec roots système/AWS-LC et des modes TLS KSP limités à `Disabled` et `VerifyFull`. Les URI sont parsées puis normalisées sous la policy typée KSP, le pool et les deadlines connect/wait/create/recycle/shutdown sont bornés, et une ouverture réussie exige une connexion physique puis un bootstrap valide. Le moteur de migrations privé commence par `V000__bootstrap.sql`, enregistre version/nom/SHA-256 dans `ksp_store_schema_migrations`, sérialise les runners par advisory transaction lock borné et refuse checksum mismatch ou schema plus récent sans down automatique. La façade fournit également `runtime_snapshot()` et `health().await` avec une projection portable/redacted de readiness et des compteurs pool sûrs.
Les canaris de hardening verrouillent 84 exports crate-root côté façade, la frontière backend/Config, l'absence de bypass environnement, la redaction des URI/erreurs, la matrice de features et l'absence de persistence `RawTransaction`/`RawAccountState`. Le gate technique final a été rejoué après `cargo clean` : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés de toutes les crates, `cargo test --workspace`, graphes Cargo et les trois builds Tauri Linux passent. Le smoke PostgreSQL réel passe sur **PostgreSQL 17** et prouve bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready` et fermeture bornée ; la policy de fondation reste PostgreSQL >= 15 sans maximum KSP artificiel.
`prompts/022-V0_3_3_START_PROMPT.md` ouvre ensuite uniquement la vertical slice PostgreSQL `RawTransaction` complète sur les mêmes crates et la même fondation : six capabilities transaction/observation/rétention, atomicité, idempotence/conflit, get/list cursorisé, tombstone et `ForceRehydrate`. `RawAccountState` PostgreSQL et la complétude RAW restent réservés à `0.3.4`. L'archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` reste obligatoire au `pre.001` de `0.3.3`, mais seulement pour réauditer l'ancien schéma/repository RAW transaction comme source d'héritage, jamais comme autorité KSP.
## 0.3.1 — Store API RAW foundation — 2026-08-29
`0.3.1` introduit `ksp-store-api` comme contrat backend-agnostic de persistence N1 RAW, sans runtime Store ni backend physique. La release stabilise deux familles réellement convergentes : `RawTransaction` avec payload canonique opaque/versionné, identité réseau+signature et observations dacquisition séparées, puis `RawAccountState` avec bytes complets, identité réseau+pubkey+slot+hash et observations pouvant conserver les enrichissements Yellowstone sans les confondre avec létat canonique. `TransactionStatusObservation` reste reporté faute de convergence sémantique suffisante entre snapshot HTTP, transition WebSocket et update Yellowstone ; `logsSubscribe`, slot/root/slotsUpdates et vote restent event-only candidats, `RawBlock` reste une idée conditionnelle et Yellowstone `Entry` reste rejeté de la taxonomie active.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 331
# version: 347
[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-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]
version = "0.3.1"
version = "0.3.2"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -14,20 +14,24 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
publish = false
[workspace.dependencies]
argon2 = { version = "^0.5", default-features = false }
argon2 = { version = "^0.6", default-features = false }
base64 = { version = "^0.23" }
chacha20poly1305 = { version = "^0.11", default-features = false }
chrono = { version = "^0.4", default-features = false }
deadpool-postgres = { version = "^0.14", default-features = false }
directories = { version = "^6.0" }
ed25519-dalek = { version = "^3.0", default-features = false }
fs2 = { version = "^0.4" }
futures-util = { version = "^0.3", default-features = false }
getrandom = { version = "^0.4", default-features = false }
http = { version = "^1.5", default-features = false }
jsonschema = { version = "^0.51", default-features = false }
jsonschema = { version = "^0.52", default-features = false }
reqwest = { version = "^0.13", default-features = false }
rustls = { version = "^0.23", default-features = false }
rustls-native-certs = { version = "^0.8", default-features = false }
serde = { version = "^1.0" }
serde_json = { version = "^1.0" }
sha2 = { version = "^0.11", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false }
tauri = { version = "^2.11" }
@@ -39,6 +43,8 @@ tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false }
tracing-appender = { version = "^0.2", default-features = false }
tokio = { version = "^1.53", default-features = false }
tokio-postgres = { version = "^0.7", default-features = false }
tokio-postgres-rustls = { version = "^0.14", default-features = false }
tokio-tungstenite = { version = "^0.30", default-features = false }
tonic = { version = "^0.14", default-features = false }
tonic-prost = { version = "^0.14", default-features = false }

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 94 -->
<!-- version: 95 -->
# Roadmap KSP
@@ -94,8 +94,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
## 0.3.x — RAW / acquisition persistée
- [X] `0.3.1``ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
- [ ] `0.3.2` Introduire ensemble `ksp-store-lib` et `ksp-store-postgres-lib` pour la **fondation runtime/backend PostgreSQL uniquement** : façade Store, feature `postgres` par défaut, dispatch des backends compilés, Config `std.store`/secrets, connexion/pool/TLS à réauditer, bootstrap/migrations privés et health/readiness seulement si un contrat portable est réellement justifié. Aucun schéma `RawTransaction`/`RawAccountState` nest ajouté dans cette slice.
- [ ] `0.3.3` — Étendre le même couple `ksp-store-lib` + `ksp-store-postgres-lib` avec la vertical slice PostgreSQL `RawTransaction` complète : persistence/observation atomiques, get/list cursorisé, idempotence/conflit, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel.
- [X] `0.3.2``ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
- [ ] `0.3.3` — Étendre le même couple avec la vertical slice PostgreSQL `RawTransaction` complète : les six capabilities transaction/observation/rétention de `ksp-store-api`, persistence acquisition atomique, get/list cursorisé, idempotence/conflit, binding réseau, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel.
- [ ] `0.3.4` — Étendre le même couple avec `RawAccountState` + observation, puis fermer la complétude/conformance RAW cross-family, les indexes/migrations physiques nécessaires et le hardening PostgreSQL final.
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/events réellement partagés par les premiers consumers dacquisition, sans dupliquer les modèles persistants de `ksp-store-api`.
- [ ] `0.3.6` — Introduire `ksp-job-api` et un premier job de backfill historique concret consommant `ksp-store-lib`, avec policy/batch-size/progression possédés par le job et non par Store.

View File

@@ -0,0 +1,78 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"network": "devnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "mainnet",
"network": "mainnet-beta",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "testnet",
"network": "testnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
}
]
}

View File

@@ -0,0 +1,168 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.store:v1",
"title": "KSP standard Store configuration",
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 1
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profile"
}
}
},
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"networkId": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9_.:-]+$"
},
"duration100To60000": {
"type": "integer",
"minimum": 100,
"maximum": 60000
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"network",
"backend",
"postgres"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"network": {
"$ref": "#/$defs/networkId"
},
"backend": {
"const": "postgres"
},
"postgres": {
"$ref": "#/$defs/postgres"
}
}
},
"postgres": {
"type": "object",
"additionalProperties": false,
"required": [
"connection_uri",
"pool",
"tls",
"bootstrap",
"shutdown_timeout_ms"
],
"properties": {
"connection_uri": {
"type": "string",
"minLength": 1
},
"pool": {
"$ref": "#/$defs/pool"
},
"tls": {
"$ref": "#/$defs/tls"
},
"bootstrap": {
"$ref": "#/$defs/bootstrap"
},
"shutdown_timeout_ms": {
"type": "integer",
"minimum": 100,
"maximum": 30000
}
}
},
"pool": {
"type": "object",
"additionalProperties": false,
"required": [
"max_connections",
"connect_timeout_ms",
"wait_timeout_ms",
"create_timeout_ms",
"recycle_timeout_ms"
],
"properties": {
"max_connections": {
"type": "integer",
"minimum": 1,
"maximum": 64
},
"connect_timeout_ms": {
"$ref": "#/$defs/duration100To60000"
},
"wait_timeout_ms": {
"$ref": "#/$defs/duration100To60000"
},
"create_timeout_ms": {
"$ref": "#/$defs/duration100To60000"
},
"recycle_timeout_ms": {
"$ref": "#/$defs/duration100To60000"
}
}
},
"tls": {
"type": "object",
"additionalProperties": false,
"required": [
"mode"
],
"properties": {
"mode": {
"enum": [
"disabled",
"verify_full"
]
}
}
},
"bootstrap": {
"type": "object",
"additionalProperties": false,
"required": [
"auto_migrate",
"migration_timeout_ms",
"migration_lock_timeout_ms"
],
"properties": {
"auto_migrate": {
"type": "boolean"
},
"migration_timeout_ms": {
"type": "integer",
"minimum": 1000,
"maximum": 300000
},
"migration_lock_timeout_ms": {
"type": "integer",
"minimum": 100,
"maximum": 120000
}
}
}
}
}

78
config/std.store.json Normal file
View File

@@ -0,0 +1,78 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"network": "devnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "mainnet",
"network": "mainnet-beta",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "testnet",
"network": "testnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
}
]
}

View File

@@ -56,11 +56,13 @@
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
// version: 10
// version: 11
//! Desktop build/shell contract audits for Config Desk.
@@ -110,7 +110,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Config resources map must exist");
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 11);
assert_eq!(resources.len(), 13);
assert_eq!(
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
@@ -124,6 +124,11 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),
);
assert_eq!(resources.get("../../config/std.store.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.store.json"),);
assert_eq!(
resources.get("../../config/schemas/std.store.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.store.schema.json"),
);
assert_eq!(
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.wallet.schema.json"),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 7
// version: 8
#[test]
fn profile_inventory_exposes_registered_profile_documents() {
@@ -15,6 +15,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_STORE;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
}));

View File

@@ -56,11 +56,13 @@
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
// version: 9
// version: 10
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
@@ -85,19 +85,21 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
}
#[test]
fn pre_003_packaging_contains_current_eleven_config_resources() {
fn pre_004_packaging_contains_current_thirteen_config_resources() {
let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path());
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 11);
assert_eq!(resources.len(), 13);
assert_eq!(
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
);
assert!(resources.contains_key("../../config/std.offchain_transport.json"));
assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json"));
assert!(resources.contains_key("../../config/std.store.json"));
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
}
let tauri_source = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri_source.contains("ksp_config_lib::prepare_packaged_runtime"));

View File

@@ -56,11 +56,13 @@
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 30
// version: 31
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -432,7 +432,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 11);
assert_eq!(resources.len(), 13);
assert_eq!(
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
@@ -449,6 +449,11 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/std.offchain_transport.json"),
);
assert_eq!(resources.get("../../config/std.store.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.store.json"),);
assert_eq!(
resources.get("../../config/schemas/std.store.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.store.schema.json"),
);
assert_eq!(
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
// version: 9
// version: 10
//! Release-wide deterministic compliance canaries for Wallet Desk.
@@ -199,7 +199,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(resources.len(), 11);
assert_eq!(resources.len(), 13);
for (source, destination) in resources {
let destination = destination.as_str();
assert!(destination.is_some(), "resource destination must be textual");

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-config-lib/Cargo.toml
# version: 9
# version: 10
[package]
name = "ksp-config-lib"
@@ -14,6 +14,7 @@ ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# ksp-config-lib
@@ -25,6 +25,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
- l'adapter du document Transport V1/V2/V3 vers `HttpTransportSettings`, `WsTransportSettings` et, en V3, `YellowstoneGrpcTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
- l'adapter de `cfg.std.offchain_transport` vers `ksp_offchain_transport_lib::MarketPriceService`, avec contrôle de provenance des credentials/public fields et sans rendre les limites provider configurables ;
- l'adapter de `cfg.std.store` vers `ksp_store_lib::StoreSettings`, avec sélection d'un target nommé, réseau explicite et URI PostgreSQL à provenance `Secret` ;
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
@@ -34,14 +35,17 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
Le registre par défaut connaît :
```text
cfg.composite.ksp-app-solprices-desk -> config/composite.ksp-app-solprices-desk.json
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
cfg.std.logging -> config/std.logging.json
cfg.std.offchain_transport -> config/std.offchain_transport.json
cfg.std.store -> config/std.store.json
cfg.std.transport -> config/std.transport.json
cfg.std.wallet -> config/std.wallet.json
schema.composite -> config/schemas/composite.schema.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.offchain_transport -> config/schemas/std.offchain_transport.schema.json
schema.std.store -> config/schemas/std.store.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.std.wallet -> config/schemas/std.wallet.schema.json
```
@@ -50,7 +54,7 @@ schema.std.wallet -> config/schemas/std.wallet.schema.json
`ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté.
`config/examples/composite.example.json` conserve lexemple générique. `config/composite.ksp-app-wallet-desk.json` est le premier composite runtime concret : il sélectionne Logging, Transport et Wallet par `file_id`, sans dépendre de leurs filenames physiques.
`config/examples/composite.example.json` conserve lexemple générique. Les composites runtime committed restent possédés par Config : `config/composite.ksp-app-solprices-desk.json` sélectionne Logging + Off-chain Transport, tandis que `config/composite.ksp-app-wallet-desk.json` sélectionne Logging + Off-chain Transport + On-chain Transport + Wallet. Tous référencent leurs documents par `file_id`, sans dépendre de filenames physiques.
Le fichier local d'environnement est :
@@ -68,11 +72,11 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging, On-chain Transport, Off-chain Transport ou Wallet depuis le JSON.
- reconstruire elles-mêmes la configuration Logging, On-chain Transport, Off-chain Transport, Store ou Wallet depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, de `ksp-onchain-transport-lib` pour construire le contrat runtime On-chain Transport et de `ksp-offchain-transport-lib` pour construire le service market-price dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, de `ksp-onchain-transport-lib` pour construire le contrat runtime On-chain Transport et de `ksp-offchain-transport-lib` pour construire le service market-price dans la direction Config -> Transport et de `ksp-store-lib` avec `default-features = false` pour construire les settings Store dans la direction Config -> Store sans forcer un backend physique. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib`, `ksp-onchain-transport-lib` et `ksp-offchain-transport-lib` ne dépendent pas de Config.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib`, `ksp-onchain-transport-lib`, `ksp-offchain-transport-lib` et les crates Store ne dépendent pas de Config.
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
@@ -84,7 +88,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.offchain_transport` exige une provenance `Secret` pour les API keys effectives et une provenance `Public` pour la paire DexScreener lorsqu'elle vient de l'environnement ; il ne permet ni URL provider arbitraire ni override de rate limit. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.offchain_transport` exige une provenance `Secret` pour les API keys effectives et une provenance `Public` pour la paire DexScreener lorsqu'elle vient de l'environnement ; il ne permet ni URL provider arbitraire ni override de rate limit. `std.store` exige une provenance `Secret` pour chaque URI PostgreSQL effective et conserve des targets réseau-spécifiques indépendants (`devnet`, `mainnet`, `testnet`) sans exposer l'URI dans les projections sûres. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation
@@ -94,6 +98,8 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
- [`../../config/std.offchain_transport.json`](../../config/std.offchain_transport.json) — document standard Off-chain Transport V1, actuellement limité au domaine `market_price` SOL/USD ;
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../config/composite.ksp-app-solprices-desk.json`](../../config/composite.ksp-app-solprices-desk.json) — composition Logging/Off-chain Transport de SOL Prices Desk ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Off-chain Transport/On-chain Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# Utilisation de ksp-config-lib
@@ -28,8 +28,11 @@ Les arguments compris par Config sont :
```text
--cfgpath=/path/to/config
--schemapath=/path/to/schemas
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
--filemap=cfg.std.logging=my-logging.json
--filemap=cfg.std.offchain_transport=my-offchain-transport.json
--filemap=cfg.std.store=my-store.json
--filemap=cfg.std.transport=my-transport.json
--filemap=cfg.std.wallet=my-wallet.json
```
@@ -212,7 +215,56 @@ Config ne permet pas de fournir `base_url`, `endpoint_url`, `rate_limit` ou `req
ksp-config-lib -> ksp-offchain-transport-lib
```
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. Une application telle que la future `ksp-app-solprices-desk` peut recevoir le service déjà composé puis utiliser uniquement `registry()`, `refresh`, `refresh_many` et `refresh_all`.
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. `ksp-app-solprices-desk` reçoit le service déjà composé puis utilise uniquement la surface provider-neutral `registry()`, `refresh`, `refresh_many` et `refresh_all`.
### 4.4 Construire le Store depuis Config
`cfg.std.store` définit des targets nommés. Chaque target sélectionne exactement un réseau logique, un backend et une URI PostgreSQL distincte. Config résout les secrets puis construit le contrat backend-neutral `ksp_store_lib::StoreSettings` sans activer la feature PostgreSQL du consumer :
```rust
let store_config = match engine.load_resolved_store_config(
std::option::Option::Some("devnet"),
&environment,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let target_id = store_config.target_id();
let network = store_config.settings().network();
let _ = (target_id, network);
let store_settings = store_config.into_settings();
let store = match ksp_store_lib::Store::open(store_settings).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let _health = store.health().await;
let closed = store.close().await;
if let std::result::Result::Err(error) = closed {
return std::result::Result::Err(error);
}
```
Targets committed :
```text
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
```
`default_profile = "devnet"` choisit un seul target. La sélection d'un autre target se fait par le `profile_id` explicite ; `ksp-store-lib` ne multiplexe pas plusieurs bases ou réseaux dans une même instance.
Chaque `connection_uri` doit provenir d'un placeholder `KSP_SECRET_*`/`KSPB_SECRET_*`. Une URI littérale ou issue d'une variable non secrète est rejetée par l'adapter effectif. La valeur réelle est transmise au runtime Store, mais `ResolvedStoreConfig`, `StoreSettings` et les projections sûres ne l'affichent pas.
La direction de dépendance reste :
```text
ksp-config-lib -> ksp-store-lib (default-features = false)
ksp-store-lib -X-> ksp-config-lib
ksp-store-postgres-lib -X-> ksp-config-lib
```
## 5. Profils et composites
@@ -237,7 +289,7 @@ let component = match composite.component("wallet") {
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
```
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite concret `cfg.composite.ksp-app-wallet-desk` référence actuellement `logging`, `transport` et `wallet`; Wallet Desk valide ces trois frontières au bootstrap.
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
## 6. Management de `std.logging.json`

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 20
// version: 21
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -9,7 +9,7 @@
//!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC), Store and Wallet documents map explicitly to their runtime consumer
//! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
//! JSON/`.env` persistence.
@@ -27,6 +27,7 @@ mod persistence;
mod profile;
mod registry;
mod sensitivity;
mod store;
mod transport;
mod wallet;
@@ -164,6 +165,10 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME;
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Store configuration document.
pub use self::registry::DEFAULT_STD_STORE_FILENAME;
/// Default physical filename for the standard Store JSON Schema document.
pub use self::registry::DEFAULT_STD_STORE_SCHEMA_FILENAME;
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
@@ -182,6 +187,8 @@ pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT;
/// Logical file identifier for the standard Store JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_STORE;
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document.
@@ -190,6 +197,8 @@ pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard Off-chain Transport configuration document.
pub use self::registry::FILE_ID_STD_OFFCHAIN_TRANSPORT;
/// Logical file identifier for the standard Store configuration document.
pub use self::registry::FILE_ID_STD_STORE;
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document.
@@ -204,6 +213,8 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
pub use self::sensitivity::ResolvedConfigJson;
/// One resolved Config string preserving real/safe representations and provenance.
pub use self::sensitivity::ResolvedConfigText;
/// Effective standard Store configuration mapped to backend-neutral Store settings.
pub use self::store::ResolvedStoreConfig;
/// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 12
// version: 13
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -17,6 +17,10 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME: &str = "std.offchain_transport.json";
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME: &str = "std.offchain_transport.schema.json";
/// Default physical filename for the standard Store configuration document.
pub const DEFAULT_STD_STORE_FILENAME: &str = "std.store.json";
/// Default physical filename for the standard Store JSON Schema document.
pub const DEFAULT_STD_STORE_SCHEMA_FILENAME: &str = "std.store.schema.json";
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
@@ -35,6 +39,8 @@ pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT: &str = "schema.std.offchain_transport";
/// Logical file identifier for the standard Store JSON Schema document.
pub const FILE_ID_SCHEMA_STD_STORE: &str = "schema.std.store";
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document.
@@ -43,6 +49,8 @@ pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard Off-chain Transport configuration document.
pub const FILE_ID_STD_OFFCHAIN_TRANSPORT: &str = "cfg.std.offchain_transport";
/// Logical file identifier for the standard Store configuration document.
pub const FILE_ID_STD_STORE: &str = "cfg.std.store";
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document.
@@ -214,6 +222,22 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = ConfigFileDescriptor::new(
FILE_ID_STD_STORE,
ConfigFileKind::Config,
DEFAULT_STD_STORE_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_STORE),
);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_STORE, ConfigFileKind::Schema, DEFAULT_STD_STORE_SCHEMA_FILENAME, std::option::Option::None);
let store_schema = match store_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport = ConfigFileDescriptor::new(
FILE_ID_STD_TRANSPORT,
ConfigFileKind::Config,
@@ -254,6 +278,8 @@ impl ConfigFileRegistry {
logging_schema,
offchain_transport,
offchain_transport_schema,
store,
store_schema,
transport,
transport_schema,
wallet,

View File

@@ -0,0 +1,280 @@
// file: crates/ksp-config-lib/src/store.rs
// version: 2
/// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`.
pub struct ResolvedStoreConfig {
effective: crate::ResolvedConfigJson,
file_id: crate::ConfigFileId,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
settings: ksp_store_lib::StoreSettings,
source_path: std::path::PathBuf,
}
impl ResolvedStoreConfig {
/// Returns the detailed environment-resolved Config view.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the logical Config file identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the selected standard Store profile identifier.
///
/// For `std.store`, the profile identifier is also the stable named Store target identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the selected named Store target identifier.
#[must_use]
pub fn target_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Store profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Borrows the backend-neutral Store settings without exposing the connection URI.
#[must_use]
pub const fn settings(&self) -> &ksp_store_lib::StoreSettings {
return &self.settings;
}
/// Consumes the resolved Config and returns the Store-owned runtime settings.
#[must_use]
pub fn into_settings(self) -> ksp_store_lib::StoreSettings {
return self.settings;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
}
impl std::fmt::Debug for ResolvedStoreConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedStoreConfig")
.field("effective", &self.effective)
.field("file_id", &self.file_id)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("settings", &self.settings)
.field("source_path", &self.source_path)
.finish();
}
}
impl crate::ConfigDocumentEngine {
/// Loads `std.store`, resolves one profile/environment and maps it to backend-neutral Store settings.
pub fn load_resolved_store_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_STORE);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = self.load_resolved_profile(&file_id, requested_profile);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_store_profile(&profile, environment);
}
/// Maps an already resolved `cfg.std.store` profile while preserving its selection provenance.
pub fn resolve_store_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_STORE {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Store document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_store_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveStoreSource {
backend: String,
format_version: u32,
network: String,
postgres: EffectivePostgresSource,
profile_id: String,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectivePostgresSource {
bootstrap: EffectivePostgresBootstrapSource,
connection_uri: String,
pool: EffectivePostgresPoolSource,
shutdown_timeout_ms: u64,
tls: EffectivePostgresTlsSource,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectivePostgresPoolSource {
connect_timeout_ms: u64,
create_timeout_ms: u64,
max_connections: u32,
recycle_timeout_ms: u64,
wait_timeout_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectivePostgresTlsSource {
mode: String,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectivePostgresBootstrapSource {
auto_migrate: bool,
migration_lock_timeout_ms: u64,
migration_timeout_ms: u64,
}
fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedStoreConfig> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Store Config profile");
let effective = profile.resolve_effective_environment_detailed(environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provenance = validate_connection_uri_provenance(&effective, profile);
if let std::result::Result::Err(error) = provenance {
return std::result::Result::Err(error);
}
let source = serde_json::from_value::<EffectiveStoreSource>(effective.value().clone());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
effective_error(profile, "effective Store Config cannot be decoded into the runtime adapter contract").with_source(error),
);
},
};
if source.format_version != 1 {
return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Store profile_id does not match the selected profile"));
}
if source.backend != "postgres" {
return std::result::Result::Err(effective_error(profile, "effective Store backend is unsupported").with_context("backend", source.backend));
}
let tls_mode = match source.postgres.tls.mode.as_str() {
"disabled" => ksp_store_lib::PostgresTlsMode::Disabled,
"verify_full" => ksp_store_lib::PostgresTlsMode::VerifyFull,
_ => return std::result::Result::Err(effective_error(profile, "effective Store PostgreSQL TLS mode is unsupported")),
};
let pool = ksp_store_lib::PostgresPoolSettings::new(
source.postgres.pool.max_connections,
std::time::Duration::from_millis(source.postgres.pool.connect_timeout_ms),
std::time::Duration::from_millis(source.postgres.pool.wait_timeout_ms),
std::time::Duration::from_millis(source.postgres.pool.create_timeout_ms),
std::time::Duration::from_millis(source.postgres.pool.recycle_timeout_ms),
);
let bootstrap = ksp_store_lib::PostgresBootstrapSettings::new(
source.postgres.bootstrap.auto_migrate,
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
);
let network = ksp_store_lib::RawNetworkId::new(source.network);
let network = match network {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(effective_error(profile, "effective Store network identifier is invalid")),
};
let postgres = ksp_store_lib::PostgresStoreSettings::new(source.postgres.connection_uri, pool, tls_mode, bootstrap);
let settings = ksp_store_lib::StoreSettings::new(
network,
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
std::time::Duration::from_millis(source.postgres.shutdown_timeout_ms),
);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(store_contract_error(profile, &error));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
network = settings.network().as_str(),
backend = settings.backend_kind().code(),
"mapped standard Store Config to Store settings"
);
return std::result::Result::Ok(ResolvedStoreConfig {
effective,
file_id: profile.file_id().clone(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
settings,
source_path: profile.path().to_path_buf(),
});
}
fn validate_connection_uri_provenance(effective: &crate::ResolvedConfigJson, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<()> {
let provenance = match effective.provenance_at("/postgres/connection_uri") {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI provenance is unavailable")),
};
let mut has_secret_environment = false;
for item in provenance {
let variable_name = match item.variable_name() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
let sensitivity = match sensitivity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !sensitivity.is_secret() {
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI may reference only secret environment variables"));
}
has_secret_environment = true;
}
if !has_secret_environment {
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI requires secret environment provenance"));
}
return std::result::Result::Ok(());
}
fn store_contract_error(profile: &crate::ResolvedConfigProfile, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
return effective_error(profile, "effective Store settings fail the Store runtime contract")
.with_context("store_error_domain", error.code().domain())
.with_context("store_error_code", error.code().code());
}
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
.with_context("file_id", profile.file_id().as_str())
.with_context("profile_id", profile.profile_id())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/store.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs
// version: 8
// version: 9
//! Workspace ownership audits for KSP application configuration boundaries.
@@ -280,6 +280,49 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
}
}
#[test]
fn store_config_adapter_does_not_force_backend_feature_or_reverse_dependency() {
let root = workspace_root();
let config_manifest_path = root.join("crates/ksp-config-lib/Cargo.toml");
let config_manifest = std::fs::read_to_string(config_manifest_path.as_path());
assert!(config_manifest.is_ok(), "unable to read {}", config_manifest_path.display());
if let std::result::Result::Ok(config_manifest) = config_manifest {
assert!(
config_manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"),
"Config -> Store dependency must not force a physical backend feature"
);
}
for crate_name in ["ksp-store-lib", "ksp-store-postgres-lib"] {
let manifest_path = root.join("crates").join(crate_name).join("Cargo.toml");
let manifest = std::fs::read_to_string(manifest_path.as_path());
assert!(manifest.is_ok(), "unable to read {}", manifest_path.display());
if let std::result::Result::Ok(manifest) = manifest {
assert!(!manifest.contains("ksp-config-lib"), "{} must not depend back on Config", manifest_path.display());
}
}
}
#[test]
fn store_runtime_sources_do_not_bypass_config_for_environment_or_libpq_files() {
let root = workspace_root();
for crate_name in ["ksp-store-lib", "ksp-store-postgres-lib"] {
let source_root = root.join("crates").join(crate_name).join("src");
let mut rust_files = std::vec::Vec::new();
collect_rust_files(source_root.as_path(), &mut rust_files);
for rust_file in rust_files {
let source = std::fs::read_to_string(rust_file.as_path());
assert!(source.is_ok(), "unable to read {}", rust_file.display());
let source = match source {
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
std::result::Result::Err(_) => continue,
};
for forbidden in ["std::env", "dotenv", "KSP_", "KSPB_", "\"PG", ".pgpass"] {
assert!(!source.contains(forbidden), "{} bypasses Config through forbidden Store environment/libpq token {forbidden}", rust_file.display());
}
}
}
}
#[test]
fn environment_name_scanner_ignores_namespace_labels_but_keeps_concrete_names() {
let source = r#"

View File

@@ -1,8 +1,8 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 25
// version: 26
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
//! Logging/Transport/Store adapters and management contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -80,19 +80,21 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 11);
assert_eq!(descriptors.len(), 13);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[5].schema_file_id();
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[6].schema_file_id();
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
@@ -211,6 +213,18 @@ fn logging_adapter_contract_is_available_from_crate_root() {
assert!(std::mem::size_of::<ksp_config_lib::ResolvedLoggingConfig>() > 0);
}
#[test]
fn store_adapter_contract_is_available_from_crate_root() {
let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_store_config;
let composite_adapter = ksp_config_lib::ConfigDocumentEngine::resolve_store_config_profile;
let _ = (adapter, composite_adapter);
assert_eq!(ksp_config_lib::FILE_ID_STD_STORE, "cfg.std.store");
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_STORE, "schema.std.store");
assert_eq!(ksp_config_lib::DEFAULT_STD_STORE_FILENAME, "std.store.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_STORE_SCHEMA_FILENAME, "std.store.schema.json");
assert!(std::mem::size_of::<ksp_config_lib::ResolvedStoreConfig>() > 0);
}
#[test]
fn management_contracts_are_available_from_crate_root() {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");

View File

@@ -0,0 +1,78 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"network": "devnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "mainnet",
"network": "mainnet-beta",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
},
{
"profile_id": "testnet",
"network": "testnet",
"backend": "postgres",
"postgres": {
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
"pool": {
"max_connections": 8,
"connect_timeout_ms": 10000,
"wait_timeout_ms": 5000,
"create_timeout_ms": 10000,
"recycle_timeout_ms": 5000
},
"tls": {
"mode": "verify_full"
},
"bootstrap": {
"auto_migrate": true,
"migration_timeout_ms": 30000,
"migration_lock_timeout_ms": 10000
},
"shutdown_timeout_ms": 5000
}
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 10
// version: 11
#[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
@@ -7,7 +7,7 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 11);
assert_eq!(descriptors.len(), 13);
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME));
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
@@ -15,17 +15,19 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(descriptors[5].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(descriptors[5].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
assert!(descriptors[0..6].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
assert!(descriptors[6..11].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_STORE);
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(descriptors[6].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(descriptors[6].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
assert!(descriptors[0..7].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
assert!(descriptors[7..13].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
}
}
@@ -114,6 +116,27 @@ fn defaults_register_offchain_transport_document_and_schema_with_distinct_roots(
}
}
#[test]
fn defaults_register_store_document_and_schema_with_distinct_roots() {
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok());
if let std::result::Result::Ok(registry) = registry {
let config_id = crate::ConfigFileId::new(crate::FILE_ID_STD_STORE);
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_STORE);
if let (std::result::Result::Ok(config_id), std::result::Result::Ok(schema_id)) = (config_id, schema_id) {
let config = registry.descriptor(&config_id);
let schema = registry.descriptor(&schema_id);
if let (std::result::Result::Ok(config), std::result::Result::Ok(schema)) = (config, schema) {
assert_eq!(config.kind(), crate::ConfigFileKind::Config);
assert_eq!(config.filename(), std::path::Path::new(crate::DEFAULT_STD_STORE_FILENAME));
assert_eq!(config.schema_file_id(), std::option::Option::Some(&schema_id));
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_STORE_SCHEMA_FILENAME));
}
}
}
}
#[test]
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
let registry = crate::ConfigFileRegistry::defaults();

View File

@@ -0,0 +1,199 @@
// file: crates/ksp-config-lib/unit_tests/store.rs
// version: 2
#[test]
fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "committed Store profile should map without opening PostgreSQL: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_STORE);
assert_eq!(resolved.profile_id(), "devnet");
assert_eq!(resolved.target_id(), "devnet");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.settings().backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
assert_eq!(resolved.settings().network().as_str(), "devnet");
assert_eq!(resolved.settings().shutdown_timeout(), std::time::Duration::from_millis(5_000));
let postgres = match resolved.settings().backend() {
ksp_store_lib::StoreBackendSettings::Postgres(postgres) => std::option::Option::Some(postgres),
_ => std::option::Option::None,
};
assert!(postgres.is_some(), "pre.004 fixture should map to the PostgreSQL Store backend");
if let std::option::Option::Some(postgres) = postgres {
assert_eq!(postgres.pool().max_connections(), 8);
assert_eq!(postgres.pool().connect_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(postgres.pool().wait_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(postgres.pool().create_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::VerifyFull);
assert!(postgres.bootstrap().auto_migrate());
assert_eq!(postgres.bootstrap().migration_timeout(), std::time::Duration::from_millis(30_000));
assert_eq!(postgres.bootstrap().migration_lock_timeout(), std::time::Duration::from_millis(10_000));
}
assert!(resolved.effective().sensitivity().is_secret());
let safe = resolved.effective().safe_value().to_string();
assert!(!safe.contains("postgresql://localhost/ksp_devnet"));
assert!(safe.contains(crate::REDACTED_CONFIG_VALUE));
let debug = format!("{resolved:?}");
assert!(!debug.contains("postgresql://localhost/ksp_devnet"));
}
}
#[test]
fn process_store_uri_wins_and_remains_redacted_in_safe_views() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "postgresql://secret-user:secret-pass@db.example/ksp_devnet";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "secret process Store URI should map: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str), std::option::Option::Some(canary));
assert!(!resolved.effective().safe_value().to_string().contains(canary));
assert!(!format!("{resolved:?}").contains(canary));
let provenance = resolved.effective().provenance_at("/postgres/connection_uri");
assert!(provenance.is_some());
if let std::option::Option::Some(provenance) = provenance {
assert!(provenance.iter().any(|item| return item.environment_source() == std::option::Option::Some(crate::ConfigEnvironmentSource::Process)));
}
}
}
#[test]
fn literal_or_nonsecret_store_uri_is_rejected_by_effective_adapter() {
for value in ["postgresql://literal.invalid/ksp", "${KSP_PUBLIC_STORE_POSTGRES_URI:-postgresql://public.invalid/ksp}"] {
let fixture = tempfile::tempdir();
assert!(fixture.is_ok());
let fixture = match fixture {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let source = committed_document_value();
let mut source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
if let std::option::Option::Some(profiles) = profiles
&& let std::option::Option::Some(profile) = profiles.first_mut()
{
profile["postgres"]["connection_uri"] = serde_json::Value::String(value.to_owned());
}
let engine = fixture_engine_with_document(fixture.path(), &source);
assert!(engine.is_ok());
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
assert!(resolved.is_err(), "Store URI without secret provenance must be rejected");
if let std::result::Result::Err(error) = resolved {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
assert!(!format!("{error:?}").contains("literal.invalid"));
assert!(!format!("{error:?}").contains("public.invalid"));
}
}
}
#[test]
fn named_store_targets_select_one_network_and_database_without_runtime_multiplexing() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), "postgresql://devnet.invalid/ksp_devnet".to_owned());
process.insert("KSP_SECRET_STORE_MAINNET_POSTGRES_URI".to_owned(), "postgresql://mainnet.invalid/ksp_mainnet".to_owned());
process.insert("KSP_SECRET_STORE_TESTNET_POSTGRES_URI".to_owned(), "postgresql://testnet.invalid/ksp_testnet".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
for (target_id, network, expected_uri) in [
("devnet", "devnet", "postgresql://devnet.invalid/ksp_devnet"),
("mainnet", "mainnet-beta", "postgresql://mainnet.invalid/ksp_mainnet"),
("testnet", "testnet", "postgresql://testnet.invalid/ksp_testnet"),
] {
let resolved = engine.load_resolved_store_config(std::option::Option::Some(target_id), &environment);
assert!(resolved.is_ok(), "named Store target should resolve independently: {target_id}: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.target_id(), target_id);
assert_eq!(resolved.profile_id(), target_id);
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
assert_eq!(resolved.settings().network().as_str(), network);
assert_eq!(
resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str),
std::option::Option::Some(expected_uri),
);
assert!(!resolved.effective().safe_value().to_string().contains(expected_uri));
}
}
return;
}
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn fixture_engine_with_document(root: &std::path::Path, document: &serde_json::Value) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let config_root = root.join("config");
if let std::result::Result::Err(error) = std::fs::create_dir_all(config_root.as_path()) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error),
);
}
let bytes = serde_json::to_vec_pretty(document);
let bytes = match bytes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Store Config cannot be encoded").with_source(error),
);
},
};
let path = config_root.join(crate::DEFAULT_STD_STORE_FILENAME);
if let std::result::Result::Err(error) = std::fs::write(path.as_path(), bytes) {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Store Config cannot be written").with_source(error),
);
}
let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root, workspace_root().join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn committed_document_value() -> std::result::Result<serde_json::Value, serde_json::Error> {
return serde_json::from_str(include_str!("../../../config/std.store.json"));
}
fn workspace_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
}

View File

@@ -0,0 +1,20 @@
# file: crates/ksp-store-lib/Cargo.toml
# version: 2
[package]
name = "ksp-store-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[features]
default = ["postgres"]
postgres = ["dep:ksp-store-postgres-lib"]
[dependencies]
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-store-api = { path = "../ksp-store-api" }
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true }
[lints]
workspace = true

View File

@@ -0,0 +1,95 @@
<!-- file: crates/ksp-store-lib/README.md -->
<!-- version: 1 -->
# ksp-store-lib
`ksp-store-lib` est la façade runtime Store commune de KSP.
Elle expose aux consumers une surface backend-neutral, réexporte les contrats RAW de `ksp-store-api`, sélectionne uniquement les backends compilés et masque leurs objets physiques. Le backend PostgreSQL officiel est activé par défaut via la feature `postgres` et reste implémenté dans `ksp-store-postgres-lib`.
## Responsabilités
`ksp-store-lib` possède :
- `StoreSettings`, avec un réseau logique unique, un backend sélectionné et un timeout de fermeture borné ;
- les settings PostgreSQL publics KSP-owned : pool, TLS, bootstrap/migrations et URI sensible ;
- la feature `postgres` par défaut et le comportement explicite `backend_not_compiled` lorsque PostgreSQL est sélectionné sans cette feature ;
- `Store::open`, qui ne retourne une instance qu'après validation, ouverture physique du backend compilé et bootstrap/history réussis ;
- `Store::runtime_snapshot()` pour les compteurs runtime sûrs sans I/O ;
- `Store::health().await` pour la readiness portable et bornée ;
- `Store::close(self).await` pour la fermeture explicite bornée ;
- le mapping des erreurs backend vers des codes Store stables sans exposer les erreurs physiques ;
- les réexports crate-root de `ksp-store-api` nécessaires aux consumers ordinaires.
## Une instance = un réseau
Une instance `Store` représente exactement :
```text
1 Store = 1 RawNetworkId + 1 backend physique sélectionné
```
Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants — par exemple Devnet, Mainnet et Testnet — mais un appel à `Store::open` reçoit les settings d'un seul target.
Cette séparation permet d'utiliser des bases PostgreSQL distinctes par réseau tout en conservant le réseau dans l'identité logique des données RAW.
## PostgreSQL
Avec la feature par défaut :
```text
ksp-store-lib
-> ksp-store-api
-> ksp-logging-lib
-> ksp-store-postgres-lib
```
`ksp-store-lib` ne réexporte aucun type `tokio-postgres`, Deadpool ou Rustls.
Les modes TLS publics sont volontairement limités à :
```text
Disabled
VerifyFull
```
`VerifyFull` impose TLS avec vérification de la chaîne et de l'identité serveur. La policy typée Store prime sur les paramètres TLS présents dans l'URI.
## Config et secrets
Store ne lit ni `.env`, ni variables `KSP_*` / `KSPB_*`, ni variables/fichiers implicites libpq (`PG*`, `.pgpass`, fichiers TLS PostgreSQL).
`ksp-config-lib` possède `std.store`, la résolution des secrets et la sélection du target. Il construit ensuite un `StoreSettings` backend-neutral. L'URI PostgreSQL reste nécessaire au runtime mais n'a aucun getter public dans `ksp-store-lib` et son `Debug` est redacted.
Les targets committed sont actuellement :
```text
devnet -> network devnet -> base indépendante
mainnet -> network mainnet-beta -> base indépendante
testnet -> network testnet -> base indépendante
```
Les credentials restent dans les variables `KSP_SECRET_STORE_*_POSTGRES_URI` ou le `.env` possédé par Config.
## Surface actuelle et hors périmètre
La fondation runtime ne fournit encore aucune implémentation PostgreSQL des capabilities métier RAW de `ksp-store-api`.
Sont volontairement hors de cette surface :
- persistence/query/rétention PostgreSQL de `RawTransaction` ;
- persistence/query/rétention PostgreSQL de `RawAccountState` ;
- batch-size, priorité, backlog ou policy de worker/job ;
- transport d'acquisition, Program decoding et materialization ;
- exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL.
Les premières vertical slices métier sont ajoutées séparément afin que la façade runtime reste stable et backend-neutral.
## Documentation
- [`USAGE.md`](USAGE.md) — construction des settings, ouverture, health et fermeture ;
- [`../ksp-store-postgres-lib/README.md`](../ksp-store-postgres-lib/README.md) — responsabilité du backend PostgreSQL physique ;
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store committed ;
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture durable Store ;
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan de fondation ;
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — matrice de validation.

View File

@@ -0,0 +1,219 @@
<!-- file: crates/ksp-store-lib/USAGE.md -->
<!-- version: 1 -->
# Utilisation de ksp-store-lib
## 1. Dépendance et features
Le consumer runtime normal dépend uniquement de la façade :
```toml
[dependencies]
ksp-store-lib = { path = "../ksp-store-lib" }
```
La feature par défaut est :
```text
postgres
```
Pour construire un binaire sans backend physique :
```toml
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
```
Dans ce mode, le type PostgreSQL reste connu par la surface de settings mais `Store::open` retourne `ERROR_CODE_BACKEND_NOT_COMPILED` avant toute I/O si PostgreSQL est sélectionné.
Un consumer ordinaire ne dépend pas directement de `ksp-store-postgres-lib`.
## 2. Construire des settings PostgreSQL programmatiquement
La construction directe est utile pour les tests, outils internes ou compositions qui n'utilisent pas `ksp-config-lib`.
```rust
fn programmatic_store_settings(connection_uri: std::string::String) -> ksp_store_lib::Result<ksp_store_lib::StoreSettings> {
let network = ksp_store_lib::RawNetworkId::new("devnet");
let network = match network {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let postgres = ksp_store_lib::PostgresStoreSettings::new(
connection_uri,
ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::VerifyFull,
ksp_store_lib::PostgresBootstrapSettings::default(),
);
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(
network,
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
);
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(settings);
}
```
`PostgresStoreSettings` ne fournit volontairement aucun getter public de l'URI. Son `Debug` remplace cette valeur par `<redacted>`.
## 3. Ouvrir et fermer un Store
`Store::open` est async et ne retourne un succès qu'après que le backend compilé a prouvé sa fondation runtime.
```rust
async fn use_store(settings: ksp_store_lib::StoreSettings) -> ksp_store_lib::Result<()> {
let store = ksp_store_lib::Store::open(settings).await;
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = store.runtime_snapshot();
let _network = runtime.network();
let _capacity = runtime.pool_capacity();
let _size = runtime.pool_size();
let _available = runtime.pool_available();
let _waiting = runtime.pool_waiting();
let health = store.health().await;
match health.state() {
ksp_store_lib::StoreHealthState::Ready => {}
ksp_store_lib::StoreHealthState::NotReady => {
let _safe_error_code = health.last_error_code();
}
_ => {}
}
return store.close().await;
}
```
`Store::close(self)` consomme l'instance afin qu'une fermeture explicite ne puisse pas être suivie d'une nouvelle opération via la même valeur.
## 4. Construire les settings depuis Config
Le chemin applicatif recommandé utilise `ksp-config-lib`, propriétaire du document `std.store`, de `.env` et des secrets.
Après construction du `ConfigDocumentEngine` :
```rust
fn resolve_store_settings(
engine: &ksp_config_lib::ConfigDocumentEngine,
environment: &ksp_config_lib::ConfigEnvironment,
target: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_store_lib::StoreSettings> {
let resolved = engine.load_resolved_store_config(target, environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(resolved.into_settings());
}
```
Targets committed :
```text
devnet -> RawNetworkId("devnet")
mainnet -> RawNetworkId("mainnet-beta")
testnet -> RawNetworkId("testnet")
```
Chaque target peut utiliser une URI PostgreSQL distincte. `default_profile` sélectionne un seul target ; `Store` ne route pas automatiquement entre plusieurs targets.
## 5. Settings disponibles
### `PostgresPoolSettings`
Valeurs par défaut :
```text
max_connections 8
connect_timeout 10 s
wait_timeout 5 s
create_timeout 10 s
recycle_timeout 5 s
```
Les getters sont :
```text
max_connections()
connect_timeout()
wait_timeout()
create_timeout()
recycle_timeout()
```
`validate()` vérifie les bornes sans I/O.
### `PostgresBootstrapSettings`
Valeurs par défaut :
```text
auto_migrate true
migration_timeout 30 s
migration_lock_timeout 10 s
```
Getters :
```text
auto_migrate()
migration_timeout()
migration_lock_timeout()
```
### `StoreSettings`
La surface expose :
```text
backend()
backend_kind()
network()
shutdown_timeout()
validate()
```
`StoreSettings::new` permet de choisir explicitement le timeout de shutdown. `StoreSettings::with_default_shutdown` utilise la borne commune par défaut de 5 secondes.
## 6. Health et diagnostics
`StoreRuntimeSnapshot` est synchrone et ne déclenche aucune I/O. Il expose uniquement :
```text
backend_kind
network
pool_capacity
pool_size
pool_available
pool_waiting
```
`StoreHealthSnapshot` ajoute une probe async bornée :
```text
state = Ready | NotReady
migration_version
pending_migration_count
last_error_code
runtime snapshot
```
Aucun snapshot n'expose URI, host, user, database, SQL, handle backend ou texte d'erreur PostgreSQL.
## 7. Limite fonctionnelle actuelle
`ksp-store-lib` réexporte les modèles et traits RAW de `ksp-store-api`, mais le backend PostgreSQL de la fondation n'implémente encore aucune capability `RawTransaction*` ou `RawAccount*`.
Les consumers ne doivent donc pas interpréter la disponibilité du runtime PostgreSQL comme une persistence métier déjà présente.

View File

@@ -0,0 +1,7 @@
// file: crates/ksp-store-lib/src/constants.rs
// version: 2
//! Store facade-owned constants.
/// Owning tracing target reserved for events emitted by the common Store runtime facade.
pub(crate) const TRACING_TARGET: &str = "ksp-store-lib";

View File

@@ -0,0 +1,29 @@
// file: crates/ksp-store-lib/src/error.rs
// version: 4
/// 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 the PostgreSQL backend rejects or cannot normalize its physical connection configuration.
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_failed");
/// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL.
pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed");
/// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history.
pub const ERROR_CODE_POSTGRES_MIGRATION_MISMATCH: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch");
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
/// Error code used when PostgreSQL history contains a migration newer than this Store runtime understands.
pub const ERROR_CODE_POSTGRES_SCHEMA_NEWER: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_schema_newer");
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_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

@@ -0,0 +1,132 @@
// file: crates/ksp-store-lib/src/health.rs
// version: 1
/// Portable Store health state independent from the selected physical backend.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum StoreHealthState {
/// The selected Store backend answered the bounded readiness probe and its migration foundation is current.
Ready,
/// The Store instance exists but its latest bounded readiness probe could not prove readiness.
NotReady,
}
/// Safe synchronous Store runtime snapshot without performing backend I/O.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StoreRuntimeSnapshot {
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
pool_available: u32,
pool_capacity: u32,
pool_size: u32,
pool_waiting: u32,
}
impl StoreRuntimeSnapshot {
/// Creates one portable runtime projection from backend-owned safe counters.
#[must_use]
pub(crate) fn new(
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
pool_capacity: u32,
pool_size: u32,
pool_available: u32,
pool_waiting: u32,
) -> Self {
return Self { backend_kind, network, pool_available, pool_capacity, pool_size, pool_waiting };
}
/// Returns the selected backend identity.
#[must_use]
pub const fn backend_kind(&self) -> crate::StoreBackendKind {
return self.backend_kind;
}
/// Returns the one logical network bound to this Store instance.
#[must_use]
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
return &self.network;
}
/// Returns the number of currently available pooled backend objects.
#[must_use]
pub const fn pool_available(&self) -> u32 {
return self.pool_available;
}
/// Returns the configured maximum pooled backend object count.
#[must_use]
pub const fn pool_capacity(&self) -> u32 {
return self.pool_capacity;
}
/// Returns the current pooled backend object count.
#[must_use]
pub const fn pool_size(&self) -> u32 {
return self.pool_size;
}
/// Returns the number of tasks currently waiting for a pooled backend object.
#[must_use]
pub const fn pool_waiting(&self) -> u32 {
return self.pool_waiting;
}
}
/// Portable Store readiness projection containing only safe runtime and migration diagnostics.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StoreHealthSnapshot {
last_error_code: std::option::Option<ksp_store_api::ErrorCode>,
migration_version: std::option::Option<u64>,
pending_migration_count: u32,
runtime: StoreRuntimeSnapshot,
state: StoreHealthState,
}
impl StoreHealthSnapshot {
/// Creates one safe health projection from already classified backend diagnostics.
#[must_use]
pub(crate) fn new(
state: StoreHealthState,
runtime: StoreRuntimeSnapshot,
migration_version: std::option::Option<u64>,
pending_migration_count: u32,
last_error_code: std::option::Option<ksp_store_api::ErrorCode>,
) -> Self {
return Self { last_error_code, migration_version, pending_migration_count, runtime, state };
}
/// Returns the latest safe error code when readiness could not be proven.
#[must_use]
pub const fn last_error_code(&self) -> std::option::Option<ksp_store_api::ErrorCode> {
return self.last_error_code;
}
/// Returns the migration version observed by the readiness probe when available.
#[must_use]
pub const fn migration_version(&self) -> std::option::Option<u64> {
return self.migration_version;
}
/// Returns the number of embedded migrations newer than the observed applied version.
#[must_use]
pub const fn pending_migration_count(&self) -> u32 {
return self.pending_migration_count;
}
/// Returns the safe synchronous runtime projection captured for this health probe.
#[must_use]
pub const fn runtime(&self) -> &StoreRuntimeSnapshot {
return &self.runtime;
}
/// Returns whether this probe proved the Store ready.
#[must_use]
pub const fn state(&self) -> StoreHealthState {
return self.state;
}
}
#[cfg(test)]
#[path = "../unit_tests/health.rs"]
mod tests;

View File

@@ -0,0 +1,199 @@
// file: crates/ksp-store-lib/src/lib.rs
// version: 6
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Common backend-neutral Store runtime facade for KSP.
//!
//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a
//! portable safe runtime snapshot and lightweight health/readiness projection,
//! while retaining the private migration/bootstrap foundation and no business
//! persistence schema.
//!
//! 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 health;
mod settings;
mod store;
/// 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 PostgreSQL physical configuration is malformed or unsupported.
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
/// Error code used when PostgreSQL physical connection establishment fails.
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
pub use self::error::ERROR_CODE_POSTGRES_HEALTH_FAILED;
/// Error code used when PostgreSQL migration/bootstrap execution fails safely.
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
/// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history.
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH;
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
/// Error code used when PostgreSQL schema history is newer than this Store runtime.
pub use self::error::ERROR_CODE_POSTGRES_SCHEMA_NEWER;
/// Error code used when PostgreSQL verified TLS setup or negotiation fails.
pub use self::error::ERROR_CODE_POSTGRES_TLS_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;
/// Portable Store health/readiness projection containing only safe diagnostics.
pub use self::health::StoreHealthSnapshot;
/// Portable Store health state independent from physical backend types.
pub use self::health::StoreHealthState;
/// Safe synchronous Store runtime snapshot containing backend-neutral pool counters.
pub use self::health::StoreRuntimeSnapshot;
/// 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 log site.
const _: &str = crate::TRACING_TARGET;

View File

@@ -0,0 +1,395 @@
// file: crates/ksp-store-lib/src/settings.rs
// version: 3
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 sensitive PostgreSQL connection URI only to the compiled backend bridge.
#[cfg(feature = "postgres")]
#[must_use]
pub(crate) fn connection_uri(&self) -> &str {
return self.connection_uri.as_str();
}
/// 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,
network: ksp_store_api::RawNetworkId,
shutdown_timeout: std::time::Duration,
}
impl StoreSettings {
/// Creates complete Store runtime settings for one explicit logical network, backend and shutdown bound.
#[must_use]
pub fn new(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self {
return Self { backend, network, shutdown_timeout };
}
/// Returns the selected backend settings.
#[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 single logical network bound to this Store instance.
#[must_use]
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
return &self.network;
}
/// Returns the maximum duration allowed for explicit Store shutdown.
#[must_use]
pub const fn shutdown_timeout(&self) -> std::time::Duration {
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 network and backend construction explicit.
#[must_use]
pub fn with_default_shutdown(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings) -> Self {
return Self::new(network, backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS));
}
}
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,236 @@
// file: crates/ksp-store-lib/src/store.rs
// version: 4
/// Opaque common Store runtime facade.
///
/// A successful value is returned only after the selected compiled backend has completed its bounded physical opening path. PostgreSQL pool, client, TLS and
/// driver types remain private to the backend crate.
pub struct Store {
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
#[cfg(feature = "postgres")]
runtime: StoreRuntime,
shutdown_timeout: std::time::Duration,
}
impl Store {
/// Validates settings, selects the requested backend and opens one ready Store instance for exactly one logical network.
///
/// A known backend whose Cargo feature is absent is rejected before any I/O. A successful PostgreSQL result proves that one physical pooled connection has
/// been established under the typed TLS and timeout policy.
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();
let network = settings.network().clone();
let shutdown_timeout = settings.shutdown_timeout();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
backend = backend_kind.code(),
network = network.as_str(),
"opening Store runtime"
);
return match settings.backend() {
crate::StoreBackendSettings::Postgres(postgres) => open_postgres(backend_kind, network, shutdown_timeout, postgres).await,
};
}
/// Returns a safe synchronous runtime snapshot without performing backend I/O.
#[must_use]
pub fn runtime_snapshot(&self) -> crate::StoreRuntimeSnapshot {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => map_postgres_runtime_snapshot(backend.runtime_snapshot(), self.backend_kind, self.network.clone()),
};
}
#[cfg(not(feature = "postgres"))]
{
return crate::StoreRuntimeSnapshot::new(self.backend_kind, self.network.clone(), 0, 0, 0, 0);
}
}
/// Runs the selected backend's lightweight bounded readiness probe and returns only portable redacted diagnostics.
pub async fn health(&self) -> crate::StoreHealthSnapshot {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let snapshot = backend.health().await;
map_postgres_health_snapshot(snapshot, self.backend_kind, self.network.clone())
},
};
}
#[cfg(not(feature = "postgres"))]
{
return crate::StoreHealthSnapshot::new(
crate::StoreHealthState::NotReady,
self.runtime_snapshot(),
std::option::Option::None,
0,
std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED),
);
}
}
/// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline.
pub async fn close(self) -> ksp_store_api::Result<()> {
let backend_kind = self.backend_kind;
#[cfg(feature = "postgres")]
{
let network = self.network;
let shutdown_timeout = self.shutdown_timeout;
let result = match self.runtime {
StoreRuntime::Postgres(backend) => backend.close(shutdown_timeout).await,
};
return match result {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
backend = backend_kind.code(),
network = network.as_str(),
"Store runtime closed"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
};
}
#[cfg(not(feature = "postgres"))]
{
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
}
}
}
impl std::fmt::Debug for Store {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("Store")
.field("backend_kind", &self.backend_kind)
.field("network", &self.network)
.field("shutdown_timeout", &self.shutdown_timeout)
.finish_non_exhaustive();
}
}
#[cfg(feature = "postgres")]
enum StoreRuntime {
Postgres(ksp_store_postgres_lib::PostgresBackend),
}
#[cfg(feature = "postgres")]
async fn open_postgres(
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
shutdown_timeout: std::time::Duration,
settings: &crate::PostgresStoreSettings,
) -> ksp_store_api::Result<Store> {
let bootstrap = settings.bootstrap();
let pool = settings.pool();
let tls_mode = match settings.tls_mode() {
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
};
let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
network.clone(),
settings.connection_uri(),
pool.max_connections(),
pool.connect_timeout(),
pool.wait_timeout(),
pool.create_timeout(),
pool.recycle_timeout(),
tls_mode,
bootstrap.auto_migrate(),
bootstrap.migration_timeout(),
bootstrap.migration_lock_timeout(),
);
let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await;
return match opened {
std::result::Result::Ok(backend) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
backend = backend_kind.code(),
network = network.as_str(),
"Store backend is physically ready"
);
std::result::Result::Ok(Store { backend_kind, network, runtime: StoreRuntime::Postgres(backend), shutdown_timeout })
},
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
};
}
#[cfg(not(feature = "postgres"))]
async fn open_postgres(
backend_kind: crate::StoreBackendKind,
_network: ksp_store_api::RawNetworkId,
_shutdown_timeout: std::time::Duration,
_settings: &crate::PostgresStoreSettings,
) -> ksp_store_api::Result<Store> {
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
}
#[cfg(feature = "postgres")]
fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error {
let code = postgres_error_code(error.kind());
return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed")
.with_context("backend", backend_kind.code())
.with_context("network", network)
.with_context("phase", error.phase());
}
#[cfg(feature = "postgres")]
fn map_postgres_runtime_snapshot(
snapshot: ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot,
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
) -> crate::StoreRuntimeSnapshot {
return crate::StoreRuntimeSnapshot::new(
backend_kind,
network,
snapshot.pool_capacity(),
snapshot.pool_size(),
snapshot.pool_available(),
snapshot.pool_waiting(),
);
}
#[cfg(feature = "postgres")]
fn map_postgres_health_snapshot(
snapshot: ksp_store_postgres_lib::PostgresBackendHealthSnapshot,
backend_kind: crate::StoreBackendKind,
network: ksp_store_api::RawNetworkId,
) -> crate::StoreHealthSnapshot {
let state = if snapshot.is_ready() { crate::StoreHealthState::Ready } else { crate::StoreHealthState::NotReady };
let error_code = snapshot.error_kind().map(|kind| return postgres_error_code(kind));
let runtime = map_postgres_runtime_snapshot(snapshot.runtime().clone(), backend_kind, network);
return crate::StoreHealthSnapshot::new(state, runtime, snapshot.migration_version(), snapshot.pending_migration_count(), error_code);
}
#[cfg(feature = "postgres")]
fn postgres_error_code(kind: ksp_store_postgres_lib::PostgresBackendErrorKind) -> ksp_store_api::ErrorCode {
return match kind {
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH,
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER,
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
};
}
#[cfg(not(feature = "postgres"))]
fn unavailable_runtime_error(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Error {
return 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

@@ -0,0 +1,60 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 6
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
#[test]
fn pre_005_manifest_keeps_backend_physical_dependencies_out_of_facade() {
let manifest = include_str!("../Cargo.toml");
assert!(manifest.contains("default = [\"postgres\"]"));
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
assert!(manifest.contains("ksp-logging-lib = { path = \"../ksp-logging-lib\" }"));
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }"));
for forbidden in [
"ksp-config-lib",
"ksp-materializer",
"ksp-program",
"ksp-onchain-transport-lib",
"ksp-offchain-transport-lib",
"tokio-postgres",
"deadpool-postgres",
"rustls",
] {
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
}
return;
}
#[test]
fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("pub use self::settings::StoreSettings;"));
assert!(crate_root.contains("pub use self::health::StoreHealthSnapshot;"));
assert!(crate_root.contains("pub use self::health::StoreRuntimeSnapshot;"));
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::",
"deadpool::managed::Pool",
"tokio_postgres::Client",
"tokio_postgres::Row",
"tokio_postgres::Statement",
] {
assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}");
}
let production = format!("{}\n{}\n{}", include_str!("../src/health.rs"), 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}");
}
return;
}

View File

@@ -0,0 +1,55 @@
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Feature-selection and pre-I/O PostgreSQL failure canaries for the common Store facade.
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!("Store pre-I/O feature/config rejection unexpectedly became pending"),
};
}
fn valid_network() -> ksp_store_lib::RawNetworkId {
return match ksp_store_lib::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
};
}
fn settings(connection_uri: &str) -> ksp_store_lib::StoreSettings {
let postgres = ksp_store_lib::PostgresStoreSettings::new(
connection_uri,
ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::Disabled,
ksp_store_lib::PostgresBootstrapSettings::default(),
);
return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres));
}
#[cfg(not(feature = "postgres"))]
#[test]
fn pre_005_known_postgres_without_feature_returns_stable_error_before_io() {
let result = poll_ready(ksp_store_lib::Store::open(settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
return;
}
#[cfg(feature = "postgres")]
#[test]
fn pre_005_compiled_postgres_rejects_malformed_uri_without_secret_leak_before_io() {
let secret_canary = "not-a-postgresql-uri-secret-canary";
let result = poll_ready(ksp_store_lib::Store::open(settings(secret_canary)));
let error = result.err();
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID));
assert!(!format!("{error:?}").contains(secret_canary));
return;
}

View File

@@ -0,0 +1,256 @@
// file: crates/ksp-store-lib/tests/hardening_completeness.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Hardening, exact-surface and feature-matrix canaries for the common Store facade.
const SECRET_CANARY: &str = "KSP-STORE-SECRET-CANARY-PRE009";
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!("Store hardening pre-I/O rejection unexpectedly became pending"),
};
}
fn valid_network() -> ksp_store_lib::RawNetworkId {
return match ksp_store_lib::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid hardening network rejected: {error:?}"),
};
}
fn hostile_settings(connection_uri: &str) -> ksp_store_lib::StoreSettings {
let postgres = ksp_store_lib::PostgresStoreSettings::new(
connection_uri,
ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::VerifyFull,
ksp_store_lib::PostgresBootstrapSettings::default(),
);
return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres));
}
fn public_reexport_names(source: &str) -> std::vec::Vec<&str> {
let mut names = std::vec::Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') {
continue;
}
let without_semicolon = trimmed.trim_end_matches(';');
let name = match without_semicolon.rsplit("::").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
names.push(name);
}
names.sort_unstable();
return names;
}
fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
let dependencies_tail = match source.split("[dependencies]").nth(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::vec::Vec::new(),
};
let dependencies = match dependencies_tail.split("[lints]").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::vec::Vec::new(),
};
let mut names = std::vec::Vec::new();
for line in dependencies.lines() {
let content = match line.split('#').next() {
std::option::Option::Some(value) => value.trim(),
std::option::Option::None => continue,
};
if content.is_empty() {
continue;
}
let key = match content.split('=').next() {
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
std::option::Option::None => continue,
};
if !key.is_empty() {
names.push(key);
}
}
names.sort_unstable();
return names;
}
#[test]
fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
let crate_root = include_str!("../src/lib.rs");
for required in ["mod constants;", "mod error;", "mod health;", "mod settings;", "mod store;"] {
assert!(crate_root.contains(required), "missing Store facade module: {required}");
}
assert!(!crate_root.contains("pub mod "));
let actual = public_reexport_names(crate_root);
let mut expected = [
"ERROR_CODE_BACKEND_CLOSED",
"ERROR_CODE_BACKEND_NOT_COMPILED",
"ERROR_CODE_BACKEND_OPEN_FAILED",
"ERROR_CODE_POSTGRES_CONFIG_INVALID",
"ERROR_CODE_POSTGRES_CONNECT_FAILED",
"ERROR_CODE_POSTGRES_HEALTH_FAILED",
"ERROR_CODE_POSTGRES_MIGRATION_FAILED",
"ERROR_CODE_POSTGRES_MIGRATION_MISMATCH",
"ERROR_CODE_POSTGRES_POOL_TIMEOUT",
"ERROR_CODE_POSTGRES_SCHEMA_NEWER",
"ERROR_CODE_POSTGRES_TLS_FAILED",
"ERROR_CODE_RAW_CONFLICT",
"ERROR_CODE_RAW_MODEL_INVALID",
"ERROR_CODE_RAW_PAYLOAD_INVALID",
"ERROR_CODE_RAW_PROVENANCE_INVALID",
"ERROR_CODE_RAW_QUERY_INVALID",
"ERROR_CODE_RAW_RETENTION_INVALID",
"ERROR_CODE_SETTINGS_INVALID",
"ERROR_CODE_SHUTDOWN_TIMEOUT",
"Error",
"ErrorCode",
"ErrorContext",
"MAX_RAW_ACCOUNT_DATA_BYTES",
"MAX_RAW_CODE_BYTES",
"MAX_RAW_PAGE_CURSOR_BYTES",
"MAX_RAW_PAYLOAD_BYTES",
"MAX_RAW_SOURCE_PAYLOAD_BYTES",
"MAX_RAW_UNIX_MILLIS",
"PostgresBootstrapSettings",
"PostgresPoolSettings",
"PostgresStoreSettings",
"PostgresTlsMode",
"Pubkey",
"RawAccountObservation",
"RawAccountObservationRead",
"RawAccountObservationWrite",
"RawAccountState",
"RawAccountStateQuery",
"RawAccountStateRead",
"RawAccountStateReference",
"RawAccountStateWrite",
"RawAcquisitionOrigin",
"RawAcquisitionProvenance",
"RawAcquisitionWriteOutcome",
"RawContentHash",
"RawEntityWriteOutcome",
"RawFormatId",
"RawNetworkId",
"RawObservationKey",
"RawObservationWriteOutcome",
"RawPage",
"RawPageCursor",
"RawPageLimit",
"RawPageRequest",
"RawPayload",
"RawProvenanceCode",
"RawRetentionState",
"RawRetentionWriteOutcome",
"RawSlotRange",
"RawSortDirection",
"RawTimestamp",
"RawTransaction",
"RawTransactionAcquisitionMode",
"RawTransactionObservation",
"RawTransactionObservationRead",
"RawTransactionObservationWrite",
"RawTransactionQuery",
"RawTransactionRead",
"RawTransactionReference",
"RawTransactionRetentionRead",
"RawTransactionRetentionTransition",
"RawTransactionRetentionWrite",
"RawTransactionSignature",
"RawTransactionTombstone",
"RawTransactionWrite",
"Result",
"Store",
"StoreApiFuture",
"StoreBackendKind",
"StoreBackendSettings",
"StoreHealthSnapshot",
"StoreHealthState",
"StoreRuntimeSnapshot",
"StoreSettings",
];
expected.sort_unstable();
assert_eq!(actual.as_slice(), expected.as_slice());
assert_eq!(actual.len(), 84);
return;
}
#[test]
fn pre_009_facade_manifest_and_feature_contract_are_exact() {
let manifest = include_str!("../Cargo.toml");
assert!(manifest.contains("default = [\"postgres\"]"));
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
let actual = manifest_dependency_names(manifest);
let expected = ["ksp-logging-lib", "ksp-store-api", "ksp-store-postgres-lib"];
assert_eq!(actual.as_slice(), expected.as_slice());
for forbidden in ["tokio-postgres", "deadpool-postgres", "rustls", "sqlx", "ksp-config-lib", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] {
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
}
return;
}
#[test]
fn pre_009_secret_canary_never_crosses_settings_or_pre_io_error_debug() {
let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}");
let settings = hostile_settings(malformed.as_str());
let rendered = std::format!("{settings:?}");
assert!(!rendered.contains(SECRET_CANARY));
assert!(rendered.contains("<redacted>"));
let result = poll_ready(ksp_store_lib::Store::open(settings));
let error = match result {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => panic!("hostile Store settings unexpectedly opened"),
};
#[cfg(feature = "postgres")]
assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID);
#[cfg(not(feature = "postgres"))]
assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED);
assert!(!std::format!("{error}").contains(SECRET_CANARY));
assert!(!std::format!("{error:?}").contains(SECRET_CANARY));
return;
}
#[test]
fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_handles_out() {
let production = std::format!(
"{}
{}
{}
{}",
include_str!("../src/error.rs"),
include_str!("../src/health.rs"),
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::Client",
"tokio_postgres::Row",
"tokio_postgres::Statement",
"deadpool_postgres::Pool",
"sqlx::",
"CREATE TABLE",
"INSERT INTO",
"UPDATE ",
"DELETE FROM",
] {
assert!(!production.contains(forbidden), "forbidden facade ownership/runtime material detected: {forbidden}");
}
return;
}

View File

@@ -0,0 +1,68 @@
// file: crates/ksp-store-lib/tests/public_api.rs
// version: 5
#![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 network = match ksp_store_lib::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid public API network rejected: {error:?}"),
};
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(network, ksp_store_lib::StoreBackendSettings::Postgres(postgres));
assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
assert_eq!(settings.network().as_str(), "devnet");
assert!(settings.validate().is_ok());
let _open = ksp_store_lib::Store::open;
let _close = ksp_store_lib::Store::close;
let _runtime_snapshot = ksp_store_lib::Store::runtime_snapshot;
let _health = ksp_store_lib::Store::health;
return;
}
#[test]
fn pre_005_common_and_postgres_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_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_SCHEMA_NEWER.code(), "postgres_schema_newer");
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_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;
}
#[test]
fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts() {
let _state = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthState>>();
let _health = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthSnapshot>>();
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreRuntimeSnapshot>>();
return;
}

View File

@@ -0,0 +1,45 @@
// file: crates/ksp-store-lib/unit_tests/health.rs
// version: 1
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid Store health test network rejected: {error:?}"),
};
}
#[test]
fn runtime_snapshot_is_backend_neutral_and_contains_only_safe_counts() {
let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 3, 2, 1);
assert_eq!(runtime.backend_kind(), crate::StoreBackendKind::Postgres);
assert_eq!(runtime.network().as_str(), "devnet");
assert_eq!(runtime.pool_capacity(), 8);
assert_eq!(runtime.pool_size(), 3);
assert_eq!(runtime.pool_available(), 2);
assert_eq!(runtime.pool_waiting(), 1);
let rendered = format!("{runtime:?}");
for forbidden in ["postgresql://", "password", "username", "database", "SELECT ", "ksp_store_schema_migrations"] {
assert!(!rendered.contains(forbidden), "unsafe runtime snapshot material detected: {forbidden}");
}
return;
}
#[test]
fn health_snapshot_carries_only_safe_state_migration_and_error_code() {
let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 1, 1, 0);
let ready = crate::StoreHealthSnapshot::new(crate::StoreHealthState::Ready, runtime.clone(), std::option::Option::Some(0), 0, std::option::Option::None);
assert_eq!(ready.state(), crate::StoreHealthState::Ready);
assert_eq!(ready.migration_version(), std::option::Option::Some(0));
assert_eq!(ready.pending_migration_count(), 0);
assert_eq!(ready.last_error_code(), std::option::Option::None);
let not_ready = crate::StoreHealthSnapshot::new(
crate::StoreHealthState::NotReady,
runtime,
std::option::Option::None,
0,
std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED),
);
assert_eq!(not_ready.state(), crate::StoreHealthState::NotReady);
assert_eq!(not_ready.last_error_code(), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED));
return;
}

View File

@@ -0,0 +1,149 @@
// file: crates/ksp-store-lib/unit_tests/settings.rs
// version: 3
fn valid_network() -> crate::RawNetworkId {
return match crate::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
};
}
fn valid_postgres_settings() -> crate::PostgresStoreSettings {
return crate::PostgresStoreSettings::new(
"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(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()));
assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres);
assert_eq!(store.network().as_str(), "devnet");
return;
}
#[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(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100));
assert!(valid.validate().is_ok());
let invalid =
crate::StoreSettings::new(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001));
let error = invalid.validate().err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
return;
}
#[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| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
assert!(!format!("{:?}", error).contains(secret));
return;
}

View File

@@ -0,0 +1,57 @@
// file: crates/ksp-store-lib/unit_tests/store.rs
// version: 4
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!("Store pre-I/O rejection unexpectedly became pending"),
};
}
fn valid_network() -> crate::RawNetworkId {
return match crate::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
};
}
fn store_settings(connection_uri: &str) -> crate::StoreSettings {
let postgres = crate::PostgresStoreSettings::new(
connection_uri,
crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::Disabled,
crate::PostgresBootstrapSettings::default(),
);
return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
}
#[test]
fn invalid_settings_are_rejected_before_backend_dispatch() {
let result = poll_ready(crate::Store::open(store_settings("")));
let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
return;
}
#[cfg(feature = "postgres")]
#[test]
fn compiled_postgres_rejects_malformed_physical_configuration_before_io() {
let secret_canary = "not-a-postgresql-uri-secret-canary";
let result = poll_ready(crate::Store::open(store_settings(secret_canary)));
let error = result.err();
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_CONFIG_INVALID));
assert!(!format!("{error:?}").contains(secret_canary));
return;
}
#[cfg(not(feature = "postgres"))]
#[test]
fn known_postgres_without_feature_is_rejected_before_io() {
let result = poll_ready(crate::Store::open(store_settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
return;
}

View File

@@ -0,0 +1,22 @@
# file: crates/ksp-store-postgres-lib/Cargo.toml
# version: 3
[package]
name = "ksp-store-postgres-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-store-api = { path = "../ksp-store-api" }
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
rustls-native-certs.workspace = true
sha2.workspace = true
tokio = { workspace = true, features = ["rt", "time"] }
tokio-postgres = { workspace = true, features = ["runtime"] }
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
[lints]
workspace = true

View File

@@ -0,0 +1,124 @@
<!-- file: crates/ksp-store-postgres-lib/README.md -->
<!-- version: 1 -->
# ksp-store-postgres-lib
`ksp-store-postgres-lib` est le backend PostgreSQL physique officiel du Store KSP.
La crate implémente la fondation connexion/pool/TLS/migrations/health derrière `ksp-store-lib`. Elle dépend directement de `ksp-store-api` mais ne dépend jamais de la façade `ksp-store-lib`.
## Responsabilités
La crate possède seule pour PostgreSQL :
- le parsing et la normalisation de la configuration physique `tokio-postgres` ;
- le pool borné `deadpool-postgres` ;
- la policy TLS physique avec Rustls ;
- les roots système et le provider cryptographique AWS-LC ;
- le bootstrap/moteur de migrations privé KSP ;
- la table metadata `ksp_store_schema_migrations` ;
- le sentinel `V000__bootstrap.sql` et son checksum SHA-256 ;
- l'advisory transaction lock borné des migrations ;
- les snapshots runtime/health sûrs destinés au bridge de façade ;
- la fermeture explicite du pool et son fallback `Drop` best-effort ;
- la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL.
## Frontière d'utilisation
Les applications, jobs et workers KSP ne dépendent normalement pas de cette crate :
```text
consumer -> ksp-store-lib -> [feature postgres] ksp-store-postgres-lib
```
La surface publique de cette crate existe pour le bridge inter-crates et les tests d'intégration backend. Elle ne constitue pas une seconde façade Store.
`ksp-store-postgres-lib` ne réexporte pas `tokio-postgres`, Deadpool ou Rustls.
## Connexion et pool
`PostgresBackend::open` :
1. valide et normalise l'URI fournie explicitement ;
2. impose la policy TLS typée ;
3. construit un pool borné ;
4. prouve une connexion physique ;
5. vérifie/applique le bootstrap selon les settings ;
6. ne retourne qu'après succès de cette fondation.
Le backend ne lit aucun environnement, `.env`, `PG*`, `.pgpass` ou fichier TLS implicite libpq.
## TLS
Les modes sont exactement :
```text
Disabled
VerifyFull
```
`VerifyFull` exige :
- TLS ;
- roots système ;
- certificat valide ;
- vérification de l'identité serveur ;
- aucune dégradation automatique en plaintext.
Les configurations ne permettant pas de vérifier une identité serveur, comme `hostaddr` seul, sont rejetées.
## Migrations
La fondation embarque uniquement :
```text
migrations/V000__bootstrap.sql
```
Elle crée la metadata privée :
```text
ksp_store_schema_migrations
```
Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
Aucune migration métier RAW n'appartient à cette fondation.
## Health et erreurs
`PostgresBackendRuntimeSnapshot` et `PostgresBackendHealthSnapshot` ne contiennent que des compteurs et états sûrs destinés à la façade.
`PostgresBackendError` ne conserve que :
```text
PostgresBackendErrorKind
phase statique
```
Le texte d'erreur PostgreSQL, l'URI, SQL et les valeurs bind ne traversent pas cette frontière.
## Support PostgreSQL
La politique de support de `0.3.2` fixe PostgreSQL 15 comme major minimal. Le test live de fondation refuse explicitement un serveur plus ancien ; le backend ne fixe aucun plafond arbitraire de major PostgreSQL. La compatibilité de migration reste basée sur le schéma KSP.
La preuve opérateur réelle et le major effectivement exercé sont conservés dans la matrice de validation, pas dans cette documentation durable.
## Hors périmètre actuel
La crate ne contient encore :
- aucune implémentation PostgreSQL des capabilities `RawTransaction*` ;
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
- aucun repository métier RAW ;
- aucune table/index métier ;
- aucune orchestration worker/job ;
- aucun transport d'acquisition ou decoder Program.
## Documentation
- [`USAGE.md`](USAGE.md) — bridge physique et lifecycle ;
- [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md) — façade runtime destinée aux consumers ;
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture Store ;
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — décisions pool/TLS/migrations ;
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — preuves déterministes et PostgreSQL réel.

View File

@@ -0,0 +1,155 @@
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
<!-- version: 1 -->
# Utilisation de ksp-store-postgres-lib
## 1. Quand utiliser cette crate directement
Le consumer applicatif normal utilise `ksp-store-lib`.
Une dépendance directe à `ksp-store-postgres-lib` est réservée aux composants qui implémentent ou testent le bridge physique PostgreSQL. La crate backend ne doit pas devenir une façade parallèle.
Un tel composant doit déclarer explicitement le backend et `ksp-store-api`, car `PostgresBackendSettings::new` reçoit le `RawNetworkId` backend-neutral sans le réexporter :
```toml
[dependencies]
ksp-store-api = { path = "../ksp-store-api" }
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib" }
```
## 2. Construire le bridge physique
`PostgresBackendSettings` reçoit des valeurs déjà possédées et validées par la couche appelante. L'URI est sensible et son `Debug` est redacted.
```rust
fn backend_settings(
network: ksp_store_api::RawNetworkId,
connection_uri: std::string::String,
) -> ksp_store_postgres_lib::PostgresBackendSettings {
return ksp_store_postgres_lib::PostgresBackendSettings::new(
network,
connection_uri,
8,
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
true,
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10),
);
}
```
Le backend reçoit un seul `RawNetworkId`. Une instance physique n'est pas un routeur multi-réseau.
## 3. Ouvrir, sonder et fermer
```rust
async fn use_backend(
settings: ksp_store_postgres_lib::PostgresBackendSettings,
) -> std::result::Result<(), ksp_store_postgres_lib::PostgresBackendError> {
let backend = ksp_store_postgres_lib::PostgresBackend::open(settings).await;
let backend = match backend {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = backend.runtime_snapshot();
let _capacity = runtime.pool_capacity();
let _size = runtime.pool_size();
let _available = runtime.pool_available();
let _waiting = runtime.pool_waiting();
let health = backend.health().await;
let _ready = health.ready();
let _migration_version = health.migration_version();
let _pending = health.pending_migration_count();
let _safe_error_kind = health.last_error_kind();
return backend.close(std::time::Duration::from_secs(5)).await;
}
```
`open` prouve la connexion et le bootstrap avant de retourner. `close` ferme le pool puis attend son drain dans la deadline fournie.
## 4. Choisir le mode TLS
### `VerifyFull`
À utiliser pour les connexions PostgreSQL protégées :
```rust
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull
```
Le backend charge les roots système et vérifie certificat + identité serveur. Il rejette une configuration ne fournissant pas d'identité vérifiable.
### `Disabled`
```rust
ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled
```
Ce mode désactive explicitement TLS. Il ne doit être utilisé que lorsque la topologie de déploiement justifie clairement une connexion non chiffrée.
La valeur typée choisie par KSP prime sur les paramètres SSL de l'URI.
## 5. Bootstrap et migrations
Le backend embarque son propre moteur de migrations. Le seul artefact initial est :
```text
migrations/V000__bootstrap.sql
```
Le bootstrap maintient :
```text
ksp_store_schema_migrations
version
name
checksum SHA-256
```
Le runner est transactionnel et sérialisé par advisory transaction lock. Une divergence de checksum/nom/version ou une history plus récente est terminale ; aucun down migration automatique n'est exécuté.
`auto_migrate = false` permet de vérifier l'état sans appliquer de migration pending.
## 6. Classifier les erreurs sans fuite
```rust
match error.kind() {
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => {}
_ => {}
}
let _safe_phase = error.phase();
```
Ne pas reconstruire un diagnostic utilisateur à partir de l'erreur brute PostgreSQL : cette erreur n'est volontairement pas conservée par le bridge.
## 7. Ce que cette crate ne permet pas encore
La fondation physique n'implémente pas les traits `RawTransaction*` ou `RawAccount*` de `ksp-store-api`.
Un backend ouvert et healthy prouve uniquement :
```text
connexion/pool
TLS selon policy
bootstrap/history
health/readiness
close borné
```
Il ne prouve aucune persistence métier RAW.

View File

@@ -0,0 +1,6 @@
CREATE TABLE ksp_store_schema_migrations (
version BIGINT PRIMARY KEY,
name TEXT NOT NULL,
checksum TEXT NOT NULL,
applied_at TIMESTAMPTZ NOT NULL
);

View File

@@ -0,0 +1,7 @@
// file: crates/ksp-store-postgres-lib/src/constants.rs
// version: 2
//! PostgreSQL Store backend-owned constants.
/// Owning tracing target reserved for events emitted by the PostgreSQL Store backend.
pub(crate) const TRACING_TARGET: &str = "ksp-store-postgres-lib";

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-store-postgres-lib/src/error.rs
// version: 3
/// Safe backend-local classification used by the Store facade for stable error mapping.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PostgresBackendErrorKind {
/// The supplied physical PostgreSQL configuration is unsupported or malformed.
ConfigInvalid,
/// A physical PostgreSQL connection could not be established.
ConnectFailed,
/// A bounded pool wait, create or recycle operation reached its deadline.
PoolTimeout,
/// A lightweight PostgreSQL health/readiness probe failed without exposing server text or SQL.
HealthFailed,
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
MigrationFailed,
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
MigrationMismatch,
/// The database schema history contains a migration newer than this runtime understands.
SchemaNewer,
/// Explicit backend shutdown did not drain inside the supplied deadline.
ShutdownTimeout,
/// Verified TLS configuration or negotiation could not be established.
TlsFailed,
}
/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PostgresBackendError {
kind: PostgresBackendErrorKind,
phase: &'static str,
}
impl PostgresBackendError {
/// Creates one backend error without retaining external error text or sensitive connection material.
#[must_use]
pub(crate) const fn new(kind: PostgresBackendErrorKind, phase: &'static str) -> Self {
return Self { kind, phase };
}
/// Returns the safe backend-local error classification.
#[must_use]
pub const fn kind(&self) -> PostgresBackendErrorKind {
return self.kind;
}
/// Returns the static safe lifecycle phase associated with the failure.
#[must_use]
pub const fn phase(&self) -> &'static str {
return self.phase;
}
}

View File

@@ -0,0 +1,101 @@
// file: crates/ksp-store-postgres-lib/src/health.rs
// version: 1
const DEFAULT_HEALTH_TIMEOUT_MS: u64 = 5_000;
const MIGRATION_VERSION_SQL: &str = "SELECT COALESCE(MAX(version), -1)::BIGINT FROM ksp_store_schema_migrations";
const READINESS_SQL: &str = "SELECT 1::BIGINT";
/// Runs one bounded lightweight readiness probe and returns only safe classified diagnostics.
pub(crate) async fn probe_health(pool: &deadpool_postgres::Pool) -> crate::PostgresBackendHealthSnapshot {
let runtime = crate::runtime_snapshot_from_status(pool.status());
let timeouts = pool.timeouts();
let timeout = match timeouts.wait {
std::option::Option::Some(value) => value,
std::option::Option::None => std::time::Duration::from_millis(DEFAULT_HEALTH_TIMEOUT_MS),
};
let bounded = tokio::time::timeout(timeout, probe_health_inner(pool, runtime.clone())).await;
return match bounded {
std::result::Result::Ok(snapshot) => snapshot,
std::result::Result::Err(_) => {
crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed)
},
};
}
async fn probe_health_inner(pool: &deadpool_postgres::Pool, runtime: crate::PostgresBackendRuntimeSnapshot) -> crate::PostgresBackendHealthSnapshot {
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let classified = crate::map_pool_error(error);
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, classified.kind());
},
};
let readiness_result = client.query_one(READINESS_SQL, &[]).await;
let readiness_row = match readiness_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
},
};
let readiness_value = match readiness_row.try_get::<usize, i64>(0) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
},
};
if readiness_value != 1 {
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
}
let migration_result = client.query_one(MIGRATION_VERSION_SQL, &[]).await;
let migration_row = match migration_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
},
};
let migration_value = migration_row.try_get::<usize, i64>(0);
let migration_version = match migration_value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
},
};
let expected = crate::current_migration_version();
if migration_version < 0 || migration_version < expected {
let observed = nonnegative_version(migration_version);
let pending = pending_migration_count(migration_version, expected);
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, observed, pending, crate::PostgresBackendErrorKind::MigrationMismatch);
}
if migration_version > expected {
return crate::PostgresBackendHealthSnapshot::not_ready(
runtime,
nonnegative_version(migration_version),
0,
crate::PostgresBackendErrorKind::SchemaNewer,
);
}
return crate::PostgresBackendHealthSnapshot::ready(runtime, migration_version as u64, 0);
}
fn nonnegative_version(value: i64) -> std::option::Option<u64> {
if value < 0 {
return std::option::Option::None;
}
return std::option::Option::Some(value as u64);
}
fn pending_migration_count(observed: i64, expected: i64) -> u32 {
if observed >= expected {
return 0;
}
let delta = expected.saturating_sub(observed);
if delta > i64::from(u32::MAX) {
return u32::MAX;
}
return delta as u32;
}
#[cfg(test)]
#[path = "../unit_tests/health.rs"]
mod tests;

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Official PostgreSQL backend implementation for KSP Store.
//!
//! `0.3.2-pre.007` owns the physical `tokio-postgres` connection, bounded
//! Deadpool pool, explicit Rustls TLS policy, private KSP migration/bootstrap
//! engine and safe lightweight health/readiness probe. Business persistence
//! remains absent from this foundation release.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
//! exposes PostgreSQL pool, client, row or statement types.
mod constants;
mod error;
mod health;
mod migration;
mod runtime;
/// Safe backend-local error returned to the common Store facade.
pub use self::error::PostgresBackendError;
/// Safe backend-local error classification used by the common Store facade.
pub use self::error::PostgresBackendErrorKind;
/// Opaque physical PostgreSQL backend owning its connection pool.
pub use self::runtime::PostgresBackend;
/// Safe PostgreSQL readiness projection returned through the backend bridge.
pub use self::runtime::PostgresBackendHealthSnapshot;
/// Safe PostgreSQL pool counter projection returned through the backend bridge.
pub use self::runtime::PostgresBackendRuntimeSnapshot;
/// Physical PostgreSQL settings bridge consumed only by the backend crate.
pub use self::runtime::PostgresBackendSettings;
/// TLS mode accepted by the physical PostgreSQL settings bridge.
pub use self::runtime::PostgresBackendTlsMode;
/// Crate-owned tracing target for PostgreSQL backend behavior.
pub(crate) use self::constants::TRACING_TARGET;
/// Private bounded health probe consumed by the physical backend runtime.
pub(crate) use self::health::probe_health;
/// Private migration/bootstrap runner consumed by the physical backend runtime.
pub(crate) use self::migration::bootstrap;
/// Current embedded migration version consumed by the private health probe.
pub(crate) use self::migration::current_migration_version;
/// Private Deadpool error mapper shared with the health probe.
pub(crate) use self::runtime::map_pool_error;
/// Private Deadpool status projector shared with the health probe.
pub(crate) use self::runtime::runtime_snapshot_from_status;
const _: &str = crate::TRACING_TARGET;

View File

@@ -0,0 +1,318 @@
// file: crates/ksp-store-postgres-lib/src/migration.rs
// version: 2
use sha2::Digest; // rust-rules: trait-import
const ADVISORY_LOCK_KEY: i64 = 0x4b53_5053_544f_5245;
const BOOTSTRAP_MIGRATION_NAME: &str = "bootstrap";
const BOOTSTRAP_MIGRATION_SQL: &str = include_str!("../migrations/V000__bootstrap.sql");
const BOOTSTRAP_MIGRATION_VERSION: i64 = 0;
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
const LOCK_POLL_INTERVAL_MS: u64 = 25;
const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
AND table_type = 'BASE TABLE'
)"#;
const METADATA_PRIMARY_KEY_SQL: &str = r#"SELECT COUNT(*)::BIGINT,
COUNT(*) FILTER (WHERE kcu.column_name = 'version')::BIGINT
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_catalog = kcu.constraint_catalog
AND tc.constraint_schema = kcu.constraint_schema
AND tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = current_schema()
AND tc.table_name = 'ksp_store_schema_migrations'
AND tc.constraint_type = 'PRIMARY KEY'"#;
const METADATA_SHAPE_SQL: &str = r#"SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
ORDER BY ordinal_position"#;
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
struct AppliedMigration {
checksum: std::string::String,
name: std::string::String,
version: i64,
}
/// Returns the latest migration version embedded by this backend runtime.
#[must_use]
pub(crate) const fn current_migration_version() -> i64 {
return BOOTSTRAP_MIGRATION_VERSION;
}
/// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client.
pub(crate) async fn bootstrap(
client: &mut deadpool_postgres::Client,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let bounded = tokio::time::timeout(migration_timeout, bootstrap_inner(client, auto_migrate, migration_timeout, migration_lock_timeout)).await;
return match bounded {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_timeout"))
},
};
}
async fn bootstrap_inner(
client: &mut deadpool_postgres::Client,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let transaction_result = client.transaction().await;
let transaction = match transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_begin"));
},
};
let lock_result = acquire_advisory_lock(&transaction, migration_lock_timeout).await;
if let std::result::Result::Err(error) = lock_result {
return std::result::Result::Err(error);
}
let timeout_result = set_statement_timeout(&transaction, migration_timeout).await;
if let std::result::Result::Err(error) = timeout_result {
return std::result::Result::Err(error);
}
let exists_result = metadata_exists(&transaction).await;
let metadata_exists = match exists_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let checksum = bootstrap_checksum();
if !metadata_exists {
if !auto_migrate {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_pending"));
}
let create_result = transaction.batch_execute(BOOTSTRAP_MIGRATION_SQL).await;
if create_result.is_err() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_create"));
}
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let insert_result = transaction.execute(HISTORY_INSERT_SQL, &[&BOOTSTRAP_MIGRATION_VERSION, &BOOTSTRAP_MIGRATION_NAME, &checksum]).await;
match insert_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_insert"));
},
}
} else {
let shape_result = verify_metadata_shape(&transaction).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let history_result = load_history(&transaction).await;
let history = match history_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let validation_result = validate_history(history.as_slice(), checksum.as_str());
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
let commit_result = transaction.commit().await;
return match commit_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_commit"))
},
};
}
async fn acquire_advisory_lock(
transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let started = tokio::time::Instant::now();
let deadline = match started.checked_add(timeout) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
},
};
loop {
let row_result = transaction.query_one("SELECT pg_try_advisory_xact_lock($1)", &[&ADVISORY_LOCK_KEY]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock"));
},
};
let acquired_result = row.try_get::<usize, bool>(0);
let acquired = match acquired_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_decode"));
},
};
if acquired {
return std::result::Result::Ok(());
}
let now = tokio::time::Instant::now();
if now >= deadline {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
}
let candidate = now + std::time::Duration::from_millis(LOCK_POLL_INTERVAL_MS);
let wake = if candidate < deadline { candidate } else { deadline };
tokio::time::sleep_until(wake).await;
}
}
async fn set_statement_timeout(
transaction: &deadpool_postgres::Transaction<'_>,
timeout: std::time::Duration,
) -> std::result::Result<(), crate::PostgresBackendError> {
let timeout_value = format!("{}ms", timeout.as_millis());
let result = transaction.query_one(SET_STATEMENT_TIMEOUT_SQL, &[&timeout_value]).await;
return match result {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "statement_timeout"))
},
};
}
async fn metadata_exists(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
let result = transaction.query_one(METADATA_EXISTS_SQL, &[]).await;
let row = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe"));
},
};
return match row.try_get::<usize, bool>(0) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe_decode"))
},
};
}
async fn verify_metadata_shape(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<(), crate::PostgresBackendError> {
let result = transaction.query(METADATA_SHAPE_SQL, &[]).await;
let rows = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape"));
},
};
let expected = [("version", "bigint", "NO"), ("name", "text", "NO"), ("checksum", "text", "NO"), ("applied_at", "timestamp with time zone", "NO")];
let mut found = [false; 4];
for row in rows {
let column_result = row.try_get::<usize, std::string::String>(0);
let data_type_result = row.try_get::<usize, std::string::String>(1);
let nullable_result = row.try_get::<usize, std::string::String>(2);
let (column, data_type, nullable) = match (column_result, data_type_result, nullable_result) {
(std::result::Result::Ok(column), std::result::Result::Ok(data_type), std::result::Result::Ok(nullable)) => (column, data_type, nullable),
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape_decode"));
},
};
for (index, expected_row) in expected.iter().enumerate() {
if column == expected_row.0 {
if found[index] || data_type != expected_row.1 || nullable != expected_row.2 {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
found[index] = true;
break;
}
}
}
for required in found {
if !required {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
}
}
let key_result = transaction.query_one(METADATA_PRIMARY_KEY_SQL, &[]).await;
let key_row = match key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key"));
},
};
let key_count = key_row.try_get::<usize, i64>(0);
let version_count = key_row.try_get::<usize, i64>(1);
return match (key_count, version_count) {
(std::result::Result::Ok(1), std::result::Result::Ok(1)) => std::result::Result::Ok(()),
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_primary_key"))
},
_ => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key_decode")),
};
}
async fn load_history(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<std::vec::Vec<AppliedMigration>, crate::PostgresBackendError> {
let result = transaction.query(HISTORY_LOAD_SQL, &[]).await;
let rows = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_load"));
},
};
let mut history = std::vec::Vec::with_capacity(rows.len());
for row in rows {
let version_result = row.try_get::<usize, i64>(0);
let name_result = row.try_get::<usize, std::string::String>(1);
let checksum_result = row.try_get::<usize, std::string::String>(2);
match (version_result, name_result, checksum_result) {
(std::result::Result::Ok(version), std::result::Result::Ok(name), std::result::Result::Ok(checksum)) => {
history.push(AppliedMigration { checksum, name, version });
},
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_decode"));
},
}
}
return std::result::Result::Ok(history);
}
fn validate_history(history: &[AppliedMigration], expected_checksum: &str) -> std::result::Result<(), crate::PostgresBackendError> {
let mut sentinel_found = false;
for applied in history {
if applied.version > BOOTSTRAP_MIGRATION_VERSION {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer"));
}
if applied.version < BOOTSTRAP_MIGRATION_VERSION {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_unknown"));
}
if applied.name != BOOTSTRAP_MIGRATION_NAME || applied.checksum != expected_checksum {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
sentinel_found = true;
}
if !sentinel_found {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
}
return std::result::Result::Ok(());
}
fn bootstrap_checksum() -> std::string::String {
let mut hasher = sha2::Sha256::new();
hasher.update(BOOTSTRAP_MIGRATION_SQL.as_bytes());
let digest = hasher.finalize();
let bytes = digest.as_slice();
let mut encoded = std::string::String::with_capacity(bytes.len() * 2);
for byte in bytes {
let value = *byte;
encoded.push(char::from(HEX_LOWER[(value >> 4) as usize]));
encoded.push(char::from(HEX_LOWER[(value & 0x0f) as usize]));
}
return encoded;
}
#[cfg(test)]
#[path = "../unit_tests/migration.rs"]
mod tests;

View File

@@ -0,0 +1,467 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 3
const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10;
/// Safe PostgreSQL runtime counters exported only through the narrow backend bridge.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresBackendRuntimeSnapshot {
pool_available: u32,
pool_capacity: u32,
pool_size: u32,
pool_waiting: u32,
}
impl PostgresBackendRuntimeSnapshot {
/// Creates a safe pool-counter projection from already bounded values.
#[must_use]
pub(crate) const fn new(pool_capacity: u32, pool_size: u32, pool_available: u32, pool_waiting: u32) -> Self {
return Self { pool_available, pool_capacity, pool_size, pool_waiting };
}
/// Returns the number of currently available pooled PostgreSQL clients.
#[must_use]
pub const fn pool_available(&self) -> u32 {
return self.pool_available;
}
/// Returns the configured maximum pooled PostgreSQL client count.
#[must_use]
pub const fn pool_capacity(&self) -> u32 {
return self.pool_capacity;
}
/// Returns the current pooled PostgreSQL client count.
#[must_use]
pub const fn pool_size(&self) -> u32 {
return self.pool_size;
}
/// Returns the number of tasks currently waiting for a pooled PostgreSQL client.
#[must_use]
pub const fn pool_waiting(&self) -> u32 {
return self.pool_waiting;
}
}
/// Safe PostgreSQL readiness projection returned to the common Store facade.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostgresBackendHealthSnapshot {
error_kind: std::option::Option<crate::PostgresBackendErrorKind>,
migration_version: std::option::Option<u64>,
pending_migration_count: u32,
ready: bool,
runtime: PostgresBackendRuntimeSnapshot,
}
impl PostgresBackendHealthSnapshot {
/// Creates one successful safe readiness projection.
#[must_use]
pub(crate) const fn ready(runtime: PostgresBackendRuntimeSnapshot, migration_version: u64, pending_migration_count: u32) -> Self {
return Self {
error_kind: std::option::Option::None,
migration_version: std::option::Option::Some(migration_version),
pending_migration_count,
ready: true,
runtime,
};
}
/// Creates one failed safe readiness projection from a classified backend error.
#[must_use]
pub(crate) const fn not_ready(
runtime: PostgresBackendRuntimeSnapshot,
migration_version: std::option::Option<u64>,
pending_migration_count: u32,
error_kind: crate::PostgresBackendErrorKind,
) -> Self {
return Self {
error_kind: std::option::Option::Some(error_kind),
migration_version,
pending_migration_count,
ready: false,
runtime,
};
}
/// Returns the safe backend error classification when readiness could not be proven.
#[must_use]
pub const fn error_kind(&self) -> std::option::Option<crate::PostgresBackendErrorKind> {
return self.error_kind;
}
/// Returns whether the latest bounded PostgreSQL probe proved readiness.
#[must_use]
pub const fn is_ready(&self) -> bool {
return self.ready;
}
/// Returns the migration version observed by the readiness probe when available.
#[must_use]
pub const fn migration_version(&self) -> std::option::Option<u64> {
return self.migration_version;
}
/// Returns the number of embedded migrations newer than the observed applied version.
#[must_use]
pub const fn pending_migration_count(&self) -> u32 {
return self.pending_migration_count;
}
/// Returns the safe PostgreSQL pool counters captured for this probe.
#[must_use]
pub const fn runtime(&self) -> &PostgresBackendRuntimeSnapshot {
return &self.runtime;
}
}
/// TLS mode accepted by the physical PostgreSQL backend bridge.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PostgresBackendTlsMode {
/// Disable TLS for the selected PostgreSQL target.
Disabled,
/// Require TLS with system-root trust and server-identity verification.
VerifyFull,
}
impl PostgresBackendTlsMode {
/// Returns the stable safe TLS mode code used only in diagnostics.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Disabled => "disabled",
Self::VerifyFull => "verify_full",
};
}
}
/// Physical settings consumed only by the PostgreSQL backend crate.
pub struct PostgresBackendSettings {
connect_timeout: std::time::Duration,
connection_uri: std::string::String,
create_timeout: std::time::Duration,
auto_migrate: bool,
max_connections: u32,
migration_lock_timeout: std::time::Duration,
migration_timeout: std::time::Duration,
network: ksp_store_api::RawNetworkId,
recycle_timeout: std::time::Duration,
tls_mode: PostgresBackendTlsMode,
wait_timeout: std::time::Duration,
}
impl PostgresBackendSettings {
/// Creates the physical PostgreSQL settings bridge from already validated facade-owned values.
#[must_use]
pub fn new(
network: ksp_store_api::RawNetworkId,
connection_uri: impl std::convert::Into<std::string::String>,
max_connections: u32,
connect_timeout: std::time::Duration,
wait_timeout: std::time::Duration,
create_timeout: std::time::Duration,
recycle_timeout: std::time::Duration,
tls_mode: PostgresBackendTlsMode,
auto_migrate: bool,
migration_timeout: std::time::Duration,
migration_lock_timeout: std::time::Duration,
) -> Self {
return Self {
auto_migrate,
connect_timeout,
connection_uri: connection_uri.into(),
create_timeout,
max_connections,
migration_lock_timeout,
migration_timeout,
network,
recycle_timeout,
tls_mode,
wait_timeout,
};
}
/// Returns the logical network bound to this one backend instance.
#[must_use]
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
return &self.network;
}
/// Returns the selected safe TLS mode.
#[must_use]
pub const fn tls_mode(&self) -> PostgresBackendTlsMode {
return self.tls_mode;
}
}
impl std::fmt::Debug for PostgresBackendSettings {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("PostgresBackendSettings")
.field("network", &self.network)
.field("connection_uri", &"<redacted>")
.field("auto_migrate", &self.auto_migrate)
.field("max_connections", &self.max_connections)
.field("migration_timeout", &self.migration_timeout)
.field("migration_lock_timeout", &self.migration_lock_timeout)
.field("connect_timeout", &self.connect_timeout)
.field("wait_timeout", &self.wait_timeout)
.field("create_timeout", &self.create_timeout)
.field("recycle_timeout", &self.recycle_timeout)
.field("tls_mode", &self.tls_mode)
.finish();
}
}
/// Opaque physical PostgreSQL backend owning the bounded Deadpool connection pool.
pub struct PostgresBackend {
network: ksp_store_api::RawNetworkId,
pool: deadpool_postgres::Pool,
}
impl PostgresBackend {
/// Parses and normalizes one supplied URI, builds a bounded pool and proves one physical connection before returning readiness.
pub async fn open(settings: PostgresBackendSettings) -> std::result::Result<Self, crate::PostgresBackendError> {
let normalized = normalized_config(&settings);
let pg_config = match normalized {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
network = settings.network().as_str(),
tls_mode = settings.tls_mode().code(),
max_connections = settings.max_connections,
"opening PostgreSQL Store backend pool"
);
let pool_result = match settings.tls_mode {
PostgresBackendTlsMode::Disabled => build_pool(pg_config, tokio_postgres::NoTls, &settings),
PostgresBackendTlsMode::VerifyFull => {
let tls_result = build_verified_tls();
let tls = match tls_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
build_pool(pg_config, tls, &settings)
},
};
let pool = match pool_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let probe = pool.get().await;
let mut client = match probe {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
network = settings.network().as_str(),
tls_mode = settings.tls_mode().code(),
"PostgreSQL Store backend established initial physical connection"
);
let bootstrap_result = crate::bootstrap(&mut client, settings.auto_migrate, settings.migration_timeout, settings.migration_lock_timeout).await;
if let std::result::Result::Err(error) = bootstrap_result {
return std::result::Result::Err(error);
}
drop(client);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
network = settings.network().as_str(),
auto_migrate = settings.auto_migrate,
"PostgreSQL Store migration/bootstrap foundation verified"
);
return std::result::Result::Ok(Self { network: settings.network, pool });
}
/// Returns a safe synchronous snapshot of bounded pool counters without performing PostgreSQL I/O.
#[must_use]
pub fn runtime_snapshot(&self) -> PostgresBackendRuntimeSnapshot {
return runtime_snapshot_from_status(self.pool.status());
}
/// Runs a bounded lightweight PostgreSQL readiness probe and returns only safe classified diagnostics.
pub async fn health(&self) -> PostgresBackendHealthSnapshot {
return crate::probe_health(&self.pool).await;
}
/// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound.
pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> {
self.pool.close();
let drain = async {
loop {
if self.pool.status().size == 0 {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_POLL_INTERVAL_MS)).await;
}
};
let result = tokio::time::timeout(timeout, drain).await;
return match result {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, network = self.network.as_str(), "PostgreSQL Store backend pool closed");
std::result::Result::Ok(())
},
std::result::Result::Err(_) => {
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ShutdownTimeout, "pool_drain"))
},
};
}
}
impl std::fmt::Debug for PostgresBackend {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("PostgresBackend").field("network", &self.network).field("state", &"open").finish();
}
}
impl std::ops::Drop for PostgresBackend {
fn drop(&mut self) {
self.pool.close();
}
}
fn normalized_config(settings: &PostgresBackendSettings) -> std::result::Result<tokio_postgres::Config, crate::PostgresBackendError> {
if settings.connection_uri.is_empty() || settings.connection_uri.len() > MAX_CONNECTION_URI_BYTES {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
}
let parsed = settings.connection_uri.parse::<tokio_postgres::Config>();
let mut config = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
},
};
if config.get_options().is_some() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "server_options"));
}
if config.get_hosts().is_empty() && config.get_hostaddrs().is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "host"));
}
if settings.tls_mode == PostgresBackendTlsMode::VerifyFull {
if config.get_hosts().is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
}
for host in config.get_hosts() {
if !is_tcp_host(host) {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
}
}
}
config.application_name(APPLICATION_NAME);
config.connect_timeout(settings.connect_timeout);
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
match settings.tls_mode {
PostgresBackendTlsMode::Disabled => {
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
},
PostgresBackendTlsMode::VerifyFull => {
config.ssl_mode(tokio_postgres::config::SslMode::Require);
},
}
return std::result::Result::Ok(config);
}
fn is_tcp_host(host: &tokio_postgres::config::Host) -> bool {
return match host {
tokio_postgres::config::Host::Tcp(_) => true,
#[cfg(unix)]
tokio_postgres::config::Host::Unix(_) => false,
};
}
fn build_pool<T>(
pg_config: tokio_postgres::Config,
tls: T,
settings: &PostgresBackendSettings,
) -> std::result::Result<deadpool_postgres::Pool, crate::PostgresBackendError>
where
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
T::Stream: std::marker::Send + std::marker::Sync,
T::TlsConnect: std::marker::Send + std::marker::Sync,
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::Future: std::marker::Send,
{
let manager_config = deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Verified };
let manager = deadpool_postgres::Manager::from_config(pg_config, tls, manager_config);
let built = deadpool_postgres::Pool::builder(manager)
.max_size(settings.max_connections as usize)
.wait_timeout(std::option::Option::Some(settings.wait_timeout))
.create_timeout(std::option::Option::Some(settings.create_timeout))
.recycle_timeout(std::option::Option::Some(settings.recycle_timeout))
.runtime(deadpool_postgres::Runtime::Tokio1)
.build();
return match built {
std::result::Result::Ok(pool) => std::result::Result::Ok(pool),
std::result::Result::Err(_) => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_build")),
};
}
fn build_verified_tls() -> std::result::Result<tokio_postgres_rustls::MakeRustlsConnect, crate::PostgresBackendError> {
let native = rustls_native_certs::load_native_certs();
let native_error_count = native.errors.len();
if native.certs.is_empty() {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, "no system TLS roots available for PostgreSQL verify_full");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
}
let mut roots = rustls::RootCertStore::empty();
let (added, ignored) = roots.add_parsable_certificates(native.certs);
if added == 0 {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, ignored, "system TLS roots could not be admitted for PostgreSQL verify_full");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
}
if native_error_count > 0 || ignored > 0 {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, added, ignored, native_error_count, "loaded PostgreSQL system TLS roots with partial diagnostics");
}
let provider = std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let builder_result = rustls::ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions();
let builder = match builder_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "protocol_versions"));
},
};
let client_config = builder.with_root_certificates(roots).with_no_client_auth();
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
}
/// Maps one Deadpool acquisition error into a redacted backend classification.
pub(crate) fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackendError {
return match error {
deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::PoolTimeout,
match timeout_type {
deadpool_postgres::TimeoutType::Wait => "pool_wait",
deadpool_postgres::TimeoutType::Create => "pool_create",
deadpool_postgres::TimeoutType::Recycle => "pool_recycle",
},
),
deadpool_postgres::PoolError::Backend(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "physical_connect"),
deadpool_postgres::PoolError::Closed => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_closed"),
deadpool_postgres::PoolError::NoRuntimeSpecified => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_runtime"),
deadpool_postgres::PoolError::PostCreateHook(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_post_create"),
};
}
/// Converts Deadpool status into a bounded safe backend runtime projection.
pub(crate) fn runtime_snapshot_from_status(status: deadpool_postgres::Status) -> PostgresBackendRuntimeSnapshot {
return PostgresBackendRuntimeSnapshot::new(
bounded_count(status.max_size),
bounded_count(status.size),
bounded_count(status.available),
bounded_count(status.waiting),
);
}
fn bounded_count(value: usize) -> u32 {
if value > u32::MAX as usize {
return u32::MAX;
}
return value as u32;
}
#[cfg(test)]
#[path = "../unit_tests/runtime.rs"]
mod tests;

View File

@@ -0,0 +1,84 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Dependency and ownership canaries for the physical PostgreSQL Store backend.
#[test]
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
let manifest = include_str!("../Cargo.toml");
for required in
["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "sha2", "tokio-postgres", "tokio-postgres-rustls"]
{
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
}
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
}
let migration = include_str!("../src/migration.rs");
let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql");
assert!(migration.contains("include_str!(\"../migrations/V000__bootstrap.sql\")"));
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
assert!(!migration.contains(forbidden), "business migration implementation leaked into foundation: {forbidden}");
assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation SQL: {forbidden}");
}
return;
}
#[test]
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("mod health;"));
assert!(crate_root.contains("mod migration;"));
assert!(crate_root.contains("mod runtime;"));
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
for forbidden in [
"pub mod ",
"ksp_store_lib",
"ksp_config_lib",
"tokio_postgres::Client",
"tokio_postgres::Row",
"tokio_postgres::Statement",
"deadpool_postgres::Pool;",
] {
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
}
let runtime = include_str!("../src/runtime.rs");
for forbidden in [
"std::env",
"dotenv",
"KSP_",
"KSPB_",
"PGHOST",
"PGPORT",
"PGUSER",
"PGPASSWORD",
".pgpass",
"CREATE TABLE",
"INSERT INTO",
"UPDATE ",
"DELETE FROM",
"SELECT ",
] {
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
}
return;
}
#[test]
fn pre_007_health_probe_remains_foundation_only_and_private_sql() {
let health = include_str!("../src/health.rs");
assert!(health.contains("SELECT 1::BIGINT"));
assert!(health.contains("ksp_store_schema_migrations"));
for forbidden in
["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED", "std::env", "dotenv", "KSP_SECRET_"]
{
assert!(!health.contains(forbidden), "forbidden health ownership/scope content detected: {forbidden}");
}
return;
}

View File

@@ -0,0 +1,220 @@
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Hardening, exact-surface and scope canaries for the physical PostgreSQL Store backend.
const SECRET_CANARY: &str = "KSP-POSTGRES-SECRET-CANARY-PRE009";
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!("PostgreSQL hardening pre-I/O rejection unexpectedly became pending"),
};
}
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid backend hardening network rejected: {error:?}"),
};
}
fn settings(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode) -> ksp_store_postgres_lib::PostgresBackendSettings {
return ksp_store_postgres_lib::PostgresBackendSettings::new(
network(),
connection_uri,
8,
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
tls_mode,
true,
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10),
);
}
fn public_reexport_names(source: &str) -> std::vec::Vec<&str> {
let mut names = std::vec::Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') {
continue;
}
let without_semicolon = trimmed.trim_end_matches(';');
let name = match without_semicolon.rsplit("::").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
names.push(name);
}
names.sort_unstable();
return names;
}
fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
let dependencies_tail = match source.split("[dependencies]").nth(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::vec::Vec::new(),
};
let dependencies = match dependencies_tail.split("[lints]").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::vec::Vec::new(),
};
let mut names = std::vec::Vec::new();
for line in dependencies.lines() {
let content = match line.split('#').next() {
std::option::Option::Some(value) => value.trim(),
std::option::Option::None => continue,
};
if content.is_empty() {
continue;
}
let key = match content.split('=').next() {
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
std::option::Option::None => continue,
};
if !key.is_empty() {
names.push(key);
}
}
names.sort_unstable();
return names;
}
fn assert_pre_io_rejection(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode, expected_phase: &str) {
let settings = settings(connection_uri, tls_mode);
let rendered_settings = std::format!("{settings:?}");
assert!(!rendered_settings.contains(SECRET_CANARY));
assert!(rendered_settings.contains("<redacted>"));
let result = poll_ready(ksp_store_postgres_lib::PostgresBackend::open(settings));
let error = match result {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => panic!("hostile PostgreSQL settings unexpectedly opened"),
};
assert_eq!(error.kind(), ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid);
assert_eq!(error.phase(), expected_phase);
assert!(!std::format!("{error:?}").contains(SECRET_CANARY));
return;
}
#[test]
fn pre_009_backend_modules_exports_and_manifest_dependencies_are_exact() {
let crate_root = include_str!("../src/lib.rs");
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod runtime;"] {
assert!(crate_root.contains(required), "missing PostgreSQL backend module: {required}");
}
assert!(!crate_root.contains("pub mod "));
let actual_exports = public_reexport_names(crate_root);
let mut expected_exports = [
"PostgresBackend",
"PostgresBackendError",
"PostgresBackendErrorKind",
"PostgresBackendHealthSnapshot",
"PostgresBackendRuntimeSnapshot",
"PostgresBackendSettings",
"PostgresBackendTlsMode",
];
expected_exports.sort_unstable();
assert_eq!(actual_exports.as_slice(), expected_exports.as_slice());
assert_eq!(actual_exports.len(), 7);
let manifest = include_str!("../Cargo.toml");
let actual_dependencies = manifest_dependency_names(manifest);
let expected_dependencies = [
"deadpool-postgres",
"ksp-logging-lib",
"ksp-store-api",
"rustls",
"rustls-native-certs",
"sha2",
"tokio",
"tokio-postgres",
"tokio-postgres-rustls",
];
assert_eq!(actual_dependencies.as_slice(), expected_dependencies.as_slice());
return;
}
#[test]
fn pre_009_hostile_uri_matrix_is_rejected_before_io_without_secret_echo() {
let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}");
assert_pre_io_rejection(malformed.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri");
let oversized = std::format!("{}{SECRET_CANARY}", "x".repeat(4_097));
assert_pre_io_rejection(oversized.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri");
let hostaddr_only = std::format!("hostaddr=127.0.0.1 user=operator password={SECRET_CANARY} dbname=ksp");
assert_pre_io_rejection(hostaddr_only.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, "tls_server_identity");
let server_options = std::format!("host=localhost user=operator password={SECRET_CANARY} dbname=ksp options='-c application_name={SECRET_CANARY}'");
assert_pre_io_rejection(server_options.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "server_options");
return;
}
#[test]
fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
let error_source = include_str!("../src/error.rs");
assert!(error_source.contains("kind: PostgresBackendErrorKind"));
assert!(error_source.contains("phase: &'static str"));
for forbidden in ["String", "source:", "message:", "tokio_postgres::Error", "deadpool_postgres::PoolError"] {
assert!(!error_source.contains(forbidden), "backend error type can retain forbidden external material: {forbidden}");
}
let runtime = include_str!("../src/runtime.rs");
assert!(runtime.contains("deadpool_postgres::PoolError::Backend(_)"));
assert!(!runtime.contains("deadpool_postgres::PoolError::Backend(error)"));
for source in [runtime, include_str!("../src/migration.rs"), include_str!("../src/health.rs")] {
for forbidden in ["format!(\"{error", "format!(\"{error:?", "error = ?", "error = %"] {
assert!(!source.contains(forbidden), "backend source renders external error material: {forbidden}");
}
}
return;
}
#[test]
fn pre_009_backend_has_no_env_bypass_or_business_persistence_capability() {
let production = std::format!(
"{}
{}
{}
{}
{}",
include_str!("../src/error.rs"),
include_str!("../src/health.rs"),
include_str!("../src/lib.rs"),
include_str!("../src/migration.rs"),
include_str!("../src/runtime.rs")
);
for forbidden in [
"std::env",
"dotenv",
"KSP_SECRET_",
"KSPB_",
"PGHOST",
"PGPORT",
"PGUSER",
"PGPASSWORD",
".pgpass",
".postgresql/",
"sslrootcert",
"sslcert",
"sslkey",
"ksp_store_lib",
"ksp_config_lib",
"sqlx::",
"impl ksp_store_api::RawTransaction",
"impl ksp_store_api::RawAccount",
] {
assert!(!production.contains(forbidden), "forbidden backend ownership/capability material detected: {forbidden}");
}
let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql");
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation migration: {forbidden}");
}
return;
}

View File

@@ -0,0 +1,416 @@
// file: crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Opt-in real PostgreSQL proof for the Store foundation runtime.
//!
//! The test reads one dedicated PostgreSQL URI from stdin, refuses to start
//! when the KSP migration metadata table already exists, never prints the URI,
//! creates no business table and cleans up only metadata it proved it created.
const LIVE_BOOTSTRAP_SQL: &str = include_str!("../migrations/V000__bootstrap.sql");
const LIVE_BROKEN_CHECKSUM_A: &str = "0000000000000000000000000000000000000000000000000000000000000000";
const LIVE_BROKEN_CHECKSUM_B: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
const LIVE_MAX_URI_BYTES: usize = 4_096;
const LIVE_METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = 'ksp_store_schema_migrations'
AND table_type = 'BASE TABLE'
)"#;
const LIVE_METADATA_DROP_SQL: &str = "DROP TABLE IF EXISTS ksp_store_schema_migrations";
const LIVE_SENTINEL_CHECKSUM_SQL: &str = "SELECT checksum FROM ksp_store_schema_migrations WHERE version = 0";
const LIVE_SENTINEL_INSERT_SQL: &str =
"INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES (0, 'bootstrap', 'pre008_rollback_injected', CURRENT_TIMESTAMP)";
const LIVE_SENTINEL_UPDATE_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = 0";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct LiveFailure {
phase: &'static str,
}
impl LiveFailure {
const fn new(phase: &'static str) -> Self {
return Self { phase };
}
const fn phase(&self) -> &'static str {
return self.phase;
}
}
#[test]
#[ignore = "opt-in real PostgreSQL foundation proof; reads one dedicated URI from stdin"]
fn pre_008_real_postgres_foundation_is_safe_idempotent_concurrent_and_recoverable() {
eprintln!("KSP Store PostgreSQL live proof: reading one dedicated URI from stdin without echoing it from the test.");
let uri_result = read_uri_from_stdin();
let uri = match uri_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PostgreSQL live input rejected at phase {}", error.phase()),
};
let runtime_result = tokio::runtime::Builder::new_current_thread().enable_all().build();
let runtime = match runtime_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => panic!("PostgreSQL live runtime could not be constructed"),
};
let outcome = runtime.block_on(run_live_test(uri.as_str()));
if let std::result::Result::Err(error) = outcome {
panic!("PostgreSQL live foundation proof failed at safe phase {}", error.phase());
}
return;
}
fn read_uri_from_stdin() -> std::result::Result<std::string::String, LiveFailure> {
let mut input = std::string::String::new();
let read_result = std::io::stdin().read_line(&mut input);
match read_result {
std::result::Result::Ok(0) | std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("stdin_read")),
std::result::Result::Ok(_) => {},
}
let uri = input.trim().to_owned();
if uri.is_empty() || uri.len() > LIVE_MAX_URI_BYTES {
return std::result::Result::Err(LiveFailure::new("stdin_uri"));
}
return std::result::Result::Ok(uri);
}
async fn run_live_test(uri: &str) -> std::result::Result<(), LiveFailure> {
let admin_result = connect_admin(uri).await;
let mut admin = match admin_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preexisting_result = metadata_exists(&admin).await;
let preexisting = match preexisting_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if preexisting {
return std::result::Result::Err(LiveFailure::new("metadata_preexisting_refusal"));
}
let major_result = postgres_major(&admin).await;
let major = match major_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if major < 15 {
return std::result::Result::Err(LiveFailure::new("postgres_major_unsupported"));
}
eprintln!("KSP Store PostgreSQL live proof: server major {major}");
let mut owns_metadata = false;
let scenario = run_foundation_scenario(&mut admin, uri, &mut owns_metadata).await;
let cleanup = if owns_metadata { drop_metadata(&admin).await } else { std::result::Result::Ok(()) };
if let std::result::Result::Err(error) = cleanup {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = scenario {
return std::result::Result::Err(error);
}
let remains_result = metadata_exists(&admin).await;
let remains = match remains_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if remains {
return std::result::Result::Err(LiveFailure::new("cleanup_verification"));
}
return std::result::Result::Ok(());
}
async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str, owns_metadata: &mut bool) -> std::result::Result<(), LiveFailure> {
let initial_result = open_backend(uri).await;
let initial = match initial_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let created_result = metadata_exists(admin).await;
let created = match created_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !created {
return std::result::Result::Err(LiveFailure::new("initial_bootstrap_metadata"));
}
*owns_metadata = true;
let initial_health = initial.health().await;
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(0) || initial_health.pending_migration_count() != 0 {
return std::result::Result::Err(LiveFailure::new("initial_health"));
}
let initial_close = close_backend(initial).await;
if let std::result::Result::Err(error) = initial_close {
return std::result::Result::Err(error);
}
let idempotent_result = open_backend(uri).await;
let idempotent = match idempotent_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let idempotent_health = idempotent.health().await;
if !idempotent_health.is_ready()
|| idempotent_health.migration_version() != std::option::Option::Some(0)
|| idempotent_health.pending_migration_count() != 0
{
return std::result::Result::Err(LiveFailure::new("idempotent_health"));
}
let idempotent_close = close_backend(idempotent).await;
if let std::result::Result::Err(error) = idempotent_close {
return std::result::Result::Err(error);
}
let reset_result = drop_metadata(admin).await;
if let std::result::Result::Err(error) = reset_result {
return std::result::Result::Err(error);
}
let concurrent_result = concurrent_bootstrap(uri).await;
if let std::result::Result::Err(error) = concurrent_result {
return std::result::Result::Err(error);
}
let after_concurrent_result = metadata_exists(admin).await;
match after_concurrent_result {
std::result::Result::Ok(true) => {},
std::result::Result::Ok(false) => return std::result::Result::Err(LiveFailure::new("concurrent_bootstrap_metadata")),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let checksum_result = sentinel_checksum(admin).await;
let checksum = match checksum_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let broken_checksum = if checksum == LIVE_BROKEN_CHECKSUM_A { LIVE_BROKEN_CHECKSUM_B } else { LIVE_BROKEN_CHECKSUM_A };
let corrupt_result = set_sentinel_checksum(admin, broken_checksum).await;
if let std::result::Result::Err(error) = corrupt_result {
return std::result::Result::Err(error);
}
let mismatch_settings = match settings(uri) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mismatch = ksp_store_postgres_lib::PostgresBackend::open(mismatch_settings).await;
match mismatch {
std::result::Result::Err(error) if error.kind() == ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {},
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("checksum_mismatch_classification")),
std::result::Result::Ok(backend) => {
let _ = close_backend(backend).await;
return std::result::Result::Err(LiveFailure::new("checksum_mismatch_accepted"));
},
}
let restore_result = set_sentinel_checksum(admin, checksum.as_str()).await;
if let std::result::Result::Err(error) = restore_result {
return std::result::Result::Err(error);
}
let recovered_result = open_backend(uri).await;
let recovered = match recovered_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let recovered_health = recovered.health().await;
if !recovered_health.is_ready() {
return std::result::Result::Err(LiveFailure::new("checksum_recovery_health"));
}
let recovered_close = close_backend(recovered).await;
if let std::result::Result::Err(error) = recovered_close {
return std::result::Result::Err(error);
}
let rollback_reset = drop_metadata(admin).await;
if let std::result::Result::Err(error) = rollback_reset {
return std::result::Result::Err(error);
}
let rollback_result = prove_transaction_rollback(admin).await;
if let std::result::Result::Err(error) = rollback_result {
return std::result::Result::Err(error);
}
let absent_after_rollback = metadata_exists(admin).await;
match absent_after_rollback {
std::result::Result::Ok(false) => {},
std::result::Result::Ok(true) => return std::result::Result::Err(LiveFailure::new("rollback_left_metadata")),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let final_result = open_backend(uri).await;
let final_backend = match final_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_health = final_backend.health().await;
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(0) || final_health.pending_migration_count() != 0 {
return std::result::Result::Err(LiveFailure::new("final_health"));
}
return close_backend(final_backend).await;
}
fn settings(uri: &str) -> std::result::Result<ksp_store_postgres_lib::PostgresBackendSettings, LiveFailure> {
let network_result = ksp_store_api::RawNetworkId::new("devnet");
let network = match network_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("network")),
};
return std::result::Result::Ok(ksp_store_postgres_lib::PostgresBackendSettings::new(
network,
uri,
4,
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
true,
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10),
));
}
async fn open_backend(uri: &str) -> std::result::Result<ksp_store_postgres_lib::PostgresBackend, LiveFailure> {
let settings_result = settings(uri);
let value = match settings_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match ksp_store_postgres_lib::PostgresBackend::open(value).await {
std::result::Result::Ok(backend) => std::result::Result::Ok(backend),
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_open")),
};
}
async fn close_backend(backend: ksp_store_postgres_lib::PostgresBackend) -> std::result::Result<(), LiveFailure> {
return match backend.close(std::time::Duration::from_secs(5)).await {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_close")),
};
}
async fn concurrent_bootstrap(uri: &str) -> std::result::Result<(), LiveFailure> {
let first_settings = match settings(uri) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let second_settings = match settings(uri) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let first = tokio::spawn(async move {
return ksp_store_postgres_lib::PostgresBackend::open(first_settings).await;
});
let second = tokio::spawn(async move {
return ksp_store_postgres_lib::PostgresBackend::open(second_settings).await;
});
let first_joined = first.await;
let second_joined = second.await;
let first_backend = match first_joined {
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
_ => return std::result::Result::Err(LiveFailure::new("concurrent_first")),
};
let second_backend = match second_joined {
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
_ => {
let _ = close_backend(first_backend).await;
return std::result::Result::Err(LiveFailure::new("concurrent_second"));
},
};
let first_close = close_backend(first_backend).await;
let second_close = close_backend(second_backend).await;
if first_close.is_err() || second_close.is_err() {
return std::result::Result::Err(LiveFailure::new("concurrent_close"));
}
return std::result::Result::Ok(());
}
async fn connect_admin(uri: &str) -> std::result::Result<tokio_postgres::Client, LiveFailure> {
let parsed = uri.parse::<tokio_postgres::Config>();
let mut config = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_config")),
};
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
let connected = config.connect(tokio_postgres::NoTls).await;
let (client, connection) = match connected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_connect")),
};
let _connection_task = tokio::spawn(async move {
let _result = connection.await;
return;
});
return std::result::Result::Ok(client);
}
async fn postgres_major(client: &tokio_postgres::Client) -> std::result::Result<u32, LiveFailure> {
let row_result = client.query_one("SHOW server_version_num", &[]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version")),
};
let value_result = row.try_get::<usize, std::string::String>(0);
let value = match value_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_decode")),
};
let parsed = value.parse::<u32>();
let version_num = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_parse")),
};
return std::result::Result::Ok(version_num / 10_000);
}
async fn metadata_exists(client: &tokio_postgres::Client) -> std::result::Result<bool, LiveFailure> {
let row_result = client.query_one(LIVE_METADATA_EXISTS_SQL, &[]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("metadata_probe")),
};
return match row.try_get::<usize, bool>(0) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_probe_decode")),
};
}
async fn drop_metadata(client: &tokio_postgres::Client) -> std::result::Result<(), LiveFailure> {
return match client.batch_execute(LIVE_METADATA_DROP_SQL).await {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_cleanup")),
};
}
async fn sentinel_checksum(client: &tokio_postgres::Client) -> std::result::Result<std::string::String, LiveFailure> {
let row_result = client.query_one(LIVE_SENTINEL_CHECKSUM_SQL, &[]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("sentinel_read")),
};
return match row.try_get::<usize, std::string::String>(0) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("sentinel_decode")),
};
}
async fn set_sentinel_checksum(client: &tokio_postgres::Client, checksum: &str) -> std::result::Result<(), LiveFailure> {
let update = client.execute(LIVE_SENTINEL_UPDATE_SQL, &[&checksum]).await;
return match update {
std::result::Result::Ok(1) => std::result::Result::Ok(()),
_ => std::result::Result::Err(LiveFailure::new("sentinel_update")),
};
}
async fn prove_transaction_rollback(client: &mut tokio_postgres::Client) -> std::result::Result<(), LiveFailure> {
let transaction_result = client.transaction().await;
let transaction = match transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("rollback_begin")),
};
let create = transaction.batch_execute(LIVE_BOOTSTRAP_SQL).await;
if create.is_err() {
return std::result::Result::Err(LiveFailure::new("rollback_create"));
}
let insert = transaction.batch_execute(LIVE_SENTINEL_INSERT_SQL).await;
if insert.is_err() {
return std::result::Result::Err(LiveFailure::new("rollback_insert"));
}
let injected = transaction.batch_execute("SELECT 1 / 0").await;
if injected.is_ok() {
return std::result::Result::Err(LiveFailure::new("rollback_injection_missing"));
}
drop(transaction);
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,60 @@
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Narrow physical bridge canaries consumed by `ksp-store-lib` without exposing driver or pool types.
#[test]
fn pre_005_backend_bridge_is_constructible_without_io() {
let network = match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid backend bridge network rejected: {error:?}"),
};
let settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
network,
"postgresql://operator:secret@localhost/ksp",
8,
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
true,
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10),
);
assert_eq!(settings.network().as_str(), "devnet");
assert_eq!(settings.tls_mode().code(), "verify_full");
let _open = ksp_store_postgres_lib::PostgresBackend::open;
let _close = ksp_store_postgres_lib::PostgresBackend::close;
return;
}
#[test]
fn pre_005_backend_error_projection_is_safe_and_static() {
let kinds = [
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
];
assert_eq!(kinds.len(), 9);
return;
}
#[test]
fn pre_007_backend_health_bridge_exposes_only_safe_snapshot_types() {
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot>>();
let _health = std::mem::size_of::<std::option::Option<ksp_store_postgres_lib::PostgresBackendHealthSnapshot>>();
let _runtime_snapshot = ksp_store_postgres_lib::PostgresBackend::runtime_snapshot;
let _health_probe = ksp_store_postgres_lib::PostgresBackend::health;
return;
}

View File

@@ -0,0 +1,42 @@
// file: crates/ksp-store-postgres-lib/unit_tests/health.rs
// version: 1
#[test]
fn pool_status_projection_is_bounded_and_contains_no_physical_handle() {
let status = deadpool_postgres::Status { max_size: 8, size: 3, available: 2, waiting: 1 };
let snapshot = crate::runtime_snapshot_from_status(status);
assert_eq!(snapshot.pool_capacity(), 8);
assert_eq!(snapshot.pool_size(), 3);
assert_eq!(snapshot.pool_available(), 2);
assert_eq!(snapshot.pool_waiting(), 1);
return;
}
#[test]
fn health_snapshot_distinguishes_ready_and_safe_failure_without_server_text() {
let status = deadpool_postgres::Status { max_size: 8, size: 1, available: 1, waiting: 0 };
let runtime = crate::runtime_snapshot_from_status(status);
let ready = crate::PostgresBackendHealthSnapshot::ready(runtime, 0, 0);
assert!(ready.is_ready());
assert_eq!(ready.migration_version(), std::option::Option::Some(0));
assert_eq!(ready.error_kind(), std::option::Option::None);
let failed =
crate::PostgresBackendHealthSnapshot::not_ready(ready.runtime().clone(), std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
assert!(!failed.is_ready());
assert_eq!(failed.error_kind(), std::option::Option::Some(crate::PostgresBackendErrorKind::HealthFailed));
let rendered = format!("{failed:?}");
for forbidden in ["postgresql://", "SELECT ", "ksp_store_schema_migrations", "password", "server error"] {
assert!(!rendered.contains(forbidden), "unsafe backend health material detected: {forbidden}");
}
return;
}
#[test]
fn migration_health_helpers_keep_unknown_and_pending_counts_safe() {
assert_eq!(super::nonnegative_version(-1), std::option::Option::None);
assert_eq!(super::nonnegative_version(0), std::option::Option::Some(0));
assert_eq!(super::pending_migration_count(-1, 0), 1);
assert_eq!(super::pending_migration_count(0, 0), 0);
assert_eq!(super::pending_migration_count(1, 0), 0);
return;
}

View File

@@ -0,0 +1,50 @@
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
// version: 1
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
}
#[test]
fn bootstrap_migration_is_static_metadata_only_and_checksum_is_stable_sha256() {
assert_eq!(super::BOOTSTRAP_MIGRATION_VERSION, 0);
assert_eq!(super::BOOTSTRAP_MIGRATION_NAME, "bootstrap");
assert!(super::BOOTSTRAP_MIGRATION_SQL.contains("CREATE TABLE ksp_store_schema_migrations"));
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
assert!(!super::BOOTSTRAP_MIGRATION_SQL.contains(forbidden), "business schema leaked into bootstrap SQL: {forbidden}");
}
let checksum = super::bootstrap_checksum();
assert_eq!(checksum.len(), 64);
assert_eq!(checksum, "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
return;
}
#[test]
fn matching_sentinel_history_is_accepted() {
let checksum = super::bootstrap_checksum();
let history = [applied(0, "bootstrap", checksum.as_str())];
assert!(super::validate_history(&history, checksum.as_str()).is_ok());
return;
}
#[test]
fn divergent_or_missing_sentinel_is_terminal_mismatch() {
let checksum = super::bootstrap_checksum();
let wrong_name = [applied(0, "changed", checksum.as_str())];
let wrong_checksum = [applied(0, "bootstrap", "00")];
let missing: [super::AppliedMigration; 0] = [];
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..]] {
let result = super::validate_history(history, checksum.as_str());
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
}
return;
}
#[test]
fn newer_history_is_rejected_without_down_migration() {
let checksum = super::bootstrap_checksum();
let history = [applied(0, "bootstrap", checksum.as_str()), applied(1, "future", "future-checksum")];
let result = super::validate_history(&history, checksum.as_str());
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
return;
}

View File

@@ -0,0 +1,102 @@
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs
// version: 3
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid backend test network rejected: {error:?}"),
};
}
fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> crate::PostgresBackendSettings {
return crate::PostgresBackendSettings::new(
network(),
connection_uri,
8,
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
tls_mode,
true,
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(10),
);
}
#[test]
fn physical_settings_debug_redacts_connection_uri() {
let secret = "postgresql://secret-user:secret-password@localhost/ksp";
let value = settings(secret, crate::PostgresBackendTlsMode::VerifyFull);
let rendered = format!("{value:?}");
assert!(!rendered.contains("secret-user"));
assert!(!rendered.contains("secret-password"));
assert!(rendered.contains("<redacted>"));
return;
}
#[test]
fn malformed_or_oversized_uri_is_rejected_without_retaining_input() {
let malformed = "not-a-postgresql-uri-secret-canary";
let malformed_result = super::normalized_config(&settings(malformed, crate::PostgresBackendTlsMode::Disabled));
let malformed_error = malformed_result.err();
assert_eq!(malformed_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
assert!(!format!("{malformed_error:?}").contains("secret-canary"));
let oversized = "x".repeat(super::MAX_CONNECTION_URI_BYTES + 1);
let oversized_result = super::normalized_config(&settings(oversized.as_str(), crate::PostgresBackendTlsMode::Disabled));
assert_eq!(oversized_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
return;
}
#[test]
fn typed_disabled_policy_overrides_uri_tls_and_connection_controls() {
let value = settings(
"postgresql://operator:secret@localhost/ksp?sslmode=require&application_name=hostile&connect_timeout=1&sslnegotiation=direct",
crate::PostgresBackendTlsMode::Disabled,
);
let config = match super::normalized_config(&value) {
std::result::Result::Ok(config) => config,
std::result::Result::Err(error) => panic!("valid disabled config rejected: {error:?}"),
};
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Disable);
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(&std::time::Duration::from_secs(10)));
return;
}
#[test]
fn typed_verify_full_policy_forces_tls_and_rejects_hostaddr_only_identity() {
let value = settings(
"postgresql://operator:secret@localhost/ksp?sslmode=disable&application_name=hostile&connect_timeout=1",
crate::PostgresBackendTlsMode::VerifyFull,
);
let config = match super::normalized_config(&value) {
std::result::Result::Ok(config) => config,
std::result::Result::Err(error) => panic!("valid verify_full config rejected: {error:?}"),
};
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Require);
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(&std::time::Duration::from_secs(10)));
let hostaddr_only = settings("hostaddr=127.0.0.1 user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
let rejected = super::normalized_config(&hostaddr_only);
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
#[cfg(unix)]
{
let unix_socket = settings("host=/var/run/postgresql user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
let unix_rejected = super::normalized_config(&unix_socket);
assert_eq!(unix_rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
}
return;
}
#[test]
fn libpq_server_options_are_rejected_in_foundation_runtime() {
let value = settings("host=localhost user=operator dbname=ksp options='-c statement_timeout=0'", crate::PostgresBackendTlsMode::Disabled);
let rejected = super::normalized_config(&value);
let error = rejected.err();
assert_eq!(error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options"));
return;
}

365
deltas/0.3.2/pre.001.md Normal file
View File

@@ -0,0 +1,365 @@
<!-- file: deltas/0.3.2/pre.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.001` — audit Store/PostgreSQL, runtime foundation et réconciliation du graphe
## 1. Base requise
Base directe attendue :
```text
v0.3.1
workspace.package.version = 0.3.1
```
Sources obligatoires réellement disponibles :
```text
khadhroony-solana-project-v0.3.1.zip
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
```
La metadata Git n'est pas incluse dans l'archive KSP ; le tag `v0.3.1` ne peut donc pas être interrogé localement. La version Cargo, `deltas/0.3.1/rel.001.md`, le prompt 021, la présence de `ksp-store-api` et l'absence des deux crates runtime concordent avec la base stable requise.
Commit attendu :
```text
v0.3.2-pre.001
```
Archive overlay attendue :
```text
ksp-general-0.3.2-pre.001.zip
```
## 2. Objectif
Ouvrir `0.3.2` uniquement par le gate prévu :
```text
lecture règles + architecture + 0.3.1
réaudit kbot3 physique ciblé
réaudit PostgreSQL/tokio-postgres/pool/TLS/migrations
brainstorming runtime/backend
threat model
dependency graph exact
Config std.store candidate
integration test strategy
sizing + prévision souple
réconciliation des règles Store contradictoires découvertes
```
Aucune crate runtime, connexion, pool, TLS, migration ou Config Store n'est implémenté dans ce delta.
## 3. Divergence stable découverte
La base stable avait déjà adopté le backend séparé dans le plan `0.3.1` et les architectures 003/004/005, mais conservait l'ancien modèle dans :
```text
KSP-API-006
DEP-STORE-002
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
```
Le prompt 021 demande en plus de relire `DEP-STORE-001..010`, alors que la base ne définissait que `001..008`.
La réconciliation livrée fixe :
```text
ksp-store-lib = façade/runtime commune
ksp-store-postgres-lib = backend physique officiel
backend -> ksp-store-api
backend -X-> ksp-store-lib
consumers ordinaires -> ksp-store-lib
```
`DEP-STORE-009` et `DEP-STORE-010` sont ajoutées pour rendre la frontière normative explicite.
## 4. Décisions du gate
### PostgreSQL/driver
```text
PostgreSQL minimal supporté : 15
PostgreSQL de référence : 18.6
driver : tokio-postgres 0.7.18
```
### Pool
```text
retenu : deadpool-postgres 0.14.x
rejeté : pool KSP maison sans besoin démontré
rejeté : bb8-postgres pour cette fondation, ownership de connection task moins adapté au close KSP
```
### TLS
```text
tokio-postgres-rustls 0.14.x
rustls 0.23
aws-lc-rs
native system roots
modes KSP : Disabled / VerifyFull uniquement
```
### Migrations
```text
moteur KSP privé
SQL embarqué
history table ksp_store_schema_migrations
sentinel version 0
SHA-256
advisory transaction lock borné
run transactionnel
aucun down automatique
aucune table RAW métier
```
`refinery 0.9.2` a été audité mais n'est pas retenu.
### Config
```text
std.store V1 possédé par ksp-config-lib
KSP_SECRET_STORE_POSTGRES_URI
settings typés
aucun serde_json::Value backend_options
aucun env/PG*/.pgpass lu par Store/backend
```
### Health
Un health/readiness portable minimal est retenu pour distinguer Store construit, prêt et fermé sans exposer pool/URI/SQL.
## 5. Graphe cible
```text
ksp-store-lib
├── ksp-store-api
├── ksp-logging-lib
└── [postgres] ksp-store-postgres-lib
ksp-store-postgres-lib
├── ksp-store-api
├── ksp-logging-lib
├── tokio
├── tokio-postgres
├── deadpool-postgres
├── tokio-postgres-rustls
├── rustls
└── sha2
```
Feature :
```text
default = postgres
--no-default-features doit compiler
Postgres connu sans feature -> STORE_BACKEND_NOT_COMPILED avant I/O
```
## 6. Héritage kbot3 ciblé
Surfaces relues : ancien `ks-store`, PostgreSQL, migration resources, health, Config et docs Store.
Résumé :
```text
REPRENDRE façade intentionnelle, pool/timeout bornés, advisory lock, health, SQL privé
REDESSINER crates séparées, tokio-postgres, settings typés, TLS, checksum/history, close, errors
REPORTER 16 tables N1-N3, 240 SQL métier, 79 index, repositories/replay/ledgers
REJETER SQLx, monolithe façade/backend, backend_options JSON, env direct, erreurs backend brutes
```
Le détail est dans le plan 023.
## 7. Threat model
Le plan couvre explicitement :
```text
credential leak URI/Debug/error/log
PG*/.pgpass bypass Config
connection string hostile
connection storm/unbounded pool
hung connect/migration/shutdown
concurrent migration runners
modified migration/checksum
partial migration
SQL/dynamic identifier injection
feature/config mismatch
connection task leaked/dropped
schema history newer than runtime
server error echo
```
## 8. PostgreSQL integration strategy
Le test `#[ignore]` de `pre.008` lira une URI dédiée depuis stdin, refusera une metadata table préexistante, ne créera aucune table métier et ne détruira ni base ni schema.
Il prouvera :
```text
connect >= PostgreSQL 15
bootstrap initial
idempotence
concurrence
checksum mismatch
rollback d'un échec injecté
health ready
close borné
cleanup metadata créée par le test
```
La cible de gate est PostgreSQL 18.6.
## 9. Sizing recalibré
La prévision reste :
```text
pre.001 audit/design/réconciliation normative
pre.002 scaffold + feature graph
pre.003 settings + selection/lifecycle contracts
pre.004 Config std.store
pre.005 connection + deadpool + Rustls
pre.006 migration/bootstrap
pre.007 composition + health/close
pre.008 PostgreSQL integration réelle
pre.009 hardening/completeness/dependency matrix
pre.010 gate technique final
pre.011 réconciliation documentaire finale
pre.012 préparation publication minimale
rel.001 publication stable
```
La release reste clôturable sans absorber `RawTransaction` ou `RawAccountState`.
## 10. Fichiers ajoutés
```text
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
deltas/0.3.2/pre.001.md
```
## 11. Fichiers modifiés
```text
Cargo.toml
docs/rules/RULES_KSP.md
docs/rules/RULES_DEPENDENCIES.md
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
```
## 12. Fichiers supprimés
```text
aucun
```
## 13. Version Cargo
La prerelease non-fix synchronise :
```text
workspace.package.version = 0.3.2-pre.1
```
Aucune crate runtime n'est encore ajoutée ; le changement versionne le gate `pre.001`.
## 14. Baseline opérateur `v0.3.1`
Le journal opérateur fourni à l'ouverture montre notamment :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 186 tables / 138 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
tests ciblés dont ksp-store-api PASS
cargo test --workspace PASS
builds Tauri SOL Prices/Wallet/Config Desk PASS
cargo tree -p ksp-store-api --edges normal ksp-store-api -> ksp-core-lib
cargo tree --duplicates exécuté
```
Cette preuve de base ne remplace pas le gate après application de `pre.001`.
## 15. Validations après application
Dans lenvironnement de génération du présent overlay, les contrôles suivants ont été réellement exécutés après modification :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
Markdown table audit: clean (186 tables, 124 files)
```
`cargo` nest pas installé dans lenvironnement de génération. Les commandes suivantes nont donc pas été rejouées ici et ne sont pas déclarées PASS :
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
```
Aucun code Rust nest modifié par `pre.001`; le seul changement Cargo est `workspace.package.version`. Le baseline opérateur `v0.3.1` reste vert, mais ne remplace pas le gate opérateur après application.
## 16. Validations non requises dans cette tranche
```text
cargo test -p ksp-store-lib crate encore absente
cargo test -p ksp-store-postgres-lib crate encore absente
PostgreSQL live réservé à pre.008
pool/TLS connection tests réservés à pre.005+
migration runtime tests réservés à pre.006+
```
## 17. Questions ouvertes
Aucune question architecturale ne bloque `pre.002`.
Les détails d'implémentation volontairement réservés aux tranches dédiées sont :
```text
noms Rust exacts des settings/errors
mapping deadpool exact des timeouts
construction rustls root store exacte
DDL final de history metadata
forme finale health snapshots
```
Ils ne rouvrent pas :
```text
tokio-postgres
deadpool-postgres
Rustls VerifyFull/Disabled
migrations KSP-owned
backend crate séparée
Config ownership
absence de RAW métier en 0.3.2
```
## 18. Application et validation opérateur
Après application de l'overlay :
```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
```
Aucun scaffold Store, SQL, migration runtime ou Config `std.store` ne doit être ajouté à ce delta.

View File

@@ -0,0 +1,122 @@
<!-- file: deltas/0.3.2/pre.002-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.002-fix.001` — ancrage compile-time des targets de tracing
## 1. Déclencheur
Le gate opérateur de `pre.002` est fonctionnellement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 186 tables / 125 files
cargo check --workspace PASS avec warnings
cargo clippy --workspace --all-targets PASS avec warnings
cargo test -p ksp-store-api 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
cargo tree Store facade/backend fourni et conforme
```
L'écart est limité aux deux nouveaux scaffolds :
```text
unused import: self::constants::TRACING_TARGET
constant TRACING_TARGET is never used
```
Le même défaut existe dans `ksp-store-lib` et `ksp-store-postgres-lib`.
## 2. Diagnostic
`pre.002` ne contient encore aucun comportement Store/PostgreSQL et n'ajoute volontairement pas `ksp-logging-lib`. Le simple réexport crate-private du `TRACING_TARGET` obligatoire ne possède donc aucun site d'usage réel à cette tranche, ce qui produit les warnings observés.
Supprimer les targets n'est pas possible : l'audit workspace impose déjà :
```text
KSP-TRACE-102 — constants.rs possède le TRACING_TARGET canonique
KSP-TRACE-103 — le crate root réexporte TRACING_TARGET
```
Ajouter un faux log, assouplir les règles ou poser un `allow(dead_code/unused_imports)` masquerait le staging réel.
## 3. Correction
Les deux targets et leurs réexports sont conservés à l'identique. Chaque crate ajoute uniquement un canari compile-time privé :
```rust
const _: &str = TRACING_TARGET;
```
Ce canari :
```text
marque le réexport et la constante comme réellement consommés par le scaffold compilé
ne crée aucun comportement runtime
n'émet aucun log artificiel
n'ajoute pas ksp-logging-lib
n'assouplit aucun lint
préserve KSP-TRACE-102/103
```
Les tests de frontière figent aussi la présence de ce canari pour empêcher le retour des warnings tant que le premier comportement réel n'a pas remplacé cet usage de scaffold.
## 4. Version
Le correctif touche des sources Rust ; conformément à `VER-ID-007` et `VER-ID-010`, la version workspace devient :
```text
0.3.2-pre.2.fix.1
```
Identifiant de livraison :
```text
0.3.2-pre.002-fix.001
```
## 5. Documentation durable
Le plan 023 et la validation 019 sont synchronisés :
```text
graphe pre.002 confirmé par le gate opérateur
--no-default-features confirmé
cycle backend absent confirmé
warning TRACING_TARGET documenté puis corrigé par fix.001
TRACING_TARGET conservé et ancré compile-time sans faux log
```
Le delta `pre.002.md` n'est pas réécrit : une livraison déjà émise reste immuable et son correctif est tracé séparément.
## 6. Hors scope
Aucun changement de périmètre fonctionnel :
```text
pas de StoreSettings
pas de Store runtime public
pas de Config std.store
pas de ksp-logging-lib
pas de Tokio/tokio-postgres/Deadpool/Rustls
pas de connexion PostgreSQL
pas de migration
pas de SQL métier
```
## 7. Validation opérateur requise
```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-lib
cargo test -p ksp-store-postgres-lib
cargo check -p ksp-store-lib --no-default-features
```
Le passage à `pre.003` reste interdit si un warning ou un nouvel écart apparaît.

244
deltas/0.3.2/pre.002.md Normal file
View File

@@ -0,0 +1,244 @@
<!-- file: deltas/0.3.2/pre.002.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.002` — scaffold Store runtime/PostgreSQL et feature graph
## 1. Base requise
```text
0.3.2-pre.001
```
Le gate opérateur de `pre.001` est fourni vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 186 tables / 124 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
```
## 2. Objectif
Créer uniquement les deux crates runtime prévues par le plan `0.3.2` et matérialiser le feature graph PostgreSQL sans avancer les settings, la Config Store, la connexion, le pool, TLS, les migrations ou les vertical slices RAW.
Crates créées :
```text
ksp-store-lib
ksp-store-postgres-lib
```
## 3. Version
Le workspace passe à :
```text
0.3.2-pre.2
```
Les deux crates sont ajoutées aux membres du workspace.
## 4. Graphe matérialisé
```text
ksp-store-lib
├── ksp-store-api
└── [default feature postgres]
└── ksp-store-postgres-lib
└── ksp-store-api
```
Invariants :
```text
ksp-store-postgres-lib -X-> ksp-store-lib
ksp-store-lib -X-> Config/Transport/Program/Materializer
ksp-store-postgres-lib -X-> Config/Transport/Program/Materializer
```
Feature exacte :
```toml
[features]
default = ["postgres"]
postgres = ["dep:ksp-store-postgres-lib"]
```
`ksp-store-postgres-lib` reste une optional dependency de la façade afin que :
```text
cargo check -p ksp-store-lib --no-default-features
```
puisse compiler sans backend PostgreSQL.
## 5. Dépendances volontairement différées
`pre.002` n'ajoute pas encore :
```text
ksp-logging-lib
tokio
tokio-postgres
deadpool-postgres
tokio-postgres-rustls
rustls
sha2
```
Ce n'est pas une remise en cause du graphe cible du plan 023. Aucun comportement de production ne consomme encore ces crates dans le scaffold ; les ajouter maintenant violerait le contrat selon lequel une dépendance de package correspond à un usage réel.
Le séquencement reste :
```text
ksp-logging-lib façade lors du premier comportement Store réel
ksp-logging-lib backend lors du premier comportement PostgreSQL réel
tokio/driver/pool/TLS pre.005
sha2 pre.006
```
Les deux crates possèdent cependant dès maintenant leur `src/constants.rs` et le `TRACING_TARGET` crate-owned attendu, sans émission de log artificielle.
## 6. Surface Rust
`ksp-store-lib` reste un scaffold sans API runtime publique. Il ne matérialise pas encore :
```text
Store
StoreSettings
StoreBackendSettings
PostgresStoreSettings
StoreBackendKind
errors Store runtime
reexports ksp-store-api
```
Ces éléments restent réservés à `pre.003`.
`ksp-store-postgres-lib` n'expose encore aucun type backend public et ne contient aucun driver, pool, SQL, migration ou health runtime.
Aucun `pub mod` n'est introduit.
## 7. Canaris ajoutés
### `ksp-store-lib`
`tests/dependency_boundary.rs` vérifie :
```text
default feature postgres exact
optional backend dependency exacte
façade -> ksp-store-api
absence Config/Transport/Program/Materializer
absence driver/pool/TLS prématurés
absence de settings/Store/backend types publics en pre.002
TRACING_TARGET possédé par constants.rs
```
### `ksp-store-postgres-lib`
`tests/dependency_boundary.rs` vérifie :
```text
backend -> ksp-store-api
absence backend -> ksp-store-lib
absence Config/Transport/Program/Materializer
absence Tokio/tokio-postgres/Deadpool/Rustls/SHA-256
absence SQL/migration/runtime surface prématurée
TRACING_TARGET possédé par constants.rs
```
## 8. Documentation mise à jour
```text
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
```
Le plan marque le scaffold comme matérialisé et précise le staging des dépendances runtime. La validation conserve les critères Cargo en `TODO` tant que le gate opérateur de cette tranche n'a pas été fourni.
## 9. Fichiers ajoutés
```text
crates/ksp-store-lib/Cargo.toml
crates/ksp-store-lib/src/constants.rs
crates/ksp-store-lib/src/lib.rs
crates/ksp-store-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/Cargo.toml
crates/ksp-store-postgres-lib/src/constants.rs
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
deltas/0.3.2/pre.002.md
```
## 10. Fichiers modifiés
```text
Cargo.toml
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
```
## 11. Fichiers supprimés
```text
aucun
```
## 12. Validations exécutées dans l'environnement de génération
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
Markdown table audit: clean (186 tables, 125 files)
```
Une validation statique complémentaire des manifests/features confirme également le graphe exact attendu et les deux canaris de frontière ont été rejoués par équivalent textuel dans lenvironnement sans Cargo.
## 13. Validations opérateur requises
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Après application :
```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-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
```
Une commande non exécutée n'est pas déclarée PASS.
## 14. Hors scope confirmé
```text
StoreSettings / backend selection runtime
STORE_BACKEND_NOT_COMPILED
Config std.store
URI/secrets Store
connexion PostgreSQL
pool Deadpool
TLS Rustls
migrations/bootstrap
health/readiness runtime
SQL métier
RawTransaction PostgreSQL
RawAccountState PostgreSQL
```
## 15. Suite
`0.3.2-pre.003` matérialise les settings backend-neutral, l'identité/sélection de backend, les erreurs stables de façade et le lifecycle contract `Store` sans encore ouvrir la connexion PostgreSQL lourde.

View File

@@ -0,0 +1,143 @@
<!-- file: deltas/0.3.2/pre.003-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.003-fix.001` — conformité Clippy et canari de frontière Store
## 1. Base requise
Base exacte :
```text
0.3.2-pre.003
workspace.package.version = 0.3.2-pre.3
```
Le gate opérateur fourni le 29 août 2026 confirme :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 186 tables / 127 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets FAIL — 5 implicit_return observés dans les tests Store
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib FAIL — faux positif dependency_boundary sur PostgresPoolSettings
cargo test -p ksp-store-lib --no-default-features FAIL — même faux positif dependency_boundary
cargo test -p ksp-store-postgres-lib PASS
cargo check -p ksp-store-lib --no-default-features PASS
```
Aucun défaut de production/runtime n'est observé : les 6 tests unitaires Store passent dans les deux configurations de feature. Le correctif reste strictement dans le couloir `pre.003`.
## 2. Objet
Corriger exclusivement :
```text
les closures de tests non conformes à clippy::implicit_return
le canari dependency_boundary trop large qui interdit le mot générique Pool
```
Aucune surface API, borne, erreur, lifecycle, feature, dépendance ou comportement Store n'est modifié.
## 3. Clippy `implicit_return`
Les closures de projection de code d'erreur utilisent désormais la forme explicite requise par `RUST-ERR-003` :
```rust
|value| return value.code()
```
La correction couvre toutes les occurrences de la tranche, y compris les branches compilées uniquement avec `--no-default-features`.
## 4. Canari de frontière physique
Le test `pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports` interdisait le token nu :
```text
Pool
```
Cette condition est incorrecte car `PostgresPoolSettings` est une surface de configuration backend-neutral explicitement prévue par `pre.003`. Elle provoquait donc un faux positif tout en ne prouvant pas mieux l'absence de fuite physique.
Le canari interdit désormais les chemins/types physiques ciblés :
```text
pub use ksp_store_postgres_lib
tokio_postgres
deadpool_postgres
rustls::
deadpool::managed::Pool
tokio_postgres::Client
tokio_postgres::Row
tokio_postgres::Statement
```
Les protections existantes sur le manifeste et les production sources restent inchangées.
## 5. Version Cargo
Le correctif modifie des fichiers Rust de tests consommés par le build. Conformément à `VER-ID-007` et `VER-ID-010` :
```text
workspace.package.version = 0.3.2-pre.3.fix.1
```
Livraison :
```text
0.3.2-pre.003-fix.001
```
## 6. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-lib/tests/dependency_boundary.rs
crates/ksp-store-lib/tests/feature_mismatch.rs
crates/ksp-store-lib/unit_tests/settings.rs
crates/ksp-store-lib/unit_tests/store.rs
```
Chaque fichier Rust réellement modifié incrémente son header de version une seule fois. Aucun autre fichier source n'est touché.
## 7. Fichier ajouté
```text
deltas/0.3.2/pre.003-fix.001.md
```
## 8. Fichiers supprimés
```text
aucun
```
## 9. Validations exécutées dans l'environnement de génération
```text
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`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Aucun gate Cargo n'est donc déclaré PASS ici.
## 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-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
```
## 11. Décisions et questions ouvertes
Décision : un canari de fuite backend doit cibler des symboles/chemins physiques, pas interdire des mots génériques faisant partie de settings publics légitimes.
Question ouverte : aucune. Si le gate est vert, la suite reste `0.3.2-pre.004` — Config `std.store`.

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

@@ -0,0 +1,77 @@
<!-- file: deltas/0.3.2/pre.004-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.004-fix.001` — Store targets réseau-spécifiques + gate fixes
## 1. Base
Base exacte : `0.3.2-pre.004`. Le gate opérateur du 29 août 2026 confirme `cargo check`, les tests Config/Store principaux et les trois builds Tauri, mais révèle :
```text
1 warning Clippy collapsible_if dans ksp-config-lib/unit_tests/store.rs
1 échec Config Desk : fixture cfg.std.store absente
```
La discussion de design menée pendant ce gate confirme aussi qu'une instance `Store` ne doit jamais multiplexer plusieurs réseaux.
## 2. Décision target/réseau
Le mécanisme de profiles Config existant devient directement le mécanisme de targets Store nommés :
```text
profile_id = target Store nommé
default_profile = target autonome
profile forcé = target explicitement imposé par job/worker/app
1 target = 1 network + 1 backend + 1 URI/base
1 StoreSettings = 1 RawNetworkId + 1 backend
```
`ksp-store-lib` ne contient aucun registry ni routeur multi-target. Pour changer de réseau, le consumer résout un autre profile `std.store` puis ouvre une autre instance Store.
## 3. Targets committed
```text
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI -> ksp_devnet
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI -> ksp_mainnet
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI -> ksp_testnet
```
`default_profile = devnet`, cohérent avec le défaut Transport et plus sûr pour les workflows de développement. Le schema reste extensible à d'autres targets/réseaux sûrs.
## 4. Deuxième protection réseau
`StoreSettings` exige désormais un `RawNetworkId`. Les futures opérations RAW devront rejeter avant I/O toute identité/query dont le réseau ne correspond pas au réseau de l'instance Store. Cette vérification ne route jamais automatiquement vers un autre target.
## 5. Correctifs de gate
- collapse du `if let` imbriqué signalé par Clippy ;
- ajout de `crates/ksp-config-lib/unit_tests/fixtures/std.store.json` ;
- Config Desk vérifie désormais explicitement que `cfg.std.store` appartient à l'inventaire profilé ;
- tests Config ajoutés pour la sélection explicite et indépendante de `devnet`, `mainnet` et `testnet`.
## 6. Version
```text
workspace.package.version = 0.3.2-pre.4.fix.1
```
## 7. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
cargo test -p ksp-app-config-desk
```
Les trois builds Tauri `pre.004` sont déjà verts et les resources packagées ne changent pas dans ce fix.
## 8. Suite
Après gate propre, `pre.005` peut ouvrir la connexion PostgreSQL physique. Elle consommera un seul target/réseau déjà résolu ; aucun multiplexage réseau n'est à introduire.

108
deltas/0.3.2/pre.004.md Normal file
View File

@@ -0,0 +1,108 @@
<!-- file: deltas/0.3.2/pre.004.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.004` — Config `std.store`
## 1. Base requise
Base exacte :
```text
0.3.2-pre.003-fix.001
workspace.package.version = 0.3.2-pre.3.fix.1
```
Le gate opérateur fourni le 29 août 2026 est entièrement vert : audits Rust/Markdown, `cargo check`, Clippy, tests Store avec et sans features par défaut, backend et compilation `--no-default-features` passent.
## 2. Objet
Matérialiser exclusivement la frontière Config de la fondation Store :
```text
config/std.store.json
config/schemas/std.store.schema.json
config/examples/std.store.example.json
registry cfg.std.store / schema.std.store
KSP_SECRET_STORE_POSTGRES_URI
adapter Config -> ksp_store_lib::StoreSettings
provenance/sensitivity/redaction
resources desktop strictement nécessaires
```
Aucune connexion, pool physique, TLS connector, migration ou SQL n'est ajouté.
## 3. Graphe de dépendance
`ksp-config-lib` ajoute :
```text
ksp-config-lib -> ksp-store-lib (default-features = false)
```
Config peut ainsi construire les types publics de settings sans forcer `postgres`. `ksp-store-lib` et `ksp-store-postgres-lib` restent interdits de dépendance vers Config.
## 4. Document V1
Le profil `postgres_default` sélectionne `backend = postgres` et fournit exactement les groupes `connection_uri`, `pool`, `tls`, `bootstrap` et `shutdown_timeout_ms` décidés en `pre.001/pre.003`. Les bornes JSON Schema reflètent les bornes runtime Store ; la validation runtime reste l'autorité finale.
Le `connection_uri` committed utilise :
```text
${KSP_SECRET_STORE_POSTGRES_URI:-postgresql://localhost/ksp}
```
Le fallback reste classé `Secret`, donc la safe projection et `Debug` ne rendent jamais l'URI. L'adapter rejette aussi un URI literal ou provenant d'un namespace non secret, même si le JSON Schema l'accepte syntaxiquement.
## 5. Adapter
`ResolvedStoreConfig` possède la Config effective et un `StoreSettings`. Il expose un emprunt sûr et `into_settings(self)` pour transférer les settings au runtime. Aucun getter de l'URI n'est ajouté.
Mapping :
```text
postgres -> StoreBackendSettings::Postgres
disabled -> PostgresTlsMode::Disabled
verify_full -> PostgresTlsMode::VerifyFull
ms -> Duration
```
Les erreurs du contrat Store sont projetées par domaine/code seulement, sans recopier de message ou de contexte arbitraire.
## 6. Packaging
`prepare_packaged_runtime` parcourt tout le registry Config. Les trois applications desktop qui utilisent cette préparation doivent donc embarquer les deux nouvelles resources runtime/schema. Leur inventaire passe de 11 à 13 resources. Aucun écran Store n'est ajouté.
## 7. Réconciliation du plan
Le prompt de démarrage et les conventions Config utilisent `std.store.schema.json` / `std.store.example.json`. Deux mentions `store.schema.json` / `store.example.json` du plan `pre.001` étaient incohérentes ; elles sont corrigées au moment de la matérialisation, sans changement de contrat.
## 8. Version
```text
workspace.package.version = 0.3.2-pre.4
```
## 9. 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-config-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
cargo test -p ksp-app-config-desk
cargo test -p ksp-app-solprices-desk
cargo test -p ksp-app-wallet-desk
cargo tree -p ksp-config-lib --edges normal
cargo tree -p ksp-store-lib -e features
```
Les builds Tauri sont recommandés dans cette tranche car les resources packagées ont réellement changé.
## 10. Suite
Si le gate est vert, `pre.005` ouvre la connexion PostgreSQL réelle, Deadpool et Rustls.

View File

@@ -0,0 +1,54 @@
<!-- file: deltas/0.3.2/pre.005-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.005-fix.001` — correction des assertions `connect_timeout`
## 1. Base
Base exacte : `0.3.2-pre.005` telle qu'appliquée et formatée par l'opérateur.
Le gate du 29 août 2026 confirme :
```text
audits Rust/Markdown propres
cargo check --workspace propre
production ksp-store-postgres-lib compilable
graphes Store/PostgreSQL conformes
tests ksp-store-lib avec et sans feature PostgreSQL propres
```
La seule erreur bloquante est dans deux assertions unitaires de `ksp-store-postgres-lib` : `tokio_postgres::Config::get_connect_timeout()` retourne `Option<&Duration>`, tandis que le test comparait à `Option<Duration>`.
## 2. Correctif
Les deux assertions de `unit_tests/runtime.rs` comparent désormais la référence retournée par l'API :
```rust
std::option::Option::Some(&std::time::Duration::from_secs(10))
```
Aucun comportement production, réglage, dépendance, policy TLS, pool ou lifecycle n'est modifié.
## 3. Version
```text
workspace.package.version = 0.3.2-pre.5.fix.1
```
## 4. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
```
## 5. Suite
Après gate propre, `pre.006` peut introduire le moteur de migrations PostgreSQL KSP sans rouvrir la fondation connexion/pool/TLS.

135
deltas/0.3.2/pre.005.md Normal file
View File

@@ -0,0 +1,135 @@
<!-- file: deltas/0.3.2/pre.005.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.005` — PostgreSQL connection + pool + TLS
## 1. Base
Base exacte : `0.3.2-pre.004-fix.001`, incluant l'ajustement opérateur de `.env.example` version `13`.
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests Config, ownership, Store avec et sans feature PostgreSQL, compilation `--no-default-features` et Config Desk passent.
## 2. Objet
Matérialiser la première ouverture PostgreSQL physique sans ouvrir encore les migrations ou le SQL métier :
```text
tokio-postgres 0.7.18+
deadpool-postgres 0.14.x
tokio-postgres-rustls 0.14.x
rustls 0.23.x / AWS-LC
roots système
pool borné
Store::open physique
Store::close borné
```
## 3. Frontière façade/backend
`ksp-store-lib` reste l'unique surface consumer et ne dépend directement d'aucun driver/pool/TLS. Il convertit ses settings vers un bridge `ksp-store-postgres-lib`, puis mappe les erreurs backend vers des `ErrorCode` Store stables.
`ksp-store-postgres-lib` possède seul :
```text
parse/normalisation tokio_postgres::Config
Deadpool Manager/Pool
connector Rustls
roots système
première acquisition physique
fermeture/drain physique
```
Les types physiques `Pool`, `Client`, `Row` et `Statement` ne sont jamais réexportés par la façade.
## 4. Normalisation de l'URI
Le backend consomme uniquement l'URI déjà résolue par Config. Après parsing, KSP impose :
```text
application_name = ksp-store
connect_timeout = settings typés
sslnegotiation = postgres
Disabled -> sslmode=disable
VerifyFull -> sslmode=require
```
`options=` est rejeté dans cette fondation. `VerifyFull` exige un host TCP afin de disposer d'une identité serveur à vérifier : `hostaddr` seul et les sockets Unix sont rejetés. Les erreurs de parsing ne conservent jamais le texte de l'URI.
## 5. Pool et lifecycle
Le pool applique les limites validées en `pre.003` : taille maximale et deadlines `wait/create/recycle`. Le manager utilise `RecyclingMethod::Verified`; la requête de vérification éventuelle reste interne à Deadpool et aucun SQL KSP n'est ajouté.
La création du pool est lazy, donc `Store::open` exécute explicitement une première `pool.get().await`. Le succès signifie ainsi qu'une connexion physique/auth/TLS a réellement été établie.
`Store::close(self)` appelle `Pool::close()` puis attend un drain `size == 0` sous `shutdown_timeout`. `Drop` ne fait qu'un close best-effort.
## 6. TLS
Modes exacts :
```text
Disabled
VerifyFull
```
`VerifyFull` charge les roots système dans `rustls::RootCertStore`, utilise explicitement le provider AWS-LC et conserve la vérification standard de certificat + identité serveur. Aucun mode `Prefer`, aucun verifier permissif et aucun fallback plaintext ne sont introduits.
## 7. Erreurs sûres
Le backend retourne uniquement une classification et une phase statique :
```text
ConfigInvalid
ConnectFailed
PoolTimeout
ShutdownTimeout
TlsFailed
```
La façade mappe vers :
```text
store.postgres_config_invalid
store.postgres_connect_failed
store.postgres_pool_timeout
store.postgres_tls_failed
store.shutdown_timeout
```
Aucune erreur `tokio-postgres`, Deadpool, Rustls ou native-certs n'est attachée comme source publique.
## 8. Hors scope
```text
migrations/bootstrap SQL
ksp_store_schema_migrations
health snapshot public
RAW transaction/account persistence
routing multi-target/multi-réseau dans Store
live PostgreSQL integration test
```
Ces éléments restent respectivement aux tranches `pre.006`, `pre.007`, `0.3.3/0.3.4` et `pre.008`.
## 9. Version
```text
workspace.package.version = 0.3.2-pre.5
```
## 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-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree -p ksp-store-lib --edges normal
cargo tree --duplicates
```

143
deltas/0.3.2/pre.006.md Normal file
View File

@@ -0,0 +1,143 @@
<!-- file: deltas/0.3.2/pre.006.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.006` — migration/bootstrap PostgreSQL foundation
## 1. Base
Base exacte : `0.3.2-pre.005-fix.001`.
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Les versions physiques réellement résolues observées sont `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2` et `tokio-postgres-rustls 0.14.0`.
## 2. Objet
Matérialiser le moteur privé de migration/bootstrap de la fondation PostgreSQL, sans aucune persistence métier :
```text
ksp_store_schema_migrations uniquement
sentinel version 0
nom bootstrap immuable
SHA-256 du SQL exact
advisory transaction lock borné
statement timeout transaction-local
transaction unique
validation metadata/history
newer-runtime guard
rollback du run courant
```
## 3. Dépendance SHA-256
`sha2 ^0.11` devient une dépendance workspace explicite et une dépendance directe de `ksp-store-postgres-lib`, car le backend calcule lui-même le checksum persistant.
Le digest SHA-256 est encodé explicitement en 64 caractères hex minuscules ; aucune dépendance de codec hex supplémentaire n'est ajoutée.
## 4. Migration bootstrap embarquée
La seule ressource SQL de production ajoutée est :
```text
crates/ksp-store-postgres-lib/migrations/V000__bootstrap.sql
```
Elle crée uniquement :
```text
ksp_store_schema_migrations
```
avec les champs minimaux :
```text
version BIGINT PRIMARY KEY
name TEXT NOT NULL
checksum TEXT NOT NULL
applied_at TIMESTAMPTZ NOT NULL
```
Aucune table/index RAW, CORE, DECODE ou SPECIALIZED n'est introduit.
## 5. Algorithme runtime
Après la première acquisition physique déjà introduite en `pre.005`, le backend conserve ce client dédié et exécute :
```text
begin transaction
pg_try_advisory_xact_lock($1) en polling borné
set_config('statement_timeout', $1, true)
probe metadata
si absente et auto_migrate=false -> migration_pending sans DDL
si absente et auto_migrate=true -> V000 + sentinel dans la même transaction
si présente -> vérification forme minimale + PK version
load history ordered
missing/divergence -> migration mismatch
version > 0 -> schema newer
commit unique
```
L'ensemble du bootstrap est aussi borné par `migration_timeout`. Une sortie avant `commit()` droppe la transaction ; le rollback PostgreSQL implicite protège DDL et history du run courant.
Les colonnes obligatoires de metadata sont vérifiées sans interdire des colonnes supplémentaires futures. Cela permet à un ancien runtime de lire une history plus récente et de produire `schema_newer` plutôt qu'un faux mismatch de forme.
## 6. Erreurs façade
Le bridge backend ajoute :
```text
MigrationFailed
MigrationMismatch
SchemaNewer
```
La façade mappe vers :
```text
store.postgres_migration_failed
store.postgres_migration_mismatch
store.postgres_schema_newer
```
Aucun texte serveur, SQL, URI ou credential n'est propagé.
## 7. Tests/canaris
Les tests unitaires sans serveur couvrent :
```text
checksum exact/stable du V000
absence de schéma métier dans le SQL
sentinel valide
sentinel missing
nom/checksum divergent
history plus récente
bridge backend étendu
frontière dépendances + sha2 direct
```
La concurrence réelle, l'idempotence réelle et le rollback injecté restent au test PostgreSQL opt-in de `pre.008`.
## 8. Version
```text
workspace.package.version = 0.3.2-pre.6
```
## 9. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree -p ksp-store-lib --edges normal
```
## 10. Suite
Après gate propre, `pre.007` ferme la composition end-to-end et ajoute la projection health/readiness sûre, sans ouvrir encore les tables RAW métier.

165
deltas/0.3.2/pre.007.md Normal file
View File

@@ -0,0 +1,165 @@
<!-- file: deltas/0.3.2/pre.007.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.007` — composition runtime et health/readiness portable
## 1. Base
Base exacte : `0.3.2-pre.006`.
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes directs passent.
## 2. Objet
Fermer la composition runtime de la fondation PostgreSQL sans persistence métier :
```text
StoreSettings
-> Store::open
-> PostgresBackend
-> connexion/pool/TLS
-> bootstrap/history
-> Store prêt
Store::runtime_snapshot()
-> projection synchrone sans I/O
Store::health().await
-> readiness PostgreSQL légère et bornée
-> projection portable/redacted
```
## 3. Surface façade
`ksp-store-lib` ajoute :
```text
StoreHealthState::{Ready, NotReady}
StoreRuntimeSnapshot
StoreHealthSnapshot
Store::runtime_snapshot()
Store::health().await
store.postgres_health_failed
```
`StoreRuntimeSnapshot` expose uniquement :
```text
backend kind
network logique
pool capacity
pool size
pool available
pool waiting
```
`StoreHealthSnapshot` ajoute :
```text
state
migration version optionnelle
pending migration count
last safe ErrorCode optionnel
```
La façade atteint 84 exports crate-root : 60 réexports `ksp-store-api` et 24 éléments runtime Store.
## 4. Bridge backend
`ksp-store-postgres-lib` ajoute uniquement les projections safe nécessaires au bridge :
```text
PostgresBackendRuntimeSnapshot
PostgresBackendHealthSnapshot
```
Aucun `Pool`, `Client`, `Row`, `Statement`, URI, host, user, database, SQL ou texte serveur n'est exposé par ces snapshots.
Les compteurs Deadpool `max_size/size/available/waiting` sont saturés en `u32` avant de traverser le bridge.
## 5. Probe health
Le probe PostgreSQL reste privé au backend et exécute uniquement :
```text
acquisition Deadpool bornée
SELECT 1
lecture de MAX(version) dans la metadata privée
```
La deadline globale réutilise le `wait_timeout` configuré du pool ; un fallback interne de 5 secondes n'est utilisé que si ce timeout n'est pas présent dans le pool, état qui ne doit pas être produit par la construction KSP normale.
Classification :
```text
probe/query/decode failure -> HealthFailed
version absente/inférieure -> MigrationMismatch
version supérieure -> SchemaNewer
version attendue -> Ready
```
La vérification complète nom/checksum reste celle de `Store::open`/bootstrap. Le health est volontairement un diagnostic léger, pas un second moteur de migration.
## 6. Sémantique readiness
`Store::open` ne change pas de sémantique : une instance n'est rendue qu'après connexion physique et bootstrap/history validés.
Le health sert ensuite à observer l'état courant : une panne transitoire produit `NotReady` avec un code KSP sûr, pas une erreur contenant du texte PostgreSQL.
Une instance reste attachée à exactement un réseau/target Store ; aucun routage ou multiplexage multi-réseaux n'est ajouté.
## 7. Scope négatif
Toujours absent de `0.3.2-pre.007` :
```text
RawTransaction PostgreSQL
RawAccountState PostgreSQL
repository RAW
SQL métier
CORE/DECODE/SPECIALIZED
health HTTP/service global
scheduler/polling de health
```
## 8. Tests/canaris
Les tests sans serveur couvrent :
```text
projection pool safe/bornée
Ready vs NotReady
migration version/pending safe
absence d'URI/SQL/credential dans les snapshots
surface façade health crate-root
bridge backend health crate-root
frontières dépendances et absence de SQL métier
```
La preuve réelle `Ready`, panne/mismatch et close reste au smoke PostgreSQL opt-in de `pre.008`.
## 9. Version
```text
workspace.package.version = 0.3.2-pre.7
```
## 10. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree -p ksp-store-lib --edges normal
```
## 11. Suite
Après gate propre, `pre.008` ajoute le test PostgreSQL réel opt-in non destructif couvrant bootstrap initial/idempotent/concurrent, mismatch, rollback/failure, health Ready et close borné.

134
deltas/0.3.2/pre.008.md Normal file
View File

@@ -0,0 +1,134 @@
<!-- file: deltas/0.3.2/pre.008.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.008` — PostgreSQL integration réelle opt-in
## Base exacte
Cette tranche s'applique sur `0.3.2-pre.007`, dont le gate opérateur du 2026-08-29 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, check `--no-default-features` et graphes Cargo.
## Objet
Matérialiser le premier test PostgreSQL réel de la fondation `0.3.2` sans ajouter de comportement de production et sans créer de table métier RAW.
## Changements
### Test live isolé
Nouveau fichier :
```text
crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
```
Le test est `#[ignore]` et :
```text
lit une URI PostgreSQL dédiée uniquement depuis stdin
borne l'entrée à 4096 octets
ne lit aucune variable d'environnement
ne loggue/imprime jamais l'URI
refuse PostgreSQL < 15
n'imprime que le major PostgreSQL
refuse de démarrer si ksp_store_schema_migrations existe déjà
```
Le scénario réel couvre :
```text
connexion PostgreSQL
bootstrap initial
health Ready version 0 / pending 0
close explicite borné
réouverture idempotente
reset contrôlé de la metadata possédée par le test
deux bootstrap concurrents
checksum sentinel corrompu -> MigrationMismatch
restore checksum -> recovery Ready
rollback physique du V000 exact après erreur SQL injectée
re-bootstrap après rollback
cleanup final de la seule table metadata possédée par le test
```
### Rollback sans hook de production
Aucun hook `cfg(test)`, feature de test ou API publique supplémentaire n'est ajouté à `ksp-store-postgres-lib`.
Le test inclut le même `migrations/V000__bootstrap.sql`, l'exécute dans une transaction PostgreSQL de test, ajoute un sentinel transitoire, provoque une erreur SQL contrôlée puis droppe la transaction. `tokio-postgres 0.7.18` rollback implicitement une `Transaction` droppée. Le test prouve ensuite l'absence physique de la metadata avant un nouveau bootstrap normal.
### Non-destructivité
Le test ne contient aucun :
```text
DROP DATABASE
DROP SCHEMA
CREATE DATABASE
CREATE SCHEMA
DDL RawTransaction
DDL RawAccountState
SQL CORE/DECODE/SPECIALIZED
```
Les `DROP TABLE` éventuels ciblent uniquement `ksp_store_schema_migrations`, après que le test a prouvé que cette relation n'existait pas avant son démarrage et qu'il en possède donc le cleanup.
## Documentation de suivi
Le plan et la matrice validation sont mis à jour pour :
```text
enregistrer le gate opérateur pre.007 vert
figer la stratégie live réelle de pre.008
remplacer l'idée d'un hook cfg(test) par une injection transactionnelle externe au runtime
marquer les critères live comme matérialisés mais non encore exécutés
ajouter la commande opérateur stdin sans environnement
```
## Hors scope confirmé
Aucun changement de production dans :
```text
ksp-store-lib
ksp-store-postgres-lib/src
ksp-store-api
Config
migrations committed
TLS/pool/runtime
```
Toujours aucun `RawTransaction` ou `RawAccountState` PostgreSQL.
## Validation locale disponible
La génération doit exécuter les audits Python et Markdown. Cargo n'est pas disponible dans l'environnement de génération ; le test live doit donc d'abord être compilé par le gate opérateur puis exécuté explicitement sur un PostgreSQL dédié.
## Gate opérateur déterministe
```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-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo check -p ksp-store-lib --no-default-features
```
## Gate PostgreSQL réel opt-in
Sur une database PostgreSQL dédiée dont `ksp_store_schema_migrations` est absente :
```bash
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
unset KSP_PG_TEST_URI
```
La variable shell n'est pas exportée ; le test lit uniquement stdin.
## Suite
Si les gates déterministes et live sont verts : `0.3.2-pre.009` — hardening, completeness et dependency matrix.

157
deltas/0.3.2/pre.009.md Normal file
View File

@@ -0,0 +1,157 @@
<!-- file: deltas/0.3.2/pre.009.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.009` — hardening, completeness et dependency matrix
## Base exacte
Cette tranche s'applique sur `0.3.2-pre.008`.
Le 29 août 2026, le gate `pre.008` est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Le gate PostgreSQL réel opt-in passe également sur **PostgreSQL 17** avec bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup contrôlé.
## Objet
Durcir et figer la fondation Store/PostgreSQL avant le gate technique final sans modifier le code de production, les migrations ou les dépendances runtime.
## Canari façade
Nouveau fichier :
```text
crates/ksp-store-lib/tests/hardening_completeness.rs
```
Il vérifie :
```text
5 modules privés exacts
84 exports crate-root exacts
manifest runtime exact
feature postgres par défaut exacte
backend PostgreSQL toujours optionnel
aucune dépendance physique PostgreSQL dans la façade
canary secret redacted dans StoreSettings/Debug
URI malformed rejetée avant I/O
code ConfigInvalid avec postgres / BackendNotCompiled sans feature
aucun canary dans Display/Debug Error
aucune lecture Config/env/PG*/.pgpass
aucun Pool/Client/Row/Statement/SQL physique dans la façade
```
Le même test est conçu pour passer sous le feature set par défaut et sous `--no-default-features`.
## Canari backend PostgreSQL
Nouveau fichier :
```text
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
```
Il vérifie :
```text
5 modules privés exacts
7 exports bridge exacts
9 dépendances physiques exactes du manifest
URI malformed rejetée avant I/O
URI > 4096 octets rejetée avant I/O
hostaddr sans host rejeté sous VerifyFull
options= libpq rejeté
canary password/URI absent de Debug/error backend
PostgresBackendError limité structurellement à kind + phase statique
PoolError::Backend(_) abandonne explicitement l'erreur remote
aucun rendu d'erreur externe dans runtime/migration/health
aucune lecture env/KSP/KSPB/PG*/.pgpass
aucun sslrootcert/sslcert/sslkey/.postgresql implicite
aucune capability PostgreSQL RawTransaction/RawAccount
V000 reste strictement metadata-only
```
Les cas hostiles sont tous forcés avant le premier `.await` physique et n'exigent donc aucun PostgreSQL réel.
## Non-régression Store API
`ksp-store-api` n'est pas modifié. Le gate `pre.009` rejoue explicitement ses tests, notamment `tests/external_backend.rs`, afin de confirmer qu'un backend externe peut toujours implémenter les dix capabilities RAW sans dépendre de `ksp-store-lib` ou de PostgreSQL.
## Documentation de suivi
Le plan et la matrice validation sont réconciliés avec :
```text
gates pre.006/pre.007 verts
pre.008 live vert sur PostgreSQL 17
scope exact des canaris pre.009
statuts hardening matérialisés mais non encore exécutés
commande de dependency matrix ciblée
```
## Fichiers ajoutés
```text
crates/ksp-store-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
deltas/0.3.2/pre.009.md
```
## Fichiers modifiés
```text
Cargo.toml
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
```
## Fichiers supprimés
Aucun.
## Version
```text
workspace.package.version = 0.3.2-pre.9
```
Aucun `src/**`, migration, Config, README/USAGE, CHANGELOG ou ROADMAP n'est modifié dans cette tranche.
## Validations exécutées dans l'environnement de génération
```text
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
contrôle statique de la version et du payload
contrôle statique des exports/manifests attendus
contrôle des chaînes interdites no-env/no-business
```
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération.
## Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo test -p ksp-config-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
cargo tree --duplicates
```
Le smoke PostgreSQL réel n'a pas besoin d'être rejoué dans `pre.009` si aucun code runtime/migration n'a changé ; il sera rejoué au gate technique final `pre.010`.
## Questions ouvertes
Aucune question fonctionnelle nouvelle. Tout échec des nouveaux canaris doit produire un correctif `pre.009-fix.NNN` avant `pre.010`.
## Suite
Après gate vert : `0.3.2-pre.010` — gate technique final sans nouveau développement fonctionnel.

125
deltas/0.3.2/pre.010.md Normal file
View File

@@ -0,0 +1,125 @@
<!-- file: deltas/0.3.2/pre.010.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.010` — gate technique final Store/PostgreSQL
## Base exacte
Cette tranche s'applique sur `0.3.2-pre.009`.
Le gate opérateur `pre.009` du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, `ksp-store-api`, backend PostgreSQL, façade avec et sans feature PostgreSQL, Config, compilation `--no-default-features`, graphes Cargo et `cargo test --workspace` passent.
Le smoke PostgreSQL réel de `pre.008` avait déjà passé sur PostgreSQL 17 ; `pre.010` le rejoue pour la preuve finale.
## Objectif
Ouvrir exclusivement le gate technique final de `0.3.2` sans nouveau développement fonctionnel.
Cette tranche ne modifie aucun :
```text
src/**
test fonctionnel
migration SQL
Config runtime
manifest de crate
dépendance runtime
README/USAGE
CHANGELOG/ROADMAP
prompt suivant
```
## Gate final attendu
```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-postgres-lib
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
cargo test --workspace
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
cargo tree --duplicates
```
Le gate PostgreSQL réel est ensuite rejoué sur une database dédiée où `ksp_store_schema_migrations` est absente avant démarrage :
```bash
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
unset KSP_PG_TEST_URI
```
Comme `pre.004` a réellement ajouté les resources `std.store` aux trois applications desktop, le gate technique final rejoue aussi :
```bash
(cd crates/ksp-app-config-desk && cargo tauri build)
(cd crates/ksp-app-solprices-desk && cargo tauri build)
(cd crates/ksp-app-wallet-desk && cargo tauri build)
```
## Fichiers ajoutés
```text
deltas/0.3.2/pre.010.md
```
## Fichiers modifiés
```text
Cargo.toml
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
```
## Fichiers supprimés
Aucun.
## Version
```text
workspace.package.version = 0.3.2-pre.10
```
## Décisions
```text
pre.010 = preuve technique finale uniquement
aucun développement fonctionnel nouveau
PostgreSQL live doit être rejoué
cargo test --workspace est obligatoire
les trois builds Tauri sont rejoués car le packaging std.store a changé en pre.004
un échec ouvre pre.010-fix.NNN
pre.011 reste la réconciliation documentaire finale
```
## Validations exécutées dans l'environnement de génération
```text
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
contrôle statique version/payload
```
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération ; aucun gate Cargo n'est déclaré PASS ici.
## Validations non exécutées
Toutes les commandes Cargo et le smoke PostgreSQL réel restent à exécuter par l'opérateur après application de l'overlay.
## Questions ouvertes
Aucune. Tout défaut découvert par le gate final doit être corrigé par `pre.010-fix.NNN` avant `pre.011`.
## Suite
Après gate final entièrement vert : `0.3.2-pre.011` — réconciliation documentaire finale, README/USAGE des deux crates Store et fermeture de la matrice de validation.

268
deltas/0.3.2/pre.011.md Normal file
View File

@@ -0,0 +1,268 @@
<!-- file: deltas/0.3.2/pre.011.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.011` — réconciliation documentaire finale Store/PostgreSQL
## 1. Base requise
Base directe attendue :
```text
0.3.2-pre.010
```
Commit attendu :
```text
v0.3.2-pre.011
```
Archive overlay attendue :
```text
ksp-general-0.3.2-pre.011.zip
```
Cette tranche suit le gate technique final `pre.010` et n'ajoute aucun comportement runtime.
## 2. Résultat du gate technique `pre.010`
Le gate opérateur `0.3.2-pre.010` a été rejoué après `cargo clean` et fournit les preuves suivantes :
```text
audits Rust PASS
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
tests ciblés de toutes les crates PASS
ksp-store-lib default + --no-default-features PASS
cargo test --workspace PASS
graphes Cargo normal/features/duplicates exécutés
3 builds Tauri Linux PASS
PostgreSQL live foundation PASS — major 17
```
La ligne d'audit Markdown du journal opérateur `pre.010` a utilisé par erreur `deltas/0.3.1` au lieu de `deltas/0.3.2`. L'overlay `pre.010` avait été audité dans l'environnement de génération avec le chemin correct ; `pre.011` rejoue explicitement le scope complet `deltas/0.3.2`. Cette erreur de scope de commande ne constitue pas un défaut runtime et n'ouvre pas un `pre.010-fix`.
## 3. Objectif
Appliquer `VER-LIFECYCLE-006` et figer les documents durables de la fondation Store/PostgreSQL avant la préparation de publication minimale :
```text
README/USAGE ksp-store-lib
README/USAGE ksp-store-postgres-lib
README/USAGE ksp-config-lib pour std.store
index docs global
index plans
index validations
plan 023
validation 019
```
Cette tranche ne finalise pas `CHANGELOG.md`, `ROADMAP.md` ni le prompt `0.3.3`.
## 4. Documentation de `ksp-store-lib`
Le nouveau README fixe durablement :
```text
façade runtime backend-neutral
feature postgres par défaut
1 Store = 1 RawNetworkId + 1 backend physique
aucun multiplexage automatique de bases/réseaux
Config propriétaire des targets et secrets
aucun type PostgreSQL physique réexporté
aucune capability RAW PostgreSQL encore implémentée
```
Le nouveau USAGE documente :
```text
dépendance consumer normale uniquement vers ksp-store-lib
construction programmatique de StoreSettings
Store::open / runtime_snapshot / health / close
résolution recommandée via ksp-config-lib
settings pool/bootstrap/TLS
surface health sûre
limite fonctionnelle de la fondation
```
## 5. Documentation de `ksp-store-postgres-lib`
Le README et le USAGE fixent la crate comme backend physique, non comme seconde façade :
```text
consumer ordinaire -X-> ksp-store-postgres-lib
bridge façade/tests seulement
pool Deadpool borné
TLS Disabled / VerifyFull via Rustls
roots système + AWS-LC
moteur de migrations KSP privé
V000 bootstrap + ksp_store_schema_migrations
history version/name/SHA-256 + advisory transaction lock
health/runtime snapshots sûrs
PostgresBackendError = kind + phase statique
aucune table/repository/capability RAW métier
```
La politique durable reste PostgreSQL >= 15 pour la validation de fondation ; le major effectivement exercé reste enregistré dans la matrice et non figé comme dépendance runtime maximale.
## 6. Réconciliation Config
`ksp-config-lib/README.md` et `USAGE.md` documentent désormais :
```text
cfg.std.store / schema.std.store
Config -> Store avec default-features = false
sélection de target nommée
RawNetworkId explicite
provenance Secret obligatoire de l'URI PostgreSQL
variables KSP_SECRET_STORE_DEVNET_POSTGRES_URI
KSP_SECRET_STORE_MAINNET_POSTGRES_URI
KSP_SECRET_STORE_TESTNET_POSTGRES_URI
targets committed devnet/mainnet/testnet
bases PostgreSQL indépendantes par target
absence de routage multi-target dans Store
```
Aucune lecture d'environnement n'est déplacée vers Store/backend.
## 7. Indexes et documents de release
Les indexes durables référencent maintenant le plan `023` et la validation `019` ainsi que la documentation des deux crates runtime.
Le plan `023` enregistre le résultat réel de `pre.010`, la correction de scope Markdown à rejouer et la matérialisation documentaire de `pre.011`.
La matrice `019` est réconciliée avec les preuves réelles de `pre.002` à `pre.010`, y compris :
```text
ksp-store-api non régressé
pool/TLS/bootstrap/health/close
hardening/security
feature mismatch
no-env
PostgreSQL live major 17 en pre.008 et pre.010
3 builds Tauri au gate final
aucun TODO technique applicable à 0.3.2
```
Le seul statut restant avant fermeture de `pre.011` est son propre gate documentaire opérateur.
## 8. Documents relus et volontairement inchangés
Les architectures suivantes ont été relues sur la surface finale `pre.010` :
```text
docs/architecture/003-COMPONENT_CONTRACTS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/architecture/005-DEPENDENCY_GRAPH.md
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
```
Elles sont déjà cohérentes avec le split façade/backend et ne sont pas modifiées artificiellement.
Le choix documentaire de `0.3.1-pre.009` de ne pas créer un README/USAGE propre à `ksp-store-api` n'est pas rouvert par cette release runtime/backend.
## 9. Hors scope préservé
Aucun élément suivant n'est introduit :
```text
RawTransaction PostgreSQL persistence/query/retention
RawAccountState PostgreSQL persistence/query/retention
SQL métier RAW
repository métier
batch/backlog/priority worker/job
Store Desk
nouvelle migration
nouvelle dépendance
nouveau test fonctionnel
modification src/**
```
## 10. Fichiers ajoutés
```text
crates/ksp-store-lib/README.md
crates/ksp-store-lib/USAGE.md
crates/ksp-store-postgres-lib/README.md
crates/ksp-store-postgres-lib/USAGE.md
deltas/0.3.2/pre.011.md
```
## 11. Fichiers modifiés
```text
Cargo.toml
crates/ksp-config-lib/README.md
crates/ksp-config-lib/USAGE.md
docs/000-README.md
docs/plans/000-README.md
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/000-README.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
```
## 12. Fichiers supprimés
```text
aucun
```
## 13. Version Cargo
La prerelease non-fix synchronise :
```text
workspace.package.version = 0.3.2-pre.11
```
Aucune dépendance, feature ou configuration Cargo n'est modifiée en dehors de cette version.
## 14. Validation de génération
L'environnement de génération ne fournit pas `cargo`/`rustc`; aucun résultat Cargo local n'est donc déclaré PASS par ce delta.
Les audits Python applicables ont été rejoués après matérialisation de l'overlay, avec le chemin Markdown exact `deltas/0.3.2` :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (186 table(s), 142 file(s))
```
Cette preuve corrige explicitement le défaut de scope de la commande Markdown observé dans le journal opérateur `pre.010`.
## 15. Gate opérateur après application
```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 test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Le smoke PostgreSQL réel, les graphes Cargo complets, `cargo test --workspace` et les trois builds Tauri ne sont pas rejoués par nécessité dans cette tranche documentaire : ils sont déjà le gate technique vert de `pre.010` et `pre.011` ne modifie aucun code, dépendance, config runtime, resource Tauri ou migration.
## 16. Suite
Si le gate documentaire `pre.011` est vert, la seule prerelease restante est `pre.012`, strictement limitée à la préparation de publication :
```text
Cargo.toml
CHANGELOG.md
ROADMAP.md
prompts/022-V0_3_3_START_PROMPT.md
deltas/0.3.2/pre.012.md
```
Aucun README, USAGE, plan, validation, architecture, code, test, schema ou config ne devra être rouvert dans `pre.012`.

320
deltas/0.3.2/pre.012.md Normal file
View File

@@ -0,0 +1,320 @@
<!-- file: deltas/0.3.2/pre.012.md -->
<!-- version: 1 -->
# Delta `0.3.2-pre.012` — préparation de publication et prompt `0.3.3`
## 1. Base requise
Base directe attendue :
```text
0.3.2-pre.011
workspace.package.version = 0.3.2-pre.11
```
Le gate opérateur de `pre.011`, exécuté le **30 août 2026**, est entièrement vert :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
python3 scripts/audit_markdown_tables.py PASS / clean — 186 tables / 142 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-lib --no-default-features PASS
cargo test -p ksp-store-postgres-lib PASS
cargo test -p ksp-config-lib PASS — 126 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
Le gate technique complet de référence de `pre.010` reste également acquis : il a été rejoué après `cargo clean`, passe `cargo test --workspace`, les graphes Cargo, les trois builds Tauri Linux et le live PostgreSQL sur **major 17**.
## 2. Objectif
Dernière prerelease avant `rel.001`, strictement limitée à la préparation de publication minimale définie par `VER-LIFECYCLE-003` :
```text
bump workspace vers 0.3.2-pre.12
entrée stable candidate 0.3.2 dans CHANGELOG.md
fermeture 0.3.2 et précision de 0.3.3 dans ROADMAP.md
création du prompt 0.3.3
delta pre.012
```
Aucun code, test, manifest de crate, README/USAGE, plan, validation, architecture, règle, config, schema ou migration n'est rouvert.
## 3. Version Cargo
```text
0.3.2-pre.11
-> 0.3.2-pre.12
```
Le header `Cargo.toml` passe de la version fichier `345` à `346` parce que le fichier est réellement modifié.
## 4. Changelog `0.3.2`
`CHANGELOG.md` enregistre la fondation stable candidate réellement livrée :
```text
ksp-store-lib + ksp-store-postgres-lib
feature postgres par défaut
Store lié à exactement un RawNetworkId
Config std.store avec targets devnet/mainnet/testnet et URI Secret séparées
tokio-postgres + Deadpool + Rustls
TLS Disabled / VerifyFull
pool/connect/wait/create/recycle/shutdown bornés
V000 bootstrap + ksp_store_schema_migrations
history version/name/SHA-256
advisory transaction lock
health/readiness portable et redacted
84 exports crate-root façade au gate hardening
aucune capability/table RawTransaction ou RawAccountState métier
```
Il enregistre également le gate technique complet, le live PostgreSQL major 17 et la policy supportée PostgreSQL >= 15.
## 5. Roadmap
`ROADMAP.md` :
```text
passe 0.3.2 à [X]
remplace les formulations prospectives par la fondation réellement livrée
conserve 0.3.3 ouverte comme vertical slice RawTransaction complète
précise que 0.3.3 couvre les six capabilities transaction/observation/rétention
conserve RawAccountState + complétude RAW pour 0.3.4
```
La séparation durable reste :
```text
Store = persistence/navigation
workers/jobs/executors = batch-size, priorité, backlog et policy
```
## 6. Prompt `0.3.3`
Le nouveau prompt :
```text
prompts/022-V0_3_3_START_PROMPT.md
```
est confronté à :
```text
docs/rules/PROMPT_STRUCTURE.md
docs/rules/VERSION_WORKFLOW.md
docs/rules/FILE_CONTRACTS.md
docs/rules/RULES_KSP.md
docs/rules/RULES_DEPENDENCIES.md
docs/architecture/003-COMPONENT_CONTRACTS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/architecture/005-DEPENDENCY_GRAPH.md
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
docs/plans/022-V0_3_1_STORE_RAW_PLAN.md
docs/validation/018-V0_3_1_STORE_RAW.md
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
crates/ksp-store-api/src/**
crates/ksp-store-lib/src/**
crates/ksp-store-postgres-lib/src/**
```
Il impose une première tranche `pre.001` d'audit/design avant tout SQL métier lourd.
## 7. Archive historique requise pour la session suivante
L'archive :
```text
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
```
reste **obligatoire au `0.3.3-pre.001`**, mais sa portée est réduite à l'ancien RAW transaction PostgreSQL :
```text
raw transaction table
transaction observations
indexes/constraints
raw queries
raw transaction repository
contrats DTO/entity/repository pertinents
```
Elle sert à produire une matrice :
```text
REPRENDRE / REDESSINER / REPORTER / REJETER
```
Elle n'est ni une base de code ni une autorité sur `ksp-store-api` ou l'architecture KSP actuelle.
## 8. Scope strict du prompt suivant
`0.3.3` doit implémenter les six capabilities :
```text
RawTransactionRead
RawTransactionWrite
RawTransactionObservationRead
RawTransactionObservationWrite
RawTransactionRetentionRead
RawTransactionRetentionWrite
```
sur le même couple Store/PostgreSQL, avec notamment :
```text
migration métier à partir de V001
atomicité canonical + observation
idempotence identical
ERROR_CODE_RAW_CONFLICT pour contenu divergent
get/list cursorisé
network mismatch avant I/O
retention/tombstone/ForceRehydrate
concurrence/rollback
PostgreSQL live gate
```
Le prompt force `pre.001` à fermer plusieurs questions physiques importantes avant code lourd :
```text
binding de la database au RawNetworkId
représentation PostgreSQL des u64/u32 sans narrowing
ordre total + format du cursor
concurrence/idempotence
rétention physique honnête pour Full/Compacted/Archived/Purged
error mapping SQL sûr
```
Sont explicitement réservés :
```text
0.3.4 -> RawAccountState PostgreSQL + complétude RAW
0.3.5 -> Interface events partagés si consumer réel
0.3.6 -> Job API + premier backfill
0.3.7 -> application backfill/inspection
```
## 9. Prévision souple intégrée au prompt
Le prompt réserve :
```text
pre.001 audit contrats/kbot3/schema/concurrence/sizing
pre.002 migration V001 + schema/indexes
pre.003 mapping physique + lectures
pre.004 persistence acquisition atomique + observations
pre.005 list/query + cursor
pre.006 retention/tombstone/ForceRehydrate
pre.007 composition façade + conformance six capabilities
pre.008 PostgreSQL integration réelle RawTransaction
pre.009 hardening/completeness
pre.010 gate technique final
pre.011 réconciliation documentaire
pre.012 préparation de publication
rel.001 publication stable
```
La prévision reste souple et doit être recalibrée par `0.3.3-pre.001` selon le budget de 1520 minutes par tranche intermédiaire.
## 10. Fichiers ajoutés
```text
prompts/022-V0_3_3_START_PROMPT.md
deltas/0.3.2/pre.012.md
```
## 11. Fichiers modifiés
```text
Cargo.toml
CHANGELOG.md
ROADMAP.md
```
## 12. Fichiers supprimés
```text
aucun
```
## 13. Surfaces explicitement non rouvertes
```text
README.md
RULES.md
.env.example
config/**
crates/**
docs/**
prompts/001..021
```
En particulier :
```text
crates/ksp-store-api/**
crates/ksp-store-lib/**
crates/ksp-store-postgres-lib/**
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
docs/architecture/**
```
## 14. Validations de préparation
À exécuter sur l'arbre reconstruit :
```text
audit Rust workspace
audit Markdown
contrôle exact du payload overlay
contrôle versions/file headers
contrôle qu'aucun fichier hors lane n'est modifié
```
Aucun résultat Cargo nouveau n'est revendiqué par l'environnement de génération si Cargo n'y est pas disponible.
## 15. Gate opérateur
La lane est documentaire/de publication ; le gate ciblé suffit :
```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-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Le live PostgreSQL, les trois builds Tauri et le workspace lourd n'ont pas besoin d'être rejoués dans cette tranche puisque `pre.012` ne touche aucune surface runtime/build/config/dependency.
## 16. Étape suivante
Si le gate reste vert :
```text
0.3.2-rel.001
```
La publication stable doit être strictement mécanique :
```text
workspace.package.version = 0.3.2
deltas/0.3.2/rel.001.md
```
`rel.001` ne doit rouvrir ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt `0.3.3`, ni aucun autre document/code.

329
deltas/0.3.2/rel.001.md Normal file
View File

@@ -0,0 +1,329 @@
<!-- file: deltas/0.3.2/rel.001.md -->
<!-- version: 1 -->
# Delta `0.3.2-rel.001` — publication stable Store/PostgreSQL foundation
## 1. Base requise
Base directe attendue :
```text
0.3.2-pre.012
workspace.package.version = 0.3.2-pre.12
```
Commit attendu pour cette livraison :
```text
v0.3.2-rel.001
```
Tag stable attendu après validation :
```text
v0.3.2
```
## 2. Responsabilité de `rel.001`
Cette livraison effectue uniquement la mécanique de publication stable définie par `VER-LIFECYCLE-012`.
Elle :
- passe `workspace.package.version` de `0.3.2-pre.12` à `0.3.2` ;
- ajoute le présent delta `rel.001` ;
- ne corrige ni code, ni test, ni manifest de crate, ni Config, ni migration, ni README/USAGE, ni plan/validation, ni architecture/règle, ni prompt, ni `CHANGELOG.md`, ni `ROADMAP.md`.
Tout défaut appartenant à un couloir antérieur doit renvoyer vers une prerelease appropriée ; `rel.001` n'est jamais une tranche de rattrapage.
## 3. Preuves acquises avant publication
Le gate technique final `0.3.2-pre.010`, exécuté le **29 août 2026** après `cargo clean`, a reconstruit et validé la workspace depuis zéro.
Il a notamment validé :
```text
cargo clean PASS
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
General Rust rule audit clean
Rust export completeness audit 0 candidate(s)
KSP workspace Rust rule audit clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test --workspace PASS
cargo tree -p ksp-store-lib --edges normal inspecté
cargo tree -p ksp-store-lib -e features inspecté
cargo tree -p ksp-store-postgres-lib --edges normal inspecté
cargo tree --duplicates inspecté
cargo tauri build — SOL Prices Desk PASS
cargo tauri build — Wallet Desk PASS
cargo tauri build — Config Desk PASS
```
Le même gate a rejoué le test PostgreSQL réel opt-in sur un serveur **PostgreSQL major 17** :
```text
postgres_foundation_live PASS
bootstrap initial PASS
réouverture idempotente PASS
bootstrap concurrent sous advisory lock PASS
health Ready PASS
mismatch checksum PASS
recovery après restauration PASS
rollback transactionnel injecté PASS
close borné PASS
```
Le gate `0.3.2-pre.011`, exécuté le **30 août 2026**, a validé la réconciliation documentaire finale et le bon scope Markdown `deltas/0.3.2` :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
General Rust rule audit clean
Rust export completeness audit 0 candidate(s)
KSP workspace Rust rule audit clean
python3 scripts/audit_markdown_tables.py PASS — 186 tables / 142 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-lib --no-default-features PASS
cargo test -p ksp-store-postgres-lib PASS
cargo test -p ksp-config-lib PASS
cargo check -p ksp-store-lib --no-default-features PASS
```
Enfin, le gate `0.3.2-pre.012`, exécuté le **30 août 2026**, a validé la préparation minimale de publication :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
General Rust rule audit clean
Rust export completeness audit 0 candidate(s)
KSP workspace Rust rule audit clean
python3 scripts/audit_markdown_tables.py PASS — 186 tables / 144 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS
cargo test -p ksp-config-lib PASS
cargo check -p ksp-store-lib --no-default-features PASS
```
`pre.011` et `pre.012` n'ont rouvert aucun code runtime, aucune migration ni aucune dépendance Store/PostgreSQL.
## 4. Version stable publiée
La version Cargo devient :
```text
workspace.package.version = 0.3.2
```
La release stable publiée est :
```text
0.3.2 — Store/PostgreSQL runtime foundation
```
La surface stable comprend notamment :
```text
ksp-store-lib
ksp-store-postgres-lib
feature postgres par défaut dans la façade
mode --no-default-features sans backend PostgreSQL
Config std.store avec targets nommés devnet/mainnet/testnet
1 Store = 1 RawNetworkId + 1 backend physique
tokio-postgres + deadpool-postgres
TLS Disabled / VerifyFull via Rustls
pool borné et timeouts connect/wait/create/recycle/shutdown
Store::open / Store::close
runtime_snapshot sans I/O
health/readiness portable et redacted
moteur privé de migrations KSP
V000 bootstrap metadata-only
ksp_store_schema_migrations
checksum SHA-256
advisory lock transactionnel borné
bootstrap idempotent et concurrent-safe
refus du schema plus récent / historique divergent
URI et erreurs PostgreSQL non exposées
PostgreSQL >= 15, validé réellement sur major 17
```
Restent volontairement absents de `0.3.2` :
```text
persistence PostgreSQL RawTransaction
persistence PostgreSQL RawAccountState
repositories SQL métier RAW
queries/cursors PostgreSQL métier
retention/tombstone/ForceRehydrate PostgreSQL métier
CORE / DECODE / SPECIALIZED
worker / job / executor
Store Desk
```
La frontière stable reste :
```text
ksp-store-api
= contrats RAW backend-agnostic
ksp-store-lib
= façade/runtime commun + sélection backend par feature
ksp-store-postgres-lib
= driver/pool/TLS/SQL/migrations/health PostgreSQL privés
ksp-config-lib
= résolution std.store + secrets/env + target réseau/backend
```
`ksp-store-postgres-lib` ne dépend jamais de `ksp-store-lib`, et les consommateurs ordinaires dépendent uniquement de la façade.
## 5. Trajectoire suivante confirmée
La suite reste :
```text
0.3.3 vertical slice PostgreSQL RawTransaction complète
0.3.4 vertical slice PostgreSQL RawAccountState + complétude Store RAW
0.3.5 Interface events partagés réellement nécessaires
0.3.6 Job API + premier backfill RAW
0.3.7 application backfill/inspection RAW
```
Le prompt actif de la prochaine release est :
```text
prompts/022-V0_3_3_START_PROMPT.md
```
`0.3.3-pre.001` doit commencer par audit/brainstorming/sizing avant SQL lourd et exige l'archive historique :
```text
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
```
Cette archive sert uniquement à réauditer l'ancienne vertical slice transaction PostgreSQL ; elle n'a aucune autorité normative sur l'architecture KSP actuelle.
## 6. Fichiers ajoutés
```text
deltas/0.3.2/rel.001.md
```
## 7. Fichiers modifiés
```text
Cargo.toml
```
## 8. Fichiers supprimés
```text
aucun
```
## 9. Validations exécutées pour préparer le delta
Contrôles statiques de l'overlay :
```text
Cargo.toml version 0.3.2
payload rel.001 2 fichiers exactement
CHANGELOG.md non rouvert
ROADMAP.md non rouvert
prompt 022 non rouvert
README/USAGE non rouverts
plan / validation non rouverts
architecture / règles non rouvertes
Config / schemas / migrations non rouverts
code / tests non rouverts
aucun fichier de rattrapage présent
```
La préparation de l'archive ne rejoue aucun test Cargo dans l'environnement de génération. Les preuves techniques acquises sont celles des gates opérateur ci-dessus ; le gate stable final reste à exécuter après application.
## 10. Validation stable après application
Exécuter avant commit/tag :
```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 --workspace
```
Il n'est pas nécessaire de rejouer les trois builds Tauri, les graphes Cargo ni le smoke PostgreSQL réel : `rel.001` ne modifie ni code/runtime, ni dépendance, ni Config, ni migration, ni surface desktop.
## 11. Questions ouvertes
Aucune question ouverte ne bloque la publication de `0.3.2`.
Les décisions métier/physiques suivantes appartiennent explicitement à `0.3.3-pre.001` :
```text
binding vérifiable database <-> RawNetworkId
représentation PostgreSQL sans narrowing des valeurs unsigned
V001 RawTransaction + observations
clés/contraintes/indexes
ordre total et version du cursor
atomicité canonical + observation
idempotence et conflits concurrents
rétention/tombstone/ForceRehydrate physiques
mapping SQL -> erreurs Store redacted
```
`RawAccountState` PostgreSQL reste réservé à `0.3.4`.
## 12. Commit et tag stable
Après succès du gate stable :
```text
commit : v0.3.2-rel.001
tag : v0.3.2
```
Aucun tag intermédiaire `rel.001` n'est requis.
Commandes Git typiques :
```bash
git add Cargo.toml deltas/0.3.2/rel.001.md
git commit -m "v0.3.2-rel.001"
git tag v0.3.2
git push
git push origin v0.3.2
```
## 13. Suite
Après publication du tag stable `v0.3.2`, ouvrir exclusivement :
```text
0.3.3-pre.001 — PostgreSQL RawTransaction vertical slice
```
avec :
```text
prompts/022-V0_3_3_START_PROMPT.md
```
La session suivante doit recevoir :
```text
archive opérateur exacte de v0.3.2
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
```
`0.3.3-pre.001` reste une tranche d'audit/brainstorming/sizing et ne doit pas commencer l'implémentation SQL lourde avant fermeture des décisions de schéma, concurrence, cursorisation et rétention.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Data, Materialization et Store
@@ -291,26 +291,39 @@ La première implementation `0.3.1` est volontairement **RAW-only** : elle ne cr
Les surfaces CORE/DECODE/SPECIALIZED sont ajoutées quand leurs couches sont réellement ouvertes.
## `ksp-store-lib`
## `ksp-store-lib` et backends physiques
`ksp-store-lib` fournit PostgreSQL comme backend officiel de référence derrière `ksp-store-api`.
`ksp-store-lib` est la façade/runtime Store commune derrière `ksp-store-api`. Il sélectionne uniquement les backends compilés par ses features, convertit ses settings KSP-owned vers le backend retenu, expose le lifecycle commun et masque les objets physiques du moteur.
Il possède :
Il possède notamment :
- migrations ;
- SQL ;
- transactions ;
- mapping backend ;
- pagination ;
- claim/lease lorsque nécessaire ;
- notifications backend si retenues.
- identité et sélection de backend côté façade ;
- settings Store publics KSP-owned indépendants de Config ;
- lifecycle commun `open` / `close` ;
- dispatch vers le backend compilé ;
- mapping des diagnostics/health backend vers une projection portable lorsque cette surface est justifiée ;
- réexport de la surface `ksp-store-api` utile aux consumers.
Il ne possède pas :
Le backend PostgreSQL officiel appartient à `ksp-store-postgres-lib`. Cette crate dépend de `ksp-store-api`, ne dépend jamais de `ksp-store-lib` et possède seule :
- transport réseau ;
- `tokio-postgres` et le pool PostgreSQL ;
- le connecteur TLS PostgreSQL ;
- SQL et statements physiques ;
- transactions PostgreSQL ;
- migrations/bootstrap et metadata de schéma ;
- mapping rows/backend ;
- détails de cursorisation physique ;
- diagnostics PostgreSQL internes.
`ksp-store-lib` et les crates backend ne possèdent pas :
- transport réseau dacquisition ;
- decoder Program ;
- materializer ;
- orchestration de worker/job.
- orchestration de worker/job ;
- politique de batch, backlog, priorité ou retry de processing.
La pagination/cursorisation reste un contrat de navigation Store. Une limite demandée par lappelant peut être validée pour sa forme/sécurité, mais aucun plafond métier global arbitraire ni batch-size de worker nest introduit par le runtime Store.
## `ksp-materializer-api` et `ksp-materializer-lib`

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md -->
<!-- version: 68 -->
<!-- version: 69 -->
# Plans KSP
@@ -31,6 +31,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`020-V0_2_13_INTERFACE_PLAN.md`](020-V0_2_13_INTERFACE_PLAN.md) — plan historique clôturé de la release stable `0.2.13 — Interface / wire foundation`; il fixe la surface finale `ProgramAccountMeta` + `ProgramInstruction`, les bornes `255` / `10_240`, le firewall Interface -> Core, les canaris de complétude/consumer externe et la frontière avec Program API/RAW/CORE.
- [`021-V0_2_14_PROGRAM_API_PLAN.md`](021-V0_2_14_PROGRAM_API_PLAN.md) — plan historique clôturé de la release stable `0.2.14 — Program API foundation`; il fixe la façade instruction-only ouverte, les enums Recognition/Outcome, `ProgramInstructionDecoder`, l'output associé possédé par l'implémentation, le canari externe avec Program Pubkey non enregistré, le firewall Core/Interface et le report du payload canonique D3, du registry runtime et de `ProgramExecutionPreparer`.
- [`022-V0_3_1_STORE_RAW_PLAN.md`](022-V0_3_1_STORE_RAW_PLAN.md) — plan candidat réconcilié de `0.3.1 — Store API RAW foundation`; il fixe `ksp-store-api` seul, les modèles transaction/account + observations, queries/outcomes/capabilities, rétention/tombstone, la frontière event-only/Interface et le report de `ksp-store-lib` + PostgreSQL à `0.3.2`.
- [`023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan candidat réconcilié de `0.3.2 — Store/PostgreSQL runtime foundation`; il fixe le split façade/backend, `std.store` multi-target réseau-spécifique, tokio-postgres/Deadpool/Rustls, migrations metadata-only, health/readiness, live PostgreSQL et les reports des vertical slices RAW vers `0.3.3`/`0.3.4`.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
<!-- version: 16 -->
<!-- version: 17 -->
# Règles des dépendances KSP
@@ -79,13 +79,15 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
- **DEP-MAT-002** — `ksp-materializer-api` et `ksp-materializer-lib` ne dépendent pas de `ksp-store-api` ou `ksp-store-lib`.
- **DEP-MAT-003** — Une matérialisation générique doit pouvoir produire un output compatible avec le journal D3 sans imposer une table PostgreSQL spécialisée par materializer.
- **DEP-STORE-001** — `ksp-store-api` ne dépend pas de Program, Materializer ou Transport.
- **DEP-STORE-002** — `ksp-store-lib` dépend de `ksp-store-api` et contient l'implémentation PostgreSQL de référence ; il ne dépend pas des implémentations Program/Materializer/Transport.
- **DEP-STORE-002** — `ksp-store-lib` dépend de `ksp-store-api`, porte la façade/runtime Store commune et peut dépendre optionnellement de crates backend compilées par feature ; il ne dépend pas des implémentations Program/Materializer/Transport.
- **DEP-STORE-003** — Les workers/jobs spécialisés sont propriétaires des conversions entre modèles runtime et DTO persistants.
- **DEP-STORE-004** — Les niveaux durables sont D1 Raw, D2 Core canonique, D3 journal de matérialisation générique et D4 projections spécialisées.
- **DEP-STORE-005** — Les replays D1 -> D2, D2 -> D3 et D3 -> D4 doivent pouvoir être exécutés indépendamment.
- **DEP-STORE-006** — Une notification de donnée persistée ne constitue jamais la source de vérité du backlog ; les queries Store et marqueurs durables d'idempotence/version de processor font autorité.
- **DEP-STORE-007** — Une notification de donnée est publiée seulement après persistence/commit réussis.
- **DEP-STORE-008** — D4 est organisé par faits canoniques quand les invariants le permettent, et non par familles de tables propres aux protocoles.
- **DEP-STORE-009** — `ksp-store-postgres-lib` dépend de `ksp-store-api`, possède seul le driver, le pool, TLS, SQL et les migrations PostgreSQL physiques, et ne dépend jamais de `ksp-store-lib`.
- **DEP-STORE-010** — Les consumers runtime ordinaires — workers, jobs, services et apps — dépendent de `ksp-store-lib` et non directement dune crate backend ; Config/composition traduit la configuration effective vers les settings publics Store sans créer de dépendance Store -> Config.
## Transport

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_KSP.md -->
<!-- version: 38 -->
<!-- version: 39 -->
# Règles spécifiques à KSP
@@ -23,7 +23,7 @@
- **KSP-API-003** — KSP ne crée pas de `ksp-api-lib` monolithique regroupant les contrats de domaines indépendants.
- **KSP-API-004** — Un contrat public extensible doit pouvoir être implémenté depuis une crate séparée du workspace principal lorsque cela est techniquement pertinent.
- **KSP-API-005** — Les signatures des contrats publics utilisent en priorité des types publics KSP et les primitives externes explicitement admises ; elles ne doivent pas imposer des détails internes instables.
- **KSP-API-006** — `ksp-store-lib` contient PostgreSQL comme implémentation officielle de référence derrière `ksp-store-api`.
- **KSP-API-006** — `ksp-store-lib` est la façade/runtime Store commune derrière `ksp-store-api` et sélectionne uniquement des backends compilés via ses features ; limplémentation PostgreSQL officielle appartient à `ksp-store-postgres-lib`, qui dépend de `ksp-store-api` et ne dépend jamais de `ksp-store-lib`.
- **KSP-API-007** — Une crate `*-api` n'est créée que lorsqu'un vrai besoin d'extension, backend ou lifecycle le justifie ; la symétrie de nommage n'est jamais une justification suffisante.
## Configuration et environnement

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/000-README.md -->
<!-- version: 32 -->
<!-- version: 33 -->
# Validations KSP
@@ -27,3 +27,4 @@ Documents :
- [`016-V0_2_13_INTERFACE.md`](016-V0_2_13_INTERFACE.md) — matrice historique clôturée de la release stable `0.2.13` : ownership Interface/Core/Transport/Program, surface passive, bornes/adversarial, façade publique exacte, consumer externe, release completeness et dependency firewall.
- [`017-V0_2_14_PROGRAM_API.md`](017-V0_2_14_PROGRAM_API.md) — matrice historique clôturée de la release stable `0.2.14 — Program API foundation` : façade instruction-only ouverte, Recognition/Outcome, trait externe, Program Pubkey non enregistré, hardening adversarial, release completeness et firewall Core/Interface.
- [`018-V0_3_1_STORE_RAW.md`](018-V0_3_1_STORE_RAW.md) — matrice candidate finale de `0.3.1 — Store API RAW foundation` : modèles transaction/account, observations, capabilities backend, pagination sans policy executor, outcomes, rétention/tombstone, hardening, gate complet `pre.008` et reports explicites vers `0.3.2+`.
- [`019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — matrice candidate finale de `0.3.2 — Store/PostgreSQL runtime foundation` : feature graph, settings/Config, pool/TLS, migrations metadata-only, health, hardening, graphes, builds Tauri et preuve PostgreSQL réelle major 17.

View File

@@ -0,0 +1,726 @@
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
<!-- version: 13 -->
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
## 1. Objet
Cette matrice est ouverte par `0.3.2-pre.001`. Elle fixe les critères de preuve de la fondation Store/PostgreSQL sans préjuger des résultats non encore exécutés.
Statuts :
```text
TODO preuve requise non encore exécutée
PASS preuve réellement exécutée et verte
FAIL preuve exécutée et en échec
N/A non applicable avec justification
```
Aucun `TODO` n'est présenté comme validé.
## 2. Base et audit `pre.001`
### V32-BASE-001 — Base stable
Critère :
```text
workspace.package.version = 0.3.1 sur l'archive source
deltas/0.3.1/rel.001.md présent
prompt 021 présent
ksp-store-api présent
ksp-store-lib absent
ksp-store-postgres-lib absent
```
Statut : `PASS` au gate documentaire `pre.001`.
Note : metadata Git absente de l'archive ; le tag `v0.3.1` n'est pas interrogé localement.
### V32-AUDIT-001 — Sources internes
Critère : règles, architecture, plan/validation `0.3.1`, source/tests `ksp-store-api`, CHANGELOG et ROADMAP relus avant design.
Statut : `PASS` au gate documentaire `pre.001`.
### V32-AUDIT-002 — Archive kbot3
Critère : ancien Store/config/PostgreSQL/migrations/health/pool/erreurs relus et classés reprendre/redessiner/reporter/rejeter.
Statut : `PASS` au gate documentaire `pre.001`.
### V32-AUDIT-003 — Audit externe actuel
Critère : PostgreSQL, tokio-postgres, pool, TLS et helper migrations réaudités avec sources actuelles.
Statut : `PASS` au gate documentaire `pre.001`.
Référence du gate :
```text
PostgreSQL 18.6
tokio-postgres 0.7.18
deadpool-postgres 0.14.2
tokio-postgres-rustls 0.14.0
sha2 0.11.0
refinery 0.9.2 audité/rejeté
```
Le gate opérateur après application de `pre.001`, fourni le 29 août 2026, est vert : audits Rust/Markdown, `cargo check --workspace` et `cargo clippy --workspace --all-targets` passent sur `0.3.2-pre.1`.
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`.
Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-lib` avec et sans feature par défaut, tests backend et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.004`.
Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`.
Le gate opérateur de `pre.005-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 5 tests runtime backend, canaris de dépendances/API, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.006`.
Le gate opérateur de `pre.006`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 9 tests backend incluant la migration V000, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent.
Le gate opérateur de `pre.007`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 12 tests backend incluant health/readiness, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent.
Le gate `pre.008` du 29 août 2026 est doublement vert : le gate déterministe passe intégralement puis `postgres_foundation_live` passe sur **PostgreSQL 17**. La preuve réelle couvre bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup de la metadata possédée par le test. Cette base est l'entrée effective de `pre.009`.
Le gate opérateur de `pre.009`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, `ksp-store-api`, backend PostgreSQL, façade avec et sans feature, Config, compilation `--no-default-features`, graphes/features/duplicates et `cargo test --workspace` passent. Les canaris hardening/completeness ajoutés par `pre.009` sont donc validés. Cette base est l'entrée effective de `pre.010`.
## 3. Frontières Cargo
### V32-DEP-001 — Façade -> API
```text
ksp-store-lib -> ksp-store-api
```
Matérialisé par `0.3.2-pre.002` dans le manifest de `ksp-store-lib`.
Statut : `PASS pre.002 opérateur` — test ciblé et `cargo tree` fournis.
### V32-DEP-002 — Backend -> API
```text
ksp-store-postgres-lib -> ksp-store-api
```
Matérialisé par `0.3.2-pre.002` dans le manifest de `ksp-store-postgres-lib`.
Statut : `PASS pre.002 opérateur` — test ciblé et `cargo tree` fournis.
### V32-DEP-003 — Pas de cycle backend
```text
ksp-store-postgres-lib -X-> ksp-store-lib
```
Preuves : manifest scanner + cargo tree.
Le canari source de `pre.002` interdit explicitement la dépendance inverse ; le `cargo tree` opérateur confirme l'absence de cycle. La frontière sera durcie à nouveau en `pre.009`.
Statut : `PASS pre.002 opérateur / PASS pre.009 opérateur`.
### V32-DEP-004 — Feature PostgreSQL
Critères :
```text
postgres est default feature de ksp-store-lib
ksp-store-postgres-lib est optional dependency
cargo check -p ksp-store-lib --no-default-features passe
```
Le manifest de `ksp-store-lib` matérialise `default = ["postgres"]` et l'optional dependency `ksp-store-postgres-lib`. La compilation sans default feature est une preuve opérateur obligatoire.
Statut : `PASS pre.002 opérateur` — default feature, optional backend edge et compilation `--no-default-features` confirmés.
### V32-DEP-005 — Firewall domaines
Interdictions :
```text
Store/backend -> Config
Store/backend -> Transport
Store/backend -> Program
Store/backend -> Materializer
consumer ordinaire -> ksp-store-postgres-lib
```
Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root.
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.009 opérateur`.
### V32-DEP-006 — `ksp-store-api` non régressé
Critères :
```text
dépendance runtime exacte ksp-core-lib seulement
exports/capabilities existants conservés
aucun type backend ajouté pour PostgreSQL
```
Statut : `PASS pre.009 opérateur / PASS pre.010 opérateur` — le canari backend externe de `ksp-store-api` est rejoué sans modification de la crate.
## 4. Public API et settings
### V32-API-001 — Settings Config-independent
Critère : `StoreSettings` est constructible sans `ksp-config-lib`, sans env et sans serde requis par la façade.
Matérialisé par `pre.003` : `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, pool/bootstrap/TLS typés, sans dépendance Config/serde/env.
Statut : `PASS pre.003-fix.001 opérateur`.
### 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.
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 : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur`.
### V32-API-003 — Aucun type backend physique public
Interdits dans crate-root `ksp-store-lib` :
```text
tokio_postgres::*
deadpool_postgres::*
rustls::*
PostgresBackend / Pool / Client / Row / Statement
```
Statut : `PASS pre.009 opérateur`.
### V32-API-004 — Réexports Store API
Critère : un consumer de `ksp-store-lib` accède aux contrats Store API utiles sans dépendre directement de la crate backend.
`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 : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur exact exports`.
### V32-API-005 — Lifecycle
Critères :
```text
Store::open async
Store::close(self) async
pas de pool/client échappé
close borné
Drop best-effort seulement
```
`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade.
Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `PASS pre.005-fix.001 opérateur` pour l'ouverture et le shutdown physiques / `PASS pre.007 opérateur` pour la composition health.
## 5. Config ownership
### V32-CONFIG-001 — Document/schema/example
Critères :
```text
std.store enregistré
schema V1 valide
example valide
profiles/targets nommés typés
un réseau explicite par target
backend postgres explicite
```
Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées.
Statut : `PASS pre.004-fix.001 opérateur`.
### V32-CONFIG-002 — Secrets/provenance
Critères :
```text
KSP_SECRET_STORE_{DEVNET,MAINNET,TESTNET}_POSTGRES_URI classés Secret
safe projection redacted
provenance sans valeur
dotenv inventory à jour
```
Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur.
Statut : `PASS pre.004-fix.001 opérateur`.
### V32-CONFIG-002B — Target/réseau sans multiplexage
Critères :
```text
profile_id std.store = identifiant de target Store
default_profile choisit exactement un target
StoreSettings porte exactement un RawNetworkId
résolution explicite mainnet/devnet/testnet retourne URI + network du target choisi
ksp-store-lib ne possède aucun routeur multi-target/multi-réseau
future RAW mismatch Store.network != entity/query.network rejeté avant I/O
```
Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`.
Statut : `PASS pre.004-fix.001 opérateur` pour la sélection target/réseau / `N/A 0.3.2` pour le mismatch d'opérations RAW, dont l'enforcement appartient aux vertical slices `0.3.3`-`0.3.4`.
### V32-CONFIG-003 — No-env Store/backend
Critère : production sources `ksp-store-lib` et `ksp-store-postgres-lib` ne lisent aucun :
```text
std::env
dotenv
KSP_*
KSPB_*
PG*
.pgpass
```
`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB.
Statut : `PASS pre.004-fix.001 opérateur / PASS pre.009 opérateur`.
### V32-CONFIG-004 — Adapter Config -> Store
Critère : `ksp-config-lib` seul transforme un profil/target résolu en `StoreSettings` réseau-spécifique et ne transmet aucun secret dans diagnostics.
Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend.
Statut : `PASS pre.004-fix.001 opérateur`.
## 6. Pool et lifecycle PostgreSQL
### V32-POOL-001 — Pool borné
Critères :
```text
max_connections 1..64
wait/create/recycle timeouts bornés
aucune taille zéro
aucune valeur pathologique
```
Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI.
Statut : `PASS pre.005-fix.001 opérateur`.
### V32-POOL-002 — Connection task ownership
Critère : chaque connection future tokio-postgres est pilotée par le manager retenu et son task handle reste possédé jusqu'au drop/close.
`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening.
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.009 opérateur`.
### V32-POOL-003 — Open failure safe
Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbitraire dans Display/Debug public.
Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr.
Statut : `PASS pre.005-fix.001 opérateur`.
### V32-POOL-004 — Close
Critères :
```text
pool fermé
nouvelles acquisitions refusées
shutdown respecte timeout
aucune tâche volontairement laissée orpheline
```
`pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`.
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`.
## 7. TLS
### V32-TLS-001 — Modes exacts
Surface initiale :
```text
Disabled
VerifyFull
```
Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée.
Statut : `PASS pre.005-fix.001 opérateur`.
### V32-TLS-002 — VerifyFull
Critères :
```text
TLS requis
root store système
authenticité du certificat vérifiée
nom serveur vérifié
aucun fallback plaintext
```
Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs.
Statut : `PASS pre.005-fix.001 opérateur`.
### V32-TLS-003 — Pas de fichier TLS implicite
Critère : backend ne lit pas `sslrootcert`, `sslcert`, `sslkey`, `.postgresql/*` ou autre fichier implicite hors settings KSP.
Statut : `PASS pre.009 opérateur`.
## 8. Migration/bootstrap
### V32-MIG-001 — Metadata privée uniquement
Critère : `0.3.2` crée au plus la relation d'infrastructure :
```text
ksp_store_schema_migrations
```
et aucune table métier RAW/CORE/DECODE/SPECIALIZED.
`pre.006` embarque exactement `migrations/V000__bootstrap.sql`, dont le seul DDL de production crée `ksp_store_schema_migrations`. Le canari source interdit les identifiants métier RAW/CORE/DECODE/SPECIALIZED dans le moteur et la ressource SQL.
Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`.
### V32-MIG-002 — Version/checksum
Critères :
```text
version entière monotone
nom immuable
SHA-256 du SQL exact
sentinel bootstrap version 0
mismatch historique terminal
```
`pre.006` calcule explicitement SHA-256 sur les octets exacts du SQL embarqué puis encode les 32 octets en 64 caractères hex minuscules. Le sentinel `(0, bootstrap, checksum)` est inséré dans la même transaction que la création metadata. Les tests unitaires figent le checksum du SQL committed et couvrent sentinel valide, missing, nom/checksum divergents et historique plus récent.
Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`.
### V32-MIG-003 — Concurrence
Critère : deux runners concurrents sont sérialisés par advisory transaction lock avec attente bornée.
`pre.006` utilise une clé KSP fixe et `pg_try_advisory_xact_lock($1)` dans une boucle bornée par `migration_lock_timeout`, avec polling de 25 ms maximum. Aucun lock bloquant illimité n'est utilisé.
Statut : `PASS pre.006 opérateur / PASS concurrence live pre.008 PostgreSQL 17`.
### V32-MIG-004 — Atomicité/recovery
Critère : échec d'une migration du run courant rollback DDL + history de ce run ; un rerun depuis état précédent reste sûr.
`pre.006` place lock, metadata DDL, sentinel et validation dans une transaction unique ; toute sortie d'erreur avant `commit()` droppe la transaction et PostgreSQL rollback le run courant. Un timeout externe borne également l'ensemble du bootstrap. L'injection d'échec et la preuve physique du rollback restent au smoke réel.
Statut : `PASS pre.006 opérateur / PASS rollback live pre.008 PostgreSQL 17`.
### V32-MIG-005 — Newer runtime guard
Critère : migration appliquée inconnue/supérieure à la liste embarquée produit `STORE_POSTGRES_SCHEMA_NEWER`, sans down migration.
`pre.006` connaît uniquement la version `0`; toute history `> 0` est classée `SchemaNewer` par le backend puis `store.postgres_schema_newer` par la façade. Aucun chemin de down migration n'existe.
Statut : `PASS pre.006 opérateur`.
### V32-MIG-006 — SQL injection
Critères :
```text
SQL de migration statique embarqué
values paramétrées
aucun identifier physique user-configurable en 0.3.2
```
`pre.006` garde le DDL versionné sous `include_str!` et toutes les values variables des requêtes de contrôle (`lock key`, `statement_timeout`, history values) passent par paramètres. Les noms physiques sont des constantes KSP, jamais des settings.
Statut : `PASS pre.006 opérateur / PASS pre.009 opérateur hardening`.
### V32-MIG-007 — No business capability
Critères :
```text
0 impl PostgreSQL de capability RawTransaction
0 impl PostgreSQL de capability RawAccountState
0 repository RAW métier
```
Statut : `PASS pre.009 opérateur / PASS pre.010 opérateur`.
## 9. Health/readiness
### V32-HEALTH-001 — Projection portable
Critère : façade expose uniquement état/backend/network/counts/migration safe, jamais URI/SQL/pool/client.
`pre.007` matérialise `StoreRuntimeSnapshot`, `StoreHealthState::{Ready, NotReady}` et `StoreHealthSnapshot`. Les compteurs pool sont bornés en `u32`; la version migration est optionnelle si le probe ne peut pas la lire.
Statut : `PASS pre.007 opérateur`.
### V32-HEALTH-002 — Readiness réelle
Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify selon settings, et `Store::health()` sonde ensuite la disponibilité physique sous deadline.
`pre.007` conserve l'ouverture stricte acquise en `pre.006` et ajoute un probe borné par le `wait_timeout` du pool : acquisition, `SELECT 1`, lecture de version migration. Le smoke réel reste nécessaire pour prouver ce chemin contre PostgreSQL.
Statut : `PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`.
### V32-HEALTH-003 — Error redaction
Critère : un health failure n'expose pas server error string, query text ou credential.
Le backend ne conserve que `PostgresBackendErrorKind`; la façade mappe vers un `ErrorCode` KSP optionnel. Le snapshot public ne contient aucune string serveur, URI, SQL, host, user, database ou handle.
Statut : `PASS pre.007 opérateur / PASS pre.009 opérateur hardening`.
## 10. PostgreSQL integration réelle
### V32-LIVE-001 — Input opérateur explicite
Critères :
```text
#[ignore]
URI lue depuis stdin
aucun env requis
URI jamais imprimée par le test
```
`pre.008` ajoute `tests/postgres_foundation_live.rs`. Le test borne l'entrée à 4096 octets, ne conserve dans ses erreurs que des phases statiques et ne rend jamais l'erreur PostgreSQL brute. L'opérateur peut masquer la saisie côté shell puis piper une seule ligne sur stdin ; aucune variable d'environnement n'est lue par le test.
Statut : `PASS live pre.008 PostgreSQL 17`.
### V32-LIVE-002 — Non destructif
Critères :
```text
refus si metadata table préexiste
aucune base/schema drop
aucune table métier créée
cleanup seulement de metadata créée par le test
```
Le test interroge `information_schema.tables` avant toute mutation. Il ne prend ownership du cleanup qu'après avoir prouvé que le bootstrap initial a créé la metadata alors que la précondition était absente. Les seuls `DROP` exécutés ciblent `ksp_store_schema_migrations` pendant les resets contrôlés et le cleanup final.
Statut : `PASS live pre.008 PostgreSQL 17`.
### V32-LIVE-003 — Scénario foundation
Preuves :
```text
connect
bootstrap initial
bootstrap idempotent
bootstrap concurrent
checksum mismatch + restore/recovery
failure rollback du V000 exact
health ready
close borné
cleanup metadata
```
La preuve rollback évite tout hook public de test : une transaction `tokio-postgres` de test exécute le V000 exact par `include_str!`, insère un sentinel transitoire, provoque ensuite une erreur SQL puis est droppée. Le test vérifie que la metadata n'existe pas après le rollback implicite et que le bootstrap KSP normal peut repartir proprement.
Statut : `PASS live pre.008 PostgreSQL 17 / PASS live pre.010 PostgreSQL 17`.
### V32-LIVE-004 — PostgreSQL support
Critère : test refuse major < 15 et enregistre seulement le major safe réellement testé.
Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur. Le gate opérateur réel a été exécuté avec succès sur PostgreSQL 17.
Statut : `PASS live pre.008 PostgreSQL 17 / PASS live pre.010 PostgreSQL 17`.
## 11. Security/adversarial
### V32-SEC-001 — URI hostile
Cas : vide, surdimensionnée, malformed, paramètres conflictuels, password contenant contrôles/URL-like.
Attendu : rejet borné sans echo.
`pre.009` ajoute un test d'intégration backend qui force avant I/O : URI malformed, URI > 4096 octets, `hostaddr` sans identité sous `VerifyFull` et `options=` libpq. Chaque cas contient un canary secret et exige une erreur `ConfigInvalid` avec phase statique seulement.
Statut : `PASS pre.009 opérateur`.
### V32-SEC-002 — Secret canary
Injecter un canary dans URI/password et prouver son absence de :
```text
Debug
Display
ErrorContext
tracing snapshots
health snapshots
```
`pre.009` injecte des canaris distincts dans la façade et le backend. Les settings Debug doivent afficher `<redacted>`; les erreurs pré-I/O publiques/backend ne doivent contenir aucun canary. Les snapshots health restent couverts par les canaris déterministes de `pre.007`.
Statut : `PASS pre.009 opérateur`.
### V32-SEC-003 — Timeouts hostiles
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close.
Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. `pre.005` applique physiquement `connect/wait/create/recycle/shutdown`; `pre.006` applique physiquement `migration_timeout` et `migration_lock_timeout`.
Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `PASS pre.006 opérateur` pour migration / `PASS pre.009 opérateur` hardening`.
### V32-SEC-004 — Feature mismatch avant I/O
`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 : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur`.
### V32-SEC-005 — Server error sanitization
Un serveur/test double qui renvoie un message contenant un canary ne doit pas le faire traverser l'erreur publique.
Statut : bridge de sanitization matérialisé en `pre.005`; `pre.009` ajoute un canari structurel prouvant que `PostgresBackendError` ne peut retenir que `kind + phase` statiques et que `PoolError::Backend(_)` abandonne le texte remote. `PASS pre.009 opérateur`.
## 12. Hardening/completeness `pre.009`
Deux nouveaux canaris d'intégration figent sans modifier la production :
```text
crates/ksp-store-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
```
Ils couvrent :
```text
84 exports façade exacts / 5 modules privés exacts
7 exports backend exacts / 5 modules privés exacts
manifests et feature postgres exacts
URI hostiles et canaris secrets avant I/O
sanitization structurelle des erreurs serveur/pool
no-env / no-.pgpass / no-libpq-TLS-files
aucune capability PostgreSQL RawTransaction/RawAccount
V000 metadata-only
compatibilité backend externe ksp-store-api rejouée au gate
```
Statut global `pre.009` : `PASS opérateur` — tests ciblés, canaris hardening, Config, graphes et `cargo test --workspace` verts.
## 13. Gate technique final `pre.010`
`pre.010` ne contient aucun développement fonctionnel. Il doit revalider sur la version `0.3.2-pre.10` :
```text
audits Rust/Markdown
workspace check + Clippy
Store API
backend PostgreSQL
façade default + no-default-features
Config std.store
cargo test --workspace
graphes normal/features/duplicates
PostgreSQL live foundation
3 builds Tauri avec resources std.store packagées
```
Statut global `pre.010` : `PASS technique opérateur`.
Le gate a été rejoué après `cargo clean` sur `0.3.2-pre.10` : audits Rust, workspace check/Clippy, tests ciblés, `cargo test --workspace`, graphes Cargo, trois builds Tauri et PostgreSQL live major 17 sont verts. La ligne d'audit Markdown opérateur a utilisé `deltas/0.3.1` par erreur ; l'overlay `pre.010` avait été audité avec `deltas/0.3.2` dans l'environnement de génération et `pre.011` rejoue le scope Markdown exact avant clôture documentaire.
## 14. Gates Rust/workspace
À chaque tranche applicable :
```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
```
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
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
cargo tree --duplicates
```
Gate PostgreSQL réel `pre.010` après les gates déterministes :
```bash
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
unset KSP_PG_TEST_URI
```
`KSP_PG_TEST_URI` est ici une variable shell non exportée servant uniquement à masquer la saisie ; le test lit exclusivement stdin et ne lit aucun environnement.
Gate technique final :
```bash
cargo test --workspace
(cd crates/ksp-app-config-desk && cargo tauri build)
(cd crates/ksp-app-solprices-desk && cargo tauri build)
(cd crates/ksp-app-wallet-desk && cargo tauri build)
```
Les trois builds Tauri sont requis au gate final parce que `pre.004` a réellement ajouté les resources `std.store` au packaging desktop.
Statut global technique : `PASS pre.010 opérateur`; audit Markdown `deltas/0.3.2` à rejouer dans le gate documentaire `pre.011`.
## 15. Réconciliation documentaire `pre.011`
Critères :
```text
README/USAGE ksp-store-lib présents et alignés sur la façade publique
README/USAGE ksp-store-postgres-lib présents et explicitement backend-only
ksp-config-lib README/USAGE documentent cfg.std.store et les targets réseau
indexes docs/plans/validation référencent 0.3.2
plan 023 reflète le gate technique réel
matrice 019 ne conserve aucun TODO applicable à 0.3.2
CHANGELOG/ROADMAP/prompt suivant inchangés
```
Les architectures générales Store ont été relues et restent cohérentes ; aucune modification artificielle n'est nécessaire.
Statut : `MATÉRIALISÉ pre.011 / TODO gate documentaire opérateur`.
## 16. Critères de fermeture
La matrice ne peut passer en finale que si tous les critères applicables sont `PASS` et que :
```text
aucun RawTransaction PostgreSQL
aucun RawAccountState PostgreSQL
aucun SQL métier RAW
aucune policy worker/job dans Store
aucune fuite backend dans façade
aucune lecture env par Store/backend
PostgreSQL live vert
workspace/clippy/tests verts
```
La réconciliation finale de cette matrice est matérialisée par `pre.011`. Après son gate documentaire vert, la seule étape restante est la préparation de publication minimale `pre.012`.

File diff suppressed because it is too large Load Diff