Files
khadhroony-solana-project/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
2026-08-30 05:55:29 +02:00

48 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 matérialisée dans ksp-store-lib par pre.007 :

StoreHealthState
StoreHealthSnapshot
StoreRuntimeSnapshot

Projection sûre seulement :

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 :

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 est matérialisé par ksp-store-postgres-lib/tests/postgres_foundation_live.rs et reste 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é
é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 :

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 opérateur vert.

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

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 :

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 :

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 :

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 :

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 :

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

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 :

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 :

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

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 :

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

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.