Files
khadhroony-solana-project/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
2026-08-29 19:53:00 +02:00

40 KiB

Plan 0.3.2 — Store/PostgreSQL runtime foundation

1. Objet et base

Cette release part exclusivement de la stable :

v0.3.1
workspace.package.version = 0.3.1

Le gate d'ouverture est :

0.3.2-pre.001

Les deux archives requises par le prompt sont disponibles et ont été auditées :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

[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 :

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 :

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 :

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 :

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 :

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 :

tokio-postgres

Raisons :

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 :

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 :

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 :

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 :

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 :

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 :

Disabled
VerifyFull

VerifyFull signifie :

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 :

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 :

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 :

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 :

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 :

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 :

Store::open(settings).await -> Result<Store>

Étapes :

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 :

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 :

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 :

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 :

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 :

ksp_store_schema_migrations

Elle contient au minimum :

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 :

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 :

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 :

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 candidate dans ksp-store-lib :

StoreHealthState
StoreHealthSnapshot
StoreRuntimeSnapshot

Projection sûre seulement :

backend kind
state
pool size/available sous forme de compteurs bornés
schema/migration version courante
pending migration count
last safe error code éventuel

Interdits : URI, host, username, database name si sensible, SQL, nom physique de relation, server error string, pool/client handles.

Le backend PostgreSQL peut utiliser SELECT 1 et une introspection minimale interne, puis mapper son résultat vers la projection portable.

12. Config std.store

12.1 Ownership

Seul ksp-config-lib possède :

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 :

{
  "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 :

# 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 :

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 :

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 :

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 :

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 :

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 sera dans ksp-store-postgres-lib/tests/ et ignoré par défaut.

Entrée :

une URI PostgreSQL dédiée lue depuis stdin
aucune variable d'environnement
aucun affichage de l'URI

Sécurité :

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 :

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é
migration failure injectée sous cfg(test) -> rollback/history inchangée
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.

17. Validation de dépendances et API

Canaris à créer au fil des tranches :

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 :

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 :

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 :

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 Cargo opérateur à exécuter.

La tranche introduit un moteur privé ksp-store-postgres-lib sans crate de migration externe :

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

Fermer StoreSettings -> Store -> PostgresBackend, health/readiness portable, close et mapping diagnostics.

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.

pre.009 — Hardening/completeness/dependency matrix

Inputs hostiles, redaction, no-env, exact exports/modules, external backend compatibility, --no-default-features, graphes/features/duplicates, non-régression API.

pre.010 — Gate technique final

Workspace/clippy/tests, targeted tests, PostgreSQL live gate, graphes Cargo finaux. Aucun développement fonctionnel nouveau.

pre.011 — Réconciliation documentaire finale

README/USAGE des deux crates, plan/validation/indexes/architecture réellement impactés. Pas de CHANGELOG/ROADMAP/prompt suivant.

pre.012 — Préparation de publication minimale

Uniquement la lane autorisée :

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 :

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

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 ne bloque pre.002.

Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :

DDL précis de ksp_store_schema_migrations en pre.006
forme finale des snapshots health en pre.007

Toute découverte qui exigerait :

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.