v0.3.3-pre.007

This commit is contained in:
2026-08-30 13:37:22 +02:00
parent 10996f11f7
commit aa56a12846
13 changed files with 683 additions and 41 deletions

View File

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

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/README.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# ksp-store-postgres-lib
@@ -83,7 +83,7 @@ ksp_store_schema_migrations
Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit. `pre.006` ajoute la navigation keyset déterministe sur l'index `(slot, signature)` et son cursor opaque lié à la requête.
V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit. `pre.006` ajoute la navigation keyset déterministe sur l'index `(slot, signature)` et son cursor opaque lié à la requête. `pre.007` ajoute les transitions de rétention atomiques `Full -> Archived -> Purged` et le rejet explicite de `Compacted` tant qu'aucune représentation compacte réelle n'existe.
## Health et erreurs
@@ -147,13 +147,26 @@ La continuation est une keyset stricte `>` / `<`, jamais un `OFFSET`. Le cursor
`RawPageLimit` n'est pas réduit par une policy KSP : PostgreSQL utilise `LIMIT requested + 1`, avec comme seule borne physique `requested <= i64::MAX - 1`. Une valeur supérieure produit `PageLimitUnsupported` sans clamp. La pagination ne promet aucun snapshot inter-pages.
## Rétention RAW `0.3.3-pre.007`
`transition_raw_transaction_retention` applique uniquement les transitions physiques supportées :
```text
Full -> Archived -> Purged
```
Le backend verrouille la ligne canonique avec `FOR UPDATE`, valide la forme physique courante, compare `current` avec `expected` et n'effectue la mutation qu'en cas de correspondance. `current == target` retourne `AlreadyAtTarget`; une race ayant déplacé l'état ailleurs retourne `ExpectedStateMismatch`. Une référence absente est `ReferenceNotFound`.
`Full -> Archived` copie d'abord les octets exacts dans `ksp_raw_transaction_archive_payloads`, puis retire le payload chaud et passe l'état à `archived` dans la même transaction. `Archived -> Purged` supprime l'archive, efface `block_time`, conserve les cinq champs de tombstone et passe l'état à `purged` atomiquement. Les écritures d'acquisition/rehydration et les transitions utilisent le même verrou canonique, ce qui sérialise les races purge/ForceRehydrate.
Toute transition dont `expected` ou `target` vaut `Compacted` est rejetée avant `pool.get()` avec `RetentionCompactionUnsupported` et le code stable `store.postgres_retention_compaction_unsupported`. PostgreSQL n'utilise pas TOAST comme faux contrat de compaction.
## Hors périmètre actuel
La crate ne contient encore :
- aucune pagination/listing `RawTransaction` ;
- aucune implémentation complète des traits `RawTransaction*` de `ksp-store-api` tant que `list_raw_transactions` manque ;
- aucune transition mutante de rétention ;
- aucune implémentation complète des six traits `RawTransaction*` de `ksp-store-api` ;
- aucun dispatch métier dans `ksp-store-lib` ;
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
- aucune orchestration worker/job ;
- aucun transport d'acquisition ou decoder Program.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Utilisation de ksp-store-postgres-lib
@@ -133,6 +133,7 @@ match error.kind() {
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => {}
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => {}
@@ -204,13 +205,27 @@ Le cursor peut être transmis uniquement à une query ayant le même binding. Un
La seule limite physique est liée au `LIMIT + 1` PostgreSQL : `requested <= i64::MAX - 1`. Au-delà, `PageLimitUnsupported` est retourné ; la demande n'est jamais ramenée à 100, 500, 1000 ou une autre policy de worker.
## 10. Ce que cette crate ne permet pas encore
## 10. Appliquer une transition de rétention
Depuis `0.3.3-pre.007`, le backend expose :
```rust
let outcome = backend.transition_raw_transaction_retention(transition).await;
```
Le backend ne choisit jamais lui-même la policy de rétention. Le caller fournit un `RawTransactionRetentionTransition` avec `expected` et `target`; PostgreSQL verrouille le canonical avec `FOR UPDATE`, puis retourne `Applied`, `AlreadyAtTarget` ou `ExpectedStateMismatch` selon l'état réellement observé.
Les transitions physiques supportées sont `Full -> Archived` puis `Archived -> Purged`. L'archivage copie le payload exact vers la relation archive avant de retirer les octets chauds, et la purge supprime cette archive puis efface le block time en conservant uniquement le tombstone minimal. Tout est transactionnel : aucun état intermédiaire n'est committé.
Une transition impliquant `Compacted` est rejetée avant acquisition du pool avec `PostgresBackendErrorKind::RetentionCompactionUnsupported`. Le code KSP correspondant est `ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED`; aucune compression transparente PostgreSQL n'est présentée comme une représentation compactée KSP.
## 11. Ce que cette crate ne permet pas encore
La tranche ne fournit pas encore :
```text
transitions de rétention
implémentations complètes des six traits RawTransaction*
dispatch métier ksp-store-lib
capabilities RawAccount*
```

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/error.rs
// version: 7
// version: 8
/// Stable KSP error code reserved for PostgreSQL retention transitions that require unsupported physical compaction.
pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode =
@@ -33,6 +33,8 @@ pub enum PostgresBackendErrorKind {
ReadFailed,
/// A RAW write requires an existing canonical reference that is not durable.
ReferenceNotFound,
/// The requested RAW retention transition requires a compacted representation unsupported by PostgreSQL.
RetentionCompactionUnsupported,
/// The database schema history contains a migration newer than this runtime understands.
SchemaNewer,
/// Explicit backend shutdown did not drain inside the supplied deadline.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 12
// version: 13
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -17,6 +17,8 @@
//! checks and safe conflict classification without exposing PostgreSQL rows or
//! SQL through the public bridge. `0.3.3-pre.006` adds deterministic keyset
//! pagination with a fixed opaque cursor bound to network, range and direction.
//! `0.3.3-pre.007` adds atomic `Full -> Archived -> Purged` retention transitions
//! with compare-and-transition outcomes and explicit rejection of `Compacted`.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
@@ -75,6 +77,8 @@ pub(crate) use self::raw_transaction::list_raw_transactions;
pub(crate) use self::raw_transaction::persist_raw_transaction_acquisition;
/// Private additional RAW transaction observation writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::record_raw_transaction_observation;
/// Private RAW transaction retention transition writer consumed by the physical backend runtime.
pub(crate) use self::raw_transaction::transition_raw_transaction_retention;
/// Private Deadpool error mapper shared with the health probe.
pub(crate) use self::runtime::map_pool_error;
/// Private Deadpool status projector shared with the health probe.

View File

@@ -1,22 +1,29 @@
// file: crates/ksp-store-postgres-lib/src/raw_transaction.rs
// version: 3
// version: 4
pub(crate) mod cursor;
const DELETE_ARCHIVE_PAYLOAD_SQL: &str = "DELETE FROM ksp_raw_transaction_archive_payloads WHERE signature = $1";
const GET_ARCHIVE_PAYLOAD_SQL: &str = "SELECT payload FROM ksp_raw_transaction_archive_payloads WHERE signature = $1";
const GET_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1";
const GET_RETENTION_SQL: &str = "SELECT retention_state FROM ksp_raw_transactions WHERE signature = $1";
const GET_TOMBSTONE_SQL: &str = "SELECT signature, slot::text AS slot_text, block_time_unix_millis, format_id, format_version, content_hash, retention_state FROM ksp_raw_transactions WHERE signature = $1";
const GET_TRANSACTION_SQL: &str = "SELECT transaction_row.signature, transaction_row.slot::text AS slot_text, transaction_row.block_time_unix_millis, transaction_row.format_id, transaction_row.format_version, transaction_row.content_hash, transaction_row.payload, transaction_row.retention_state, archive_row.payload AS archive_payload FROM ksp_raw_transactions AS transaction_row LEFT JOIN ksp_raw_transaction_archive_payloads AS archive_row ON archive_row.signature = transaction_row.signature WHERE transaction_row.signature = $1";
const INSERT_ARCHIVE_PAYLOAD_SQL: &str = "INSERT INTO ksp_raw_transaction_archive_payloads (signature, payload) VALUES ($1, $2)";
const INSERT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_transaction_observations (observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
const INSERT_TRANSACTION_SQL: &str = "INSERT INTO ksp_raw_transactions (signature, slot, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state) VALUES ($1, $2::TEXT::NUMERIC, $3, $4, $5, $6, $7, 'full') ON CONFLICT (signature) DO NOTHING RETURNING signature";
const LIST_TRANSACTIONS_ASC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) > ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot ASC, signature ASC LIMIT $5";
const LIST_TRANSACTIONS_DESC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) < ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot DESC, signature DESC LIMIT $5";
const LOCK_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE";
const LOCK_RETENTION_TRANSACTION_SQL: &str =
"SELECT block_time_unix_millis, payload, retention_state FROM ksp_raw_transactions WHERE signature = $1 FOR UPDATE";
const LOCK_TRANSACTION_SQL: &str = "SELECT signature, slot::text AS slot_text, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state, NULL::BYTEA AS archive_payload FROM ksp_raw_transactions WHERE signature = $1 FOR UPDATE";
const LOCK_TRANSACTION_STATE_SQL: &str = "SELECT retention_state FROM ksp_raw_transactions WHERE signature = $1 FOR UPDATE";
const REHYDRATE_TRANSACTION_SQL: &str =
"UPDATE ksp_raw_transactions SET block_time_unix_millis = $2, payload = $3, retention_state = 'full' WHERE signature = $1";
const UPDATE_ARCHIVED_TRANSACTION_SQL: &str =
"UPDATE ksp_raw_transactions SET payload = NULL, retention_state = 'archived' WHERE signature = $1 AND retention_state = 'full'";
const UPDATE_PURGED_TRANSACTION_SQL: &str = "UPDATE ksp_raw_transactions SET block_time_unix_millis = NULL, payload = NULL, retention_state = 'purged' WHERE signature = $1 AND retention_state = 'archived'";
struct RawListDbRow {
signature: std::vec::Vec<u8>,
@@ -40,6 +47,12 @@ struct RawObservationDbRow {
transaction_signature: std::vec::Vec<u8>,
}
struct RawRetentionDbRow {
block_time_unix_millis: std::option::Option<i64>,
payload: std::option::Option<std::vec::Vec<u8>>,
retention_state: std::string::String,
}
struct RawTransactionDbRow {
archive_payload: std::option::Option<std::vec::Vec<u8>>,
block_time_unix_millis: std::option::Option<i64>,
@@ -450,12 +463,250 @@ pub(crate) async fn record_raw_transaction_observation(
return std::result::Result::Ok(outcome);
}
/// Applies one atomic compare-and-transition RAW transaction retention mutation.
pub(crate) async fn transition_raw_transaction_retention(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
transition: ksp_store_api::RawTransactionRetentionTransition,
) -> std::result::Result<ksp_store_api::RawRetentionWriteOutcome, crate::PostgresBackendError> {
let input_result = ensure_retention_transition_inputs(network, &transition);
if let std::result::Result::Err(error) = input_result {
return std::result::Result::Err(error);
}
let client_result = pool.get().await;
let mut client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let sql_transaction_result = client.transaction().await;
let sql_transaction = match sql_transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_retention_begin")),
};
let signature = transition.reference().signature();
let signature_bytes: &[u8] = signature.as_bytes();
let row_result = sql_transaction.query_opt(LOCK_RETENTION_TRANSACTION_SQL, &[&signature_bytes]).await;
let row = match row_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(reference_not_found("raw_retention_transition_reference")),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_retention_lock_transaction")),
};
let physical = match raw_retention_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let current = match decode_retention_state(physical.retention_state.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let shape_result = validate_retention_shape(&sql_transaction, signature_bytes, &physical, current).await;
if let std::result::Result::Err(error) = shape_result {
return std::result::Result::Err(error);
}
let decision = match retention_transition_decision(current, transition.expected(), transition.target()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match decision {
RetentionTransitionDecision::AlreadyAtTarget => {
return commit_retention_outcome(sql_transaction, ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget).await;
},
RetentionTransitionDecision::ExpectedStateMismatch => {
return commit_retention_outcome(sql_transaction, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch).await;
},
RetentionTransitionDecision::Archive => {
let payload = match physical.payload.as_ref() {
std::option::Option::Some(value) => value.as_slice(),
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_retention_full_payload")),
};
let archive_result = archive_full_transaction(&sql_transaction, signature_bytes, payload).await;
if let std::result::Result::Err(error) = archive_result {
return std::result::Result::Err(error);
}
},
RetentionTransitionDecision::Purge => {
let purge_result = purge_archived_transaction(&sql_transaction, signature_bytes).await;
if let std::result::Result::Err(error) = purge_result {
return std::result::Result::Err(error);
}
},
}
return commit_retention_outcome(sql_transaction, ksp_store_api::RawRetentionWriteOutcome::Applied).await;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ExistingTransactionMatch {
Active,
Purged,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RetentionTransitionDecision {
AlreadyAtTarget,
Archive,
ExpectedStateMismatch,
Purge,
}
async fn archive_full_transaction(
sql_transaction: &deadpool_postgres::Transaction<'_>,
signature_bytes: &[u8],
payload: &[u8],
) -> std::result::Result<(), crate::PostgresBackendError> {
let insert_result = sql_transaction.execute(INSERT_ARCHIVE_PAYLOAD_SQL, &[&signature_bytes, &payload]).await;
match insert_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) => return std::result::Result::Err(data_invalid("raw_retention_archive_insert_cardinality")),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_retention_archive_insert")),
}
let update_result = sql_transaction.execute(UPDATE_ARCHIVED_TRANSACTION_SQL, &[&signature_bytes]).await;
return match update_result {
std::result::Result::Ok(1) => std::result::Result::Ok(()),
std::result::Result::Ok(_) => std::result::Result::Err(data_invalid("raw_retention_archive_update_cardinality")),
std::result::Result::Err(_) => std::result::Result::Err(write_failed("raw_retention_archive_update")),
};
}
async fn commit_retention_outcome(
sql_transaction: deadpool_postgres::Transaction<'_>,
outcome: ksp_store_api::RawRetentionWriteOutcome,
) -> std::result::Result<ksp_store_api::RawRetentionWriteOutcome, crate::PostgresBackendError> {
let commit_result = sql_transaction.commit().await;
if commit_result.is_err() {
return std::result::Result::Err(write_failed("raw_retention_commit"));
}
return std::result::Result::Ok(outcome);
}
async fn load_retention_archive_payload(
sql_transaction: &deadpool_postgres::Transaction<'_>,
signature_bytes: &[u8],
) -> std::result::Result<std::option::Option<std::vec::Vec<u8>>, crate::PostgresBackendError> {
let row_result = sql_transaction.query_opt(GET_ARCHIVE_PAYLOAD_SQL, &[&signature_bytes]).await;
let row = match row_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_retention_archive_query")),
};
let archive_row = match row {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let payload_result = archive_row.try_get::<_, std::vec::Vec<u8>>("payload");
return match payload_result {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_retention_archive_decode")),
};
}
async fn purge_archived_transaction(
sql_transaction: &deadpool_postgres::Transaction<'_>,
signature_bytes: &[u8],
) -> std::result::Result<(), crate::PostgresBackendError> {
let delete_result = sql_transaction.execute(DELETE_ARCHIVE_PAYLOAD_SQL, &[&signature_bytes]).await;
match delete_result {
std::result::Result::Ok(1) => {},
std::result::Result::Ok(_) => return std::result::Result::Err(data_invalid("raw_retention_purge_archive_cardinality")),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_retention_purge_archive")),
}
let update_result = sql_transaction.execute(UPDATE_PURGED_TRANSACTION_SQL, &[&signature_bytes]).await;
return match update_result {
std::result::Result::Ok(1) => std::result::Result::Ok(()),
std::result::Result::Ok(_) => std::result::Result::Err(data_invalid("raw_retention_purge_update_cardinality")),
std::result::Result::Err(_) => std::result::Result::Err(write_failed("raw_retention_purge_update")),
};
}
fn raw_retention_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawRetentionDbRow, crate::PostgresBackendError> {
let block_time_unix_millis = match row.try_get::<_, std::option::Option<i64>>("block_time_unix_millis") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_retention_transition_decode")),
};
let payload = match row.try_get::<_, std::option::Option<std::vec::Vec<u8>>>("payload") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_retention_transition_decode")),
};
let retention_state = match row.try_get::<_, std::string::String>("retention_state") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_retention_transition_decode")),
};
return std::result::Result::Ok(RawRetentionDbRow { block_time_unix_millis, payload, retention_state });
}
fn retention_transition_decision(
current: ksp_store_api::RawRetentionState,
expected: ksp_store_api::RawRetentionState,
target: ksp_store_api::RawRetentionState,
) -> std::result::Result<RetentionTransitionDecision, crate::PostgresBackendError> {
if current == target {
return std::result::Result::Ok(RetentionTransitionDecision::AlreadyAtTarget);
}
if current != expected {
return std::result::Result::Ok(RetentionTransitionDecision::ExpectedStateMismatch);
}
return match (expected, target) {
(ksp_store_api::RawRetentionState::Full, ksp_store_api::RawRetentionState::Archived) => std::result::Result::Ok(RetentionTransitionDecision::Archive),
(ksp_store_api::RawRetentionState::Archived, ksp_store_api::RawRetentionState::Purged) => std::result::Result::Ok(RetentionTransitionDecision::Purge),
(ksp_store_api::RawRetentionState::Full, ksp_store_api::RawRetentionState::Compacted)
| (ksp_store_api::RawRetentionState::Compacted, ksp_store_api::RawRetentionState::Archived) => {
std::result::Result::Err(retention_compaction_unsupported("raw_retention_compaction"))
},
_ => std::result::Result::Err(data_invalid("raw_retention_transition_unreachable")),
};
}
async fn validate_retention_shape(
sql_transaction: &deadpool_postgres::Transaction<'_>,
signature_bytes: &[u8],
row: &RawRetentionDbRow,
state: ksp_store_api::RawRetentionState,
) -> std::result::Result<(), crate::PostgresBackendError> {
let archive_payload = match load_retention_archive_payload(sql_transaction, signature_bytes).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match state {
ksp_store_api::RawRetentionState::Full => {
let payload = match row.payload.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_retention_full_shape")),
};
let payload_result = validate_retained_payload_bytes(payload.as_slice(), "raw_retention_full_payload");
if let std::result::Result::Err(error) = payload_result {
return std::result::Result::Err(error);
}
if archive_payload.is_some() {
return std::result::Result::Err(data_invalid("raw_retention_full_archive_residue"));
}
std::result::Result::Ok(())
},
ksp_store_api::RawRetentionState::Archived => {
if row.payload.is_some() {
return std::result::Result::Err(data_invalid("raw_retention_archived_hot_payload"));
}
let payload = match archive_payload.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_retention_archived_payload_missing")),
};
validate_retained_payload_bytes(payload.as_slice(), "raw_retention_archived_payload")
},
ksp_store_api::RawRetentionState::Purged => {
if row.payload.is_some() || archive_payload.is_some() || row.block_time_unix_millis.is_some() {
return std::result::Result::Err(data_invalid("raw_retention_purged_shape"));
}
std::result::Result::Ok(())
},
ksp_store_api::RawRetentionState::Compacted => std::result::Result::Err(retention_compaction_unsupported("raw_retention_compaction")),
_ => std::result::Result::Err(data_invalid("raw_retention_state")),
};
}
fn validate_retained_payload_bytes(payload: &[u8], phase: &'static str) -> std::result::Result<(), crate::PostgresBackendError> {
if payload.is_empty() || payload.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
return std::result::Result::Err(data_invalid(phase));
}
return std::result::Result::Ok(());
}
async fn insert_canonical_transaction(
sql_transaction: &deadpool_postgres::Transaction<'_>,
raw_transaction: &ksp_store_api::RawTransaction,
@@ -717,6 +968,20 @@ fn ensure_acquisition_inputs(
return std::result::Result::Ok(());
}
fn ensure_retention_transition_inputs(
network: &ksp_store_api::RawNetworkId,
transition: &ksp_store_api::RawTransactionRetentionTransition,
) -> std::result::Result<(), crate::PostgresBackendError> {
let network_result = ensure_network(network, transition.reference(), "raw_retention_transition_network");
if let std::result::Result::Err(error) = network_result {
return std::result::Result::Err(error);
}
if transition.expected() == ksp_store_api::RawRetentionState::Compacted || transition.target() == ksp_store_api::RawRetentionState::Compacted {
return std::result::Result::Err(retention_compaction_unsupported("raw_retention_compaction"));
}
return std::result::Result::Ok(());
}
fn encode_origin(origin: ksp_store_api::RawAcquisitionOrigin) -> std::result::Result<&'static str, crate::PostgresBackendError> {
return match origin {
ksp_store_api::RawAcquisitionOrigin::Backfill => std::result::Result::Ok("backfill"),
@@ -732,6 +997,10 @@ fn conflict(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::Conflict, phase);
}
fn retention_compaction_unsupported(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::RetentionCompactionUnsupported, phase);
}
fn reference_not_found(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReferenceNotFound, phase);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 8
// version: 9
const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -390,6 +390,14 @@ impl PostgresBackend {
return crate::record_raw_transaction_observation(&self.pool, &self.network, observation).await;
}
/// Applies one policy-authorized atomic RAW transaction retention transition.
pub async fn transition_raw_transaction_retention(
&self,
transition: ksp_store_api::RawTransactionRetentionTransition,
) -> std::result::Result<ksp_store_api::RawRetentionWriteOutcome, crate::PostgresBackendError> {
return crate::transition_raw_transaction_retention(&self.pool, &self.network, transition).await;
}
/// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound.
pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> {
self.pool.close();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 11
// version: 12
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -166,12 +166,7 @@ fn pre_005_raw_write_sql_is_atomic_idempotent_and_keeps_later_scope_closed() {
] {
assert!(raw.contains(required), "missing pre.005 RAW write contract: {required}");
}
for forbidden in [
"DELETE FROM",
"transition_raw_transaction_retention",
"impl ksp_store_api::RawTransactionWrite",
"impl ksp_store_api::RawTransactionObservationWrite",
] {
for forbidden in ["DELETE FROM", "impl ksp_store_api::RawTransactionWrite", "impl ksp_store_api::RawTransactionObservationWrite"] {
assert!(!raw.contains(forbidden), "pre.005 opened later RAW scope prematurely: {forbidden}");
}
return;
@@ -215,9 +210,41 @@ fn pre_006_raw_pagination_is_keyset_cursor_bound_and_policy_free() {
}
assert!(index.contains("ON ksp_raw_transactions (slot, signature)"));
assert!(index.contains("WHERE retention_state <> 'purged'"));
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000", "DELETE FROM", "transition_raw_transaction_retention"] {
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] {
assert!(!raw.contains(forbidden), "pre.006 contains forbidden pagination/policy/later-scope material: {forbidden}");
assert!(!cursor.contains(forbidden), "pre.006 cursor contains forbidden pagination/policy/later-scope material: {forbidden}");
}
return;
}
#[test]
fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compaction() {
let raw = include_str!("../src/raw_transaction.rs");
let runtime = include_str!("../src/runtime.rs");
for required in [
"LOCK_RETENTION_TRANSACTION_SQL",
"FOR UPDATE",
"INSERT_ARCHIVE_PAYLOAD_SQL",
"INSERT INTO ksp_raw_transaction_archive_payloads (signature, payload)",
"UPDATE_ARCHIVED_TRANSACTION_SQL",
"SET payload = NULL, retention_state = 'archived'",
"DELETE_ARCHIVE_PAYLOAD_SQL",
"DELETE FROM ksp_raw_transaction_archive_payloads",
"UPDATE_PURGED_TRANSACTION_SQL",
"SET block_time_unix_millis = NULL, payload = NULL, retention_state = 'purged'",
"if current == target",
"if current != expected",
"RawRetentionWriteOutcome::AlreadyAtTarget",
"RawRetentionWriteOutcome::ExpectedStateMismatch",
"RawRetentionWriteOutcome::Applied",
"RetentionCompactionUnsupported",
"transition_raw_transaction_retention",
] {
assert!(raw.contains(required), "missing pre.007 retention contract: {required}");
}
assert!(runtime.contains("pub async fn transition_raw_transaction_retention"));
for forbidden in ["retention_state = 'compacted'", "impl ksp_store_api::RawTransactionRetentionWrite", "flate", "zstd", "lz4", "snappy"] {
assert!(!raw.contains(forbidden), "pre.007 contains fake compaction/direct-trait scope: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
// version: 7
// version: 8
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -48,13 +48,14 @@ fn pre_005_backend_error_projection_is_safe_and_static() {
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid,
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound,
ksp_store_postgres_lib::PostgresBackendErrorKind::RetentionCompactionUnsupported,
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed,
ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork,
];
assert_eq!(kinds.len(), 17);
assert_eq!(kinds.len(), 18);
return;
}
@@ -95,3 +96,9 @@ fn pre_006_raw_list_bridge_uses_backend_independent_query_page_and_reference_mod
let _list = ksp_store_postgres_lib::PostgresBackend::list_raw_transactions;
return;
}
#[test]
fn pre_007_raw_retention_write_bridge_uses_backend_independent_transition_and_outcome_models() {
let _transition = ksp_store_postgres_lib::PostgresBackend::transition_raw_transaction_retention;
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs
// version: 3
// version: 4
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
@@ -458,3 +458,114 @@ fn pre_006_page_limit_exposes_only_the_real_postgres_limit_plus_one_boundary() {
assert_eq!(rejected.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::PageLimitUnsupported));
return;
}
#[test]
fn pre_007_retention_transition_decision_matches_compare_and_transition_contract() {
assert_eq!(
super::retention_transition_decision(
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Archived,
),
std::result::Result::Ok(super::RetentionTransitionDecision::Archive),
);
assert_eq!(
super::retention_transition_decision(
ksp_store_api::RawRetentionState::Archived,
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Archived,
),
std::result::Result::Ok(super::RetentionTransitionDecision::AlreadyAtTarget),
);
assert_eq!(
super::retention_transition_decision(
ksp_store_api::RawRetentionState::Archived,
ksp_store_api::RawRetentionState::Archived,
ksp_store_api::RawRetentionState::Purged,
),
std::result::Result::Ok(super::RetentionTransitionDecision::Purge),
);
assert_eq!(
super::retention_transition_decision(
ksp_store_api::RawRetentionState::Purged,
ksp_store_api::RawRetentionState::Archived,
ksp_store_api::RawRetentionState::Purged,
),
std::result::Result::Ok(super::RetentionTransitionDecision::AlreadyAtTarget),
);
assert_eq!(
super::retention_transition_decision(
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Archived,
ksp_store_api::RawRetentionState::Purged,
),
std::result::Result::Ok(super::RetentionTransitionDecision::ExpectedStateMismatch),
);
return;
}
#[test]
fn pre_007_compacted_transitions_are_rejected_by_the_pre_io_guard() {
let backend_network = network();
let reference = ksp_store_api::RawTransactionReference::new(backend_network.clone(), ksp_store_api::RawTransactionSignature::new([5; 64]));
let compact = match ksp_store_api::RawTransactionRetentionTransition::try_new(
reference.clone(),
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Compacted,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid logical compact transition rejected by API: {error:?}"),
};
let compact_error = super::ensure_retention_transition_inputs(&backend_network, &compact).err();
assert_eq!(
compact_error.as_ref().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::RetentionCompactionUnsupported),
);
assert_eq!(compact_error.as_ref().map(|value| return value.phase()), std::option::Option::Some("raw_retention_compaction"));
let compact_to_archive = match ksp_store_api::RawTransactionRetentionTransition::try_new(
reference,
ksp_store_api::RawRetentionState::Compacted,
ksp_store_api::RawRetentionState::Archived,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid logical compacted archive transition rejected by API: {error:?}"),
};
let compact_to_archive_error = super::ensure_retention_transition_inputs(&backend_network, &compact_to_archive).err();
assert_eq!(
compact_to_archive_error.as_ref().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::RetentionCompactionUnsupported),
);
return;
}
#[test]
fn pre_007_retention_transition_pre_io_guard_rejects_wrong_network() {
let backend_network = network();
let other_network = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid alternate network rejected: {error:?}"),
};
let reference = ksp_store_api::RawTransactionReference::new(other_network, ksp_store_api::RawTransactionSignature::new([6; 64]));
let transition = match ksp_store_api::RawTransactionRetentionTransition::try_new(
reference,
ksp_store_api::RawRetentionState::Full,
ksp_store_api::RawRetentionState::Archived,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid retention transition rejected by API: {error:?}"),
};
let rejected = super::ensure_retention_transition_inputs(&backend_network, &transition);
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
return;
}
#[test]
fn pre_007_retained_payload_shape_rejects_empty_and_oversized_bytes() {
assert!(super::validate_retained_payload_bytes(&[1], "test_retention_payload").is_ok());
let empty = super::validate_retained_payload_bytes(&[], "test_retention_payload");
assert_eq!(empty.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
let oversized = vec![0; ksp_store_api::MAX_RAW_PAYLOAD_BYTES + 1];
let oversized = super::validate_retained_payload_bytes(oversized.as_slice(), "test_retention_payload");
assert_eq!(oversized.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
return;
}

158
deltas/0.3.3/pre.007.md Normal file
View File

@@ -0,0 +1,158 @@
<!-- file: deltas/0.3.3/pre.007.md -->
<!-- version: 1 -->
# Delta `0.3.3-pre.007` — rétention atomique RawTransaction
## 1. Base et gate d'entrée
Base opérateur obligatoire :
```text
0.3.3-pre.6.fix.1
```
Le gate opérateur fourni le 2026-08-30 est entièrement vert :
```text
cargo fmt --all PASS
scripts/audit_rust_workspace_rules.py PASS
scripts/audit_markdown_tables.py PASS
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS (34 unit backend, live foundation ignoré)
cargo test -p ksp-config-lib PASS (128 unit + ownership/public API)
cargo check -p ksp-store-lib --no-default-features PASS
```
La pagination/cursor `pre.006` est donc acquise et la tranche peut ouvrir la rétention mutante.
## 2. Version
```text
workspace.package.version = 0.3.3-pre.7
```
## 3. Scope exact
Cette tranche ajoute uniquement le bridge backend PostgreSQL de transition :
```text
PostgresBackend::transition_raw_transaction_retention
Full -> Archived
Archived -> Purged
compare-and-transition
Compacted unsupported
```
Ne sont pas encore ouverts :
```text
implémentations complètes des six traits RawTransaction*
dispatch ksp-store-lib
RawAccountState
worker/job policy
```
## 4. Pré-I/O
Avant `pool.get()` :
- le réseau de `transition.reference()` doit égaler le binding backend ;
- si `expected` ou `target` vaut `Compacted`, le backend retourne `RetentionCompactionUnsupported` ;
- le code KSP stable reste `store.postgres_retention_compaction_unsupported`.
Aucune ligne `retention_state = 'compacted'` n'est créée et aucune dépendance de compression n'est ajoutée.
## 5. Compare-and-transition
Sous transaction PostgreSQL, le canonical est lu avec `FOR UPDATE`. Sa forme physique est validée avant de retourner un outcome ou de muter :
```text
current == target -> AlreadyAtTarget
current != expected -> ExpectedStateMismatch
Full + expected Full + target Archived -> archive
Archived + expected Archived + target Purged -> purge
```
Une référence absente est `ReferenceNotFound`. Aucun succès n'est rendu avant commit.
## 6. `Full -> Archived`
La transition :
1. exige un payload hot non vide et borné ;
2. exige l'absence d'une archive résiduelle ;
3. insère les octets exacts dans `ksp_raw_transaction_archive_payloads` ;
4. met `payload = NULL` et `retention_state = 'archived'` sur le canonical ;
5. commit les deux opérations ensemble.
Aucun état `Archived` sans payload archive n'est committé par KSP.
## 7. `Archived -> Purged`
La transition :
1. exige l'absence de payload hot et la présence d'un payload archive valide ;
2. supprime la ligne archive ;
3. met `block_time_unix_millis = NULL`, `payload = NULL` et `retention_state = 'purged'` ;
4. conserve signature, slot, format id/version et content hash ;
5. commit atomiquement.
Le tombstone reste donc limité aux cinq champs du contrat API.
## 8. Races croisées
Les acquisitions/idempotence/ForceRehydrate de `pre.005` et les transitions de cette tranche verrouillent la même ligne canonical avant mutation. Les races archive/purge/rehydrate sont donc sérialisées ; la preuve concurrente PostgreSQL réelle reste réservée à `pre.009`.
## 9. Erreurs
La classification backend ajoute :
```text
RetentionCompactionUnsupported
```
Les autres classifications réutilisées sont :
```text
WrongNetwork
ReferenceNotFound
DataInvalid
WriteFailed
```
Aucun texte serveur, SQLSTATE, query ou bind n'est retenu.
## 10. Migrations
Aucune ressource V000/V001 n'est modifiée. Les checksums attendus restent :
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
```
## 11. Gate opérateur demandé
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.3
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Le test PostgreSQL live reste `#[ignore]`; la preuve métier rétention/concurrence est réservée à `pre.009`.
## 12. Suite si gate vert
```text
0.3.3-pre.008 — six implémentations de capabilities + dispatch ksp-store-lib
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# Plan `0.3.3` — Store/PostgreSQL RawTransaction vertical slice
@@ -31,15 +31,16 @@ Tranches techniques validées :
0.3.3-pre.003 + fix.001/fix.002/fix.003 — V001 physique, compatibilité de schéma et binding réseau
0.3.3-pre.004 — mapping et lectures RAW
0.3.3-pre.005 — écriture atomique et observations
0.3.3-pre.006 + fix.001 — pagination keyset + cursor V1
```
Tranche technique courante :
```text
0.3.3-pre.006pagination keyset + cursor V1
0.3.3-pre.007archive, purge, compare-and-transition et rejet Compacted
```
Les gates opérateur jusqu'à `pre.005` sont verts. `pre.006` complète la surface backend-specific de lecture `RawTransaction` avec `list_raw_transactions`, sans ouvrir encore la rétention mutante, les implémentations complètes des six traits ni le dispatch dans `ksp-store-lib`.
Les gates opérateur jusqu'à `pre.006-fix.001` sont verts. `pre.007` complète la surface backend-specific `RawTransaction` avec les transitions physiques `Full -> Archived -> Purged`, sans implémenter encore les six traits ni leur dispatch dans `ksp-store-lib`.
## 2. Sources et autorité
@@ -1131,12 +1132,19 @@ Tranche matérialisée :
### `0.3.3-pre.007` — archive, purge, tombstone, ForceRehydrate
- `Full -> Archived -> Purged` ;
- compare-and-transition ;
- archive relation ;
- normal skip purged ;
- force rehydrate ;
- rejet sûr et stable de `Compacted`, sans fausse compression.
Matérialisation :
- `PostgresBackend::transition_raw_transaction_retention` ajouté sans implémentation directe de trait ;
- validation réseau et rejet de toute transition impliquant `Compacted` avant `pool.get()` ;
- `SELECT ... FOR UPDATE` du canonical avant toute décision/mutation ;
- `current == target` -> `AlreadyAtTarget` après validation de la forme physique ;
- `current != expected` -> `ExpectedStateMismatch` sans mutation ;
- `Full -> Archived` -> payload exact inséré dans la relation archive, puis payload hot `NULL` et état `archived`, dans la même transaction ;
- `Archived -> Purged` -> archive supprimée, block time effacé, état `purged`, tombstone minimal conservé ;
- unknown reference -> `ReferenceNotFound` ;
- `Compacted` -> `RetentionCompactionUnsupported` / `store.postgres_retention_compaction_unsupported` sans fausse compression ;
- acquisition normale et `ForceRehydrate` acquis en `pre.005` continuent d'utiliser le même verrou canonical et sont donc sérialisés avec archive/purge ;
- V000/V001 restent byte-identiques.
### `0.3.3-pre.008` — façade `ksp-store-lib`

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Validation `0.3.3` — Store/PostgreSQL RawTransaction vertical slice
@@ -594,12 +594,20 @@ cap 500/1000 dans Store pagination
- `LIMIT requested + 1`, limite `i64::MAX - 1`, aucun cap worker : PASS unit/statique ;
- index V001 `(slot, signature)` partiel réutilisé, migration inchangée : PASS statique ;
- snapshot inter-pages explicitement non garanti : PASS doc ;
- gate Cargo opérateur : À EXÉCUTER.
- premier gate opérateur : FAIL local sur re-export cursor inutilisé + placeholder manquant du canari hardening ;
- `pre.006-fix.001` corrige uniquement ces deux défauts ; gate opérateur complet du 2026-08-30 : PASS (`check`, Clippy, Store/API/PostgreSQL/Config, no-default-features ; 34 tests unit backend).
### `pre.007`
- archive/purge/tombstone/ForceRehydrate ;
- Compacted unsupported stable.
- méthode backend-specific de transition : PASS statique ;
- network + `Compacted` rejetés pré-I/O : PASS unit design ;
- `Full -> Archived` et `Archived -> Purged` sous `FOR UPDATE` : PASS statique/unit decision ;
- `AlreadyAtTarget` / `ExpectedStateMismatch` / `Applied` : PASS unit decision ;
- archive payload/hot payload/block time validés selon l'état avant mutation/no-op : PASS statique ;
- unknown reference -> `ReferenceNotFound` : PASS statique ;
- `RetentionCompactionUnsupported` stable, aucune représentation `compacted` SQL : PASS statique/unit ;
- preuve des races et atomicité PostgreSQL réelle : différée à `pre.009` ;
- gate Cargo opérateur : À EXÉCUTER.
### `pre.008`
@@ -631,7 +639,7 @@ cap 500/1000 dans Store pagination
- publication stable.
## 22. Gate courant `pre.006`
## 22. Gate courant `pre.007`
```bash
cargo fmt --all
@@ -706,12 +714,24 @@ cargo check -p ksp-store-lib --no-default-features
- [PASS] replay network/direction/range et cursor hostile classés `QueryInvalid` ;
- [PASS] limite physique `requested <= i64::MAX - 1`, au-delà `PageLimitUnsupported` sans clamp ;
- [PASS] aucune migration SQL modifiée, aucun scope rétention mutante/façade ouvert ;
- [À FAIRE] gate Cargo opérateur complet de `pre.006`.
- [PASS] premier gate a isolé deux défauts purement locaux, corrigés en `pre.006-fix.001`.
### `pre.006-fix.001` — correction du gate
- [PASS] re-export crate-private `RawTransactionDecodedCursor` inutilisé supprimé sans modifier le codec cursor ;
- [PASS] canari `hardening_completeness` corrigé pour concaténer les huit sources backend inspectées ;
- [PASS] aucune logique SQL, pagination, migration ou checksum V000/V001 modifiée ;
- [À FAIRE] gate Cargo opérateur complet de `pre.006-fix.001`.
- [PASS] gate Cargo opérateur complet fourni le 2026-08-30.
### `pre.007` — état de la tranche
- [PASS] bridge backend-specific `transition_raw_transaction_retention` ajouté ;
- [PASS] validation réseau et `Compacted` avant pool I/O ;
- [PASS] compare-and-transition sérialisé par verrou canonical `FOR UPDATE` ;
- [PASS] `Full -> Archived` copie le payload exact vers archive puis retire le payload hot atomiquement ;
- [PASS] `Archived -> Purged` supprime l'archive, efface block time et conserve le tombstone minimal ;
- [PASS] `AlreadyAtTarget`, `ExpectedStateMismatch`, `Applied` décidés depuis l'état verrouillé ;
- [PASS] `RetentionCompactionUnsupported` utilise le code stable déjà acquis ;
- [PASS] aucune migration SQL modifiée, aucun trait Store complet ni dispatch façade ouvert ;
- [À FAIRE] gate Cargo opérateur complet de `pre.007`.