v0.3.4-pre.001

This commit is contained in:
2026-08-30 19:21:15 +02:00
parent bcce89f076
commit d95b1095e5
4 changed files with 1145 additions and 2 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 363
# version: 364
[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.3"
version = "0.3.4-pre.1"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

206
deltas/0.3.4/pre.001.md Normal file
View File

@@ -0,0 +1,206 @@
<!-- file: deltas/0.3.4/pre.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.001` — audit et physical design `RawAccountState` PostgreSQL
## 1. Objet
Ouverture de :
```text
0.3.4 — Store/PostgreSQL RawAccountState + complétude/conformance RAW
```
Cette tranche reste volontairement un gate de lecture/audit/design/sizing. Aucun SQL V002, aucune table account, aucun repository account et aucune impl capability account ne sont ajoutés.
## 2. Base
Base canonique :
```text
v0.3.3
workspace.package.version = 0.3.3
```
Le gate opérateur fourni avec la reprise est propre sur audits Rust/Markdown, checks, Clippy et suites ciblées/workspace de la release stable ; les live tests restent opt-in/ignored.
## 3. Version
Version workspace après overlay :
```text
0.3.4-pre.1
```
Label de livraison :
```text
0.3.4-pre.001
```
## 4. Fichiers ajoutés
```text
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
deltas/0.3.4/pre.001.md
```
## 5. Fichier modifié
```text
Cargo.toml
```
Modifications :
```text
workspace.package.version : 0.3.3 -> 0.3.4-pre.1
file header version : 363 -> 364
```
## 6. Fichiers supprimés
Aucun.
## 7. Audit effectué
Ont été relus avant design : règles globales/KSP/Rust/dépendances/docs/versioning ; architectures Store/data/execution/workers ; plans/validations `0.3.1`, `0.3.2`, `0.3.3` ; source/tests Store API/façade/backend ; Config `std.store` ; migrations V000/V001 ; archive kbot3 ciblée account.
Inventaire confirmé :
```text
10 capabilities RAW contractuelles
6 RawTransaction implémentées physiquement dans PostgresBackend + Store
4 RawAccount encore non implémentées et explicitement fermées par canaries 0.3.3
```
Checksums recalculés :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
```
## 8. Décisions majeures
### 8.1 V002
V002 sera additive, sous `migrations/v002_raw_account_state/`, avec seulement :
```text
ksp_raw_account_states
ksp_raw_account_observations
```
V000/V001 restent byte-immutables.
### 8.2 Identité state
```text
network API + (pubkey, slot, state_hash)
```
Le réseau est le binding DB existant. PK physique `(pubkey, slot, state_hash)`. Pubkey/owner/hash en BYTEA fixed-width ; slot/lamports/rent_epoch en NUMERIC(20,0). Data account en BYTEA complet, vide autorisé, maximum 16 MiB.
### 8.3 Observation
L'observation référence le state par FK composite et conserve provenance + `is_startup` + `transaction_signature` + `write_version`. Ces metadata restent observation-only. Aucune FK transaction n'est ajoutée.
### 8.4 Atomicité/idempotence
Pattern retenu : transaction PostgreSQL, `INSERT ... ON CONFLICT DO NOTHING RETURNING`, puis `SELECT ... FOR UPDATE` et comparaison exacte sur collision. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*`, aucun overwrite silencieux.
`record_raw_account_observation` vérifie le state préexistant et ne le crée jamais implicitement.
### 8.5 Pagination
Ordre total :
```text
(slot, pubkey, state_hash)
```
Cursor V1 account fixe 109 bytes, magic `KSPA`, domaine `KSP/raw-account-state-cursor/v1`, digest lié au réseau, filtre pubkey, range, direction et dernière clé. Le cursor transaction `KSPT` est donc non rejouable.
Index métier additionnel unique :
```text
(slot, pubkey, state_hash)
```
Aucun index owner/provider/time/status.
## 9. Audit kbot3
Classification :
- **REPRENDRE** : invariants unicité/FK et principe d'index account+slot ; NUMERIC pour valeurs monétaires ;
- **REDESSINER** : identité durable, BYTEA fixed-width, bytes complets, u64 exact, observation séparée, atomicité/conflict/cursor ;
- **REPORTER** : normalisation CORE, processing ledger, batch 1000, indexes owner/provider/time/status ;
- **REJETER** : sqlx/monolithe, BIGSERIAL comme identité publique, base64 physique, BIGINT narrowing, status/error RAW, maintenance destructive comme comportement normal.
Aucun code historique n'est copié.
## 10. Threat model
Le plan couvre mauvaise DB/réseau, fixed-width hostile, overflow u64, 16 MiB, observation orpheline, rollback partiel, même pubkey+slot multi-state, conflits exacts, collisions observation, races, cursor hostile/cross-query/cross-family, ordre pagination, metadata Yellowstone, fuite SQL/server text, cancellation et migration mismatch.
## 11. Sizing recalibré
Le forecast est étendu à `pre.013` afin de séparer V002 registry/tables de ses contraintes/schema compatibility, puis acquisition write de la concurrence/observation supplémentaire. Le gate technique final reste suivi d'une lane documentaire et d'une lane publication distinctes.
## 12. Hors scope conservé
```text
ksp-store-api redesign
account retention/archive/purge
STRUCTURAL/CORE/DECODED/DOMAIN persistence
Interface events
workers/jobs/backfills
batch/priorité/backlog policy
nouveaux indexes sans query
nouveaux backends/apps
```
## 13. Questions ouvertes
Aucune question de contrat bloquante après `pre.001`. Les numéros exacts de ressources SQL et le découpage de helpers privés sont laissés à `pre.002/pre.003` sans pouvoir modifier les invariants figés.
## 14. Validations exécutées
```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.4
Markdown table audit: clean (240 table(s), 134 file(s))
assertions structurelles locales
workspace.package.version = 0.3.4-pre.1
Cargo.toml header version = 364
plan 025 / validation 021 / delta pre.001 présents
aucun répertoire migrations/v002_raw_account_state
aucun module backend raw_account créé
```
## 15. Validations non exécutées dans le conteneur d'assemblage
L'exécutable `cargo` n'est pas disponible dans cet environnement. L'appel `cargo fmt --all` s'arrête donc immédiatement avec `cargo: command not found`; aucune commande Cargo post-overlay n'est déclarée PASS.
À exécuter côté opérateur :
```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
```
Le gate opérateur de la base `v0.3.3` fourni avec la reprise était intégralement vert avant cet overlay. Cet historique ne remplace pas le gate post-overlay ci-dessus.

View File

@@ -0,0 +1,610 @@
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
<!-- version: 1 -->
# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
## 1. Statut du gate `pre.001`
`0.3.4-pre.001` est un gate de conception. Il ne crée **aucun** SQL V002, aucune table account et aucun repository account. Il fixe la forme physique et le séquencement nécessaires avant implémentation.
Base canonique auditée :
```text
v0.3.3
workspace.package.version = 0.3.3
```
Version de travail de cette prerelease :
```text
workspace.package.version = 0.3.4-pre.1
label = 0.3.4-pre.001
```
Décision de scope : `ksp-store-api` reste inchangée. L'audit n'a révélé aucun gap backend-agnostic bloquant ; la difficulté restante est exclusivement l'implémentation physique PostgreSQL et son dispatch par la façade.
## 2. Invariants hérités et frontières
La trajectoire reste :
```text
RAW -> STRUCTURAL -> DECODED -> DOMAIN
```
`0.3.4` ferme seulement la complétude physique **RAW Store/PostgreSQL**. Store persiste et sert ; il ne choisit ni batch-size, ni priorité, ni backlog, ni retry job, ni stratégie de backfill.
Frontières inchangées :
```text
ksp-store-lib -> ksp-store-api
ksp-store-lib[postgres] -> ksp-store-postgres-lib
ksp-store-postgres-lib -> ksp-store-api
ksp-store-postgres-lib -X-> ksp-store-lib
Config -> Store autorisé
Store/backend -X-> Config
Transport -X-> Store
consumer ordinaire -> ksp-store-lib
consumer ordinaire -X-> ksp-store-postgres-lib
```
Une instance `Store` reste liée à un `RawNetworkId` et une base physique. Aucun multiplexage réseau/database n'est ajouté.
## 3. Inventaire de capabilities : 6/10 -> 10/10
| Capability | Impl physique stable | État base | Action 0.3.4 |
|--------------------------------|-------------------------|---------------|--------------|
| RawTransactionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionObservationRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionObservationWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionRetentionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionRetentionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawAccountStateRead | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountStateWrite | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountObservationRead | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountObservationWrite | aucune impl physique | pending 0.3.4 | implémenter |
Canaries qui ferment encore volontairement la surface account dans `v0.3.3` :
- `crates/ksp-store-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for Store` ;
- `crates/ksp-store-lib/tests/hardening_completeness.rs` fige six impl transaction et interdit les quatre account ;
- `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for PostgresBackend` ;
- `crates/ksp-store-postgres-lib/tests/hardening_completeness.rs` interdit encore `impl ksp_store_api::RawAccount*` et toute matérialisation `ksp_raw_account` dans la slice transaction.
Ces canaries devront être **transformées**, pas simplement supprimées : à `pre.008`, elles devront prouver exactement six capabilities transaction + quatre account. Les canaries qui imposent que V000 et `health.rs` restent business-free demeurent valides et ne doivent pas être assouplies.
### 3.1 Matrice détaillée des quatre capabilities account
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
## 4. Immutabilité V000/V001
Recalcul indépendant sur les bytes de l'archive stable :
```text
V000 legacy SQL SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 logical resources = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V001 resource count = 40
```
L'algorithme V001 est celui du moteur stable : domaine `ksp-migration-resources-v1\0`, puis pour chaque ressource ordonnée `id + NUL + SQL bytes + NUL` avant SHA-256.
Règles V002 :
```text
V000 : immuable
V001 : immuable
V002 : additive uniquement
nom logique proposé : raw_account_state
répertoire proposé : migrations/v002_raw_account_state/
resources proposées : tables/ + constraints/ + indexes/
hook migration supplémentaire : aucun
```
Le binding réseau singleton V001 est réutilisé ; V002 ne duplique pas `network` dans chaque ligne métier.
## 5. Audit field-by-field du contrat account
### 5.1 `RawAccountStateReference` + `RawAccountState`
Matrice complète modèle -> physique :
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|------------|----------------|-------------|--------------------|-----------------------|----------------------------------|------------------------|---------------------------------|
| network | RawNetworkId | 1 | ksp_store_identity | exact backend network | binding runtime -> reference | aucune colonne row | aucun si guard exact |
| pubkey | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
| slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes | fixed bytes -> RawContentHash | hash bytes -> BYTEA | aucun ; pas de recalcul backend |
| lamports | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| owner | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
| executable | bool | 1 | BOOLEAN | NOT NULL | bool direct | bool direct | aucun |
| rent_epoch | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| data | Box<[u8]> | 1 | BYTEA | 0..=16 MiB | bytes + borne -> RawAccountState | bytes exacts -> BYTEA | base64/parse interdits |
Vue synthétique de projection :
| Champ API | Type API | Projection V002 | Invariant |
|------------|---------------------------|------------------------------------------|-----------------------------------------------------------------|
| network | RawNetworkId | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
| pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
| slot | u64 | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
| state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
| lamports | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| owner | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` |
| executable | bool | BOOLEAN NOT NULL | mapping direct |
| rent_epoch | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| data | Box<[u8]> | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
L'identité physique canonique retenue est :
```text
PRIMARY KEY (pubkey, slot, state_hash)
```
Le réseau fait partie de la référence API mais est porté physiquement par `ksp_store_identity` pour toute la base. Cette clé permet explicitement plusieurs états du même pubkey au même slot lorsque `state_hash` diffère.
`state_hash` est un digest **producer-owned**. Le backend ne le recalcule pas et ne suppose pas qu'il s'agit du hash de `data` seulement. L'idempotence vérifie donc le contenu complet malgré l'égalité du hash.
### 5.2 `RawAccountObservation`
Matrice complète modèle -> physique :
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|---------------------------|---------------------------------|-------------|--------------------|------------------------|----------------------------|------------------------|---------------------------|
| observation_key | RawObservationKey | 1 | BYTEA | exact 32 bytes | fixed bytes -> key | key bytes -> BYTEA | aucun |
| account.pubkey | Pubkey | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
| account.slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX + FK | decimal exact -> reference | decimal NUMERIC | narrowing interdit |
| account.state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
| provider | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| protocol | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| acquisition_method | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| origin | RawAcquisitionOrigin | 1 | TEXT | 5 valeurs stables | static decode | static encode | unknown DB -> DataInvalid |
| received_at | RawTimestamp | 1 | BIGINT | 0..253402300799999 | checked i64 -> timestamp | u64 bounded -> i64 | borne API < i64::MAX |
| capture_session_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| commitment | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| endpoint_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| filter_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| observed_at | Option<RawTimestamp> | 0..1 | BIGINT NULL | borne + <= received_at | NULL/checked timestamp | Option -> NULL/i64 | borne API < i64::MAX |
| source_payload_hash | Option<RawContentHash> | 0..1 | BYTEA NULL | NULL ou 32 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucun |
| source_payload_size_bytes | Option<u64> | 0..1 | BIGINT NULL | 0..=64 MiB | NULL/checked u64 | Option -> NULL/i64 | borne API < i64::MAX |
| is_startup | Option<bool> | 0..1 | BOOLEAN NULL | nullable | NULL/bool | Option -> NULL/bool | ne pas inventer false |
| transaction_signature | Option<RawTransactionSignature> | 0..1 | BYTEA NULL | NULL ou 64 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucune FK transaction |
| write_version | Option<u64> | 0..1 | NUMERIC(20,0) NULL | NULL ou 0..u64::MAX | NULL/decimal exact | Option -> NULL/NUMERIC | narrowing interdit |
Vue synthétique de projection :
| Champ API | Type API | Projection V002 | Invariant |
|---------------------------|---------------------------------|------------------------|--------------------------------------------------|
| observation_key | RawObservationKey / 32 bytes | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
| account.pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | FK composite vers state |
| account.slot | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
| account.state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | FK composite |
| provider | RawProvenanceCode | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
| protocol | RawProvenanceCode | TEXT NOT NULL | idem V001 |
| acquisition_method | RawProvenanceCode | TEXT NOT NULL | idem V001 |
| origin | RawAcquisitionOrigin | TEXT NOT NULL | backfill/import/live/repair/replay |
| received_at | RawTimestamp | BIGINT NOT NULL | 0..253402300799999 |
| capture_session_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| commitment | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| endpoint_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| filter_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| observed_at | Option<RawTimestamp> | BIGINT NULL | borne + `observed_at <= received_at` |
| source_payload_hash | Option<RawContentHash> | BYTEA NULL | NULL ou 32 bytes |
| source_payload_size_bytes | Option<u64> | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
| is_startup | Option<bool> | BOOLEAN NULL | Yellowstone metadata observation-only |
| transaction_signature | Option<RawTransactionSignature> | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
| write_version | Option<u64> | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
La relation observation -> state est :
```text
FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)
```
Aucun `ON DELETE CASCADE` n'est nécessaire : l'API account ne possède ni delete ni retention. Aucune FK n'est créée vers `ksp_raw_transactions` pour `transaction_signature`; cette signature est une metadata source optionnelle et la transaction correspondante n'est pas garantie présente dans le Store.
`observation_key` est unique **dans la famille account**. Le contrat API expose des reads family-specific et ne définit aucune unicité globale transaction+account ; une registry globale ajouterait un couplage physique non requis.
### 5.3 `RawAccountStateQuery`
| Entrée query | Cardinalité | Projection physique |
|-----------------------|------------------------|------------------------------------------------------------------|
| network | obligatoire | guard DB mono-réseau + digest cursor |
| pubkey | optionnel | filtre égalité ; aucune query owner |
| slots.start_inclusive | optionnel | `slot >=` |
| slots.end_inclusive | optionnel | `slot <=` |
| direction | Ascending / Descending | ordre total entièrement inversé |
| page.limit | u64 > 0 | limite physique PostgreSQL seulement ; `LIMIT + 1` |
| page.cursor | opaque <= 4 KiB | V1 account fixe, authentification de contexte par digest SHA-256 |
Aucune query API par owner/provider/time/status n'existe. Aucun index pour ces dimensions ne sera introduit dans V002.
## 6. Audit kbot3 ciblé
L'archive historique contient notamment `k_sol_obs_account_observations`, `k_sol_core_account_states`, `AccountStateStore` et le pipeline de normalisation `account_state_normalization`. Cette surface mélangeait acquisition, normalisation CORE et processing policy ; elle n'est pas transposable telle quelle.
| Élément historique | Classement | Décision KSP |
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
| observation duplique létat canonique | REDESSINER | observation référence un state complet séparé |
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
Aucun code kbot3 ne sera copié. Les optional Yellowstone actuelles (`write_version`, `transaction_signature`, `is_startup`) n'existaient pas comme contrat durable équivalent dans cette persistence historique et restent donc définies uniquement par `ksp-store-api` actuel.
Points héritage supplémentaires explicitement classés :
- **REPRENDRE** : l'ancien repository utilisait déjà une transaction pour rendre atomiques state CORE + mutation observation + processing ledger ; seul le principe transactionnel est repris, jamais le couplage CORE/ledger.
- **REPRENDRE** : l'unicité `observation_key` et la validation additive des contraintes/indexes sont de bons invariants physiques.
- **REDESSINER** : `write_version`, `transaction_signature` et `is_startup` n'ont pas d'équivalent persistant account fiable dans cette surface historique ; V002 suit exclusivement les optionalités du contrat KSP actuel.
- **REDESSINER** : l'idempotence historique `source_observation_id` + `ON CONFLICT DO UPDATE` est remplacée par clé producer-owned + comparaison exacte + conflit stable.
- **REJETER** : les erreurs historiques interpolent l'erreur SQL dans `ks_core::Error::db(...)`; KSP conserve uniquement kind/phase statiques et sûres.
## 7. Design physique V002 figé par `pre.001`
### 7.1 Tables minimales
```text
ksp_raw_account_states
ksp_raw_account_observations
```
Aucune table archive/tombstone account n'est ajoutée : `ksp-store-api` ne définit aucune capability de retention account.
### 7.2 Colonnes `ksp_raw_account_states`
```text
pubkey BYTEA NOT NULL
slot NUMERIC(20,0) NOT NULL
state_hash BYTEA NOT NULL
lamports NUMERIC(20,0) NOT NULL
owner BYTEA NOT NULL
executable BOOLEAN NOT NULL
rent_epoch NUMERIC(20,0) NOT NULL
data BYTEA NOT NULL
```
Contraintes minimales : fixed width 32 pour `pubkey`, `owner`, `state_hash`; domaine u64 pour `slot`, `lamports`, `rent_epoch`; `octet_length(data) <= 16777216`; aucune borne basse sur data.
### 7.3 Colonnes `ksp_raw_account_observations`
```text
observation_key BYTEA NOT NULL
account_pubkey BYTEA NOT NULL
account_slot NUMERIC(20,0) NOT NULL
account_state_hash BYTEA NOT NULL
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
is_startup BOOLEAN NULL
transaction_signature BYTEA NULL
write_version NUMERIC(20,0) NULL
```
Les contraintes provenance doivent être sémantiquement identiques à V001 : code 1..128 octets/alphabet sûr, origin fermé, timestamps bornés/ordonnés, source hash 32 bytes et source payload <= 64 MiB.
### 7.4 Indexes justifiés
Indexes provenant des PK :
```text
PK ksp_raw_account_states(pubkey, slot, state_hash)
PK ksp_raw_account_observations(observation_key)
```
Un seul index métier additionnel est justifié par la query non filtrée :
```text
ix_ksp_raw_account_states_slot_pubkey_state_hash(slot, pubkey, state_hash)
```
Le PK couvre déjà efficacement la navigation lorsque `pubkey = ?` est imposé. Aucun index owner/provider/protocol/method/time/hash isolé n'est admis dans cette release.
## 8. Ordre total et pagination keyset
Ordre canonique :
```text
Ascending : (slot ASC, pubkey ASC, state_hash ASC)
Descending : (slot DESC, pubkey DESC, state_hash DESC)
```
Le backend PostgreSQL compare `BYTEA` selon son ordre btree déterministe. Aucun `OFFSET` n'est utilisé.
Cursor account V1 proposé :
```text
bytes 0..4 magic = "KSPA"
byte 4 version = 1
bytes 5..13 last_slot, u64 big-endian
bytes 13..45 last_pubkey, 32 bytes
bytes 45..77 last_state_hash, 32 bytes
bytes 77..109 SHA-256 binding digest
longueur fixe = 109 bytes
```
Domaine de digest :
```text
KSP/raw-account-state-cursor/v1
```
Le digest lie au minimum : network, présence/valeur du filtre pubkey, direction, start/end slot, last slot, last pubkey, last state_hash. Le magic/domain distinct de `KSPT`/`KSP/raw-transaction-cursor/v1` rend le replay cross-family impossible même si les deux cursors ont 109 octets.
Le plafond physique PostgreSQL reste le précédent réel nécessaire à `LIMIT + 1` :
```text
9_223_372_036_854_775_806
```
Ce plafond n'est pas une policy métier KSP.
## 9. Algorithmes d'écriture
### 9.1 Acquisition state + observation atomiques
Préconditions avant I/O métier :
```text
state.reference.network == backend.network
observation.account.network == backend.network
observation.account == state.reference
```
Algorithme :
```text
BEGIN
INSERT state ... ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING ...
if inserted:
entity_outcome = Inserted
else:
SELECT canonical state BY PK FOR UPDATE
decode + compare exact lamports/owner/executable/rent_epoch/data
identical -> AlreadyPresent
divergent -> ERROR_CODE_RAW_CONFLICT
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
if inserted:
observation_outcome = Inserted
else:
SELECT observation BY key FOR UPDATE
decode + compare exact reference + provenance + all optional metadata
identical -> AlreadyPresent
divergent -> ERROR_CODE_RAW_CONFLICT
COMMIT
```
Une divergence d'observation après insertion d'un nouveau state fait rollback de ce state. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*` préalable, aucun overwrite silencieux.
### 9.2 Observation supplémentaire
```text
validate network before I/O
BEGIN
SELECT state BY composite reference FOR KEY SHARE
absent -> ReferenceNotFound
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
collision -> SELECT FOR UPDATE + exact compare
COMMIT
```
Cette opération ne crée jamais implicitement le state et ne retransmet pas ses bytes.
## 10. Matrice de concurrence
| Scénario | Résultat permis | Invariant |
|------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------|
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | attente unique + comparaison exacte ; aucun doublon |
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | aucun overwrite ; résultat gagnant dépend de lordonnancement |
| même pubkey+slot, state_hash différents | les deux states sont représentables | PK inclut state_hash |
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | transaction perdante rollback son state nouvellement créé |
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | state préexistant inchangé |
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | aucun update silencieux |
| annulation après insertion state avant observation | aucune moitié durable | transaction PostgreSQL rollback/drop |
Le backend ne promet pas quel concurrent divergent gagne ; il promet qu'un seul contenu incompatible devient durable et que l'autre reçoit le conflit stable.
## 11. Taxonomie d'erreurs
Aucun nouveau `PostgresBackendErrorKind` n'est requis par le design actuel.
| Cas | Taxonomie stable | Règle |
|---------------------------------------------------|------------------------------------------------------|---------------------------------|
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | guard avant SQL métier |
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | pas de state implicite |
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | message/context statiques |
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | aucun bytes hostile dans erreur |
| page > limite physique PostgreSQL | PageLimitUnsupported | limite physique, pas policy KSP |
| row DB incompatible/malformée | DataInvalid | aucune valeur hostile recopiée |
| échec SELECT | ReadFailed | pas de server text exposé |
| échec INSERT/transaction | WriteFailed | pas de server text exposé |
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | réutiliser taxonomy stable |
Les erreurs PostgreSQL externes ne doivent jamais être conservées comme source publique ni recopiées dans message/context.
## 12. Threat model `RawAccountState`
| Menace | Risque | Mitigation | Preuve prévue |
|---------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|----------------------------|
| URI pointe vers DB dun autre réseau | corruption cross-network | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
| pubkey/hash/signature DB de mauvaise largeur | construction de modèle invalide | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
| slot/lamports/rent_epoch/write_version > i64::MAX | narrowing/perte | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
| data account > 16 MiB | allocation/persistence hostile | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
| data account vide | rejet incorrect | aucune borne basse SQL ; vide explicitement valide | unit |
| observation sans state | orphelin | FK composite + record pré-vérifie state | unit + live |
| échec observation après state insert | demi-acquisition durable | une transaction PostgreSQL commune | live cancellation/rollback |
| même ref, contenu divergent | overwrite silencieux | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
| même pubkey/slot, hashes distincts | collision didentité | PK `(pubkey,slot,state_hash)` | unit + live |
| collision observation key | fusion de sources distinctes | comparaison exacte de tous les champs ; conflict sinon | unit + live |
| cursor transaction injecté dans account | replay cross-family | magic/domain account distincts | unit cursor |
| cursor account rejoué sur autre query | pagination incohérente | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
| ordre même slot ambigu | duplication/skip entre pages | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
| metadata Yellowstone promue au state | couplage provider | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
| transaction_signature impose transaction row | couplage cross-family non garanti | aucune FK vers `ksp_raw_transactions` | schema canary |
| SQL/server text fuit | secret/PII leak | projection PostgresBackendError statique | hardening |
| V000/V001 modifiées | history incompatible | checksums figés et canaries exactes | migration tests |
| index non justifié | write amplification/bloat | un seul index métier V002 pour query unfiltered | schema completeness |
## 13. Compatibilité schema/migration
V002 devra suivre le moteur resource-owned de V001 : ressources ordonnées, checksums logiques, vérification catalogique et mutation selon les policies foundation existantes.
Principes :
```text
schema_creation = disabled : aucune création implicite
schema_creation = create_if_missing : bootstrap seulement selon contrat existant
schema_update = disabled : historique incomplet -> mismatch sûr
schema_update = update_if_needed : V002 appliquée additivement
history > latest embedded : SchemaNewer
history V000/V001 divergente : MigrationMismatch
extra columns externes : acceptables seulement si elles ne peuvent bloquer les INSERT KSP
colonnes/constraints/index requis incompatibles : mismatch avant ready
```
Les numéros de fichiers SQL exacts sont une responsabilité de `pre.002/pre.003`; leur sémantique est figée ici, pas leurs noms séquentiels.
## 14. Mapping/read privé
Le module backend proposé est `raw_account` privé, analogue structurellement à `raw_transaction` mais sans retention/archive.
Il devra contenir :
```text
mapping row -> RawAccountState
mapping row -> RawAccountObservation
mapping u64 <-> NUMERIC text sans narrowing
get state par composite reference
get observation par observation_key
list references par keyset
cursor account privé
write acquisition/observation privé
```
Aucun row/statement/client PostgreSQL ne traverse `ksp-store-postgres-lib` publiquement ni `ksp-store-lib`.
## 15. Stratégie de preuve PostgreSQL réelle
La preuve live restera `#[ignore]`, opt-in, avec une URI dédiée lue depuis stdin et sans environnement direct. Elle devra refuser de détruire des objets KSP préexistants non créés par le test.
Scénarios minimum :
```text
bootstrap V000+V001+V002 et health ready
réouverture idempotente du schéma
state bytes vide et 16 MiB
u64 au-delà de i64::MAX et u64::MAX pour colonnes NUMERIC
acquisition identique puis divergente
same pubkey + same slot + distinct state_hash
observation supplémentaire / reference not found
collision observation
pagination filtered/unfiltered ASC/DESC multi-page
cursor hostile / replay query / replay transaction
concurrence identique/divergente
cancellation entre state et observation -> rollback
coexistence avec au moins un canari RawTransaction stable dans la même DB V002
cleanup vérifié uniquement pour les objets créés par le test
```
Aucun appel Solana réseau n'est nécessaire à cette preuve.
## 16. Conformance cross-family finale
À `pre.008+`, les canaries devront compter exactement :
```text
PostgresBackend : 10 impl RAW
Store : 10 impl RAW
```
La façade doit continuer de n'exposer que les modèles/capabilities backend-agnostic. Le test `--no-default-features` de `ksp-store-lib` doit rester vert : un build sans backend PostgreSQL compile et rejette proprement un backend non compilé au runtime.
La conformance finale doit aussi démontrer que l'ajout account ne modifie pas les six comportements transaction : lecture/write/observation/list/retention/force-rehydrate restent couverts par leurs suites existantes.
## 17. Sizing recalibré
Le forecast initial du prompt concentrait migration et schema compatibility dans une seule tranche, ainsi que write+concurrence. Ces lots sont séparés pour rester dans la cible opérateur `~15-20 min/pre`.
| Tranche | Objet | Budget | Note |
|---------|-------------------------------------------------------------------|-----------|----------------------------------------|
| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | 15-20 min | cette tranche |
| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | 15-20 min | split du forecast initial |
| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | 15-20 min | split du forecast initial |
| pre.004 | mapping privé state/observation + get reads + hostile rows | 15-20 min | pas de write |
| pre.005 | acquisition atomique state+observation + idempotence/conflict | 15-20 min | write principal |
| pre.006 | observation supplémentaire + races/cancellation unitaires | 15-20 min | concurrence isolée |
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | 15-20 min | navigation uniquement |
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | 15-20 min | frontières cross-crate |
| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | 15-20 min | ignored/opt-in |
| pre.010 | hardening/completeness cross-family + canaries ownership | 15-20 min | sécurité/completude |
| pre.011 | gate technique final + replay live ciblé + graphes | 15-20 min | avant docs finale |
| pre.012 | réconciliation docs finale sans rouvrir le code | 10-15 min | README/USAGE/plan/validation si requis |
| pre.013 | publication prep minimale + prompt 0.3.5 | 10-15 min | lane publication |
| rel.001 | stabilisation/tag après gate opérateur | 5-10 min | aucun scope nouveau |
## 18. Hors scope explicite
```text
modification ksp-store-api
retention/archivage/purge account
STRUCTURAL/CORE materialization
Interface events
workers/jobs/backfill executors
batch-size/priorité/backlog policy
Transport -> Store direct
index owner/provider/time/status sans query
compression account
nouveau backend autre que PostgreSQL
application Store Desk
```
## 19. Questions ouvertes après `pre.001`
Aucune question **bloquante** de contrat ne subsiste.
Les détails suivants restent volontairement d'implémentation : numérotation exacte des ressources V002, découpage précis des helpers de mapping et noms internes des statements. Ils ne doivent pas modifier les invariants de ce plan.
## 20. Gate d'acceptation `pre.001`
`pre.001` est acceptable si :
```text
base v0.3.3 vérifiée
V000/V001 checksums recalculés et figés
inventaire 6/10 -> 10/10 explicite
field mapping account complet
kbot3 classifié REPRENDRE/REDESSINER/REPORTER/REJETER
V002 figée sans SQL écrit
ordre/cursor figés
algorithmes atomicité/idempotence/concurrence figés
threat model complet
forecast recalibré
aucune modification fonctionnelle hors version prerelease
audits statiques applicables verts ; gate Cargo opérateur rejoué
```

View File

@@ -0,0 +1,327 @@
<!-- file: docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md -->
<!-- version: 1 -->
# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
## 1. Portée
Cette matrice est ouverte par `0.3.4-pre.001`. La tranche courante valide **le design et le sizing uniquement** ; elle n'affirme aucune présence de V002 ou d'implémentation account.
Base :
```text
v0.3.3
```
Cible release :
```text
PostgresBackend = 10/10 capabilities RAW
Store = 10/10 capabilities RAW
```
## 2. Gate base `pre.001`
| Preuve | Attendu | État pre.001 |
|------------------------------------|------------------|-------------------------------------------------|
| workspace.package.version base | 0.3.3 | PASS audit |
| V000 checksum | d29068b8…46a1450 | PASS recalcul indépendant |
| V001 checksum | 31488cda…d72cc51 | PASS recalcul indépendant / 40 resources |
| capabilities transaction physiques | 6 | PASS inventaire source |
| capabilities account physiques | 0 | PASS inventaire source ; volontairement pending |
| canaries anti-account 0.3.3 | présentes | PASS inventaire tests |
| ksp-store-api change nécessaire | non | PASS audit |
| SQL V002 créé en pre.001 | non | PASS scope |
## 3. Matrice capability finale
| Capability | Base v0.3.3 | Cible 0.3.4 | Preuve finale |
|--------------------------------|-------------|-------------|-------------------------------|
| RawTransactionRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionWrite | PASS | PASS | régression suite 0.3.3 |
| RawTransactionObservationRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionObservationWrite | PASS | PASS | régression suite 0.3.3 |
| RawTransactionRetentionRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionRetentionWrite | PASS | PASS | régression suite 0.3.3 |
| RawAccountStateRead | PENDING | PASS | tests backend + façade + live |
| RawAccountStateWrite | PENDING | PASS | tests backend + façade + live |
| RawAccountObservationRead | PENDING | PASS | tests backend + façade + live |
| RawAccountObservationWrite | PENDING | PASS | tests backend + façade + live |
### 3.1 Contrat opérationnel détaillé des quatre capabilities account
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
## 4. Matrice `RawAccountState`
| Champ | Représentation décidée | Validation requise |
|------------|------------------------------------------|-----------------------------------------------------------------|
| network | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
| pubkey | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
| slot | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
| state_hash | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
| lamports | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| owner | BYTEA NOT NULL | `octet_length = 32` |
| executable | BOOLEAN NOT NULL | mapping direct |
| rent_epoch | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| data | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
Points structurants :
```text
identity = network API + (pubkey, slot, state_hash)
network SQL = singleton ksp_store_identity, pas colonne row
PK state = (pubkey, slot, state_hash)
state_hash n'est pas une preuve suffisante d'idempotence : comparaison contenu exacte obligatoire
empty data = valide
```
## 5. Matrice observation
| Champ | Représentation décidée | Validation requise |
|---------------------------|------------------------|--------------------------------------------------|
| observation_key | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
| account.pubkey | BYTEA NOT NULL | FK composite vers state |
| account.slot | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
| account.state_hash | BYTEA NOT NULL | FK composite |
| provider | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
| protocol | TEXT NOT NULL | idem V001 |
| acquisition_method | TEXT NOT NULL | idem V001 |
| origin | TEXT NOT NULL | backfill/import/live/repair/replay |
| received_at | BIGINT NOT NULL | 0..253402300799999 |
| capture_session_id | TEXT NULL | mêmes checks V001 |
| commitment | TEXT NULL | mêmes checks V001 |
| endpoint_id | TEXT NULL | mêmes checks V001 |
| filter_id | TEXT NULL | mêmes checks V001 |
| observed_at | BIGINT NULL | borne + `observed_at <= received_at` |
| source_payload_hash | BYTEA NULL | NULL ou 32 bytes |
| source_payload_size_bytes | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
| is_startup | BOOLEAN NULL | Yellowstone metadata observation-only |
| transaction_signature | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
| write_version | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
Invariants additionnels :
```text
FK observation -> state composite
record_raw_account_observation n'insère jamais le state
transaction_signature observation-only, sans FK transaction
write_version/is_startup observation-only
observation_key family-local unique
```
## 6. Query/pagination
| Élément | Décision | Canari requis |
|-----------------|------------------------------------------------|--------------------------------|
| ordre ASC | slot, pubkey, state_hash ASC | multi-page sans duplicate/skip |
| ordre DESC | slot, pubkey, state_hash DESC | symétrique ASC |
| filtre pubkey | égalité optionnelle | cursor lié au filtre |
| slot range | inclusive | cursor lié aux deux bornes |
| cursor magic | KSPA | rejette KSPT transaction |
| cursor version | 1 | rejette version hostile |
| cursor longueur | 109 bytes | rejette taille hostile |
| cursor domain | KSP/raw-account-state-cursor/v1 | digest family-bound |
| cursor digest | network+pubkey filter+range+direction+last key | anti-replay query |
| pagination | keyset seulement | aucun OFFSET |
| limit | limite physique PostgreSQL uniquement | pas de batch policy |
## 7. Migration/schema V002
| Objet | Décision pre.001 | État |
|----------------------------------|---------------------------------------|-------------------------|
| V000 | immuable | PASS design |
| V001 | immuable | PASS design |
| V002 logical name | raw_account_state | PASS design |
| table state | ksp_raw_account_states | PASS design |
| table observation | ksp_raw_account_observations | PASS design |
| archive/tombstone account | aucune | PASS negative scope |
| index métier | (slot,pubkey,state_hash) | PASS design |
| index owner/provider/time/status | aucun | PASS negative scope |
| SQL/resource files | pas encore créés | PENDING pre.002/pre.003 |
| V002 checksum | inconnu tant que resources non créées | PENDING pre.003 |
## 8. Idempotence et concurrence
| Scénario | Résultat attendu | État |
|------------------------------------------------------------|-------------------------------------------------------|-------------------|
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | PENDING impl/live |
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | PENDING impl/live |
| même pubkey+slot, state_hash différents | les deux states sont représentables | PENDING impl/live |
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | PENDING impl/live |
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | PENDING impl/live |
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | PENDING impl/live |
| annulation après insertion state avant observation | aucune moitié durable | PENDING impl/live |
Le test de contenu divergent doit comparer les fields complets et ne jamais traiter l'égalité de `state_hash` comme preuve suffisante.
## 9. Threat model
| Menace | Mitigation figée | Preuve |
|---------------------------------------------------|-----------------------------------------------------------------------|----------------------------|
| URI pointe vers DB dun autre réseau | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
| pubkey/hash/signature DB de mauvaise largeur | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
| slot/lamports/rent_epoch/write_version > i64::MAX | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
| data account > 16 MiB | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
| data account vide | aucune borne basse SQL ; vide explicitement valide | unit |
| observation sans state | FK composite + record pré-vérifie state | unit + live |
| échec observation après state insert | une transaction PostgreSQL commune | live cancellation/rollback |
| même ref, contenu divergent | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
| même pubkey/slot, hashes distincts | PK `(pubkey,slot,state_hash)` | unit + live |
| collision observation key | comparaison exacte de tous les champs ; conflict sinon | unit + live |
| cursor transaction injecté dans account | magic/domain account distincts | unit cursor |
| cursor account rejoué sur autre query | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
| ordre même slot ambigu | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
| metadata Yellowstone promue au state | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
| transaction_signature impose transaction row | aucune FK vers `ksp_raw_transactions` | schema canary |
| SQL/server text fuit | projection PostgresBackendError statique | hardening |
| V000/V001 modifiées | checksums figés et canaries exactes | migration tests |
| index non justifié | un seul index métier V002 pour query unfiltered | schema completeness |
## 10. Audit historique kbot3
| Élément | Classement | Disposition |
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
| observation duplique létat canonique | REDESSINER | observation référence un state complet séparé |
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
Le pipeline historique `k_sol_obs_account_observations -> k_sol_core_account_states` reste une source de comparaison, pas une architecture cible. La normalisation versionnée et ses caps de batch sont hors scope `0.3.4`.
## 11. Canaries `0.3.3` à faire évoluer
À l'ouverture effective de la surface account :
```text
ksp-store-lib/tests/dependency_boundary.rs
remplacer interdiction RawAccount par exigence des 4 impl + absence types SQL
ksp-store-lib/tests/hardening_completeness.rs
remplacer inventaire 6 + RawAccount fermé par inventaire exact 10
ksp-store-postgres-lib/tests/dependency_boundary.rs
ajouter module raw_account privé et exiger les 4 impl dans runtime bridge
conserver V000 et health business-free
ksp-store-postgres-lib/tests/hardening_completeness.rs
remplacer interdiction RawAccount globale par inventaire exact 10
remplacer l'absence V002/account par checks de surface V002 minimale
```
Les tests historiques nommés `pre_XXX` peuvent conserver leurs noms si le contrat qu'ils vérifient reste valide ; seul leur contenu doit être élargi lorsqu'il fige intentionnellement le scope `0.3.3`.
## 12. Taxonomie d'erreur à prouver
| Cas | Code/projection attendue | État |
|---------------------------------------------------|------------------------------------------------------|--------------|
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | PENDING impl |
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | PENDING impl |
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | PENDING impl |
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | PENDING impl |
| page > limite physique PostgreSQL | PageLimitUnsupported | PENDING impl |
| row DB incompatible/malformée | DataInvalid | PENDING impl |
| échec SELECT | ReadFailed | PENDING impl |
| échec INSERT/transaction | WriteFailed | PENDING impl |
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | PENDING impl |
Aucune nouvelle variante backend n'est justifiée au gate `pre.001`.
## 13. Preuve live finale
Le test live account devra être opt-in/ignored, URI stdin, sans environnement ni fuite de secret. Il doit couvrir V002 et rejouer un canari transaction dans la même base afin de prouver la coexistence cross-family.
| Scénario live | Obligation |
|-----------------------------------------|-------------|
| schema update V001 -> V002 | obligatoire |
| reopen idempotent | obligatoire |
| u64 > i64::MAX / u64::MAX | obligatoire |
| data empty / 16 MiB | obligatoire |
| identical/divergent acquisition | obligatoire |
| same pubkey+slot / distinct hash | obligatoire |
| additional observation + missing ref | obligatoire |
| collision observation | obligatoire |
| keyset filtered/unfiltered ASC/DESC | obligatoire |
| cursor hostile/cross-query/cross-family | obligatoire |
| concurrency | obligatoire |
| cancellation rollback | obligatoire |
| RawTransaction regression canary | obligatoire |
| cleanup isolation | obligatoire |
## 14. Sizing
| Tranche | Objet | État |
|---------|-------------------------------------------------------------------|---------|
| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | CURRENT |
| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PLANNED |
| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PLANNED |
| pre.004 | mapping privé state/observation + get reads + hostile rows | PLANNED |
| pre.005 | acquisition atomique state+observation + idempotence/conflict | PLANNED |
| pre.006 | observation supplémentaire + races/cancellation unitaires | PLANNED |
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PLANNED |
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PLANNED |
| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | PLANNED |
| pre.010 | hardening/completeness cross-family + canaries ownership | PLANNED |
| pre.011 | gate technique final + replay live ciblé + graphes | PLANNED |
| pre.012 | réconciliation docs finale sans rouvrir le code | PLANNED |
| pre.013 | publication prep minimale + prompt 0.3.5 | PLANNED |
| rel.001 | stabilisation/tag après gate opérateur | PLANNED |
## 15. Negative scope final
| Surface | Doit rester absent en 0.3.4 |
|--------------------------------------------|-----------------------------|
| ksp-store-api redesign | oui |
| account retention/archive/purge | oui |
| CORE/STRUCTURAL persistence | oui |
| workers/jobs/backfills | oui |
| Store batch/priorité policy | oui |
| owner/provider/time/status queries/indexes | oui |
| Transport -> Store coupling | oui |
| PostgreSQL types via ksp-store-lib | oui |
| nouveau backend | oui |
## 16. Gate opérateur prévu pour clôture technique
```text
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.4
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
cargo test --workspace
cargo tree -p ksp-store-lib --edges normal
cargo tree -p ksp-store-lib -e features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree --duplicates
```
Les preuves live restent séparées et explicitement `--ignored --nocapture`.
## 17. Verdict `pre.001`
Design : **PASS**. Les audits Rust/Markdown de l'overlay sont propres.
Compilation/Clippy/tests post-overlay : **NON EXÉCUTÉS dans le conteneur d'assemblage**, faute d'exécutable `cargo`; ils restent à rejouer par l'opérateur.
Implémentation V002/account : **PENDING**, volontairement non ouverte dans cette tranche.