diff --git a/Cargo.toml b/Cargo.toml index 4ce5d16..9ff4e14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 347 +# version: 348 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.2" +version = "0.3.3-pre.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/deltas/0.3.3/pre.001.md b/deltas/0.3.3/pre.001.md new file mode 100644 index 0000000..e662612 --- /dev/null +++ b/deltas/0.3.3/pre.001.md @@ -0,0 +1,283 @@ + + + +# Delta `0.3.3-pre.001` — audit et physical design RawTransaction PostgreSQL + +## 1. Objet + +Ouverture de : + +```text +0.3.3 — Store/PostgreSQL RawTransaction vertical slice +``` + +Cette tranche reste volontairement documentaire/conception : elle fixe l'audit, le threat model, le design physique V001 et le séquencement avant toute migration ou persistence métier. + +## 2. Base + +Base canonique : + +```text +v0.3.2 +``` + +Le gate opérateur fourni pour `0.3.2` est propre : audits Rust/Markdown, workspace check, Clippy, tests Store/API/PostgreSQL foundation/Config et `ksp-store-lib --no-default-features` passent ; les tests live restent opt-in/ignored conformément à la fondation. + +## 3. Version + +Version workspace après overlay : + +```text +0.3.3-pre.1 +``` + +Label de livraison : + +```text +0.3.3-pre.001 +``` + +## 4. Fichiers ajoutés + +```text +docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md +docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md +deltas/0.3.3/pre.001.md +``` + +## 5. Fichier modifié + +```text +Cargo.toml +``` + +Modifications : + +```text +workspace.package.version : 0.3.2 -> 0.3.3-pre.1 +file header version : 347 -> 348 +``` + +## 6. Fichiers supprimés + +Aucun. + +## 7. Audit effectué + +Ont été relus avant design : + +- règles générales/KSP/Rust/dépendances/docs/file contracts/version workflow/prompt structure ; +- architectures Layers/Dependencies/Store/API/Config concernées ; +- plan/validation `0.3.1` ; +- source/tests complets `ksp-store-api` ; +- plan/validation `0.3.2` ; +- source/tests complets `ksp-store-lib` et `ksp-store-postgres-lib` ; +- surface Config `std.store` ; +- archive historique kbot3 ciblée demandée par le prompt ; +- documentation PostgreSQL/tokio-postgres nécessaire aux choix numériques et de concurrence. + +## 8. Décisions majeures + +### 8.1 `slot: u64` + +`BIGINT` est rejeté pour `slot` car il narrowe le domaine API. V001 utilisera : + +```text +NUMERIC(20,0) +``` + +avec conversion décimale fallible et tests jusqu'à `u64::MAX`. + +### 8.2 Réseau physique + +V001 introduira : + +```text +ksp_store_identity +``` + +Une base V001 est mono-réseau. Le binding est créé/validé sous le bootstrap lock afin qu'une URI pointant vers un autre réseau échoue avant exposition d'un backend prêt. + +### 8.3 Schéma RAW minimal + +Tables décidées : + +```text +ksp_store_identity +ksp_raw_transactions +ksp_raw_transaction_observations +ksp_raw_transaction_archive_payloads +``` + +Index métier additionnel unique : + +```text +(slot, signature) WHERE retention_state <> 'purged' +``` + +Aucun index provider/status/created_at/hash n'est ajouté sans requête correspondante. + +### 8.4 Atomicité/idempotence + +Le repository n'utilisera aucun `has_*` préalable. La stratégie est : + +```text +INSERT ... ON CONFLICT DO NOTHING RETURNING +-> si conflit : SELECT ... FOR UPDATE +-> comparaison exacte du contenu +-> AlreadyPresent ou ERROR_CODE_RAW_CONFLICT +``` + +Le canonique et son observation d'acquisition sont dans la même transaction PostgreSQL. + +### 8.5 Pagination + +Ordre total : + +```text +(slot, signature) +``` + +Cursor backend-private V1 fixe 109 octets, lié par SHA-256 au réseau, à la direction, au slot range et à la dernière clé. Aucun cap métier arbitraire n'est ajouté. + +### 8.6 Rétention + +`Full`, `Archived` et `Purged` sont représentables honnêtement par le backend `0.3.3` avec une relation archive séparée du hot path. + +`Compacted` n'est pas assimilé au TOAST PostgreSQL et n'est pas simulé. Le plan `0.3.1` le qualifiait déjà d'optionnel ; aucun changement `ksp-store-api` n'est donc justifié. Les transitions qui l'impliquent seront rejetées avant I/O par `store.postgres_retention_compaction_unsupported`. + +## 9. Audit kbot3 + +Classification effectuée : + +- **REPRENDRE** : unicités, FK, keyset/index slot conceptuels ; +- **REDESSINER** : fixed bytes, payload opaque, u64 exact, atomicité, conflit réel, réseau, retention compare-and-transition ; +- **REPORTER** : indexes provider/status/time, processing/replay, batches, apps, AccountState ; +- **REJETER** : sqlx, `has_*`, JSONB canonique, narrowing i64/i32, FK `SET NULL`, error text SQL, caps 500/1000, processing_state RAW. + +Aucun code historique n'est copié. + +## 10. Threat model couvert + +Le plan couvre explicitement : + +- mauvaise URI/réseau ; +- DB bytes malformés ; +- overflow u64 ; +- observation orpheline ; +- inserts identiques/divergents concurrents ; +- collision observation ; +- cursor hostile/replay ; +- ambiguïté pagination ; +- retention race ; +- purge/read/write race ; +- ForceRehydrate race ; +- fuite SQL/server text ; +- cancellation/rollback ; +- migration mismatch. + +## 11. Séquencement recalibré + +Le forecast initial est étendu à `pre.013` afin de séparer proprement : + +1. généralisation du moteur de migration ; +2. V001/binding réseau ; +3. mapping/read ; +4. write/observation atomiques ; +5. pagination ; +6. rétention ; +7. façade ; +8. live PostgreSQL ; +9. hardening ; +10. gate ; +11. docs ; +12. publication. + +## 12. Hors scope conservé + +Aucun changement fonctionnel n'est apporté à : + +```text +RawAccountState +N2 +workers/jobs/backfills +apps +transport +codecs wire +event bus +compression/archive worker global +autres backends +``` + +## 13. Validations exécutées + +Audit statique de la base et de l'overlay : + +```text +python3 scripts/audit_rust_workspace_rules.py + General Rust rule audit: clean + Rust export completeness audit: 0 candidate(s) + KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.3 + Markdown table audit: clean (214 table(s), 131 file(s)) + +assertions structurelles locales + workspace.package.version = 0.3.3-pre.1 + headers/version des trois nouveaux Markdown corrects + aucune référence résiduelle au faux code API RAW_RETENTION_UNSUPPORTED +``` + +L'audit de base couvre aussi manifests/features, modules/exports, V000 et checksum, Config `std.store`, plans/validations `0.3.1`/`0.3.2`, tests hardening/completeness et preuve live documentée. + +## 14. Validations non exécutées + +Le conteneur d'assemblage ne fournit pas l'exécutable `cargo`. Les commandes suivantes sont donc **NON EXÉCUTÉES**, jamais déclarées PASS : + +```text +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +Les tests PostgreSQL live ne sont pas exécutés non plus. `pre.001` ne modifie aucun SQL/runtime métier, la preuve foundation réelle `0.3.2` est déjà acquise et le futur test RawTransaction n'existe pas encore. + +## 15. Questions ouvertes + +Aucune question de design bloquante n'est laissée ouverte par `pre.001`. + +Les valeurs runtime retenues sont déjà fixées : + +```text +store.postgres_retention_compaction_unsupported +store.wrong_network +store.raw_reference_not_found +store.postgres_read_failed +store.postgres_write_failed +store.postgres_data_invalid +store.postgres_page_limit_unsupported +``` + +Le checksum V001 n'est pas une question de design : il sera calculé sur les bytes exacts lorsque `V001__raw_transaction.sql` sera créé en `pre.003`. + +## 16. Gate demandé + +```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.3 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +Aucun test PostgreSQL live n'est requis dans cette tranche documentaire. diff --git a/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md b/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md new file mode 100644 index 0000000..b9ebd69 --- /dev/null +++ b/docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md @@ -0,0 +1,1161 @@ + + + +# Plan `0.3.3` — Store/PostgreSQL RawTransaction vertical slice + +## 1. Statut du document + +Ce document fixe le plan de la release : + +```text +0.3.3 — Store/PostgreSQL RawTransaction vertical slice +``` + +Base canonique auditée : + +```text +v0.3.2 +``` + +Première tranche : + +```text +0.3.3-pre.001 — audit, threat model, physical design, sizing et planning +``` + +`pre.001` est volontairement une tranche de conception. Elle ne crée ni migration métier `V001`, ni repository PostgreSQL métier, ni dispatch RAW dans `ksp-store-lib`. + +## 2. Sources et autorité + +L'ordre d'autorité retenu est : + +1. règles et architectures KSP courantes ; +2. contrats stables `ksp-store-api` acquis en `0.3.1` ; +3. fondation runtime/backend PostgreSQL stable acquise en `0.3.2` ; +4. documentation officielle PostgreSQL et `tokio-postgres` lorsque le design physique en dépend ; +5. archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` uniquement comme matériau de comparaison. + +L'archive historique n'est ni une base de code, ni une autorité d'architecture. + +## 3. Rappel des frontières acquises + +La release conserve strictement les dépendances suivantes : + +```text +consommateurs + -> ksp-store-lib + -> ksp-store-api + -> ksp-store-postgres-lib [feature postgres] + +ksp-store-postgres-lib + -> ksp-store-api + -X-> ksp-store-lib +``` + +Les responsabilités restent : + +- `ksp-store-api` : modèles, capacités et contrats backend-agnostiques ; +- `ksp-store-lib` : façade runtime commune, validation, feature gating et dispatch ; +- `ksp-store-postgres-lib` : pool, TLS, SQL, migrations, mapping physique et implémentation PostgreSQL ; +- `ksp-config-lib` : propriété exclusive de `std.store`, des variables d'environnement, de leur sensibilité et de leur mapping vers les settings runtime. + +Aucun worker, job, transport, decoder, Program, Materializer ou application n'entre dans cette release. + +## 4. Surface RAW exacte à livrer + +La vertical slice `RawTransaction` couvre exactement six capacités déjà stables dans `ksp-store-api` : + +```text +RawTransactionRead +RawTransactionWrite +RawTransactionObservationRead +RawTransactionObservationWrite +RawTransactionRetentionRead +RawTransactionRetentionWrite +``` + +Les contrats essentiels associés sont : + +- identité transaction = `(network, signature)` ; +- `RawTransactionSignature` = 64 octets ; +- `RawObservationKey` = 32 octets ; +- `RawContentHash` = 32 octets ; +- `slot: u64` sans narrowing ; +- `format_version: u32` ; +- `RawTimestamp` = millisecondes Unix `u64`, borné à `253_402_300_799_999` ; +- payload RAW non vide, borné à 16 MiB ; +- source payload size borné à 64 MiB ; +- observations séparées du contenu canonique ; +- insertion idempotente seulement si le contenu canonique est réellement identique ; +- divergence sous la même identité = `ERROR_CODE_RAW_CONFLICT` ; +- pagination Store = navigation/cursorisation, jamais politique de batch worker ; +- tombstone minimal après purge ; +- rehydration forcée explicite et distincte d'une transition de rétention ordinaire. + +## 5. Inventaire exact de la fondation `0.3.2` + +### 5.1 Cargo/features/exports/modules + +La base stable expose exactement : + +```text +ksp-store-api + modules privés crate-root : capability, error, model + pub use crate-root : 60 + dépendance runtime : ksp-core-lib uniquement + +ksp-store-lib + modules privés crate-root : constants, error, health, settings, store + pub use crate-root : 84 + pub(crate) use : TRACING_TARGET + default feature : postgres + feature postgres : dep:ksp-store-postgres-lib + deps : ksp-logging-lib, ksp-store-api, backend optionnel + +ksp-store-postgres-lib + modules privés crate-root : constants, error, health, migration, runtime + pub use crate-root : 7 + pub(crate) use crate-root : 6 + deps physiques : deadpool-postgres, rustls, rustls-native-certs, sha2, tokio, tokio-postgres, tokio-postgres-rustls + dépendance Store : ksp-store-api seulement ; jamais ksp-store-lib +``` + +Versions workspace pertinentes acquises : + +```text +deadpool-postgres ^0.14 +rustls ^0.23 +rustls-native-certs ^0.8 +sha2 ^0.11 +tokio-postgres ^0.7 +tokio-postgres-rustls ^0.14 +``` + +`0.3.3` n'est pas une release de mise à jour de dépendances ; aucune version n'est changée par `pre.001`. + +### 5.2 Migration V000 exacte + +Ressource acquise : + +```text +crates/ksp-store-postgres-lib/migrations/V000__bootstrap.sql +version = 0 +name = bootstrap +SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +``` + +Le SQL crée uniquement `ksp_store_schema_migrations`. Le moteur stable considère toute migration appliquée `> 0` comme `SchemaNewer`; il doit donc être généralisé avant V001. + +Convention figée pour la nouvelle ressource : + +```text +crates/ksp-store-postgres-lib/migrations/V001__raw_transaction.sql +version = 1 +name = raw_transaction +checksum SHA-256 calculé sur les bytes embedded exacts +``` + +### 5.3 Config `std.store` final + +`config/std.store.json` est `format_version = 1`, profil par défaut `devnet` et contient exactement : + +```text +profile_id=devnet network=devnet backend=postgres +profile_id=mainnet network=mainnet-beta backend=postgres +profile_id=testnet network=testnet backend=postgres +``` + +Chaque profil possède sa propre URI secrète KSP, pool borné, TLS `verify_full`, `auto_migrate=true`, timeouts migration/lock et shutdown. Le backend ne lit ni env ni fichier libpq directement. + +### 5.4 Lifecycle/network final + +`Store` possède `backend_kind`, `network`, backend compilé et `shutdown_timeout`. `Store::open` valide avant dispatch. `PostgresBackend` possède son `network` et son pool, puis bootstrappe avant de devenir exploitable. + +Le réseau est actuellement une propriété runtime ; aucune identité réseau n'est encore persistée en V000. C'est précisément le gap physique traité par V001. + +### 5.5 Hardening/completeness final + +Les canaris `0.3.2` figent notamment : + +- aucun handle PostgreSQL/SQL/env dans `ksp-store-lib` ; +- aucun reverse edge `ksp-store-postgres-lib -> ksp-store-lib` ; +- erreurs URI/driver redacted ; +- modules/exports/manifests exacts ; +- aucun repository/capability métier dans la foundation. + +### 5.6 Preuve PostgreSQL réelle `0.3.2` + +`pre.008` puis `pre.010` ont réellement exécuté `postgres_foundation_live` sur PostgreSQL 17. La preuve acquise couvre bootstrap initial/idempotent/concurrent, checksum mismatch + recovery, rollback transactionnel, health Ready, fermeture bornée et cleanup. + +Dans le gate final `v0.3.2`, ce test reste normalement `#[ignore]`; son statut ignored dans un `cargo test` standard ne remet pas en cause la preuve live antérieure documentée. + +### 5.7 `ksp-store-lib` + +La fondation stable contient uniquement les modules métier-neutres : + +```text +constants +error +health +settings +store +``` + +`Store::open` valide les settings puis construit le backend compilé. `Store` possède le réseau logique, le backend et le timeout de fermeture. Il ne dispatch encore aucune des six capacités RAW. + +### 5.8 `ksp-store-postgres-lib` + +La fondation stable contient : + +```text +constants +error +health +migration +runtime +``` + +`PostgresBackend` possède le réseau et le pool. L'ouverture : + +1. normalise la configuration physique ; +2. construit le pool `deadpool-postgres` borné ; +3. acquiert un client ; +4. exécute le bootstrap/migration sous transaction et advisory lock ; +5. publie uniquement des erreurs sûres et statiques. + +Aucune capacité de persistence métier n'existe encore. + +### 5.9 Migration existante + +La seule migration acquise est : + +```text +V000__bootstrap.sql +``` + +Elle crée `ksp_store_schema_migrations`. Le moteur `0.3.2` est volontairement spécialisé sur V000 : toute version appliquée `> 0` est actuellement classée `SchemaNewer`. + +Avant V001, le moteur doit donc être généralisé en registre ordonné de migrations embedded, sans perdre : + +- checksum SHA-256 ; +- advisory transaction lock ; +- validation de l'historique ; +- refus d'un schéma plus récent ; +- absence de down-migration ; +- transaction atomique ; +- erreur sûre sans SQL/server text. + +## 5.10 Audit exact des six capabilities + +Les six traits sont déjà dyn-compatible et retournent `StoreApiFuture`; `0.3.3` ne les fusionne pas. La matrice suivante fixe leur traduction physique. + +### `RawTransactionRead` + +| Axe | Décision | +|----------------------|--------------------------------------------------------------------------------------------------------------------------| +| Input | `RawTransactionReference` pour `get`; `RawTransactionQuery` pour `list` | +| Pré-I/O | network de reference/query = backend network; query/cursor déjà validés par l'API puis cursor backend décodé strictement | +| Transaction SQL | non pour `get`/`list`; un statement cohérent suffit | +| Rows | `get`: transaction + LEFT JOIN archive; `list`: index transaction `(slot, signature)` hors purged | +| Idempotence | N/A, lecture pure | +| Conflit | N/A; incohérence physique = erreur sûre, jamais conflict métier | +| Absence | `get -> None`; `list -> page vide` | +| Error mapping | query invalid API; wrong-network sûr; query/data-corrupt PostgreSQL statiques | +| Concurrence | snapshot de statement; aucune promesse de snapshot inter-pages | +| Test déterministe | mapping full/archived/purged, u64 max, ranges, directions, cursor hostile | +| Test PostgreSQL live | exact get + pagination multi-page/ties/ranges | + +### `RawTransactionWrite` + +| Axe | Décision | +|----------------------|----------------------------------------------------------------------------------| +| Input | `RawTransaction`, `RawTransactionObservation`, `RawTransactionAcquisitionMode` | +| Pré-I/O | deux networks égaux au backend; observation.transaction = transaction.reference | +| Transaction SQL | oui, une transaction pour canonique + observation + éventuelle rehydration | +| Rows | transaction; observation; archive seulement si état existant Archived/Force path | +| Idempotence | comparaison exacte du contenu; même identité + même contenu = AlreadyPresent | +| Conflit | même identité + divergence = `ERROR_CODE_RAW_CONFLICT` | +| Absence | insert canonique; Purged compatible Normal = SkippedPurged/NotRecorded | +| Error mapping | model/provenance/conflict API; wrong-network et physical write/data errors sûrs | +| Concurrence | unique insert + `FOR UPDATE` du gagnant; rollback atomique | +| Test déterministe | equality/conflict/tombstone modes/error redaction | +| Test PostgreSQL live | inserts identiques/divergents concurrents + rollback + ForceRehydrate | + +### `RawTransactionObservationRead` + +| Axe | Décision | +|----------------------|---------------------------------------------------------------------------------------------| +| Input | `RawObservationKey` | +| Pré-I/O | key déjà validée; aucun network dans l'input, donc backend identity doit déjà être vérifiée | +| Transaction SQL | non, un SELECT | +| Rows | observation uniquement; network reconstruit depuis backend bound | +| Idempotence | N/A | +| Conflit | N/A | +| Absence | `None` | +| Error mapping | malformed stored row -> data-corrupt sûr; query failure statique | +| Concurrence | statement snapshot; observation immuable après insert | +| Test déterministe | provenance complète/optionnelle, row hostile, no secret echo | +| Test PostgreSQL live | inserted observation round-trip exact | + +### `RawTransactionObservationWrite` + +| Axe | Décision | +|----------------------|------------------------------------------------------------------------------------------| +| Input | `RawTransactionObservation` | +| Pré-I/O | observation.transaction.network = backend network | +| Transaction SQL | oui | +| Rows | canonical row verrouillée + observation | +| Idempotence | same key + all fields identical = AlreadyPresent | +| Conflit | same key + any divergent field = `ERROR_CODE_RAW_CONFLICT` | +| Absence | canonical absent = safe not-found; Purged = NotRecorded | +| Error mapping | provenance/conflict API; not-found/write failure statiques | +| Concurrence | canonical `FOR UPDATE` sérialise avec purge; observation unique sérialise les collisions | +| Test déterministe | equality matrix, missing/purged, hostile errors | +| Test PostgreSQL live | identical/divergent key races | + +### `RawTransactionRetentionRead` + +| Axe | Décision | +|----------------------|--------------------------------------------------------------------------| +| Input | `RawTransactionReference` pour state et tombstone | +| Pré-I/O | reference.network = backend network | +| Transaction SQL | non, un SELECT par méthode | +| Rows | canonical metadata; jamais besoin du payload pour state/tombstone | +| Idempotence | N/A | +| Conflit | N/A | +| Absence | unknown reference -> None; tombstone non-Purged -> None | +| Error mapping | wrong-network ou stored metadata invalid -> code sûr | +| Concurrence | chaque lecture voit un état committé; aucun snapshot multi-appels promis | +| Test déterministe | Full/Archived/Purged + tombstone minimal + malformed row | +| Test PostgreSQL live | transitions puis reads exacts | + +### `RawTransactionRetentionWrite` + +| Axe | Décision | +|----------------------|----------------------------------------------------------------------------------------------------------| +| Input | `RawTransactionRetentionTransition` | +| Pré-I/O | reference.network = backend network; transition impliquant Compacted non supportée rejetée sans pool I/O | +| Transaction SQL | oui | +| Rows | canonical + archive relation pour Full->Archived/Archived->Purged | +| Idempotence | current == target -> AlreadyAtTarget | +| Conflit | current différent de expected et target -> ExpectedStateMismatch; pas `RAW_CONFLICT` | +| Absence | reference inconnue -> erreur/not-found sûre, aucune création | +| Error mapping | invalid transition reste API; unsupported Compacted et physical write statiques | +| Concurrence | canonical `FOR UPDATE` pour transition/purge/rehydrate | +| Test déterministe | compare-and-transition matrix + unsupported Compacted | +| Test PostgreSQL live | races transition/purge/write/ForceRehydrate | + +## 6. Audit historique ciblé kbot3 + +L'audit a porté uniquement sur les fichiers Store/raw demandés par le prompt : manifest, README/USAGE, DTO/entity RAW, repository API, queries PostgreSQL, repository RAW, tables `raw_transactions` / `transaction_observations`, indexes/constraints associés, guide PostgreSQL et plan de normalisation `0.5.3`. + +### 6.1 REPRENDRE + +Les idées suivantes restent utiles à condition d'être réancrées dans les contrats KSP actuels : + +- unicité physique de l'identité canonique ; +- unicité physique de `observation_key` ; +- lien relationnel entre observation et transaction canonique ; +- index ordonné commençant par `slot` pour la navigation ; +- principe de keyset pagination plutôt qu'offset pagination ; +- contraintes SQL simples pour rendre impossibles les tailles/états manifestement invalides. + +### 6.2 REDESSINER + +Les éléments historiques suivants sont des idées valides mais leur forme ancienne n'est pas compatible : + +- `signature TEXT` devient `BYTEA` de longueur exactement 64 ; +- `observation_key TEXT` devient `BYTEA` de longueur exactement 32 ; +- `canonical_json JSONB` devient le payload opaque versionné de `RawPayload` ; +- `slot BIGINT` devient une représentation couvrant tout `u64` ; +- insertion canonique et observation sont une seule transaction métier ; +- les doublons sont comparés champ par champ, payload compris, avant `AlreadyPresent` ; +- le FK observation -> transaction est non nullable et ne peut pas devenir orphelin ; +- le réseau est lié physiquement à la base ; +- les transitions de rétention sont compare-and-transition sous verrou ; +- les erreurs physiques sont projetées vers des codes sûrs et ne conservent aucun texte serveur. + +### 6.3 REPORTER + +Les éléments suivants n'ont pas de requête ou de propriétaire dans `0.3.3` et restent reportés : + +- indexes observation par provider/method/status/received_at ; +- index du content hash ; +- index `created_at` ; +- état de processing/replay et ledger de traitement ; +- batch size et priorité ; +- inspection/administration applicative ; +- RawAccountState ; +- structures N2 ; +- maintenance/destruction globale. + +Un index ne sera pas ajouté « au cas où ». Chaque index V001 doit correspondre à une clé, un FK ou une requête effective des six capacités. + +### 6.4 REJETER + +Les pratiques historiques suivantes sont explicitement rejetées : + +- `sqlx` dans le backend de référence ; +- modèle Store monolithique ; +- `has_*` avant une écriture ; +- `ON CONFLICT DO NOTHING` interprété seul comme preuve d'idempotence ; +- erreur publique contenant le texte brut SQL/driver ; +- narrowing `u64 -> i64` pour `slot` ; +- narrowing `u32 -> i32` pour `format_version` ; +- caps Store arbitraires `500`, `1000`, etc. hérités de workers/UX ; +- `processing_state` dans la table RAW canonique ; +- FK nullable avec `ON DELETE SET NULL` ; +- `BIGSERIAL id` comme identité fonctionnelle ou comme passage obligé des six capacités ; +- JSONB comme représentation du payload RAW opaque. + +## 7. Audit PostgreSQL / tokio-postgres nécessaire au design + +### 7.1 `u64` et `BIGINT` + +PostgreSQL `BIGINT` est signé. Il ne couvre donc pas `slot: u64` dans son intégralité. `tokio-postgres` mappe naturellement `BIGINT` vers `i64`, pas vers `u64`. + +Décision : + +```text +slot -> NUMERIC(20,0) +``` + +Le backend encode un `u64` sous forme décimale canonique et le caste vers `NUMERIC`; à la lecture il récupère une forme décimale entière, la parse en `u64` et rejette toute valeur hors contrat. Aucun `as`, clamp ou perte de domaine n'est admis. + +Les autres entiers tiennent dans `BIGINT` grâce à leurs bornes API : + +- `format_version: u32` -> `BIGINT` avec contrainte `1..=4_294_967_295` ; +- `RawTimestamp` -> `BIGINT` avec borne `253_402_300_799_999` ; +- `source_payload_size_bytes` -> `BIGINT` avec borne `0..=67_108_864`. + +### 7.2 Concurrence et `ON CONFLICT` + +La vertical slice ne fera pas un `SELECT has_*` avant l'insertion. + +Pour une identité unique : + +1. `INSERT ... ON CONFLICT DO NOTHING RETURNING ...` tente l'insertion ; +2. si aucune ligne n'est retournée, une commande SQL suivante exécute `SELECT ... FOR UPDATE` ; +3. sous `READ COMMITTED`, cette nouvelle commande voit le gagnant concurrent après résolution du conflit unique ; +4. le backend compare ensuite le contenu réel avant de décider `AlreadyPresent` ou `ERROR_CODE_RAW_CONFLICT`. + +Ce choix évite un faux `UPDATE` uniquement destiné à obtenir `RETURNING` et évite le churn MVCC d'un no-op update. + +### 7.3 BYTEA + +Les signatures, hashes, keys et payloads restent des octets. `BYTEA` est donc le mapping naturel. Les longueurs déterministes sont contraintes au niveau SQL pour les identifiants fixes. + +## 8. Liaison physique du réseau + +### 8.1 Problème + +`std.store` expose des targets séparées par réseau, mais une URI accidentellement pointée vers la mauvaise base ne doit jamais permettre de relire des octets d'un autre réseau en les réinterprétant sous `Store.network`. + +### 8.2 Décision + +V001 introduira une relation singleton : + +```text +ksp_store_identity +``` + +Schéma prévu : + +```text +singleton SMALLINT PRIMARY KEY +network TEXT NOT NULL +``` + +Contraintes : + +- `singleton = 1` ; +- réseau non vide ; +- longueur <= 128 octets ; +- alphabet identique à `RawNetworkId`. + +Le binding réseau est réalisé dans le bootstrap sous le même advisory lock que les migrations : + +- lors de l'application initiale de V001, la ligne singleton est créée avec le réseau runtime avant commit ; +- lors des ouvertures suivantes, exactement une ligne doit exister ; +- la valeur doit être strictement égale à `PostgresBackend.network` ; +- ligne absente, multiple/impossible ou réseau différent = échec terminal sûr avant exposition du backend prêt. + +Une base déjà marquée V001 dont la ligne d'identité a disparu n'est jamais « réclamée » automatiquement : elle est considérée incohérente afin d'empêcher une rebind accidentelle. + +Les tables RAW n'ont donc pas besoin de répéter `network` sur chaque ligne : une base V001 est physiquement mono-réseau. + +## 9. Design physique V001 proposé + +`pre.001` fige le design ; le SQL embedded sera créé dans une tranche ultérieure. + +### 9.1 `ksp_store_identity` + +```text +singleton SMALLINT PRIMARY KEY +network TEXT NOT NULL +``` + +### 9.2 `ksp_raw_transactions` + +```text +signature BYTEA PRIMARY KEY +slot NUMERIC(20,0) NOT NULL +block_time_unix_millis BIGINT NULL +format_id TEXT NOT NULL +format_version BIGINT NOT NULL +content_hash BYTEA NOT NULL +payload BYTEA NULL +retention_state TEXT NOT NULL +``` + +Contraintes prévues : + +- `octet_length(signature) = 64` ; +- `slot BETWEEN 0 AND 18446744073709551615` ; +- `block_time_unix_millis IS NULL OR BETWEEN 0 AND 253402300799999` ; +- `format_id` valide selon le même alphabet/bornage que `RawFormatId` ; +- `format_version BETWEEN 1 AND 4294967295` ; +- `octet_length(content_hash) = 32` ; +- payload, lorsqu'il est présent, `1..=16_777_216` octets ; +- états physiques `full`, `archived`, `purged` seulement en `0.3.3` ; +- `full` implique payload principal présent ; +- `archived` et `purged` impliquent payload principal absent ; +- `purged` implique `block_time_unix_millis IS NULL` afin de respecter le tombstone minimal. + +Aucun ID SQL auxiliaire n'est requis : la signature est la clé physique puisque la base est mono-réseau. + +### 9.3 `ksp_raw_transaction_observations` + +```text +observation_key BYTEA PRIMARY KEY +transaction_signature BYTEA NOT NULL REFERENCES ksp_raw_transactions(signature) +provider TEXT NOT NULL +protocol TEXT NOT NULL +acquisition_method TEXT NOT NULL +origin TEXT NOT NULL +received_at_unix_millis BIGINT NOT NULL +capture_session_id TEXT NULL +commitment TEXT NULL +endpoint_id TEXT NULL +filter_id TEXT NULL +observed_at_unix_millis BIGINT NULL +source_payload_hash BYTEA NULL +source_payload_size_bytes BIGINT NULL +``` + +Contraintes prévues : + +- observation key exactement 32 octets ; +- transaction signature exactement 64 octets ; +- FK non nullable et non orphanable ; +- tous les codes optionnels/non optionnels réutilisent le bornage/alphabet API ; +- `origin` appartient exactement à `backfill/import/live/repair/replay` ; +- timestamps dans les bornes API ; +- `observed_at <= received_at` ; +- source hash, lorsqu'il existe, exactement 32 octets ; +- source size dans `0..=67_108_864`. + +Aucun champ `status`, `error_message`, `processing_state`, `created_at` ou équivalent n'est ajouté : aucun n'appartient au contrat RAW actuel. + +### 9.4 `ksp_raw_transaction_archive_payloads` + +```text +signature BYTEA PRIMARY KEY REFERENCES ksp_raw_transactions(signature) +payload BYTEA NOT NULL +``` + +Cette relation constitue le tier archive local au backend : elle est séparée de la table chaude et n'est consultée que pour un `get` exact d'une transaction archivée, une transition de rétention ou une rehydration. Elle n'entre pas dans l'index de navigation principal. + +Le payload archivé reste byte-for-byte identique au payload canonique et conserve la borne 16 MiB. Aucune compression implicite n'est revendiquée. + +### 9.5 Indexes V001 + +Indexes justifiés : + +```text +PRIMARY KEY ksp_raw_transactions(signature) +PRIMARY KEY ksp_raw_transaction_observations(observation_key) +PRIMARY KEY ksp_raw_transaction_archive_payloads(signature) +INDEX ix_ksp_raw_transactions_slot_signature + ON ksp_raw_transactions(slot, signature) + WHERE retention_state <> 'purged' +``` + +Le dernier index sert exactement `list_raw_transactions` et définit l'ordre total. Aucun index observation secondaire n'est requis par les six capacités actuelles. + +## 10. Représentation de `Compacted` : gap explicite + +### 10.1 Constat + +Le contrat `RawRetentionState` contient : + +```text +Full +Compacted +Archived +Purged +``` + +`Compacted` promet que le contenu RAW complet est retenu dans une représentation compactée. + +Le stockage TOAST PostgreSQL ne suffit pas pour déclarer cet état : la compression est un détail physique transparent et peut ne pas être appliquée à une valeur incompressible. Marquer arbitrairement une ligne `Compacted` sans représentation réellement compactée violerait le contrat. + +### 10.2 Décision `0.3.3` + +Le plan `0.3.1` qualifie déjà `Compacted` d'**optionnel** et autorise les backends à n'implémenter que les capabilities/formes réellement supportées. Il n'existe donc aucun gap backend-agnostique bloquant à corriger dans `ksp-store-api`. + +Le backend PostgreSQL `0.3.3` implémente honnêtement : + +```text +Full -> Archived -> Purged +Full -> Archived +``` + +et le no-op `AlreadyAtTarget` correspondant. + +Toute transition impliquant `Compacted` est rejetée sans I/O avec le code stable : + +```text +store.postgres_retention_compaction_unsupported +``` + +Ce code suit le modèle des erreurs physiques PostgreSQL déjà exposées par `ksp-store-lib`. `ksp-store-postgres-lib`, qui ne peut pas dépendre de la façade, produit le même `ErrorCode` KSP par valeur ; les tests de boundary/conformance figent cette égalité sans créer de reverse dependency. Aucun modèle ou outcome stable `ksp-store-api` n'est modifié. + +Aucune dépendance de compression n'est ajoutée à `0.3.3` uniquement pour faire semblant de supporter cet état. Une release ultérieure pourra ajouter une représentation compactée réellement auditée sans changer l'identité RAW ni le cycle logique. + +## 11. Algorithme d'écriture atomique + +### 11.1 Pré-I/O + +`persist_raw_transaction_acquisition` rejette avant acquisition du pool : + +- `transaction.reference.network != backend.network` ; +- `observation.transaction.network != backend.network` ; +- `observation.transaction != transaction.reference`. + +`Store` effectue le même contrôle contre `Store.network` avant dispatch. Le backend le répète afin que son implémentation directe des capacités reste sûre. + +### 11.2 Transaction canonique absente + +Sous une transaction PostgreSQL : + +1. tenter l'insert canonique avec `ON CONFLICT DO NOTHING RETURNING` ; +2. résultat présent => candidat `Inserted` ; +3. insérer l'observation ; +4. si l'observation est en conflit divergent, rollback de toute la transaction ; +5. commit seulement lorsque les deux décisions sont établies. + +Ainsi, une observation ne peut jamais être commise sans le canonique qu'elle référence. + +### 11.3 Transaction canonique déjà présente + +Après conflit unique : + +1. `SELECT ... FOR UPDATE` du canonique ; +2. relire son payload selon l'état de rétention ; +3. comparer strictement `slot`, `block_time`, `format_id`, `format_version`, `content_hash` et les octets de payload lorsque ceux-ci existent encore ; +4. contenu identique => `AlreadyPresent` ; +5. divergence => `ERROR_CODE_RAW_CONFLICT`, sans observation supplémentaire. + +Le hash fourni par l'appelant n'est jamais utilisé seul comme preuve d'égalité tant que les octets sont disponibles. + +### 11.4 Tombstone purgé + +La purge ne conserve que : + +```text +reference +slot +format_id +format_version +content_hash +``` + +Pour une acquisition visant cette identité : + +- divergence sur un champ de tombstone => `ERROR_CODE_RAW_CONFLICT` ; +- `Normal` + tombstone compatible => `SkippedPurged / NotRecorded` ; +- `ForceRehydrate` + tombstone compatible => restauration `Full`, payload entrant, block time entrant, puis observation dans la même transaction ; résultat `Rehydrated`. + +Le mode Force ne masque donc jamais un conflit détectable avec le tombstone. + +### 11.5 Observation idempotente + +Après `ON CONFLICT DO NOTHING` sur `observation_key` : + +- ligne absente avant l'appel => `Inserted` ; +- ligne présente et tous les champs identiques => `AlreadyPresent` ; +- même key mais référence/provenance divergente => `ERROR_CODE_RAW_CONFLICT`. + +## 12. `record_raw_transaction_observation` + +Cette capacité ne crée jamais implicitement un canonique. + +Séquence : + +1. validation réseau pré-I/O ; +2. transaction SQL ; +3. lecture du canonique référencé ; +4. absence => erreur sûre `store.raw_reference_not_found` ; +5. état `Purged` => `NotRecorded` ; +6. sinon insertion/idempotence observation comme ci-dessus ; +7. commit. + +`store.raw_reference_not_found` est un code runtime commun : il décrit l'absence d'une entité requise par une opération Store, sans prétendre qu'un modèle d'entrée est invalide. Aucun nouveau code `ksp-store-api` n'est nécessaire. + +## 13. Lecture et corruption physique + +### 13.1 `get_raw_transaction` + +- `Full` : le payload doit être présent dans `ksp_raw_transactions` ; +- `Archived` : le payload principal doit être absent et exactement une ligne archive doit être trouvée ; +- `Purged` : retourne `None` ; +- tout état impossible, taille invalide, entier non convertible ou archive incohérente => erreur de donnée physique sûre. + +Le backend ne fabrique jamais de valeur de modèle pour masquer une ligne SQL corrompue. + +### 13.2 `get_raw_transaction_observation` + +`RawObservationKey` ne transporte pas de réseau. Le réseau de l'observation reconstruite vient donc exclusivement du binding mono-réseau vérifié à l'ouverture du backend. + +### 13.3 Erreurs + +Les erreurs de driver/serveur sont réduites à une catégorie et une phase statiques. Sont interdits dans `Display`, `Debug`, source publique ou message KSP : + +- texte serveur ; +- SQLSTATE ; +- query text ; +- bind values ; +- URI ; +- signature/hash/payload ; +- valeurs hostiles issues de la base. + +## 14. Pagination déterministe + +### 14.1 Ordre total + +L'ordre canonique est : + +```text +Ascending -> (slot ASC, signature ASC) +Descending -> (slot DESC, signature DESC) +``` + +`slot` seul n'est pas un ordre total. La signature fixe 64 octets constitue le tie-breaker. + +Les lignes `Purged` sont exclues de `list_raw_transactions`, car elles ne représentent plus un `RawTransaction` récupérable ; elles restent accessibles via les capacités de rétention/tombstone. + +### 14.2 Cursor opaque privé + +Format binaire backend-private V1 prévu : + +```text +magic 4 bytes = "KSPT" +version 1 byte = 1 +last_slot 8 bytes = u64 big-endian +last_signature 64 bytes +binding_digest 32 bytes = SHA-256 +``` + +Taille fixe : + +```text +109 bytes +``` + +Le digest est calculé avec domaine séparé sur : + +```text +KSP/raw-transaction-cursor/v1 +network +direction +slot range start/end avec flags de présence +last_slot +last_signature +``` + +Le decode rejette : + +- taille différente ; +- magic/version inconnus ; +- digest incohérent ; +- replay avec réseau, direction ou slot range différents ; +- last slot hors range demandée. + +Le digest est une protection d'intégrité et de binding de requête, pas un mécanisme d'autorisation ni un secret. Un cursor ne peut donc jamais étendre les droits d'un appelant. + +### 14.3 Page limit + +`RawPageLimit` reste `u64 > 0` sans maximum politique KSP. + +Le chemin PostgreSQL utilise `LIMIT requested + 1` afin de décider si un cursor de continuation est nécessaire. PostgreSQL attend un domaine signé pour ce paramètre. La limite physique du backend est donc documentée exactement : + +```text +requested <= i64::MAX - 1 +``` + +Au-delà, le backend retourne une erreur physique sûre ; il ne clamp jamais la demande vers une valeur arbitraire. Ce n'est ni un batch size ni une priorité worker. + +## 15. Rétention compare-and-transition + +### 15.1 Lecture + +`get_raw_transaction_retention_state` retourne l'état du canonique/tombstone connu. + +`get_raw_transaction_tombstone` retourne une valeur uniquement pour `Purged` et reconstruit exactement les cinq champs minimaux acquis. + +### 15.2 `Full -> Archived` + +Sous transaction et `SELECT ... FOR UPDATE` : + +1. vérifier l'état courant ; +2. si déjà `Archived`, `AlreadyAtTarget` ; +3. si l'état courant n'est pas `expected`, `ExpectedStateMismatch` ; +4. copier le payload exact vers `ksp_raw_transaction_archive_payloads` ; +5. passer le payload principal à `NULL` et l'état à `archived` ; +6. commit. + +Aucun instant committé ne peut exposer `Archived` sans son payload archive. + +### 15.3 `Archived -> Purged` + +Sous le même verrou de ligne : + +1. vérifier l'état ; +2. supprimer le payload archive ; +3. mettre l'état à `purged` ; +4. supprimer `block_time` ; +5. conserver uniquement les champs du tombstone ; +6. commit. + +### 15.4 Concurrence + +Deux transitions concurrentes sur la même signature sont sérialisées par `FOR UPDATE`. La seconde observe l'état committé par la première et produit soit `AlreadyAtTarget`, soit `ExpectedStateMismatch`, jamais une transition inventée. + +Une acquisition, une purge et une ForceRehydrate visant la même identité utilisent le même verrou de ligne canonique avant mutation de l'état/payload afin d'éviter les races croisées. + +## 16. Taxonomie d'erreurs à stabiliser + +Les codes API acquis restent propriétaires des erreurs de modèle/input/conflit. + +La tranche d'implémentation devra disposer de codes sûrs pour au minimum : + +```text +raw conflict -> store_api.raw_conflict +retention Compacted unsupported -> store.postgres_retention_compaction_unsupported +wrong network -> store.wrong_network +raw reference absent -> store.raw_reference_not_found +postgres read failure -> store.postgres_read_failed +postgres write failure -> store.postgres_write_failed +postgres stored-data invalid -> store.postgres_data_invalid +postgres page-limit unsupported -> store.postgres_page_limit_unsupported +``` + +Aucun code ne doit dépendre d'un SQLSTATE pour son interface publique. Le SQLSTATE peut être utilisé uniquement en classification interne si un cas le nécessite et s'il n'est jamais rendu. + +## 17. Threat model `0.3.3` + +### 17.1 Mauvais réseau / mauvaise URI + +Menace : target Config correcte mais URI pointant vers une base d'un autre réseau. + +Protection : binding singleton atomique au bootstrap + validation réseau pré-I/O sur toutes les capacités portant un réseau. + +### 17.2 Octets DB malformés + +Menace : corruption/manipulation hors KSP de signature, hash, payload, code ou entier. + +Protection : contraintes SQL + décodage fallible + absence de `unwrap/expect/panic` + erreur statique sans écho. + +### 17.3 Overflow `u64` + +Menace : slot valide KSP supérieur à `i64::MAX`. + +Protection : `NUMERIC(20,0)` et conversion décimale fallible, jamais `BIGINT`/cast signé pour slot. + +### 17.4 Orphelin observation/canonique + +Menace : observation committée alors que le canonique échoue/rollback. + +Protection : FK non nullable + transaction métier unique. + +### 17.5 Inserts concurrents identiques/divergents + +Menace : double insertion, faux idempotent, overwrite. + +Protection : unique key + `ON CONFLICT DO NOTHING` + verrou/lecture du gagnant + comparaison exacte + rollback sur divergence. + +### 17.6 Collision d'observation + +Menace : même observation key avec provenance différente. + +Protection : comparaison exacte, `AlreadyPresent` uniquement si tous les champs sont égaux, sinon conflit. + +### 17.7 Cursor hostile/replay + +Menace : bytes aléatoires, version inconnue, cursor réutilisé pour autre réseau/range/direction. + +Protection : format borné/fixe, magic/version, digest lié à la requête et validation stricte. + +### 17.8 Pagination duplicate/skip + +Menace : plusieurs transactions au même slot. + +Protection : ordre total `(slot, signature)` et keyset strict `>` / `<`. + +La pagination n'offre pas un snapshot inter-pages. Une mutation concurrente positionnée avant le cursor peut ne pas apparaître dans une navigation déjà commencée ; ce comportement doit être documenté et ne doit pas être présenté comme un snapshot transactionnel. + +### 17.9 Race rétention + +Menace : deux transitions contradictoires. + +Protection : `SELECT FOR UPDATE`, compare-and-transition et transaction atomique. + +### 17.10 Purge/read/write race + +Menace : un reader ou writer voit un état moitié migré entre table chaude et archive. + +Protection : toutes les mutations de payload/state sont atomiques ; chaque commande de lecture voit un état committé cohérent ou retourne une erreur de corruption sûre. + +### 17.11 ForceRehydrate race + +Menace : deux rehydrations concurrentes ou rehydration pendant purge. + +Protection : même verrou canonique ; le gagnant établit l'état, le suivant compare le contenu et produit idempotence ou conflit. + +### 17.12 Fuite d'erreur SQL/serveur + +Menace : SQLSTATE, message PostgreSQL, bind ou secret rendu à l'appelant. + +Protection : projection vers kind/phase/code statiques uniquement. + +### 17.13 Cancellation + +Menace : future Rust abandonnée pendant une transaction métier. + +Protection : ownership du `tokio_postgres::Transaction`; drop provoque rollback implicite. Aucun outcome de succès n'est rendu avant commit. + +### 17.14 Migration mismatch + +Menace : V001 divergent, history absent, checksum modifié ou schéma plus récent. + +Protection : moteur ordonné/checksum acquis, étendu sans auto-repair destructif ; mismatch terminal avant backend prêt. + +## 18. Tests et preuves prévues + +### 18.1 Tests sans PostgreSQL live + +À répartir dans les tranches : + +- exact schema/migration inventory ; +- checksum V001 stable ; +- contraintes/tables/indexes attendus ; +- conversion slot `0`, `i64::MAX`, `i64::MAX + 1`, `u64::MAX` ; +- format version `u32::MAX` ; +- timestamps bornes ; +- encode/decode cursor et matrice hostile ; +- comparaison canonique/observation ; +- mapping d'erreurs sans valeurs hostiles ; +- boundary scans interdisant SQL/env/backend handles dans la façade ; +- six implémentations de capability sur `PostgresBackend` ; +- six dispatches sur `Store` lorsque feature `postgres` active ; +- comportement sans feature par défaut conservé. + +### 18.2 Test PostgreSQL live opt-in + +La preuve live doit couvrir au minimum sur une base dédiée : + +1. bootstrap V000 + V001 et réouverture idempotente ; +2. refus d'un réseau différent ; +3. insert canonique + observation atomique ; +4. insert identique concurrent ; +5. insert divergent concurrent ; +6. observation identique/divergente ; +7. `get` canonique et observation ; +8. pagination multi-page avec plusieurs signatures au même slot, dans les deux directions ; +9. cursor replay sous autre range/direction rejeté ; +10. `Full -> Archived`, lecture archivée ; +11. transition compare mismatch ; +12. `Archived -> Purged`, tombstone minimal ; +13. acquisition normale sur tombstone ; +14. ForceRehydrate compatible ; +15. ForceRehydrate divergent ; +16. transitions `Compacted` rejetées sans état modifié ; +17. cancellation/rollback observable ; +18. réouverture après les opérations. + +Le test reste `#[ignore]`, opt-in et lit une URI dédiée sans la logger. + +## 19. Sizing et séquencement recalibré + +L'audit montre que le moteur de migration `0.3.2` doit être généralisé avant V001 et que la rétention honnête exige une tranche propre. Le séquencement est donc étendu d'une tranche par rapport au forecast initial. + +### `0.3.3-pre.001` — audit et design + +- lecture complète des règles/architectures/contrats ; +- audit stable `0.3.2` ; +- audit historique ciblé ; +- audit PostgreSQL/tokio-postgres ; +- threat model ; +- design V001, réseau, concurrence, pagination, rétention ; +- plan + validation. + +### `0.3.3-pre.002` — moteur de migrations multi-version + +- généraliser V000 vers un registre embedded ordonné ; +- conserver checksums/mismatch/newer/no-down ; +- préparer le hook atomique de binding réseau ; +- ajouter les tests du moteur sans créer encore de repository métier. + +### `0.3.3-pre.003` — V001 physique + binding réseau + +- créer V001 ; +- tables/constraints/indexes exacts ; +- créer/valider `ksp_store_identity` atomiquement ; +- tests de checksum/inventory/bounds ; +- matérialiser `store.postgres_retention_compaction_unsupported`, sans modifier les contrats stables sauf preuve nouvelle d'un blocage réel. + +### `0.3.3-pre.004` — mapping et lectures + +- codecs SQL privés ; +- conversions exactes `u64/u32/timestamps/bytes/codes` ; +- `get_raw_transaction` ; +- `get_raw_transaction_observation` ; +- lectures rétention/tombstone ; +- corruption DB -> erreurs sûres. + +### `0.3.3-pre.005` — écriture atomique et observations + +- acquisition atomique canonique + observation ; +- idempotence réelle ; +- divergence -> conflict ; +- observation write séparée ; +- concurrence logique testable sans API `has_*`. + +### `0.3.3-pre.006` — pagination/cursor + +- ordre total ; +- index utilisé ; +- cursor V1 opaque ; +- binding de query ; +- limites physiques explicites ; +- matrice hostile. + +### `0.3.3-pre.007` — archive, purge, tombstone, ForceRehydrate + +- `Full -> Archived -> Purged` ; +- compare-and-transition ; +- archive relation ; +- normal skip purged ; +- force rehydrate ; +- rejet sûr et stable de `Compacted`, sans fausse compression. + +### `0.3.3-pre.008` — façade `ksp-store-lib` + +- six implémentations/dispatches ; +- validation réseau pré-I/O ; +- erreurs sûres ; +- feature mismatch/no-default-features ; +- aucune fuite de type PostgreSQL. + +### `0.3.3-pre.009` — preuve PostgreSQL live + +- test opt-in dédié ; +- concurrence réelle ; +- migration/réseau ; +- atomicité ; +- pagination ; +- rétention/rehydrate ; +- rollback/cancellation. + +### `0.3.3-pre.010` — hardening/completeness + +- scans boundaries ; +- hostile rows/cursors/errors ; +- inventaires modules/exports/deps ; +- conformance des six capacités ; +- absence de scope creep. + +### `0.3.3-pre.011` — gate technique final + +- fmt ; +- audits Rust/Markdown ; +- check workspace ; +- Clippy all-targets ; +- tests Store/API/Config ; +- no-default-features ; +- live opt-in si URI dédiée fournie. + +### `0.3.3-pre.012` — réconciliation documentaire + +- plan/validation finalisés ; +- docs Store concernées uniquement ; +- ROADMAP/CHANGELOG selon workflow de clôture ; +- tableaux reformattés RustRover. + +### `0.3.3-pre.013` — préparation publication + +- version/payload minimal de publication ; +- prompt de démarrage `0.3.4` ; +- delta de clôture prerelease ; +- aucun code fonctionnel rouvert. + +### `0.3.3-rel.001` + +- tag stable `v0.3.3` ; +- gate final ; +- publication minimale. + +## 20. Non-objectifs explicites + +Restent hors scope : + +```text +RawAccountState PostgreSQL +N2 / structural storage +jobs / workers / executors / backfills +Store Desk ou autre application +NOTIFY / event bus +Transport HTTP / WS / gRPC +codecs wire +compression globale / archive worker global +maintenance destructive +nouveaux backends SQLite/MySQL/RocksDB/ClickHouse/Oracle +policy de batch/priorité/retry worker +``` + +## 21. Gate de `pre.001` + +Le gate attendu après overlay est : + +```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.3 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +Aucune preuve PostgreSQL live n'est requise en `pre.001` car aucun SQL métier n'est encore créé. + +## 22. Critères de sortie de `pre.001` + +`pre.001` est fermée seulement si : + +- la base `v0.3.2` est confirmée ; +- les six capacités sont inventoriées sans extension de scope ; +- le mapping physique ne narrow aucun contrat entier ; +- V001 a un schéma/clé/index minimal décidé ; +- la base est physiquement liée à un seul réseau ; +- les algorithmes d'idempotence/concurrence sont décidés ; +- l'ordre total et le cursor sont décidés ; +- la rétention ne prétend jamais `Compacted` sans représentation compactée réelle ; +- l'absence volontaire de support physique `Compacted` est explicite et conforme à son caractère optionnel acquis ; +- le threat model couvre les races et fuites demandées ; +- le séquencement est calibré ; +- aucune migration/repository métier n'a été ajoutée prématurément. diff --git a/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md new file mode 100644 index 0000000..1481178 --- /dev/null +++ b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md @@ -0,0 +1,594 @@ + + + +# Validation `0.3.3` — Store/PostgreSQL RawTransaction vertical slice + +## 1. Objet + +Cette validation accompagne : + +```text +0.3.3 — Store/PostgreSQL RawTransaction vertical slice +``` + +Elle démarre en `0.3.3-pre.001` comme matrice de preuve. Les lignes non encore implémentées restent explicitement `À FAIRE`; elles ne sont pas présentées comme acquises. + +## 2. Baseline stable + +| Preuve | Attendu `pre.001` | Statut | +|-----------------------------------------|--------------------------------------------------------------------|--------| +| Base de travail | archive/tag `v0.3.2` | PASS | +| workspace package version avant overlay | `0.3.2` | PASS | +| Gate opérateur `0.3.2` | audits/check/clippy/tests Store+Config/no-default-features propres | PASS | +| PostgreSQL live métier `0.3.2` | absent ; seule fondation opt-in existe | PASS | +| Prompt release | `prompts/022-V0_3_3_START_PROMPT.md` | PASS | +| Archive historique kbot3 | audit ciblé uniquement | PASS | + +## 2.1 Inventaire exact `0.3.2` + +```text +ksp-store-api modules=capability,error,model root pub use=60 +ksp-store-lib modules=constants,error,health,settings,store root pub use=84, default feature=postgres +ksp-store-postgres-lib modules=constants,error,health,migration,runtime root pub use=7 +V000__bootstrap.sql SHA-256=d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +std.store format_version 1 +std.store profiles devnet/devnet, mainnet/mainnet-beta, testnet/testnet +foundation live PASS PostgreSQL 17 en pre.008 et pre.010 +``` + +Le test live est `#[ignore]` dans les gates ordinaires mais sa preuve réelle antérieure est documentée dans `019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`. + +## 3. Scope exact des capacités + +| Capacité | Backend PostgreSQL | Façade Store | Preuve finale | +|----------------------------------|--------------------|--------------|---------------------------| +| `RawTransactionRead` | À FAIRE | À FAIRE | unit + integration + live | +| `RawTransactionWrite` | À FAIRE | À FAIRE | unit + concurrency live | +| `RawTransactionObservationRead` | À FAIRE | À FAIRE | unit + live | +| `RawTransactionObservationWrite` | À FAIRE | À FAIRE | unit + concurrency live | +| `RawTransactionRetentionRead` | À FAIRE | À FAIRE | unit + live | +| `RawTransactionRetentionWrite` | À FAIRE | À FAIRE | unit + race live | + +Aucune capacité `RawAccountState`, structurale N2, job ou worker n'est admise dans cette matrice. + +### 3.1 Matrice `RawTransactionRead` + +| Axe | Preuve/attendu | +|----------------------|-------------------------------------------------------------| +| input | reference pour get; query pour list | +| invariants pré-I/O | network exact; cursor lié à query | +| transaction SQL | aucune; statement cohérent | +| rows touchées | transaction + archive pour get; index transaction pour list | +| idempotence | N/A | +| conflit | N/A; corruption != conflict métier | +| absence/not-found | get None; list vide | +| error mapping | query/wrong-network/data/query-failed sûrs | +| concurrence | statement snapshot; aucun snapshot inter-pages | +| test déterministe | full/archive/purged, u64 max, ranges, cursor hostile | +| test PostgreSQL live | get exact + pages multi-slot/ties dans deux directions | + +### 3.2 Matrice `RawTransactionWrite` + +| Axe | Preuve/attendu | +|----------------------|-------------------------------------------------------------------| +| input | transaction + observation + acquisition mode | +| invariants pré-I/O | deux networks exacts; observation reference = canonical reference | +| transaction SQL | une transaction atomique | +| rows touchées | canonical + observation + archive si état existant Archived | +| idempotence | contenu exact identique -> AlreadyPresent | +| conflit | divergence même signature -> ERROR_CODE_RAW_CONFLICT | +| absence/not-found | insert; Purged Normal compatible -> SkippedPurged/NotRecorded | +| error mapping | API model/conflict; physical write/data sûrs | +| concurrence | unique insert + FOR UPDATE du gagnant | +| test déterministe | equality/conflict/tombstone/modes/redaction | +| test PostgreSQL live | identical/divergent concurrent + rollback + force | + +### 3.3 Matrice `RawTransactionObservationRead` + +| Axe | Preuve/attendu | +|----------------------|-----------------------------------------------| +| input | observation key | +| invariants pré-I/O | key API valide; database network déjà bound | +| transaction SQL | aucune | +| rows touchées | observation | +| idempotence | N/A | +| conflit | N/A | +| absence/not-found | None | +| error mapping | stored-row invalid/query-failed sûrs | +| concurrence | observation immuable, statement snapshot | +| test déterministe | provenance complète/optionnelle + hostile row | +| test PostgreSQL live | observation round-trip exact | + +### 3.4 Matrice `RawTransactionObservationWrite` + +| Axe | Preuve/attendu | +|----------------------|-----------------------------------------------------------| +| input | observation | +| invariants pré-I/O | observation transaction network exact | +| transaction SQL | oui | +| rows touchées | canonical verrouillé + observation | +| idempotence | same key + fields identical -> AlreadyPresent | +| conflit | same key + divergence -> ERROR_CODE_RAW_CONFLICT | +| absence/not-found | canonical absent -> safe not-found; Purged -> NotRecorded | +| error mapping | provenance/conflict API + not-found/write sûrs | +| concurrence | canonical FOR UPDATE + unique observation key | +| test déterministe | equality/missing/purged/error canaries | +| test PostgreSQL live | identical/divergent observation races | + +### 3.5 Matrice `RawTransactionRetentionRead` + +| Axe | Preuve/attendu | +|----------------------|----------------------------------------------| +| input | transaction reference | +| invariants pré-I/O | network exact | +| transaction SQL | aucune | +| rows touchées | canonical metadata | +| idempotence | N/A | +| conflit | N/A | +| absence/not-found | state unknown -> None; tombstone only Purged | +| error mapping | wrong-network/stored-metadata invalid sûrs | +| concurrence | état committé par statement | +| test déterministe | Full/Archived/Purged + tombstone minimal | +| test PostgreSQL live | reads après chaque transition | + +### 3.6 Matrice `RawTransactionRetentionWrite` + +| Axe | Preuve/attendu | +|----------------------|---------------------------------------------------------| +| input | retention transition | +| invariants pré-I/O | network exact; Compacted non supporté rejeté avant pool | +| transaction SQL | oui | +| rows touchées | canonical + archive | +| idempotence | current == target -> AlreadyAtTarget | +| conflit | expected perdu -> ExpectedStateMismatch | +| absence/not-found | unknown reference -> safe not-found | +| error mapping | invalid transition API; unsupported/physical write sûrs | +| concurrence | FOR UPDATE canonique | +| test déterministe | transition matrix + Compacted unsupported | +| test PostgreSQL live | transition/purge/write/force races | + +## 4. Design physique V001 + +| Élément | Décision `pre.001` | Statut | +|-------------------|----------------------------------------------------|-------------| +| Migration | V001 embedded, après généralisation du moteur V000 | PASS design | +| Database identity | singleton `ksp_store_identity` | PASS design | +| Réseau | un réseau exact par base V001 | PASS design | +| Transaction key | signature `BYTEA(64)` logique | PASS design | +| Observation key | `BYTEA(32)` | PASS design | +| Slot | `NUMERIC(20,0)` ; aucun narrowing `u64` | PASS design | +| Format version | `BIGINT`, check domaine `u32` non nul | PASS design | +| Timestamp | `BIGINT`, check borne `RawTimestamp` | PASS design | +| Payload | `BYTEA`, 1..=16 MiB | PASS design | +| Archive | relation dédiée hors index hot-path | PASS design | +| Purge | tombstone minimal dans ligne canonique | PASS design | +| Compacted | non simulé ; rejet stable tant qu'aucun codec réel | PASS design | +| Navigation | index partiel `(slot, signature)` hors purged | PASS design | + +## 5. Inventaire SQL minimal prévu + +### 5.1 Tables + +```text +ksp_store_identity +ksp_raw_transactions +ksp_raw_transaction_observations +ksp_raw_transaction_archive_payloads +``` + +Statut `pre.001` : **design seulement**. Aucun de ces objets V001 n'est encore créé. + +### 5.2 Indexes + +```text +PK ksp_raw_transactions(signature) +PK ksp_raw_transaction_observations(observation_key) +PK ksp_raw_transaction_archive_payloads(signature) +ix_ksp_raw_transactions_slot_signature(slot, signature) + WHERE retention_state <> 'purged' +``` + +Tout index additionnel doit être justifié par une requête effectivement ajoutée à la release. + +## 6. Mapping numérique sans narrowing + +| Valeur API | Représentation SQL | Cas limites obligatoires | +|-----------------------|----------------------------------------------------|-------------------------------------------| +| `slot: u64` | `NUMERIC(20,0)` | 0, `i64::MAX`, `i64::MAX + 1`, `u64::MAX` | +| `format_version: u32` | `BIGINT` | 1, `i32::MAX + 1`, `u32::MAX` | +| `RawTimestamp` | `BIGINT` | 0, max documenté, max+1 DB hostile | +| source payload size | `BIGINT` | 0, 64 MiB, >64 MiB DB hostile | +| page limit | bind signé de `limit+1` seulement si représentable | `i64::MAX - 1`, valeur supérieure rejetée | + +Critère : aucune branche ne doit utiliser `as i64`, `as i32`, clamp ou saturation pour faire tenir une valeur valide API. + +## 7. Binding réseau + +### 7.1 Bootstrap + +| Cas | Résultat attendu | +|-------------------------------------------|------------------------------------------------------------------------| +| V001 appliquée pour la première fois | identity créée avec le réseau runtime dans la transaction de bootstrap | +| réouverture même réseau | succès idempotent | +| réouverture autre réseau | erreur terminale sûre | +| V001 présente mais identity absente | mismatch/corruption, jamais rebind silencieuse | +| identity malformée | erreur terminale sûre | +| migration pending + auto_migrate disabled | backend non prêt, aucune persistence métier | + +### 7.2 Pré-I/O capability + +Pour tout input portant un `RawNetworkId`, un mismatch avec `Store.network` ou `PostgresBackend.network` doit être détecté avant acquisition d'un client du pool. + +## 8. Idempotence canonique + +| État existant | Input | Mode | Outcome attendu | +|--------------------------------|-------------------------------|----------------|-------------------------------------------| +| absent | canonique+observation valides | Normal | `Inserted / Inserted` | +| Full identique | observation identique | Normal | `AlreadyPresent / AlreadyPresent` | +| Full identique | observation nouvelle | Normal | `AlreadyPresent / Inserted` | +| Full divergent | n'importe quelle observation | Normal | `ERROR_CODE_RAW_CONFLICT`, aucun write | +| Archived identique | observation nouvelle | Normal | `AlreadyPresent / Inserted` | +| Purged compatible | observation | Normal | `SkippedPurged / NotRecorded` | +| Purged divergent sur tombstone | observation | Normal | `ERROR_CODE_RAW_CONFLICT` | +| Purged compatible | observation | ForceRehydrate | `Rehydrated / Inserted ou AlreadyPresent` | +| Purged divergent | observation | ForceRehydrate | `ERROR_CODE_RAW_CONFLICT` | + +L'égalité canonique compare les octets réels lorsqu'ils sont encore retenus. Le content hash seul n'est jamais une preuve suffisante dans `Full` ou `Archived`. + +## 9. Atomicité canonique + observation + +### 9.1 Cas obligatoires + +| Scénario | Preuve attendue | +|--------------------------------------------------------------|----------------------------------| +| nouvel insert, observation valide | les deux commit | +| nouvel insert, observation divergente sur key déjà existante | rollback canonique + observation | +| canonical conflict | aucune nouvelle observation | +| cancellation avant commit | aucun succès partiel visible | +| erreur SQL observation | rollback canonique | + +### 9.2 FK + +Le FK observation -> transaction est : + +- non nullable ; +- sans `ON DELETE SET NULL` ; +- impossible à contourner dans le repository métier ; +- cohérent avec le fait que purge conserve la ligne canonique/tombstone. + +## 10. Concurrence + +| Race | Attendu | +|-----------------------------------------|-------------------------------------------------------------| +| deux inserts identiques même signature | un `Inserted`, l'autre `AlreadyPresent`; données identiques | +| deux inserts divergents même signature | un gagnant, l'autre `ERROR_CODE_RAW_CONFLICT` | +| deux observations identiques même key | un `Inserted`, l'autre `AlreadyPresent` | +| deux observations divergentes même key | un gagnant, l'autre conflit | +| deux transitions vers même target | un transitionne, l'autre `AlreadyAtTarget` | +| transitions avec expected incompatibles | gagnant déterministe + `ExpectedStateMismatch` | +| purge vs acquisition | sérialisation sur ligne canonique | +| purge vs ForceRehydrate | aucun état moitié purgé/moitié full | +| deux ForceRehydrate | idempotence ou conflit selon contenu | + +La preuve live doit utiliser de vraies tâches concurrentes, pas une simulation séquentielle renommée « concurrency ». + +## 11. Observation write séparée + +| Cas | Attendu | +|-----------------------------------------|----------------------------------------| +| canonique Full présent, key absente | `Inserted` | +| canonique Archived présent, key absente | `Inserted` | +| key identique | `AlreadyPresent` | +| key divergente | `ERROR_CODE_RAW_CONFLICT` | +| canonique absent | erreur sûre, aucune création implicite | +| canonique Purged | `NotRecorded` | +| mauvais réseau | rejet pré-I/O | + +L'absence du canonical requis est classée `store.raw_reference_not_found`. Ce code runtime ne modifie pas les invariants de construction de `ksp-store-api`. + +## 12. Lecture + +### 12.1 RawTransaction + +| État physique | `get_raw_transaction` | +|-----------------------------------------|------------------------------------------------| +| Full cohérent | `Some(RawTransaction)` depuis payload hot | +| Archived cohérent | `Some(RawTransaction)` depuis archive relation | +| Purged | `None` | +| row inconnue | `None` | +| état/bytes/taille/numérique incohérents | erreur sûre de donnée physique | + +### 12.2 Observation + +| Cas | Résultat | +|----------------------------------|--------------------------------------------------------| +| key connue | observation reconstruite avec network du backend bound | +| key inconnue | `None` | +| bytes/provenance invalides en DB | erreur sûre | + +## 13. Pagination + +### 13.1 Ordre + +```text +Ascending = (slot ASC, signature ASC) +Descending = (slot DESC, signature DESC) +``` + +### 13.2 Dataset obligatoire + +Le test doit contenir au minimum : + +- plusieurs slots ; +- au moins trois signatures distinctes sur le même slot ; +- suffisamment de rows pour trois pages ; +- une row Purged exclue de la liste ; +- bornes start/end présentes et absentes. + +### 13.3 Cursor V1 + +| Test | Attendu | +|----------------------------|---------------------------------| +| round-trip | exact | +| taille != 109 | rejet | +| magic modifié | rejet | +| version inconnue | rejet | +| digest modifié | rejet | +| direction différente | rejet | +| range différente | rejet | +| network différent | rejet | +| last slot hors range | rejet | +| random bytes <=4096 | rejet sans panic/echo | +| cursor >4096 au niveau API | déjà rejeté par `RawPageCursor` | + +### 13.4 Pas de cap métier + +Aucun test ne doit imposer 100, 500 ou 1000 comme limite Store. La seule erreur de taille côté backend doit correspondre à la représentation physique nécessaire au `LIMIT + 1`. + +## 14. Rétention + +### 14.1 États supportés physiquement en `0.3.3` + +```text +Full +Archived +Purged +``` + +`Compacted` reste un état logique API connu, explicitement optionnel depuis le plan `0.3.1`, mais non représenté mensongèrement par PostgreSQL `0.3.3`. + +### 14.2 Matrice + +| Current | Expected | Target | Attendu PostgreSQL 0.3.3 | +|-------------------|-----------|-----------|------------------------------------------------------------| +| Full | Full | Archived | `Transitioned` | +| Archived | Full | Archived | `AlreadyAtTarget` | +| Archived | Archived | Purged | `Transitioned` | +| Purged | Archived | Purged | `AlreadyAtTarget` | +| Full | Archived | Purged | input transition invalide au niveau API | +| Full | Full | Compacted | `store.postgres_retention_compaction_unsupported`, pré-I/O | +| Compacted attendu | Compacted | Archived | `store.postgres_retention_compaction_unsupported`, pré-I/O | +| Full | Archived | Archived | `ExpectedStateMismatch` | + +### 14.3 Tombstone + +Après purge, la reconstruction autorisée contient exactement : + +```text +reference +slot +format_id +format_version +content_hash +``` + +Le block time et les octets de payload ne restent pas dans la table hot ni dans la table archive. + +## 15. ForceRehydrate + +| Cas | Attendu | +|--------------------------------------|------------------------------------------------| +| Purged + tombstone compatible | restaure Full et payload, outcome `Rehydrated` | +| Purged + slot divergent | conflict | +| Purged + format id/version divergent | conflict | +| Purged + content hash divergent | conflict | +| Full/Archived identique + Force | idempotence normale, pas faux `Rehydrated` | +| race avec purge | sérialisation ; état final cohérent | + +## 16. Erreurs et redaction + +### 16.1 Canary hostile + +Les tests devront injecter des valeurs sentinelles dans : + +```text +URI +server error text +SQL text simulé +signature +observation key +content hash +payload +provider/protocol/method +cursor bytes +row DB invalide +``` + +Aucune sentinelle ne doit apparaître dans `Display`, `Debug` ou message sûr retourné. + +### 16.2 Classification + +| Classe | Interface attendue | +|----------------------------------------|---------------------------------------------------------------| +| input/model invalide | codes `ksp-store-api` acquis | +| canonical/observation divergent | `ERROR_CODE_RAW_CONFLICT` | +| retention representation non supportée | `store.postgres_retention_compaction_unsupported` | +| réseau incohérent | code statique, pas de réseau hostile rendu | +| DB row invalide | code statique de corruption/data invalid | +| query/write PostgreSQL | code statique backend/runtime | +| pool/cancel/migration | catégories runtime existantes ou extension statique justifiée | + +SQLSTATE et texte PostgreSQL ne font pas partie du contrat public. + +## 17. Migration engine + +### 17.1 Multi-version + +Preuves nécessaires avant V001 : + +- V000 reste checksum-identique ; +- registre embedded ordonné `[V000, V001]` ; +- historique vide -> bootstrap correct ; +- V000 seule + auto migrate -> V001 appliquée ; +- V000 seule + auto migrate disabled -> pending/non-ready ; +- V001 checksum divergent -> mismatch ; +- version > V001 -> schema newer ; +- ligne de migration manquante/divergente -> mismatch ; +- deux open concurrents -> advisory lock, historique unique. + +### 17.2 Binding réseau atomique + +L'identité réseau doit être créée/validée avant commit du bootstrap V001. Un crash ne doit pas pouvoir laisser V001 « appliquée » avec une base prête mais sans identité exploitable. + +## 18. Boundaries + +Les scans/tests doivent garantir : + +```text +ksp-store-lib -X-> tokio-postgres/deadpool-postgres/rustls +ksp-store-lib -X-> SQL literal +ksp-store-lib -X-> std::env / dotenv / libpq files +ksp-store-postgres-lib -X-> ksp-store-lib +ksp-store-postgres-lib -X-> direct Config ownership +ksp-store-api -X-> backend physical dependency +``` + +La façade ne publie aucun `Client`, `Transaction`, pool, TLS type, SQL row ou type backend. + +## 19. Feature matrix + +| Build | Attendu | +|---------------------------------------|----------------------------------------------------------------------------| +| `ksp-store-lib` default | feature postgres active, six capacités dispatchables une fois implémentées | +| `ksp-store-lib --no-default-features` | compile ; aucune dépendance backend tirée | +| config backend postgres sans feature | erreur `backend_not_compiled` avant I/O | +| malformed postgres URI avec feature | erreur safe pré-I/O comme `0.3.2` | + +## 20. Audit historique — preuves de non-régression + +Les tests/source scans devront empêcher le retour des patterns historiques rejetés : + +```text +has_raw_transaction_signature +BIGSERIAL id comme identity publique +slot BIGINT dans V001 +canonical_json JSONB +processing_state +ON DELETE SET NULL +raw SQL error text dans Error +cap 500/1000 dans Store pagination +``` + +## 21. Planning de validation par tranche + +### `pre.001` + +- documents plan/validation ; +- version `0.3.3-pre.1` ; +- aucun SQL métier ; +- gate workspace actuel. + +### `pre.002` + +- migration registry multi-version ; +- tests V000 conservés ; +- préparation binding. + +### `pre.003` + +- V001 + tables/constraints/indexes ; +- binding réseau ; +- checksum/inventory tests. + +### `pre.004` + +- row codecs et lectures ; +- malformed DB matrix. + +### `pre.005` + +- atomic writes/idempotence/conflict ; +- observation writes. + +### `pre.006` + +- list/cursor/order/ranges ; +- hostile cursor matrix. + +### `pre.007` + +- archive/purge/tombstone/ForceRehydrate ; +- Compacted unsupported stable. + +### `pre.008` + +- façade/feature dispatch ; +- no physical leak ; +- pre-I/O mismatch. + +### `pre.009` + +- PostgreSQL live opt-in complet. + +### `pre.010` + +- hardening/completeness. + +### `pre.011` + +- gate technique final. + +### `pre.012` + +- réconciliation documentaire. + +### `pre.013` + +- publication prep. + +### `rel.001` + +- publication stable. + +## 22. Gate courant `pre.001` + +```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.3 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +## 23. Critère de clôture de release + +`0.3.3` ne pourra être déclarée stable que si : + +- les six capacités sont réellement implémentées sur `PostgresBackend` et dispatchées par `Store` ; +- aucune valeur valide API n'est narrowée ; +- l'idempotence distingue strictement identité identique et contenu divergent ; +- canonical+observation est atomique ; +- pagination est déterministe et cursorisée ; +- réseau physique incorrect est refusé ; +- rétention supportée est honnête et atomique ; +- `Compacted` n'est pas prétendu sans représentation réelle ; +- tombstone et ForceRehydrate respectent les contrats acquis ; +- les erreurs ne fuient aucun secret/SQL/server text ; +- le live PostgreSQL opt-in couvre concurrence et rollback ; +- tous les gates techniques/doc sont propres ; +- aucun scope `RawAccountState`/worker/app/N2 n'a été ouvert.