# Delta `0.3.3-pre.005` — écriture atomique RawTransaction + observation ## 1. Base requise ```text 0.3.3-pre.4 ``` Le gate opérateur fourni pour `pre.004` est entièrement vert : ```text cargo fmt --all PASS audit Rust général / exports / workspace PASS audit Markdown PASS — 214 tables / 137 files cargo check --workspace PASS cargo clippy --workspace --all-targets PASS cargo test -p ksp-store-api PASS cargo test -p ksp-store-lib PASS cargo test -p ksp-store-postgres-lib PASS — 25 unit tests + canaris, live ignored cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API cargo check -p ksp-store-lib --no-default-features PASS ``` Les lectures physiques `RawTransaction` de `pre.004` sont donc acquises. ## 2. Objectif Implémenter la tranche écriture de la vertical slice PostgreSQL sans ouvrir encore pagination ni transitions mutantes de rétention : ```text persist_raw_transaction_acquisition record_raw_transaction_observation ``` La première opération doit rendre le canonical et son observation atomiques. Les deux opérations doivent être réellement idempotentes et ne jamais confondre une collision de clé avec un contenu identique. ## 3. Version Le workspace passe à : ```text 0.3.3-pre.5 ``` ## 4. Pré-I/O Avant `pool.get()` : ```text transaction.reference.network == backend.network observation.transaction.network == backend.network observation.transaction == transaction.reference ``` Un mauvais réseau retourne `WrongNetwork`. Une acquisition composée de deux références logiques différentes retourne `Conflict` sans toucher PostgreSQL. `record_raw_transaction_observation` valide également son réseau avant toute acquisition du pool. ## 5. Acquisition atomique canonique + observation L'algorithme est : ```text BEGIN INSERT canonical ON CONFLICT (signature) DO NOTHING RETURNING signature si inséré -> candidate Inserted sinon SELECT canonical FOR UPDATE SELECT archive payload éventuel comparer le contenu réel écrire/idempotenter observation COMMIT ``` Toute erreur après l'insert canonique, notamment une collision d'observation divergente, fait sortir sans commit : PostgreSQL rollbacke donc l'ensemble de l'acquisition. Aucun `has_*` ni SELECT préventif n'est introduit. ## 6. Idempotence canonique Pour `Full` et `Archived`, l'égalité exige exactement : ```text reference slot block_time format_id format_version content_hash payload bytes ``` Le hash n'est jamais utilisé seul lorsque les octets restent disponibles. Résultats : ```text première insertion -> Inserted même identité + contenu identique -> AlreadyPresent même identité + contenu divergent -> Conflict ``` Le conflit backend sera projeté vers `ERROR_CODE_RAW_CONFLICT` par la façade dans `pre.008`. ## 7. Tombstone et ForceRehydrate Lorsqu'une signature existe en `Purged`, le backend exige que la forme physique soit réellement minimale : ```text payload hot absent payload archive absent block_time absent ``` Il compare ensuite uniquement les métadonnées conservées par le tombstone : ```text reference slot format_id format_version content_hash ``` Une divergence produit `Conflict`. Pour un tombstone compatible : ```text Normal -> SkippedPurged / NotRecorded ForceRehydrate -> UPDATE payload + block_time + state=full -> observation dans la même transaction -> Rehydrated ``` Le `ForceRehydrate` ne masque donc jamais une divergence détectable. ## 8. Observation idempotente L'observation utilise : ```text INSERT ... ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key ``` Après collision : ```text SELECT observation ... FOR UPDATE ``` Puis le modèle reconstruit est comparé intégralement à l'observation entrante : référence transaction + provenance complète. Résultats : ```text key absente + insert réussi -> Inserted key présente + contenu identique -> AlreadyPresent key présente + contenu divergent -> Conflict ``` ## 9. Observation write séparée `record_raw_transaction_observation` ne crée jamais un canonical implicite. Sous transaction : ```text SELECT retention_state FROM canonical FOR UPDATE ``` Puis : ```text canonical absent -> ReferenceNotFound canonical Purged -> NotRecorded Full / Archived -> insertion/idempotence observation ``` Le verrou canonical prépare aussi la sérialisation future avec purge/rehydrate de `pre.007`. ## 10. Erreurs backend `PostgresBackendErrorKind` ajoute : ```text Conflict ReferenceNotFound WriteFailed ``` Ces erreurs restent composées exclusivement de : ```text kind phase &'static str ``` Aucun texte PostgreSQL, SQLSTATE, query, bind, URI, signature, hash ou payload n'est retenu. ## 11. Surface backend `PostgresBackend` expose désormais en plus : ```text persist_raw_transaction_acquisition record_raw_transaction_observation ``` Les traits `RawTransactionWrite` / `RawTransactionObservationWrite` ne sont pas encore implémentés directement : la tranche finale des six traits attend `list_raw_transactions` en `pre.006`, puis la rétention mutante en `pre.007` et le dispatch façade en `pre.008`. ## 12. Tests déterministes Le miroir `unit_tests/raw_transaction.rs` couvre en plus : ```text pré-I/O réseau + référence acquisition exacte égalité canonique stricte même hash mais payload différent -> Conflict tombstone compatible -> Purged match tombstone divergent -> Conflict mapping exact des origins d'observation ``` Les canaris d'intégration figent : ```text INSERT unique direct sans has_* FOR UPDATE après collision observation unique idempotente ForceRehydrate limité à UPDATE aucun DELETE aucune pagination/cursor aucune transition retention mutante aucune impl complète de trait prématurée ``` La concurrence PostgreSQL réelle et les preuves de rollback restent réservées au test live `pre.009`. ## 13. Migrations Aucune ressource de migration n'est modifiée. Checksums inchangés : ```text V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 ``` ## 14. Fichiers modifiés ```text Cargo.toml crates/ksp-store-postgres-lib/README.md crates/ksp-store-postgres-lib/USAGE.md crates/ksp-store-postgres-lib/src/error.rs crates/ksp-store-postgres-lib/src/lib.rs crates/ksp-store-postgres-lib/src/raw_transaction.rs crates/ksp-store-postgres-lib/src/runtime.rs crates/ksp-store-postgres-lib/tests/dependency_boundary.rs crates/ksp-store-postgres-lib/tests/hardening_completeness.rs crates/ksp-store-postgres-lib/tests/public_api.rs crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md ``` ## 15. Fichiers ajoutés ```text deltas/0.3.3/pre.005.md ``` ## 16. Fichiers supprimés Aucun. ## 17. Hors scope confirmé Aucun changement n'est apporté à : ```text ksp-store-api contrats ksp-store-lib dispatch métier list_raw_transactions / cursor transition Full -> Archived -> Purged Compacted physique RawAccountState worker/job/app migration SQL V000/V001 ``` ## 18. Gate opérateur ```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 ``` Le test PostgreSQL live de fondation reste `#[ignore]`. Aucune URI réelle n'est requise pour cette tranche.