# Plan `0.3.17` — résilience opérationnelle et cycle de vie complet des variantes RAW ## 1. Objet Ce document ferme le gate d'ouverture `0.3.17-pre.001` demandé par `prompts/036-V0_3_17_START_PROMPT.md`. La release part exclusivement de la stable `v0.3.16` et doit compléter les fondations multi-variantes sans développer encore la Store Desk. La cible est volontairement backend/runtime : ```text Store API inspection variantes/conflits/historique actions revisionnées classification Store Transient / Terminal PostgreSQL conflict cases complets participants historique append-only résolution/reopen concurrent-safe rétention/purge guards des variantes Store facade dispatch backend-neutral classification publique sûre Worker retry Store borné backpressure activity Blocked cancellation/drain Transport/Config durcissement des reconnects propriétaires existants aucune nouvelle pile réseau reconnect/replay/coverage toujours distincts ``` `0.3.18` consommera ensuite ces contrats dans `ksp-app-store-desk`. ## 2. Base stable et audit d'ouverture Baseline logique requise : ```text v0.3.16 workspace.package.version = 0.3.16 ``` L'archive reçue contient bien : ```text prompts/036-V0_3_17_START_PROMPT.md prompts/035-V0_3_16_START_PROMPT.md docs/plans/037-V0_3_16_RAW_RESILIENCE_CONFLICT_HANDOFF.md docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md docs/validation/033-V0_3_16_RAW_RESILIENCE_CONFLICT.md deltas/0.3.16/pre.001.md ... pre.010.md deltas/0.3.16/pre.*-fix.* réellement publiés deltas/0.3.16/rel.001.md ``` Le workspace comporte 22 membres et tous les manifests membres héritent de `workspace.package.version` et de l'édition workspace. Les audits statiques du dépôt exécutés sur l'archive avant modification sont propres : ```text General Rust rule audit: clean Rust export completeness audit: 0 candidate(s) KSP workspace Rust rule audit: clean Markdown table audit: clean (352 table(s), 962 file(s)) ``` Le toolchain Cargo n'est pas disponible dans l'environnement d'assemblage de cette tranche. Les commandes Cargo ne sont donc pas déclarées réexécutées ici. Le gate utilisateur de la stable `0.3.16` fourni avec l'archive reste la preuve externe de `fmt/check/clippy` et des suites ciblées de fermeture. L'archive binaire a été matérialisée localement et son SHA-256 est enregistré dans `docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md`. L'environnement d'assemblage n'a pas permis une comparaison réseau indépendante du commit du tag Gitea. La provenance « ZIP téléchargé depuis le tag `v0.3.16` » reste donc fournie par l'opérateur ; l'identité stable interne de l'arbre est cohérente avec cette provenance. ## 3. Règles normatives réauditées Les sources suivantes ont été relues dans l'ordre imposé par le prompt : ```text RULES.md ROADMAP.md CHANGELOG.md docs/000-README.md docs/rules/RULES_GENERAL.md docs/rules/RULES_KSP.md docs/rules/RULES_RUST.md docs/rules/RULES_DEPENDENCIES.md docs/rules/RULES_DOCUMENTATION.md docs/rules/FILE_CONTRACTS.md docs/rules/VERSION_WORKFLOW.md docs/rules/PROMPT_STRUCTURE.md ``` Conséquences bloquantes pour `0.3.17` : - Rust 2024 reste la seule édition workspace ; - `unsafe` reste interdit ; - `unwrap`, `expect`, `panic` et retours implicites restent interdits selon les lints KSP ; - aucun `pub mod` n'est ajouté ; - tout item partagé `pub`/`pub(crate)` est réexporté au crate root et consommé via `crate::Item` intra-crate ; - les tests unitaires restent sous `unit_tests/`, les tests d'intégration sous `tests/` ; - V000/V001/V002/V003 sont immuables une fois publiés ; toute évolution physique devient une migration additive suivante ; - le Store physique reste privé derrière `ksp-store-lib` ; - le Worker ne crée ni backend Store direct, ni client PostgreSQL, ni seconde pile Transport ; - le Job Backfill et le Worker restent des producteurs indépendants ; - aucune queue ou retry non borné n'est autorisé ; - un PASS live n'est déclaré que lorsqu'il a réellement été exécuté ; - `pre.001` est un gate de brainstorming/planification et ne doit pas contenir d'implémentation Rust/SQL lourde ; - la lane technique/live finale, la réconciliation documentaire et la préparation de publication restent trois étapes distinctes ; - la première prerelease `0.3.17-pre.001` utilise la version Cargo `0.3.17-pre.1`. ## 4. Baseline fonctionnelle héritée de `0.3.16` ### 4.1 Store API La stable fournit déjà : ```text RawTransactionVariantId RawTransactionVariantReference RawTransactionVariantOrigin RawTransactionVariantRelation RawTransactionVariantRelationReason RawTransactionVariantComparison RawTransactionVariantWriteOutcome RawAcquisitionWriteOutcome::transaction_variant() RawTransactionConflictStatus { Open, Resolved } ``` Le comparateur partagé reste l'autorité automatique et conserve exactement les relations : ```text Exact CompatibleLessComplete CompatibleMoreComplete Conflict Incomparable ``` La seule dominance actuellement prouvée est la troncature stricte de `logMessages`. Aucun élargissement de dominance n'est prévu dans `0.3.17`. Les capabilities actuelles couvrent canonical transaction, observations, inspection canonical/observations et rétention V001. Elles ne fournissent pas encore de capability publique dédiée aux variantes, conflict cases, historique ou actions de résolution. ### 4.2 PostgreSQL V003 V003 possède quatre objets métier centraux : ```text ksp_raw_transaction_variants ksp_raw_transaction_canonical_selectors ksp_raw_transaction_observation_variants ksp_raw_transaction_conflicts ``` Le conflict case V003 est volontairement minimal : une ligne courante par signature, avec : ```text status revision canonical_variant_id incoming_variant_id latest_relation latest_reason_code created_at updated_at ``` Il ne contient ni participant ledger complet, ni journal append-only, ni résolution explicite vers une variante, ni historique d'actions. ### 4.3 Promotion et conflit actuels L'acquisition PostgreSQL sérialise d'abord l'identité canonical V001 avec `FOR UPDATE`, bootstrap le ledger V003, compare les bytes et : ```text Exact -> réutilise le canonique CompatibleLessComplete -> conserve la variante reçue, canonique inchangé CompatibleMoreComplete -> promotion atomique selector + projection V001 Conflict / Incomparable -> conserve la variante, ouvre/met à jour le conflict case ``` Une divergence durable `Conflict/Incomparable` retourne `QuarantinedConflict`, puis l'observation est liée à la variante réellement reçue avant commit. ### 4.4 Worker actuel Le Worker dépend déjà de `ksp-store-lib`, pas du backend PostgreSQL. La convergence run-local ne fait que sérialiser une même identité. Chaque acquisition atteint ensuite le Store. `QuarantinedConflict` est un succès durable ; le Worker reste `Running` et projette une health `Degraded`. Une autre erreur Store est aujourd'hui projetée comme erreur de persistence et devient terminale. Il n'existe pas encore de retry Store dédié. `WorkerActivity` contient actuellement : ```text Unknown Idle Active ``` `WorkerHealth` contient : ```text Unknown Healthy Degraded Unhealthy ``` La notion `Blocked` n'est donc pas encore matérialisée. ### 4.5 Store error surface actuelle `ksp-store-lib` expose des codes sûrs mais plusieurs sont backend-spécifiques : ```text postgres_connect_failed postgres_pool_timeout postgres_read_failed postgres_write_failed postgres_data_invalid postgres_migration_failed postgres_migration_mismatch postgres_schema_newer postgres_tls_failed wrong_network ... ``` Le backend PostgreSQL redacted les erreurs physiques en `PostgresBackendErrorKind`, mais les échecs `tokio-postgres` de statement/transaction sont encore souvent aplatis en `ReadFailed` ou `WriteFailed` sans conserver le SQLSTATE structuré. Cette perte d'information doit être corrigée avant de prétendre classifier sûrement `Transient/Terminal`. ### 4.6 Transport/Config actuels WebSocket et Yellowstone possèdent déjà chacun un reconnect Transport-owned : ```text max_retries initial_backoff max_backoff ``` Config sait déjà projeter les defaults et overrides vers ces settings. Yellowstone possède déjà une borne KSP `MAX_GRPC_RECONNECT_RETRIES = 100`, ainsi que connect/unary/close timeouts bornés. Le WebSocket valide les backoffs et leur ordre mais ne borne pas encore explicitement `max_retries` à un maximum KSP. Cette asymétrie est une cible de hardening `0.3.17`, pas la justification d'une nouvelle boucle de reconnect. Yellowstone conserve déjà des projections distinctes pour : ```text reconnect requested from_slot replay delivery replay coverage unproven SubscribeReplayInfo first_available ``` Les contrats gap/coverage de `0.3.14` restent autoritaires. ## 5. Réaudit upstream utile à `pre.001` ### 5.1 PostgreSQL PostgreSQL 17 documente que les row locks sont tenus jusqu'à la fin de transaction, que les deadlocks sont détectés avec abandon d'une transaction, et recommande un ordre de locks cohérent. Il documente également le retry des `serialization_failure` SQLSTATE `40001` et, lorsque pertinent, des `deadlock_detected` SQLSTATE `40P01`. Politique KSP retenue : ```text ordre de locks déterministe transactions courtes aucune attente opérateur sous transaction 40001 -> Transient 40P01 -> Transient 23505/23P01 -> pas de retry générique ; classifier selon l'invariant KSP concret ``` ### 5.2 tokio-postgres / deadpool-postgres `tokio-postgres` expose un `DbError::code()` structuré de type `SqlState`. Deadpool distingue notamment `Timeout`, `Backend`, `Closed`, `NoRuntimeSpecified` et `PostCreateHook`. Politique KSP retenue : ```text inspecter type/SQLSTATE avant redaction ne jamais classifier par texte serveur ne jamais conserver le message DB arbitraire dans l'erreur publique projeter uniquement classe publique + code KSP sûr + phase statique ``` ### 5.3 Solana WebSocket Les subscriptions Solana fournissent des notifications de session ; `signatureSubscribe` est explicitement one-shot et supprimé par le serveur après notification. Les docs PubSub ne fournissent pas une garantie générale de replay/coverage après remplacement d'une session. Politique KSP retenue : ```text reconnect physique -> fait Transport resubscribe -> restauration logique de subscription aucune de ces étapes ne prouve la coverage la réparation/gap ledger Worker reste séparée ``` ### 5.4 Yellowstone Le proto upstream expose `SubscribeRequest.from_slot`; KSP possède déjà `SubscribeReplayInfo` et une projection conservatrice du replay. Politique KSP retenue : ```text from_slot = demande de replay ReplayInfo = borne de disponibilité annoncée replay delivery = preuve de réception de matériau coverage = preuve distincte et plus forte ``` Aucun reconnect réussi n'efface un gap par lui-même. ### 5.5 Tokio Le runtime de retry doit être cancellable. `tokio::select!` annule les branches non retenues ; les primitives à file d'attente équitable comme `Mutex::lock` et `Semaphore::acquire` demandent une attention particulière à la cancellation safety. Politique KSP retenue : le backoff Store utilise un sleep stop-preemptible et ne réorganise pas silencieusement l'admission ou les permits de persistence. ## 6. Modèle conceptuel conflict case retenu ### 6.1 Identité Un conflict case est durablement identifié par la même identité transactionnelle backend-neutral : ```text RawTransactionConflictReference transaction: RawTransactionReference ``` Aucun UUID ou identifiant provider n'est nécessaire. Le backend PostgreSQL reste lié à un seul network, mais la DTO publique conserve le `RawTransactionReference` complet afin que l'identité publique reste `(network, signature)`. ### 6.2 Etats Aucun troisième état n'est justifié au gate : ```text Open Resolved ``` Les notions « stale », « blocked », « archived » ou « unavailable payload » sont des outcomes/états d'autres objets et ne deviennent pas des statuts artificiels du conflict case. ### 6.3 Revision Chaque mutation durable du conflict case incrémente une revision `u64` strictement monotone : ```text création Open revision = 1 nouveau participant/relation revision + 1 résolution revision + 1 reopen revision + 1 nouvelle divergence après Resolved -> reopen atomique + revision + 1 ``` Une réobservation strictement identique d'un état Open déjà représenté reste idempotente et ne consomme pas une revision supplémentaire. L'épuisement `u64` est terminal/fail-closed ; aucun wrap n'est autorisé. ### 6.4 Participants Le participant ledger est monotone par conflict case : ```text (transaction, variant_id) unique first_seen_revision first_seen_at ``` Le canonique impliqué dans la divergence et chaque variante divergente observée deviennent participants. Un participant n'est pas supprimé lors de la résolution ou d'un reopen ; le journal conserve l'audit de la case complète. ### 6.5 Historique append-only Chaque transition réelle produit un événement append-only : ```text Opened ParticipantAdded ResolvedKeepCanonical ResolvedPromoteVariant ResolvedRestoreVariant Reopened ``` Un événement contient seulement les références/codes nécessaires : ```text conflict reference conflict revision event kind canonical before candidate/participant lorsque pertinent canonical after relation/reason lorsque pertinent resolution action lorsque pertinent timestamp KSP borné ``` Aucun payload RAW, SQL, texte serveur ou chaîne acteur libre n'entre dans l'historique. `0.3.17` ne possède pas de système d'identité/authentification opérateur. Il ne doit donc pas inventer un `actor` arbitraire. Le journal distingue seulement l'origine sémantique sûre de l'action lorsqu'elle est utile : ```text Automatic Operator ``` L'identité humaine authentifiée éventuelle appartient à une couche future disposant réellement de ce contrat. ### 6.6 Résolution courante Le conflict case courant expose : ```text status revision canonical variant courante selector revision courante latest incoming participant latest relation/reason resolved canonical variant si Resolved latest resolution action si Resolved created_at updated_at participant_count ``` Le couple `conflict_revision + canonical_revision` évite de confondre évolution du conflit et évolution du selector. ## 7. Contrats Store API proposés Les noms exacts peuvent être ajustés lors de `pre.002` pour respecter les inventaires publics, mais la sémantique suivante est fixée. ### 7.1 Inspection des variantes DTOs backend-neutral : ```text RawTransactionVariantSummary RawTransactionVariantInspectionQuery RawTransactionVariantDetail ``` Le summary reste payload-free et expose au maximum : ```text variant reference origin slot block_time format identity retention state payload size si disponible is_current_canonical observation count created_at ``` Le `Debug` ne rend pas la signature brute, les bytes ou un hash de contenu sensible lorsque les règles existantes l'interdisent. Le detail explicite peut fournir la transaction complète uniquement via une capability dédiée et bornée ; les listes interactives restent payload-free. Capability cible : ```text RawTransactionVariantInspectionRead ``` ### 7.2 Inspection des conflict cases DTOs : ```text RawTransactionConflictReference RawTransactionConflictSummary RawTransactionConflictInspectionQuery RawTransactionConflictDetail RawTransactionConflictParticipantSummary ``` Capability cible : ```text RawTransactionConflictInspectionRead ``` La liste est random-access bornée comme les inspections Store actuelles. La query peut filtrer au minimum par network/status et, lorsque nécessaire, par transaction exacte ; elle n'invente pas de recherche texte libre. ### 7.3 Historique DTOs : ```text RawTransactionConflictEventKind RawTransactionConflictEventSummary RawTransactionConflictHistoryQuery ``` Capability cible : ```text RawTransactionConflictHistoryRead ``` L'historique est ordonné par revision et paginé ; aucun event n'embarque de payload RAW. ### 7.4 Actions mutables Une seule capability cohérente peut porter des requests typées : ```text RawTransactionConflictActionWrite ``` Actions : ```text KeepCurrentCanonical PromoteVariant RestoreVariant Resolve Reopen ``` `Resolve` n'est pas une fusion implicite ; il clôt un case avec une décision canonique explicite. `PromoteVariant` et `RestoreVariant` sélectionnent une variante durable donnée. Le nom `Restore` signale l'intention opérateur mais utilise les mêmes garanties atomiques de selector que `Promote`. Chaque request mutable contient : ```text conflict reference expected_conflict_revision expected_canonical_revision cible variant lorsque pertinent ``` Les revisions valent `u64 > 0`. ### 7.5 Outcomes d'action Les outcomes ne transforment pas une concurrence métier attendue en erreur opaque : ```text Applied AlreadyAtTarget StaleRevision ConflictNotOpen ConflictNotResolved VariantNotParticipant VariantPayloadUnavailable ``` La liste exacte sera minimisée en `pre.002`, mais `StaleRevision` et `AlreadyAtTarget` doivent rester distincts. Idempotence retenue : une répétition d'une action dont la cible est déjà exactement l'état durable demandé renvoie `AlreadyAtTarget` sans nouvelle revision. Une requête stale visant un autre état renvoie `StaleRevision` et ne modifie rien. ## 8. Classification publique Store `Transient / Terminal` ### 8.1 Type public `ksp-store-api` doit posséder un enum backend-neutral non exhaustif : ```text StoreErrorClass Transient Terminal ``` La classification d'une erreur concrète doit être obtenue via un contrat stable, pas par comparaison ad hoc de strings dans le Worker. Contrat cible : ```text StoreErrorClassifier classify_store_error(&Error) -> StoreErrorClass ``` `ksp-store-lib::Store` implémente ce contrat. Le Worker enrichit son port privé de persistence avec cette capability au lieu de connaître les codes PostgreSQL. Fail-closed : un code inconnu est `Terminal` jusqu'à classification explicite. ### 8.2 Matrice KSP retenue `Transient` : ```text connexion physique indisponible/reset après configuration valide pool temporairement indisponible / timeout d'acquisition SQLSTATE classe connection exception lorsque l'opération est rejouable 40001 serialization_failure 40P01 deadlock_detected 57P01 admin_shutdown 57P02 crash_shutdown 57P03 cannot_connect_now ``` `Terminal` : ```text configuration/secret/TLS local invalide backend non compilé network KSP mismatch modèle/query invalide stored data incompatible migration mismatch schema newer constraint révélant une violation d'invariant KSP legacy raw content conflict stale revision retention/pin conflict variant absent ou payload indisponible pour action locale page limit non représentable retention compaction unsupported ``` Cas à ne pas généraliser : ```text 23505 unique_violation 23P01 exclusion_violation ``` Ils ne deviennent pas `Transient` par défaut. Une opération KSP spécifique peut prouver qu'un de ces SQLSTATE représente une race retryable ; sinon fail-closed `Terminal`. ### 8.3 Cancellation Une cancellation demandée localement par le Worker n'est pas un outage Store à retryer. Le runtime traite d'abord son token de stop/cancellation ; une erreur résultante n'est jamais recyclée en boucle de retry. ### 8.4 Refactor PostgreSQL requis Le backend ne doit plus convertir immédiatement toute erreur de statement en `ReadFailed/WriteFailed` avant inspection. Une fonction privée unique doit : ```text inspecter tokio_postgres::Error extraire DbError::code() si présent classifier les SQLSTATE explicitement supportés classifier les erreurs connection/closed/IO de façon sûre jeter le texte/source externe après projection retourner PostgresBackendErrorKind + phase statique ``` Les détails SQLSTATE restent privés ; la frontière publique ne reçoit que le code KSP redacted et `StoreErrorClass`. ## 9. Stratégie PostgreSQL V004 ### 9.1 Principe V000/V001/V002/V003 restent byte/checksum-identiques. `0.3.17` crée une migration additive V004 uniquement après stabilisation des contrats `pre.002`. ### 9.2 Objets requis La forme physique minimale envisagée : ```text extension additive de ksp_raw_transaction_conflicts resolved_canonical_variant_id nullable latest_resolution_action nullable resolved_at_unix_millis nullable ksp_raw_transaction_conflict_participants transaction_signature variant_id first_seen_revision first_seen_at_unix_millis ksp_raw_transaction_conflict_events transaction_signature revision event_kind canonical_before_variant_id candidate_variant_id nullable canonical_after_variant_id relation nullable reason_code nullable resolution_action nullable action_origin created_at_unix_millis ``` Les colonnes exactes sont validées dans `pre.003` avec les contraintes réelles ; aucun champ libre n'est ajouté « au cas où ». ### 9.3 Bootstrap legacy V003 Pour un conflict case déjà présent avant V004 : ```text conserver status/revision V003 créer les participants prouvables depuis canonical_variant_id + incoming_variant_id créer un événement BaselineImported ou équivalent explicitement qualifié legacy ne pas inventer les transitions antérieures perdues ne pas prétendre reconstruire un ancien canonical non prouvé ``` Si `BaselineImported` n'apporte pas assez de valeur, `pre.003` peut retenir un marqueur `history_complete_from_revision` sur la projection. Il est interdit de fabriquer un historique détaillé. ## 10. Ordre de locks et CAS Ordre global retenu pour toute mutation d'une transaction RAW concernée : ```text 1. ksp_raw_transactions identité V001 FOR UPDATE 2. canonical selector FOR UPDATE / CAS 3. conflict current row FOR UPDATE / CAS 4. variants nécessaires ordre variant_id croissant si plusieurs locks sont requis 5. participant/event/pin sidecars inserts/updates déterministes 6. projection V001 + selector + conflict même transaction 7. commit ``` Aucune interaction UI, sleep, réseau ou retry n'a lieu à l'intérieur de cette transaction. La résolution manuelle doit relire et comparer : ```text expected_conflict_revision expected_canonical_revision ``` avant toute mutation. La promotion automatique `CompatibleMoreComplete` doit utiliser le même ordre de locks et continuer de ne s'appliquer qu'à une dominance prouvée. ## 11. Matrice de races ### Ingestion pendant résolution La première transaction qui détient l'identité V001 décide. La seconde relit selector/conflict après acquisition du lock et réévalue sa request/comparaison. Aucun résultat calculé avant lock n'est appliqué aveuglément. ### Promotion automatique pendant résolution manuelle Même lock order. Si la promotion automatique gagne, la `canonical_revision` change et la résolution manuelle devient stale sauf si sa cible est déjà exactement atteinte. ### Deux résolutions concurrentes Une seule peut satisfaire les deux expected revisions. L'autre retourne `StaleRevision`, sauf répétition exacte de la cible déjà durable -> `AlreadyAtTarget`. ### Nouvelle divergence après Resolved L'ingestion ouvre de nouveau la même conflict identity, incrémente la conflict revision, ajoute le participant si nouveau et écrit `Reopened`/`ParticipantAdded` de manière append-only. La décision précédente reste historique. ### Rétention pendant résolution La rétention acquiert d'abord l'identité V001 selon le même ordre et réévalue les guards sous lock. Elle ne peut purger les bytes d'une variante requise par le selector courant ou un conflict case Open. ### ForceRehydrate Le contrat V001 reste : la tombstone purgée conserve assez d'identité/hash pour refuser une rehydratation incompatible. `ForceRehydrate` ne choisit jamais automatiquement une autre variante et ne devient pas un mécanisme de rollback réseau. ## 12. Rétention des variantes ### 12.1 Deux notions distinctes Le plan distingue : ```text identity retention payload retention ``` L'identité d'une variante référencée par selector, observation mapping ou historique n'est jamais supprimée. Les bytes peuvent suivre : ```text Full -> Archived -> Purged ``` uniquement lorsque les guards l'autorisent. ### 12.2 Hard payload pins Purge interdite lorsque la variante est : ```text canonical selector courant participant d'un conflict case Open cible d'une mutation en cours sous transaction ``` Une observation mapping et l'historique conservent l'identité mais ne pin pas éternellement le payload à eux seuls. Cette décision évite qu'un historique append-only transforme toute divergence passée en rétention infinie des bytes. ### 12.3 Resolution et rollback Après résolution : ```text la variante canonique reste pin par le selector les variantes perdantes peuvent être archivées une purge reste une action explicite de rétention, jamais automatique dans Store ``` `RestoreVariant` fonctionne uniquement avec une variante dont les bytes sont encore localement disponibles `Full` ou `Archived`. Si le payload a été explicitement purgé : ```text VariantPayloadUnavailable ``` est retourné ; le Store ne consulte pas le réseau et ne fabrique pas les bytes. L'historique continue de pointer vers l'identité de la variante purgée et rend donc la perte de capacité de rollback visible, non silencieuse. ### 12.4 Archive physique des variantes V003 sait représenter `retention_state` sur les variantes mais ne possède pas encore le sidecar d'archive équivalent au canonical V001. `pre.005` doit décider/implémenter l'objet physique minimal permettant : ```text Full variant payload inline Archived variant payload local sidecar Purged variant payload absent mais metadata/identity conservées ``` Aucune suppression de row de variante n'est nécessaire. ## 13. Worker Store retry ### 13.1 Ownership Le backend classe et retourne. Le Worker décide : ```text retry count backoff stop preemption backpressure health/activity exhaustion ``` Aucune boucle retry cachée n'est ajoutée dans PostgreSQL ou `ksp-store-lib`. ### 13.2 Settings ciblés Extension de `RawTransactionIngestSettings` : ```text store_retry_max_retries store_retry_initial_backoff store_retry_max_backoff ``` Valeurs proposées : ```text default max_retries = 5 maximum max_retries = 20 default initial_backoff = 250 ms default max_backoff = 5 s minimum backoff = 10 ms maximum backoff setting = 30 s ``` `max_retries = 0` désactive le retry et conserve un comportement terminal immédiat sur une erreur `Transient`. Aucun jitter n'est introduit dans la première implémentation. L'ownership reste explicitement Worker et peut être réaudité plus tard si les mesures live montrent un herd effect réel. La concurrence de persistence et l'admission bornée constituent déjà la première limite anti-storm. ### 13.3 Backoff Backoff exponentiel borné : ```text initial * 2^(attempt - 1) cap = max_backoff ``` Le calcul est overflow-safe et saturé au cap. Le sleep est stop-preemptible avec `tokio::select!`. ### 13.4 Aucune retry queue supplémentaire Une acquisition admise conserve son slot de travail pendant toute sa séquence de retry. Il n'existe pas de queue de retry distincte : ```text source -> admission queue bornée -> persistence concurrency permit -> attempt/backoff/attempt -> durable outcome ou exhaustion ``` Ainsi : - le nombre de persistence sequences actives reste borné par `persistence_concurrency` ; - lorsque ces séquences sont bloquées sur Store, l'admission queue se remplit puis applique naturellement la backpressure existante ; - aucune croissance mémoire sans borne n'est possible. ### 13.5 Ambiguous commit Un échec de connexion après envoi peut être ambigu quant au commit serveur. Le retry est autorisé uniquement parce que l'acquisition RAW Store est conçue idempotente : identité canonical, variante exacte et observation key déterministe convergent vers le même état durable. Le Worker ne mémorise jamais « failed donc absent ». Après retry, un résultat durable `AlreadyPresent/AlreadyPresent` est un succès normal. ### 13.6 Blocked / health `Blocked` est une activité, pas une health. Evolution prévue de `ksp-worker-api::WorkerActivity` : ```text Unknown Idle Active Blocked ``` Projection : ```text retry Store en attente/backoff avec travail durable non terminé state = Running activity = Blocked health = Degraded Store récupéré et pipeline reprend state = Running activity = Active/Idle selon travail health = politique normale, sauf autre dégradation retry exhausté / erreur Terminal state = Faulted(code) health = Unhealthy ``` Le snapshot RAW ajoute des compteurs bornés/checked nécessaires, sans code PostgreSQL physique : ```text store_retry_total store_transient_failure_total store_retry_exhaustion_total store_blocked_in_flight ``` Les noms définitifs seront minimisés en `pre.007`. ### 13.7 Stop et drain Pendant backoff : stop interrompt immédiatement le sleep et passe au drain normal. Une persistence déjà envoyée au Store n'est pas relancée aveuglément en parallèle. La séquence attend son résultat ou le mécanisme de drain/abort existant, puis aucune nouvelle tentative n'est admise après la décision de stop. ## 14. Transport/Config `0.3.17` ### 14.1 Pas de nouvelle abstraction universelle Les acteurs existants restent propriétaires : ```text WsSession -> reconnexion WebSocket Yellowstone subscribe stream -> reconnexion/replay gRPC HTTP runtime -> retry HTTP existant ``` Le Worker ne reçoit aucun socket ou channel bas niveau. ### 14.2 WebSocket Hardening prévu : ```text ajouter une borne KSP explicite à reconnect.max_retries aligner les validations Config/Transport vérifier overflow-safe du backoff conserver stop preemption et resubscribe existants conserver remote subscription IDs privés ``` Aucun changement de semantics de coverage. ### 14.3 Yellowstone Le gRPC possède déjà les primitives requises. `0.3.17` doit principalement vérifier : ```text bornes Config identiques aux bornes Transport connect/session deadlines réellement appliquées close/stop toujours bornés reconnect attempt snapshot cohérent from_slot/ReplayInfo conservateurs aucune fermeture de gap par simple reconnect ``` ### 14.4 Reset d'un budget de reconnect Le comportement existant reset le budget après un reconnect réussi. Le gate technique doit vérifier qu'un flapping durable reste observable et ne produit pas une boucle CPU : chaque perte de session réapplique le backoff borné et les compteurs de lifecycle restent visibles. Aucun `reset_after_stable_duration` supplémentaire n'est justifié dans cette release sans défaut concret. ## 15. Compatibilité Job Backfill Le Job Backfill reste inchangé dans son ownership : ```text Transport observé -> canonicalisation RAW commune -> Store facade ``` Il doit continuer à : - traiter `QuarantinedConflict` selon son contrat actuel ; - ne pas dépendre des nouvelles capacités opérateur pour persister ; - ne pas implémenter le retry live Worker ; - ne pas recevoir de dépendance Worker ; - rester compilable lorsque les traits Store existants sont étendus par de nouveaux traits séparés plutôt que par modification incompatible des traits déjà implémentés. C'est une raison supplémentaire pour ajouter des capabilities fines séparées au lieu d'ajouter des méthodes obligatoires à `RawTransactionWrite`. ## 16. Sécurité et redaction Les nouvelles surfaces ne doivent jamais exposer : ```text PostgreSQL URI/password SQL statement DbError message/detail/hint endpoint complet avec secret payload RAW dans summary/history/error signature brute dans Debug lorsque la politique actuelle la redacted content hash dans Debug si la surface existante l'interdit backend row/client/transaction handle ``` Les SQLSTATE sont utilisés en interne pour classification, pas comme dépendance publique du Worker. Les reason/action codes publics sont des enums/codes KSP bornés, jamais des chaînes serveur ou saisies utilisateur libres. ## 17. Tests et preuves nécessaires ### 17.1 Store API Canaris attendus : ```text constructibilité crate-root object safety des nouveaux traits non-exhaustive des enums évolutifs validation des revisions non nulles queries/histories bornées Debug payload-free/redacted external backend peut implémenter les nouveaux contrats sans dépendance runtime classification inconnue fail-closed Terminal ``` ### 17.2 V004/PostgreSQL Canaris : ```text V000/V001/V002/V003 bytes/checksums inchangés registry V004 exacte bootstrap legacy sans historique fabriqué participants monotones history append-only revision CAS selector/projection atomiques ordre de locks stable résolution concurrente reopen concurrent rollback complet sur erreur tardive purge guard sous race variant archive/purge shape exact ``` ### 17.3 Worker Canaris : ```text Transient -> retry borné Terminal -> aucun retry max_retries=0 -> faute immédiate backoff cap/overflow exact stop pendant sleep stop pendant attempt queue bornée sous outage prolongé activity Blocked + health Degraded recovery -> reprise sans respawn Worker exhaustion -> Faulted/Unhealthy ambiguous durable success -> idempotence, pas double observation QuarantinedConflict reste succès non terminal ``` ### 17.4 Transport Canaris : ```text WS reconnect max_retries borné WS/grpc backoff borné et stop-preemptible Config projection exacte defaults/overrides reconnect != replay delivery replay delivery != coverage Yellowstone from_slot/ReplayInfo non régressés ``` ### 17.5 Live PostgreSQL La lane technique finale doit exécuter un test opt-in PostgreSQL réel couvrant au minimum : ```text open conflict -> participants/history résolution CAS stale concurrent action reopen par nouvelle divergence restore d'une variante locale purge guard d'un participant Open archive puis restore lorsque supporté transient-classification path observable sans secret rollback transactionnel ``` La version serveur réellement observée est journalisée de manière sûre ; aucune URI n'est rendue. ### 17.6 Outage/recovery Store Une preuve ciblée doit simuler ou provoquer de manière déterministe : ```text Store temporairement indisponible Worker Running/Blocked/Degraded admission bornée recovery avant exhaustion persistence idempotente retour à activité normale ``` Si une panne PostgreSQL réelle contrôlée n'est pas raisonnablement automatisable, le test déterministe sur le port privé Worker prouve la politique et le live PostgreSQL prouve séparément la classification physique. ## 18. Découpage recalibré de `0.3.17` Le découpage initial `pre.001 -> pre.009` reste trop dense autour du couple contrats/migration/backend. Il est subdivisé sans fusionner les lanes de fermeture. ### `0.3.17-pre.001` Gate présent : lecture, audit, upstream re-audit, modèle conceptuel, contrats proposés, error classification, retry/reconnect policy, sizing et plan. Aucun Rust/SQL lourd. ### `0.3.17-pre.002` Store API : DTOs/capabilities variantes/conflicts/history/actions, expected revisions, outcomes stale/idempotent et type/contrat `StoreErrorClass` backend-neutral. ### `0.3.17-pre.003` PostgreSQL V004 additive : participants, history, projection de résolution et bootstrap legacy, avec canaris byte/checksum V000-V003. ### `0.3.17-pre.004` PostgreSQL lifecycle : inspection, resolve/promote/keep/restore/reopen, CAS double revision, lock order, races ingestion/résolution et history append-only. ### `0.3.17-pre.005` Rétention des variantes : archive locale, payload states, purge guards, interaction selector/open conflict, restore local et ForceRehydrate non régressé. ### `0.3.17-pre.006` `ksp-store-lib` : dispatch des nouvelles capabilities, classification publique, raffinement SQLSTATE/backend privé, erreurs redacted et régression Job Backfill. ### `0.3.17-pre.007` Worker : settings Store retry, backoff borné, `WorkerActivity::Blocked`, backpressure, cancellation, exhaustion, recovery et snapshots/counters sûrs. ### `0.3.17-pre.008` Transport/Config : bornage WebSocket manquant, cohérence reconnect settings, hardening session/stop et non-régression Yellowstone replay/coverage. ### `0.3.17-pre.009` Gate technique/live final : races cross-layer, Store outage/recovery, retry, reconnect, PostgreSQL live, tests workspace pertinents et graphes de dépendances requis. ### `0.3.17-pre.010` Réconciliation documentaire finale uniquement : plan, validation, README/USAGE et architectures durables concernées. Aucun nouveau comportement runtime. ### `0.3.17-pre.011` Préparation de publication minimale : `CHANGELOG.md`, `ROADMAP.md`, prompt `0.3.18`, version et delta. ### `0.3.17-rel.001` Publication stable mécanique après gates propres. La prévision reste souple : un défaut local peut produire un `fix.*`; une tranche trop lourde peut être subdivisée, mais la lane technique/live doit rester avant la réconciliation documentaire et la publication prep. ## 19. Fichiers attendus par tranche Le plan ne pré-approuve pas des fichiers non nécessaires, mais l'ownership attendu est : ```text pre.002 ksp-store-api pre.003 ksp-store-postgres-lib migration/schema registry/resources pre.004 ksp-store-postgres-lib runtime/SQL/tests pre.005 ksp-store-api + ksp-store-postgres-lib retention variant pre.006 ksp-store-lib + PostgreSQL classification + Backfill regression tests pre.007 ksp-worker-api + ksp-worker-raw-transaction-ingest-lib pre.008 ksp-onchain-transport-lib + ksp-config-lib pre.009 tests/hardening/live only selon besoins prouvés pre.010 documentation durable pre.011 release metadata/prompt ``` Toute dépendance nouvelle doit être justifiée séparément avant ajout. Aucune nouvelle crate n'est prévue. ## 20. Hors périmètre confirmé `0.3.17` ne contient pas : ```text Store Desk frontend/backend Tauri Backfill multi-route/multi-stratégie Backfill Desk multi-route RAW -> STRUCTURAL STRUCTURAL persistence DECODED / DOMAIN majority voting provider provider priority arbitrage canonique par provenance fusion heuristique JSON retry Store illimité retry caché dans le backend nouveau client PostgreSQL dans Worker nouvelle pile WebSocket/gRPC dans Worker nouvelle dépendance Worker <-> Backfill reconstruction réseau comme rollback normal identité opérateur/authentication inventée ``` ## 21. Critères de fermeture du gate `pre.001` Après ce plan, le gate conceptuel est considéré fermé lorsque la validation associée confirme : ```text base stable cohérente règles relues et audits statiques propres contrats Store API proposés et bornés conflict lifecycle complet défini revision/CAS et races définis migration V004 justifiée et additive rétention/pins/rollback définis Transient/Terminal défini sans texte libre Worker retry/backpressure/Blocked défini Transport reconnect réutilise les mécanismes existants Backfill reste indépendant et compatible preuves techniques/live identifiées prereleases recalibrées hors périmètre explicite ``` L'implémentation commence seulement en `0.3.17-pre.002`.