v0.3.2-pre.008

This commit is contained in:
2026-08-29 21:54:00 +02:00
parent e39cf656b2
commit 80eea82398
5 changed files with 618 additions and 33 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 341
# version: 342
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.3.2-pre.7"
version = "0.3.2-pre.8"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

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

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.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
@@ -863,7 +863,7 @@ Mesures : conversion immédiate vers code KSP + contexte allowlisté ; source er
## 16. PostgreSQL integration test réel
Le test opt-in sera dans `ksp-store-postgres-lib/tests/` et ignoré par défaut.
Le test opt-in est matérialisé par `ksp-store-postgres-lib/tests/postgres_foundation_live.rs` et reste ignoré par défaut.
Entrée :
@@ -891,13 +891,15 @@ second bootstrap idempotent
deux bootstrap concurrents après reset contrôlé
corruption test-only du checksum sentinel -> mismatch explicite
restore/reset contrôlé
migration failure injectée sous cfg(test) -> rollback/history inchangée
échec SQL contrôlé dans une transaction de test exécutant le V000 exact -> drop implicite de `tokio-postgres::Transaction` -> rollback DDL/history prouvé
health ready
close explicite borné
cleanup metadata
```
Le gate de référence visera PostgreSQL 18.6. Le test peut accepter un serveur >= 15, mais le rapport opérateur doit enregistrer le major réellement utilisé sans imprimer d'identité sensible.
Le gate de référence visera PostgreSQL 18.6. Le test accepte un serveur >= 15, n'imprime que le major et refuse tout serveur plus ancien. La connexion d'administration du test et le backend live utilisent `Disabled` afin que ce gate porte sur la fondation PostgreSQL/migrations ; la policy TLS `VerifyFull` reste prouvée séparément par les canaris déterministes de `pre.005`. Le test ne lit aucune variable d'environnement.
La preuve de rollback n'ajoute aucun hook public ou feature de test à la crate de production : le test réutilise par `include_str!` le V000 committed exact, ouvre une transaction PostgreSQL, exécute le DDL et un sentinel transitoire, injecte ensuite une erreur SQL contrôlée puis droppe la transaction. `tokio-postgres 0.7.18` garantit le rollback implicite d'une `Transaction` droppée ; le test vérifie ensuite physiquement l'absence de la table metadata avant de relancer le bootstrap KSP normal.
## 17. Validation de dépendances et API
@@ -1018,7 +1020,7 @@ Avec les quatre codes PostgreSQL ajoutés en `pre.005` puis les trois codes migr
### `pre.007` — Composition end-to-end + health
Statut : matérialisé par `0.3.2-pre.007`, gate Cargo opérateur requis.
Statut : matérialisé par `0.3.2-pre.007`, gate Cargo opérateur `PASS` le 2026-08-29.
La façade ferme la projection runtime/health sans exposer de type physique :
@@ -1036,7 +1038,28 @@ Avec `StoreHealthState`, `StoreRuntimeSnapshot`, `StoreHealthSnapshot` et `ERROR
### `pre.008` — PostgreSQL integration réelle
Ajouter/exécuter le smoke opt-in non destructif : initial/idempotent/concurrent/mismatch/failure/close sur serveur réel.
Statut : test opt-in matérialisé par `0.3.2-pre.008`; compilation/gates déterministes puis exécution PostgreSQL réelle opérateur requises.
Le scénario `postgres_foundation_live` :
```text
lit une URI dédiée uniquement depuis stdin
refuse si ksp_store_schema_migrations existe avant le test
refuse PostgreSQL < 15 et n'imprime que le major
bootstrap initial + health Ready + close borné
réouverture idempotente + health Ready
reset contrôlé de la metadata possédée par le test
deux bootstrap concurrents sérialisés par advisory lock
corruption checksum sentinel -> MigrationMismatch
restauration checksum -> recovery Ready
reset contrôlé
transaction test avec V000 exact + sentinel transitoire + erreur injectée
Drop Transaction -> rollback implicite -> metadata absente
re-bootstrap normal + health Ready + close borné
cleanup uniquement de la metadata prouvée créée par le test
```
Aucun hook test-only n'est ajouté au runtime de production et aucune table RAW métier n'est créée.
### `pre.009` — Hardening/completeness/dependency matrix
@@ -1109,12 +1132,7 @@ Store Desk
Aucune question architecturale ne bloque `pre.002`.
Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :
```text
DDL précis de ksp_store_schema_migrations en pre.006
forme finale des snapshots health en pre.007
```
Les décisions foundation encore ouvertes après `pre.008` sont uniquement des preuves de hardening/completeness et de clôture ; le DDL metadata, le health/readiness et la stratégie du test PostgreSQL réel sont désormais matérialisés.
Toute découverte qui exigerait :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
@@ -207,7 +207,7 @@ 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 / `TODO gate opérateur pre.007` pour la composition health.
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
@@ -331,7 +331,7 @@ 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 / TODO gate pre.007 / TODO pre.008 live`.
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / TODO exécution live pre.008`.
## 7. TLS
@@ -384,7 +384,7 @@ 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 / TODO pre.008 live`.
Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`.
### V32-MIG-002 — Version/checksum
@@ -400,7 +400,7 @@ 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 / TODO pre.008 live`.
Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`.
### V32-MIG-003 — Concurrence
@@ -408,7 +408,7 @@ Critère : deux runners concurrents sont sérialisés par advisory transaction l
`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 / TODO pre.008 concurrence réelle`.
Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO concurrence live opérateur`.
### V32-MIG-004 — Atomicité/recovery
@@ -416,7 +416,7 @@ Critère : échec d'une migration du run courant rollback DDL + history de ce ru
`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 / TODO pre.008 rollback réel`.
Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO rollback live opérateur`.
### V32-MIG-005 — Newer runtime guard
@@ -460,7 +460,7 @@ Critère : façade expose uniquement état/backend/network/counts/migration safe
`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 : `TODO gate opérateur pre.007`.
Statut : `PASS pre.007 opérateur`.
### V32-HEALTH-002 — Readiness réelle
@@ -468,7 +468,7 @@ Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify
`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 : `TODO gate opérateur pre.007 / TODO pre.008 live`.
Statut : `PASS pre.007 opérateur / TODO exécution live pre.008`.
### V32-HEALTH-003 — Error redaction
@@ -476,7 +476,7 @@ Critère : un health failure n'expose pas server error string, query text ou cre
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 : `TODO gate opérateur pre.007 / TODO pre.009 hardening`.
Statut : `PASS pre.007 opérateur / TODO pre.009 hardening`.
## 10. PostgreSQL integration réelle
@@ -488,10 +488,12 @@ Critères :
#[ignore]
URI lue depuis stdin
aucun env requis
URI jamais imprimée
URI jamais imprimée par le test
```
Statut : `TODO pre.008`.
`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 : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`.
### V32-LIVE-002 — Non destructif
@@ -504,7 +506,9 @@ aucune table métier créée
cleanup seulement de metadata créée par le test
```
Statut : `TODO pre.008`.
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 : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`.
### V32-LIVE-003 — Scénario foundation
@@ -515,21 +519,24 @@ connect
bootstrap initial
bootstrap idempotent
bootstrap concurrent
checksum mismatch
failure rollback
checksum mismatch + restore/recovery
failure rollback du V000 exact
health ready
close borné
cleanup metadata
```
Statut : `TODO pre.008/pre.010`.
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 : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`.
### 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.
Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur.
Statut : `TODO pre.008/pre.010`.
Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`.
## 11. Security/adversarial
@@ -561,7 +568,7 @@ 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 / `TODO gate opérateur pre.006` pour migration / `TODO pre.009` hardening.
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 / `TODO pre.009` hardening.
### V32-SEC-004 — Feature mismatch avant I/O
@@ -602,6 +609,16 @@ cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree --duplicates
```
Gate PostgreSQL réel `pre.008` 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