# Plan `0.3.2` — Store/PostgreSQL runtime foundation ## 1. Objet et base Cette release part exclusivement de la stable : ```text v0.3.1 workspace.package.version = 0.3.1 ``` Le gate d'ouverture est : ```text 0.3.2-pre.001 ``` Les deux archives requises par le prompt sont disponibles et ont été auditées : ```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. Les preuves internes concordantes sont : ```text workspace.package.version = 0.3.1 deltas/0.3.1/rel.001.md présent prompts/021-V0_3_2_START_PROMPT.md présent crates/ksp-store-api présent crates/ksp-store-lib absent crates/ksp-store-postgres-lib absent ``` `ksp-store-api` est conservé comme contrat backend-agnostic publié par `0.3.1`. `0.3.2` ne doit pas le remodeler pour simplifier PostgreSQL. ## 2. Scope de la release `0.3.2` introduit ensemble : ```text ksp-store-lib ksp-store-postgres-lib Config std.store connexion/pool/TLS PostgreSQL moteur privé de migrations/bootstrap diagnostics/health runtime minimaux integration test PostgreSQL réel opt-in ``` La release reste une fondation runtime/backend. Elle n'implémente aucune persistence métier : ```text RawTransaction réservé à 0.3.3 RawAccountState réservé à 0.3.4 CORE / DECODE / D3 / D4 hors scope ``` Aucune table, index, repository ou capability RAW n'est créé uniquement pour démontrer que PostgreSQL fonctionne. ## 3. Divergence normative découverte à l'ouverture La base stable contient une divergence réelle entre les documents les plus récents et deux règles plus anciennes. Les documents déjà cohérents avec la trajectoire `0.3.1` sont : ```text docs/plans/022-V0_3_1_STORE_RAW_PLAN.md docs/architecture/003-COMPONENT_CONTRACTS.md docs/architecture/004-COMPONENT_INVENTORY.md docs/architecture/005-DEPENDENCY_GRAPH.md ``` Ils imposent : ```text ksp-store-lib -> ksp-store-api ksp-store-lib[postgres] -> ksp-store-postgres-lib ksp-store-postgres-lib -> ksp-store-api ksp-store-postgres-lib -X-> ksp-store-lib ``` En revanche, la base `v0.3.1` conservait encore : ```text KSP-API-006 DEP-STORE-002 section ksp-store-lib de docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md ``` avec l'ancien modèle où PostgreSQL était physiquement contenu dans `ksp-store-lib`. `pre.001` réconcilie cette divergence sans changer le scope : ```text KSP-API-006 façade/runtime Store dans ksp-store-lib ; PostgreSQL dans ksp-store-postgres-lib DEP-STORE-002 façade commune + backends optionnels par feature DEP-STORE-009 ownership physique PostgreSQL par ksp-store-postgres-lib DEP-STORE-010 consumers ordinaires -> ksp-store-lib, jamais backend direct architecture 008 même séparation durable ``` Cette réconciliation est un prérequis du scaffold `pre.002` ; elle ne constitue pas une implémentation backend. ## 4. Graphe Cargo cible ### 4.1 Graphe KSP Le graphe cible de `0.3.2` est : ```text ksp-store-lib ├── ksp-store-api ├── ksp-logging-lib └── [feature 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 à partir de la tranche migrations ``` Interdictions : ```text ksp-store-postgres-lib -X-> ksp-store-lib ksp-store-lib -X-> ksp-config-lib ksp-store-postgres-lib -X-> ksp-config-lib ksp-store-* -X-> Transport/Program/Materializer apps/workers/jobs -X-> ksp-store-postgres-lib ``` `ksp-config-lib` pourra dépendre de `ksp-store-lib` pour construire des settings effectifs, comme pour les autres adapters Config -> runtime. ### 4.2 Features de `ksp-store-lib` Contrat retenu : ```toml [features] default = ["postgres"] postgres = ["dep:ksp-store-postgres-lib"] ``` `ksp-store-lib --no-default-features` reste compilable. Le type public identifiant PostgreSQL existe même lorsque la feature n'est pas compilée afin qu'une configuration connue mais indisponible puisse produire une erreur stable et explicite : ```text STORE_BACKEND_NOT_COMPILED ``` Aucune feature de backend ne modifie `ksp-store-api`. ### 4.3 Versions externes candidates retenues au gate Audit au 29 août 2026 : ```text PostgreSQL stable courant 18.6 tokio-postgres 0.7.18 deadpool-postgres 0.14.1 tokio-postgres-rustls 0.14.0 rustls 0.23 sha2 0.11.0 refinery audité mais rejeté 0.9.2 ``` Les manifests utiliseront les contraintes compatibles avec `DEP-CARGO-*`, sans pin patch arbitraire. Les versions réellement résolues seront enregistrées par les graphes de la tranche qui les ajoute. Licences auditées : ```text tokio-postgres MIT OR Apache-2.0 deadpool-postgres MIT OR Apache-2.0 tokio-postgres-rustls MIT sha2 MIT OR Apache-2.0 refinery MIT ``` Sources primaires/techniques consultées : ```text https://www.postgresql.org/support/versioning/ https://www.postgresql.org/docs/current/libpq-ssl.html https://www.postgresql.org/docs/release/18.6/ https://docs.rs/tokio-postgres/0.7.18/tokio_postgres/ https://github.com/rust-postgres/rust-postgres/blob/master/tokio-postgres/Cargo.toml https://docs.rs/deadpool-postgres/0.14.1/deadpool_postgres/ https://docs.rs/tokio-postgres-rustls/0.14.0/tokio_postgres_rustls/ https://docs.rs/sha2/0.11.0/sha2/ https://docs.rs/refinery/0.9.2/refinery/ ``` ## 5. PostgreSQL support policy ### 5.1 Major minimal KSP `0.3.2` retient : ```text major PostgreSQL minimal supporté : 15 major testé prioritairement : 18 patch de référence au gate : 18.6 ``` PostgreSQL 14 est encore supporté upstream au moment du gate mais atteint son EOL le 12 novembre 2026. L'ajouter à une nouvelle fondation en août 2026 créerait une dette immédiate sans bénéfice démontré. Aucun plafond runtime strict n'est fixé à 18 : un serveur plus récent n'est pas rejeté uniquement à cause de son major. Les garanties de release portent sur le minimum 15 et le major courant réellement testé. ### 5.2 Driver Décision acquise confirmée : ```text tokio-postgres ``` Raisons : ```text driver async natif ciblé API de transaction explicite support TLS via MakeTlsConnect connection future explicite pas d'ORM ni macro SQL imposée compatible avec l'ownership backend séparé ``` La future implémentation ne doit jamais appeler un chemin qui lit implicitement `PG*`, `.pgpass` ou l'environnement KSP. La configuration `tokio_postgres::Config` est construite uniquement depuis des settings reçus de la façade. ## 6. Pooling ### 6.1 Décision Le pool externe retenu est : ```text deadpool-postgres 0.14.x ``` Le choix est fondé sur l'ownership/lifecycle et non sur la popularité. L'audit source montre que le manager : ```text crée Client + connection future tokio-postgres spawn la connection future conserve le JoinHandle dans ClientWrapper abort le JoinHandle au Drop du wrapper ``` Le pool possède aussi un `close()` et des timeouts wait/create/recycle. ### 6.2 Alternative rejetée `bb8-postgres` a été audité comme candidat maintenu, mais son manager spawn la connection future lors de `connect` sans conserver le `JoinHandle` dans l'objet retourné. Cette propriété est moins adaptée au critère KSP : ```text aucune connection task orpheline shutdown borné et observable ownership explicite du lifecycle ``` Construire un pool KSP complet est également rejeté pour `0.3.2` : cela réimplémenterait admission, waiters, recycle et fermeture sans besoin métier démontré. ### 6.3 Bornes runtime Settings KSP candidats, à matérialiser exactement en `pre.003`/`pre.005` : ```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 ``` Ces bornes sont des garde-fous de ressources/lifecycle, pas des policies de workers/jobs. Aucun pool, client, statement cache ou type deadpool/tokio-postgres n'est exposé par `ksp-store-lib`. ## 7. TLS PostgreSQL ### 7.1 Connecteur retenu Décision : ```text tokio-postgres-rustls 0.14.x rustls 0.23 crypto provider aws-lc-rs root store système via native-certs ``` Ce choix reste cohérent avec la stack Rustls déjà présente dans le workspace et évite d'ajouter OpenSSL/native-tls comme dépendance fonctionnelle Store. ### 7.2 Modes publics La façade KSP n'expose initialement que : ```text Disabled VerifyFull ``` `VerifyFull` signifie : ```text TLS obligatoire chaîne certificat vérifiée contre les roots retenus nom serveur vérifié par rustls aucun fallback plaintext ``` Les modes `Prefer` et `Require` sans vérification de certificat ne sont pas exposés dans `0.3.2`. Le type `tokio_postgres::config::SslMode` ne suffit donc pas à exprimer la policy publique KSP ; il est un détail backend et sera forcé conformément au mode KSP. Custom CA, certificat client/mTLS et pinning sont reportés jusqu'à un besoin concret. Le backend ne lit jamais implicitement `sslrootcert`, `sslcert`, `sslkey` ou des fichiers libpq. ## 8. Store settings et backend selection ### 8.1 Ownership `StoreSettings` appartient à `ksp-store-lib` et ne dépend pas de Config. La forme retenue est typée, sans `serde_json::Value` opaque : ```text StoreSettings └── StoreBackendSettings └── Postgres(PostgresStoreSettings) ``` `PostgresStoreSettings` est un type KSP-owned de la façade, pas un type de `ksp-store-postgres-lib`. Il contient uniquement des primitives/settings KSP et reste disponible sans feature `postgres`. Lorsque la feature est active, `ksp-store-lib` convertit en privé ces settings vers un `PostgresBackendSettings` possédé par `ksp-store-postgres-lib`. Ce dernier n'est pas réexporté vers les consumers. ### 8.2 Surface candidate Les groupes de settings sont : ```text network RawNetworkId explicite, un seul réseau logique par instance Store connection_uri String possédée, Debug redacted, aucun getter de diagnostic pool bornes de section 6 tls Disabled ou VerifyFull bootstrap auto_migrate + migration_timeout_ms + lock_timeout_ms shutdown_timeout_ms lifecycle commun ``` Bornes bootstrap : ```text 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 ``` Il n'existe pas de `Default` qui invente une URI de production. Config et les callers programmatiques doivent construire explicitement le backend settings. ### 8.3 Surface exacte matérialisée en `pre.003` Les noms publics sont figés comme suit : ```text StoreBackendKind::Postgres StoreBackendSettings::Postgres(PostgresStoreSettings) PostgresTlsMode::{Disabled, VerifyFull} PostgresPoolSettings PostgresBootstrapSettings PostgresStoreSettings StoreSettings Store ``` `PostgresPoolSettings::default()` et `PostgresBootstrapSettings::default()` portent exactement les bornes décidées par le gate `pre.001`. À partir de `pre.004-fix.001`, `StoreSettings` exige aussi un `RawNetworkId` explicite et `StoreSettings::with_default_shutdown(network, ...)` fournit uniquement la borne commune de shutdown à 5 s. Il n'existe toujours aucun `Default` pour `PostgresStoreSettings` ou `StoreSettings`, afin de ne jamais inventer une URI ou un backend de production. Les validations `pre.003` restent backend-neutral et sans parsing PostgreSQL : URI non vide, pool 1..64, timeouts pool/connect 100..60 000 ms, migration 1 000..300 000 ms, lock 100..120 000 ms et shutdown 100..30 000 ms. Le parsing/normalisation de l'URI et les options réellement honorées restent propriétaires de `pre.005`. ### 8.4 URI et secrets La connexion URI est considérée sensible intégralement, même lorsqu'elle ne contient pas de mot de passe visible. Interdictions : ```text Debug de l'URI URI dans ErrorContext URI dans tracing URI dans health snapshot copie d'un message serveur contenant des valeurs arbitraires ``` Le backend parse la chaîne avec `tokio-postgres`, mais la policy TLS KSP ne peut pas être contournée par des paramètres URI conflictuels. Les options libpq susceptibles de charger des fichiers ou de modifier la policy TLS ne sont pas implicitement honorées par KSP ; les paramètres effectifs sont construits/normalisés depuis les settings typés. ## 9. Lifecycle `Store` ### 9.1 Ouverture Surface cible : ```text Store::open(settings).await -> Result ``` Étapes : ```text validate settings resolve backend kind reject known backend not compiled before I/O convert facade settings -> backend settings construct pool/TLS prove one bounded backend connection/readiness path run/verify bootstrap according to auto_migrate return Store only after readiness success ``` Le `Store` n'est pas `Clone` dans la fondation. Aucun pool/client backend ne peut s'échapper de la façade. ### 9.2 Fermeture Surface cible : ```text Store::close(self).await -> Result<()> ``` La fermeture consomme le Store, ferme le pool, interdit de nouvelles acquisitions et attend la terminaison nécessaire dans la borne `shutdown_timeout_ms`. `Drop` reste un fallback best-effort non bloquant ; le contrat de fermeture vérifiable est `close(self).await`. Cette forme limite structurellement le risque de close concurrent avec une opération qui emprunte encore le Store. ### 9.3 Erreurs stables matérialisées Domaine Store commun, figé en `pre.003` : ```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 ``` Backend PostgreSQL : ```text STORE_POSTGRES_CONFIG_INVALID STORE_POSTGRES_CONNECT_FAILED STORE_POSTGRES_POOL_TIMEOUT STORE_POSTGRES_TLS_FAILED STORE_POSTGRES_MIGRATION_FAILED STORE_POSTGRES_MIGRATION_MISMATCH STORE_POSTGRES_SCHEMA_NEWER STORE_POSTGRES_HEALTH_FAILED ``` Le mapping final doit conserver un contexte sûr seulement : backend code, action, état, version de migration non secrète, durée/compteur borné. Aucun texte d'erreur distant arbitraire n'est propagé. ## 10. Migrations/bootstrap ### 10.1 Décision Le moteur de migrations `0.3.2` est KSP-owned et privé à `ksp-store-postgres-lib`. `refinery 0.9.2` a été audité : il fournit migrations embarquées, table d'historique, détection divergent/missing et transaction groupée. Il n'est pas retenu car la fondation KSP a besoin d'un contrat plus petit, d'un nom/table/lock précis, d'un mapping d'erreurs KSP strict et d'un checksum cryptographique explicitement stable. Son checksum courant repose sur un `u64` produit à partir du `DefaultHasher` Rust, ce qui n'est pas le contrat de persistence que KSP souhaite figer. ### 10.2 Ressources Les migrations futures sont : ```text SQL statique KSP-owned embarquées au build avec include_str! ordonnées par version entière croissante nom immuable borné checksum SHA-256 du SQL exact aucune découverte filesystem runtime aucun down migration implicite ``` Une correction de schéma est une nouvelle migration forward. ### 10.3 Metadata interne `0.3.2` autorise uniquement la relation d'infrastructure fixe : ```text ksp_store_schema_migrations ``` Elle contient au minimum : ```text version BIGINT PRIMARY KEY name TEXT NOT NULL checksum TEXT NOT NULL applied_at TIMESTAMPTZ NOT NULL ``` Aucun nom de table/schema n'est configurable par l'utilisateur dans cette fondation. Cela élimine le besoin normal de SQL dynamique pour les identifiants. La création/forme de cette table est elle-même protégée par un sentinel de bootstrap version `0` avec checksum connu ; les migrations métier futures commenceront à `1`. ### 10.4 Algorithme Bootstrap : ```text acquire dedicated pooled client begin transaction acquire advisory transaction lock KSP fixe avec attente bornée set transaction-local statement timeout depuis valeur bornée create/verify history metadata verify bootstrap sentinel load applied migrations ordered reject unknown/newer versions reject missing historical versions reject name/checksum divergence apply each pending embedded migration in order insert corresponding history row commit once ``` L'advisory lock est acquis sans attente infinie : KSP utilise une tentative non bloquante répétée jusqu'à la deadline plutôt qu'un lock bloquant non borné. Une erreur de migration fait rollback de la transaction entière du run courant. Les migrations déjà commitées lors d'un run antérieur restent intactes. ### 10.5 Aucun schéma métier en `0.3.2` La liste de migrations de production `0.3.2` ne crée : ```text aucune table RawTransaction aucune table RawAccountState aucune table CORE/DECODE/SPECIALIZED aucun index métier ``` La seule persistence autorisée est la metadata privée de migration/bootstrap. ## 11. Health/readiness Le health portable est retenu dans `0.3.2`, mais limité à la fondation runtime. Justification : ```text Store::open doit distinguer construit vs réellement prêt integration réelle doit prouver connect/bootstrap ops futures ont besoin d'un diagnostic safe sans pool leak close doit être observable sans exposer le backend ``` Surface matérialisée dans `ksp-store-lib` par `pre.007` : ```text StoreHealthState StoreHealthSnapshot StoreRuntimeSnapshot ``` Projection sûre seulement : ```text StoreRuntimeSnapshot backend kind network logique pool capacity/size/available/waiting bornés StoreHealthSnapshot Ready | NotReady runtime snapshot migration version observée éventuelle pending migration count last safe ErrorCode éventuel ``` `Store::runtime_snapshot()` est synchrone et ne réalise aucun I/O. `Store::health().await` est un probe borné : acquisition Deadpool sous deadline, `SELECT 1`, puis lecture interne de la version maximale de `ksp_store_schema_migrations`. Un échec ne renvoie jamais le texte PostgreSQL ; il produit `NotReady` et un code KSP déjà classifié. La deadline du probe réutilise le `wait_timeout` du pool, avec un fallback interne borné uniquement si le pool ne fournit pas ce paramètre. Interdits : URI, host, username, database name si sensible, SQL, nom physique de relation dans la projection publique, server error string, pool/client handles. Le probe ne vérifie pas toute l'intégrité checksum à chaque appel : `Store::open` reste propriétaire de la vérification complète bootstrap/history avant de rendre une instance. Le health relit la disponibilité et la version de schéma comme diagnostic léger. ## 12. Config `std.store` ### 12.1 Ownership Seul `ksp-config-lib` possède : ```text config/std.store.json config/schemas/std.store.schema.json config/examples/std.store.example.json registry descriptors placeholder interpolation .env / process environment provenance/sensitivity adapter Config -> StoreSettings ``` Store et backend ne dépendent pas de Config et ne lisent aucun environnement. ### 12.2 Shape candidate Le document V1 est profilé comme les autres documents standards : ```json { "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 } } ] } ``` Chaque profile `std.store` est un **target Store nommé** : son `profile_id` est l'identifiant de target, son champ `network` fixe exactement un réseau logique et son backend désigne exactement une base/configuration physique. `default_profile` choisit le target autonome ; un job, worker ou app peut imposer un autre `profile_id` via le mécanisme Config existant. `ksp-store-lib` ne route jamais entre plusieurs targets. Le target par défaut committed est `devnet`, cohérent avec le défaut Transport et plus sûr pour les workflows de développement. Les targets `mainnet` et `testnet` utilisent des URI distinctes. Le fallback localhost de chaque target est classé `Secret` car il appartient à un placeholder `KSP_SECRET_*`; sa safe projection reste redacted. Il ne provoque aucune connexion automatique. `.env.example` ajoutera dans la même tranche : ```text # PostgreSQL connection URIs used by named Config std.store targets. # KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_devnet # KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_mainnet # KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://user:password@localhost/ksp_testnet ``` La valeur d'exemple commentée n'est jamais un vrai credential. ### 12.3 Feature mismatch Config reconnaît `backend = "postgres"` indépendamment de la feature du consumer. L'adapter peut construire le type public `StoreSettings::Postgres`; c'est `ksp-store-lib` qui retourne `STORE_BACKEND_NOT_COMPILED` si la feature manque. Ainsi Config ne doit pas connaître les `cfg(feature = "postgres")` de chaque consumer. ### 12.4 Invariant target/réseau Une instance `Store` ouverte reçoit exactement un `StoreSettings.network`. Ce réseau provient du target Config sélectionné, mais reste un contrat Store backend-neutral. Les futures opérations RAW doivent comparer ce réseau à celui de l'identité/query avant I/O : une transaction/account/query `devnet` soumise à un Store `mainnet-beta` doit être rejetée, jamais routée vers un autre Store. La sélection d'un autre réseau se fait en résolvant un autre profile/target puis en ouvrant une autre instance `Store`. ## 13. Logging et redaction Les deux crates runtime auront un `constants.rs` privé avec un target explicite : ```text ksp-store-lib target ksp-store-lib ksp-store-postgres-lib target ksp-store-postgres-lib ``` Elles utilisent `ksp-logging-lib`, jamais `tracing` directement comme surface KSP. Événements sûrs candidats : ```text store_open_start / store_open_ready / store_close postgres_pool_open / postgres_pool_close postgres_migration_start / applied / current / failed postgres_health ``` Champs autorisés : action, backend code, durations, counts, migration version/name bornée. Sont interdits URI, SQL, bind values, password, remote error detail et payload arbitraire. Le `tracing` transitif éventuel de `deadpool-postgres` reste une dépendance externe interne ; KSP ne le réexporte pas et la politique Logging existante garde les targets externes hors des diagnostics KSP normaux sauf configuration explicite. ## 14. Audit d'héritage kbot3 ciblé L'archive historique a été relue sur : ```text ks-store/Cargo.toml ks-store/src/store.rs ks-store/src/postgres/** ks-store/migrations/postgres/** ks-config/src/store.rs config/store.config.json config/schemas/std.store.schema.json docs/architecture/STORAGE_ARCHITECTURE.md docs/guides/POSTGRES_STORAGE.md ``` Constats : ```text backend PostgreSQL monolithique à l'intérieur de ks-store SQLx PgPool StoreOpenOptions avec backend_options serde_json::Value max_connections / connect_timeout / auto_initialize_schema initialisation transactionnelle + advisory lock health SELECT 1 / current_schema / version schema N1-N3 large et repositories déjà métier configuration historique capable de résoudre directement environnement KS* ``` Classification : ```text REPRENDRE façade backend-agnostic comme intention pool borné + timeout explicite auto-init explicite advisory lock pour bootstrap health léger SQL/objets physiques privés logs structurés sans SQL/binds REDESSINER séparation ksp-store-lib / ksp-store-postgres-lib tokio-postgres au lieu de SQLx settings typés au lieu de backend_options JSON TLS explicite vérifié migration history version + SHA-256 lifecycle close explicite error mapping sans texte SQLx/serveur Config actuel KSP avec KSP_SECRET_* seulement REPORTER 16 tables N1-N3 historiques 240 ressources SQL métier 79 index repositories RAW/CORE/DECODE processing ledgers/replay maintenance DROP/TRUNCATE REJETER SQLx comme driver KSP backend PostgreSQL physiquement dans la façade lecture directe de l'environnement par Store/backend anciens prefixes KS*/KB* backend options JSON opaques exposition d'erreurs backend brutes statut de schéma basé uniquement sur présence d'objets sans historique immuable ``` L'archive kbot3 est une source de design historique, jamais une base de copie de code ou de schéma. ## 15. Threat model `0.3.2` ### 15.1 Credential/diagnostic leak Risque : DSN/password via Debug, errors, tracing, snapshots ou panics. Mesures : wrapper/settings Debug redacted, aucun DSN dans ErrorContext, mapping stable des erreurs, aucune remote error string, tests canary avec secret hostile. ### 15.2 Bypass Config par environnement libpq Risque : `PGHOST`, `PGPASSWORD`, `.pgpass`, `sslrootcert` ou autres sources implicites changent le runtime. Mesures : Store/backend ne lisent aucun env, configuration driver construite depuis settings explicites, source scanner KSP, integration test stdin sans env. ### 15.3 Connection string hostile Risque : URI surdimensionnée, malformed, options conflictuelles TLS, valeurs loggées. Mesures : borne de longueur, parse avant I/O, erreurs génériques, policy TLS typée qui prime, pas d'écho input. ### 15.4 Connection storm / pool non borné Risque : nombre excessif de connexions, waiters ou timeouts infinis. Mesures : `max_connections <= 64`, tous les timeouts bornés, deadpool configuré explicitement, aucun retry loop de connexion caché dans Store. ### 15.5 Hung connect / migration / shutdown Risque : futur bloqué indéfiniment. Mesures : connect/pool timeouts, migration deadline, advisory try-lock borné, statement timeout local, close timeout. ### 15.6 Concurrent migration runners Risque : DDL/history race. Mesures : transaction unique + advisory transaction lock fixe, history relue sous lock, test concurrent réel. ### 15.7 Historical migration modifiée Risque : source rebuildée avec SQL différent sous même version. Mesures : nom/version immuables + SHA-256 persisté, mismatch terminal avant migration suivante. ### 15.8 Migration partielle Risque : DDL appliqué sans history ou inversement. Mesures : transaction unique pour le run, history insert dans la même transaction, rollback sur erreur, test d'échec injecté privé. ### 15.9 SQL/dynamic identifier injection Risque : nom de schema/table fourni par Config interpolé dans SQL. Mesures : aucun nom physique configurable en `0.3.2`; ressources SQL statiques et valeurs paramétrées. ### 15.10 Feature/config mismatch Risque : Config demande PostgreSQL mais binaire sans feature. Mesures : type backend connu toujours présent + `STORE_BACKEND_NOT_COMPILED` avant I/O. ### 15.11 Connection task leaked/dropped Risque : future tokio-postgres non pilotée ou tâche orpheline après close. Mesures : deadpool-postgres retenu précisément pour ownership du JoinHandle par client wrapper ; aucun client/pool ne fuit de la façade ; close explicite + Drop fallback. ### 15.12 Schema history newer than runtime Risque : ancien binaire lancé contre DB migrée par une version plus récente. Mesures : version appliquée inconnue/supérieure => `STORE_POSTGRES_SCHEMA_NEWER`, aucune migration/down automatique. ### 15.13 Server error echo Risque : message PostgreSQL incorpore table/value/credential/SQL snippet. Mesures : conversion immédiate vers code KSP + contexte allowlisté ; source error brute non incluse dans Display/Debug public. ## 16. PostgreSQL integration test réel 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 : ```text une URI PostgreSQL dédiée lue depuis stdin aucune variable d'environnement aucun affichage de l'URI ``` Sécurité : ```text refuser de démarrer si ksp_store_schema_migrations existe déjà ne créer aucune table métier ne DROP ni database ni schema cleanup best-effort uniquement de la table metadata que le test a prouvé avoir créée ``` Scénario minimal : ```text connect et major >= 15 bootstrap initial second bootstrap idempotent deux bootstrap concurrents après reset contrôlé corruption test-only du checksum sentinel -> mismatch explicite restore/reset contrôlé é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 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 Canaris à créer au fil des tranches : ```text ksp-store-lib --no-default-features compile postgres est feature default known-but-not-compiled error observable ksp-store-postgres-lib ne dépend pas ksp-store-lib Store/backend ne dépendent pas Config/Transport/Program/Materializer Store/backend production sources ne lisent pas env aucun type tokio-postgres/deadpool/rustls dans crate root ksp-store-lib ksp-store-api inchangé et dépend toujours seulement de ksp-core-lib aucune des 10 capabilities RAW implémentée par PostgreSQL en 0.3.2 aucune table RAW créée par migrations ``` La façade `ksp-store-lib` réexportera la surface crate-root de `ksp-store-api` nécessaire aux consumers, mais ne réexportera aucun type `ksp-store-postgres-lib`. ## 18. Sizing et prévision souple recalibrée Le scope reste clôturable dans une session si les responsabilités restent fines. La prévision initiale du prompt est conservée avec deux précisions : la réconciliation normative est absorbée par `pre.001`, et le choix migrations KSP-owned évite une tranche supplémentaire. ### `pre.001` — Audit, design, threat model et réconciliation normative Livrer le présent plan, la matrice validation, l'audit externe/kbot3, les décisions pool/TLS/migrations/Config, le graphe exact et la correction des règles Store obsolètes. Aucun code runtime. ### `pre.002` — Scaffold des deux crates + feature graph Créer les crates, manifests, modules minimaux, `postgres` default, `--no-default-features`, constants/logging targets et canaris de dépendances. Ajouter seulement les dépendances nécessaires au scaffold retenu. **Statut : matérialisé par `0.3.2-pre.002`. Le gate opérateur confirme le graphe et les tests ; `pre.002-fix.001` corrige les warnings `TRACING_TARGET` sans modifier le scope avant passage à `pre.003`.** La tranche matérialise exactement : ```text ksp-store-lib -> ksp-store-api ksp-store-lib[postgres] -> ksp-store-postgres-lib ksp-store-postgres-lib -> ksp-store-api ksp-store-postgres-lib -X-> ksp-store-lib ``` Les deux crates possèdent déjà leur `src/constants.rs` et leur `TRACING_TARGET`, comme l'exigent les règles KSP-TRACE-102/103. Le gate opérateur de `pre.002` a toutefois révélé que le simple réexport privé produisait `unused_imports` et `dead_code` tant qu'aucun comportement de production n'émet encore de log. `pre.002-fix.001` conserve donc l'invariant de tracing et ajoute uniquement un canari compile-time privé `const _: &str = TRACING_TARGET;` : le target participe au scaffold compilé sans faux log, sans `allow(...)` et sans ajouter prématurément `ksp-logging-lib`. Tokio, tokio-postgres, Deadpool, Rustls et SHA-256 restent absents tant que leurs tranches d'usage réel ne sont pas ouvertes. ### `pre.003` — Settings + backend selection + lifecycle contracts Matérialiser `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, erreurs stable, façade `Store` sans connexion lourde et réexports API. Tester la feature mismatch. **Statut : matérialisé par `0.3.2-pre.003`, gate opérateur requis.** La tranche fixe initialement : ```text 73 exports crate-root de ksp-store-lib : 60 reexports ksp-store-api + 13 éléments runtime Store settings Config-independent sans serde/env URI PostgreSQL intégralement redacted et sans getter public pool/bootstrap/shutdown bornés selon pre.001 Store::open(settings).await Store::close(self).await postgres absent de la build -> store.backend_not_compiled avant I/O postgres compilé avant pre.005 -> store.backend_open_failed/runtime_foundation_pending avant I/O ``` Le dernier comportement est volontairement transitoire : `pre.003` ne retourne jamais un faux `Store` prêt. `pre.005` remplacera uniquement ce stop de staging par la construction physique, puis `pre.007` fermera le shutdown/health end-to-end. Aucun type PostgreSQL physique n'est public. ### `pre.004` — Config `std.store` Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity/redaction, targets nommés réseau-spécifiques, packaging resources strictement nécessaires. Aucun env dans Store/backend. `pre.004` matérialise les noms canoniques `std.store.json`, `std.store.schema.json` et `std.store.example.json`, conformément au prompt de démarrage et aux conventions Config existantes. ### `pre.005` — PostgreSQL connection + deadpool + Rustls Matérialiser le bridge physique backend sans modifier la façade publique backend-neutral : ```text ksp-store-lib -> PostgresBackendSettings privé à la composition -> PostgresBackend::open/close ksp-store-postgres-lib -> tokio-postgres >= 0.7.18 -> deadpool-postgres ^0.14.2 -> tokio-postgres-rustls 0.14.x -> rustls 0.23.x + AWS-LC -> roots système via rustls-native-certs ``` Après parsing, la policy KSP réécrit `application_name`, `connect_timeout`, `sslnegotiation` et `sslmode`; les `options=` serveur sont rejetées. `Disabled` force le plaintext, `VerifyFull` force TLS + roots système + identité serveur et interdit un target `hostaddr` sans `host` ainsi que les sockets Unix, sur lesquels PostgreSQL ne négocie pas TLS. Le pool applique `max_size` et deadlines `wait/create/recycle`, utilise le recycling `Verified`, et `Store::open` ne réussit qu'après une première acquisition physique. Les erreurs externes ne sont jamais conservées comme source/texte public. `Store::close` ferme et draine sous deadline; `Drop` reste best-effort. Aucun SQL KSP, migration ou table métier n'est introduit. Le gate opérateur de `pre.005-fix.001` est vert : audits, workspace check/Clippy, tests backend, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Les versions résolues observées sont `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2` et `tokio-postgres-rustls 0.14.0`. ### `pre.006` — Migration/bootstrap foundation Statut : matérialisé par `0.3.2-pre.006`, gate opérateur vert. La tranche introduit un moteur privé `ksp-store-postgres-lib` sans crate de migration externe : ```text sha2 0.11.x explicite V000__bootstrap.sql embarqué par include_str! ksp_store_schema_migrations uniquement version 0 / nom bootstrap / SHA-256 exact du SQL transaction unique pg_try_advisory_xact_lock($1) KSP fixe sous deadline statement_timeout transaction-local via set_config(..., true) forme metadata minimale vérifiée history divergente/missing -> migration mismatch version appliquée > 0 -> schema newer aucun down migration aucun schéma RAW ``` `auto_migrate = false` ne crée jamais la metadata absente : l'ouverture échoue avec un état `migration_pending` sûr. Si la metadata existe, elle est toujours vérifiée. Une table valide mais sans sentinel n'est jamais réparée implicitement : elle est considérée divergente. Les colonnes metadata obligatoires sont vérifiées sans interdire d'éventuelles colonnes supplémentaires futures, afin qu'un ancien runtime puisse encore lire l'historique puis classer correctement une version plus récente comme `schema_newer`. Le bootstrap version `0` est spécial : lorsque la metadata est absente et l'auto-migration autorisée, le DDL embarqué est exécuté puis le sentinel est inséré dans la même transaction. Toute erreur ou expiration fait abandonner la transaction ; le rollback PostgreSQL implicite au drop protège DDL + history du run courant. Les preuves concurrentes et rollback injecté sur serveur réel restent à `pre.008`. Avec les quatre codes PostgreSQL ajoutés en `pre.005` puis les trois codes migration de `pre.006`, la façade atteint désormais 80 exports crate-root : 60 réexports `ksp-store-api` et 20 éléments runtime Store. ### `pre.007` — Composition end-to-end + health 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 : ```text Store::runtime_snapshot() -> StoreRuntimeSnapshot Store::health().await -> StoreHealthSnapshot StoreHealthState = Ready | NotReady ERROR_CODE_POSTGRES_HEALTH_FAILED = store.postgres_health_failed ``` Le backend ajoute uniquement deux DTO bridge safe (`PostgresBackendRuntimeSnapshot`, `PostgresBackendHealthSnapshot`). Les compteurs `max_size/size/available/waiting` sont saturés en `u32`. Le probe est borné, exécute seulement une disponibilité légère et la lecture de version de migration, puis mappe tout échec vers une classification backend statique sans source externe. `Store::open` ne change pas de sémantique : il ne rend une instance qu'après connexion physique et bootstrap/history vérifiés. Aucun SQL métier RAW n'est introduit. Avec `StoreHealthState`, `StoreRuntimeSnapshot`, `StoreHealthSnapshot` et `ERROR_CODE_POSTGRES_HEALTH_FAILED`, la façade atteint désormais 84 exports crate-root : 60 réexports `ksp-store-api` et 24 éléments runtime Store. ### `pre.008` — PostgreSQL integration réelle 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 Base requise : `0.3.2-pre.008` avec gate déterministe vert et preuve PostgreSQL réelle verte. Le 29 août 2026, le smoke opt-in a validé la fondation sur PostgreSQL 17 : bootstrap initial, réouverture idempotente, concurrence sous advisory lock, corruption/restauration checksum, rollback transactionnel, health `Ready` et fermeture bornée. Cette tranche reste un hardening de preuve : aucun `src/**`, aucune migration SQL et aucune dépendance runtime ne doivent être modifiés sauf découverte d'un défaut réel par les canaris. Canaris ajoutés : ```text ksp-store-lib modules privés exacts 84 exports crate-root exacts manifest exact : logging + Store API + backend optionnel feature postgres default exacte URI hostile/secret canary redacted dans Settings et Error même preuve avec et sans default features aucune lecture Config/env/PG*/.pgpass aucun type PostgreSQL physique ou SQL dans la façade ksp-store-postgres-lib modules privés exacts 7 exports bridge exacts manifest physique exact URI malformed/surdimensionnée/hostaddr-only/options rejetées avant I/O canary secret absent des Debug/errors publics PostgresBackendError ne peut retenir que kind + phase statique PoolError::Backend perd explicitement le texte remote aucune lecture env/PG*/.pgpass ni fichiers TLS libpq implicites aucune capability RawTransaction/RawAccount implémentée V000 reste metadata-only ``` La compatibilité backend externe reste prouvée par `ksp-store-api/tests/external_backend.rs`; `pre.009` la rejoue sans modifier `ksp-store-api`. Gate ciblé 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 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 ``` ### `pre.010` — Gate technique final Statut : matérialisé et gate technique opérateur exécuté le 29 août 2026. Le gate part de `0.3.2-pre.009` avec hardening/completeness entièrement vert. Il a été rejoué après `cargo clean` sur `0.3.2-pre.10` et confirme : ```text audits Rust clean workspace check + Clippy PASS toutes les crates ciblées PASS ksp-store-lib default + no-default-features PASS cargo test --workspace PASS graphes normal/features/duplicates exécutés 3 builds Tauri Linux PASS PostgreSQL live foundation PASS sur major 17 ``` La commande opérateur d'audit Markdown a ciblé par erreur `deltas/0.3.1` au lieu de `deltas/0.3.2`. L'overlay `pre.010` avait été audité avec le bon chemin dans l'environnement de génération ; `pre.011` rejoue obligatoirement l'audit Markdown complet avec `deltas/0.3.2` avant de figer la documentation. Ce défaut de scope de commande ne révèle aucun échec runtime et n'ouvre pas de `pre.010-fix`. Cette tranche ne modifie aucun code de production, test fonctionnel, migration, Config ou dépendance runtime. Elle synchronise uniquement la version de prerelease et porte le gate technique final. Gate final exécuté : ```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 smoke PostgreSQL réel est ensuite rejoué sur une base 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 modifié les resources de packaging desktop pour `std.store`, le gate 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) ``` Aucun développement fonctionnel nouveau n'est autorisé dans `pre.010`. Tout défaut découvert ouvre un `pre.010-fix.NNN` avant la réconciliation documentaire. ### `pre.011` — Réconciliation documentaire finale Statut : matérialisé par `0.3.2-pre.011`; gate documentaire opérateur requis après application. La tranche fige les références durables réellement concernées : ```text ksp-store-lib/README.md + USAGE.md ksp-store-postgres-lib/README.md + USAGE.md ksp-config-lib README/USAGE complétés pour std.store docs/000-README.md docs/plans/000-README.md docs/validation/000-README.md plan 023 validation 019 ``` Les architectures `003/004/005/008` ont été relues sur la base `pre.010` et restent cohérentes avec la surface réellement livrée ; elles ne sont pas modifiées artificiellement. Le choix historique de `0.3.1-pre.009` de ne pas créer un README/USAGE `ksp-store-api` n'est pas rouvert dans cette release backend/runtime. `CHANGELOG.md`, `ROADMAP.md` et le prompt `0.3.3` restent strictement réservés à `pre.012`. ### `pre.012` — Préparation de publication minimale Uniquement la lane autorisée : ```text Cargo.toml CHANGELOG.md ROADMAP.md prompts/022-V0_3_3_START_PROMPT.md deltas/0.3.2/pre.012.md ``` ### `rel.001` — Publication stable Version stable + delta de publication uniquement. ## 19. Critères de clôture de la release `0.3.2` ne ferme que si : ```text les deux crates existent feature postgres default et no-default compile graphe backend séparé prouvé tokio-postgres/deadpool/Rustls bornés et privés Config std.store possédée par ksp-config-lib aucune lecture env/PG*/.pgpass dans Store/backend migration history version/checksum/lock/rollback prouvée health/readiness safe prouvé PostgreSQL réel vert close borné vert aucun RawTransaction/RawAccountState implémenté aucun SQL métier RAW livré ksp-store-api non régressé README/USAGE et validation réconciliés workspace/clippy/tests/graphes verts ``` ## 20. Hors scope explicite ```text RawTransaction persistence/queries/retention RawAccountState persistence/queries/retention batch/backlog/priority worker/job claim/lease métier notifications persistées replay Core/Decode schema métier kbot3 SQLx ORM custom CA/mTLS sans besoin concret maintenance destructive publique Store Desk ``` ## 21. Questions restantes Aucune question architecturale ou technique ne bloque la préparation de publication de `0.3.2`. La fondation runtime/backend est fermée fonctionnellement. Les seules surfaces volontairement reportées sont les vertical slices métier déjà réservées à `0.3.3+` : persistence/query/rétention `RawTransaction`, puis `RawAccountState` et conformance RAW. Toute découverte qui exigerait : ```text un type PostgreSQL dans ksp-store-api une dépendance backend -> ksp-store-lib une lecture directe d'environnement une table RAW métier dans 0.3.2 ``` est considérée comme contradiction architecturale et doit être réauditée avant poursuite.