v0.3.1-pre.006

This commit is contained in:
2026-08-29 09:10:07 +02:00
parent c83e3261d0
commit b76baf6835
19 changed files with 1175 additions and 60 deletions

View File

@@ -6,7 +6,7 @@ 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-wallet-lib"] 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-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.3.1-pre.5" version = "0.3.1-pre.6"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability.rs // file: crates/ksp-store-api/src/capability.rs
// version: 2 // version: 3
//! Private home for backend-agnostic Store capability contracts. //! Private home for backend-agnostic Store capability contracts.
//! //!
@@ -9,6 +9,7 @@
//! outside `ksp-store-api`. //! outside `ksp-store-api`.
pub(crate) mod raw_account; pub(crate) mod raw_account;
pub(crate) mod raw_retention;
pub(crate) mod raw_transaction; pub(crate) mod raw_transaction;
/// Boxed async operation returned by object-safe Store capability contracts. /// Boxed async operation returned by object-safe Store capability contracts.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability/raw_account.rs // file: crates/ksp-store-api/src/capability/raw_account.rs
// version: 1 // version: 2
/// Read capability for complete canonical RAW account states. /// Read capability for complete canonical RAW account states.
/// ///
@@ -12,21 +12,31 @@ pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {
&'a self, &'a self,
reference: &'a crate::RawAccountStateReference, reference: &'a crate::RawAccountStateReference,
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawAccountState>>>; ) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawAccountState>>>;
/// Lists deterministic account-state references using an opaque backend cursor.
///
/// The backend must not invent a KSP policy ceiling below the caller's
/// request. A real backend/physical limitation may determine the page that
/// can be served and continuation is represented by the opaque cursor.
fn list_raw_account_states<'a>(
&'a self,
query: &'a crate::RawAccountStateQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawAccountStateReference>>>;
} }
/// Write capability for complete RAW account-state acquisitions. /// Write capability for complete RAW account-state acquisitions.
/// ///
/// The account state and its acquisition observation form one logical /// The account state and its acquisition observation form one logical
/// persistence operation. An implementation must not leave one side durable if /// persistence operation. An implementation must not leave one side durable if
/// the other side fails. Detailed idempotence/conflict outcomes are introduced /// the other side fails. Divergent content for one logical identity is a stable
/// by `0.3.1-pre.006`; this tranche exposes only success/failure. /// conflict error, never a silent overwrite.
pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync { pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync {
/// Persists one complete RAW account state together with one observation atomically. /// Persists one complete RAW account state together with one observation atomically.
fn persist_raw_account_acquisition<'a>( fn persist_raw_account_acquisition<'a>(
&'a self, &'a self,
state: crate::RawAccountState, state: crate::RawAccountState,
observation: crate::RawAccountObservation, observation: crate::RawAccountObservation,
) -> crate::StoreApiFuture<'a, crate::Result<()>>; ) -> crate::StoreApiFuture<'a, crate::Result<crate::RawAcquisitionWriteOutcome>>;
} }
/// Read capability for persisted RAW account-state observations. /// Read capability for persisted RAW account-state observations.
@@ -41,9 +51,11 @@ pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {
/// Write capability for an additional observation of an already persisted RAW account state. /// Write capability for an additional observation of an already persisted RAW account state.
/// ///
/// This capability allows repeated HTTP/WS/gRPC acquisitions to be retained /// This capability allows repeated HTTP/WS/gRPC acquisitions to be retained
/// without resubmitting account bytes. The referenced state must already exist; /// without resubmitting account bytes. The referenced state must already exist.
/// detailed outcomes are deferred to `0.3.1-pre.006`.
pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync { pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync {
/// Persists one additional acquisition observation for an existing RAW account state. /// Persists one additional acquisition observation for an existing RAW account state.
fn record_raw_account_observation<'a>(&'a self, observation: crate::RawAccountObservation) -> crate::StoreApiFuture<'a, crate::Result<()>>; fn record_raw_account_observation<'a>(
&'a self,
observation: crate::RawAccountObservation,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawObservationWriteOutcome>>;
} }

View File

@@ -0,0 +1,29 @@
// file: crates/ksp-store-api/src/capability/raw_retention.rs
// version: 1
/// Read capability for canonical RAW transaction retention metadata.
pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {
/// Reads the logical retention state for one known transaction identity.
fn get_raw_transaction_retention_state<'a>(
&'a self,
reference: &'a crate::RawTransactionReference,
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawRetentionState>>>;
/// Reads the minimal durable tombstone when the transaction payload was purged.
fn get_raw_transaction_tombstone<'a>(
&'a self,
reference: &'a crate::RawTransactionReference,
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawTransactionTombstone>>>;
}
/// Write capability for policy-authorized RAW transaction retention transitions.
///
/// This contract applies a transition selected by an external worker/job policy;
/// the Store backend never decides by itself that higher processing is complete.
pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {
/// Applies one atomic forward lifecycle transition chosen by the caller.
fn transition_raw_transaction_retention<'a>(
&'a self,
transition: crate::RawTransactionRetentionTransition,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawRetentionWriteOutcome>>;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability/raw_transaction.rs // file: crates/ksp-store-api/src/capability/raw_transaction.rs
// version: 1 // version: 2
/// Read capability for canonical RAW transactions. /// Read capability for canonical RAW transactions.
/// ///
@@ -12,21 +12,32 @@ pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {
&'a self, &'a self,
reference: &'a crate::RawTransactionReference, reference: &'a crate::RawTransactionReference,
) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawTransaction>>>; ) -> crate::StoreApiFuture<'a, crate::Result<std::option::Option<crate::RawTransaction>>>;
/// Lists deterministic transaction references using an opaque backend cursor.
///
/// The backend must honor the caller-requested page limit unless a real
/// backend/physical limitation prevents it. `ksp-store-api` imposes no
/// arbitrary smaller result ceiling or executor policy.
fn list_raw_transactions<'a>(
&'a self,
query: &'a crate::RawTransactionQuery,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawPage<crate::RawTransactionReference>>>;
} }
/// Write capability for canonical RAW transaction acquisitions. /// Write capability for canonical RAW transaction acquisitions.
/// ///
/// The transaction and its acquisition observation form one logical persistence /// The transaction and its acquisition observation form one logical persistence
/// operation. An implementation must not leave one side durable if the other /// operation. An implementation must not leave one side durable if the other
/// side fails. Detailed idempotence/conflict outcomes are introduced by /// side fails. Divergent content for one logical identity is a stable conflict
/// `0.3.1-pre.006`; this tranche exposes only success/failure. /// error, never a silent overwrite.
pub trait RawTransactionWrite: std::marker::Send + std::marker::Sync { pub trait RawTransactionWrite: std::marker::Send + std::marker::Sync {
/// Persists one complete RAW transaction together with one observation atomically. /// Persists one complete RAW transaction together with one observation atomically.
fn persist_raw_transaction_acquisition<'a>( fn persist_raw_transaction_acquisition<'a>(
&'a self, &'a self,
transaction: crate::RawTransaction, transaction: crate::RawTransaction,
observation: crate::RawTransactionObservation, observation: crate::RawTransactionObservation,
) -> crate::StoreApiFuture<'a, crate::Result<()>>; mode: crate::RawTransactionAcquisitionMode,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawAcquisitionWriteOutcome>>;
} }
/// Read capability for persisted RAW transaction observations. /// Read capability for persisted RAW transaction observations.
@@ -42,9 +53,11 @@ pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {
/// ///
/// This capability exists so repeated acquisitions can be recorded without /// This capability exists so repeated acquisitions can be recorded without
/// resubmitting the potentially large canonical transaction payload. The /// resubmitting the potentially large canonical transaction payload. The
/// referenced transaction must already exist; detailed outcomes are deferred to /// referenced transaction must already exist.
/// `0.3.1-pre.006`.
pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync { pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {
/// Persists one additional acquisition observation for an existing RAW transaction. /// Persists one additional acquisition observation for an existing RAW transaction.
fn record_raw_transaction_observation<'a>(&'a self, observation: crate::RawTransactionObservation) -> crate::StoreApiFuture<'a, crate::Result<()>>; fn record_raw_transaction_observation<'a>(
&'a self,
observation: crate::RawTransactionObservation,
) -> crate::StoreApiFuture<'a, crate::Result<crate::RawObservationWriteOutcome>>;
} }

View File

@@ -1,9 +1,15 @@
// file: crates/ksp-store-api/src/error.rs // file: crates/ksp-store-api/src/error.rs
// version: 1 // version: 2
/// Error code used when a RAW write collides with divergent content for the same logical identity.
pub const ERROR_CODE_RAW_CONFLICT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_conflict");
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants. /// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
pub const ERROR_CODE_RAW_MODEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_model_invalid"); pub const ERROR_CODE_RAW_MODEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_model_invalid");
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract. /// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
pub const ERROR_CODE_RAW_PAYLOAD_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_payload_invalid"); pub const ERROR_CODE_RAW_PAYLOAD_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_payload_invalid");
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. /// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
pub const ERROR_CODE_RAW_PROVENANCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_provenance_invalid"); pub const ERROR_CODE_RAW_PROVENANCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_provenance_invalid");
/// Error code used when one RAW query or cursor violates backend-agnostic query invariants.
pub const ERROR_CODE_RAW_QUERY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_query_invalid");
/// Error code used when a RAW retention transition violates the logical lifecycle contract.
pub const ERROR_CODE_RAW_RETENTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_retention_invalid");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/lib.rs // file: crates/ksp-store-api/src/lib.rs
// version: 4 // version: 5
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -30,6 +30,10 @@ pub use self::capability::raw_account::RawAccountObservationWrite;
pub use self::capability::raw_account::RawAccountStateRead; pub use self::capability::raw_account::RawAccountStateRead;
/// Write capability for complete canonical RAW account-state acquisitions. /// Write capability for complete canonical RAW account-state acquisitions.
pub use self::capability::raw_account::RawAccountStateWrite; pub use self::capability::raw_account::RawAccountStateWrite;
/// Read capability for canonical RAW transaction retention metadata.
pub use self::capability::raw_retention::RawTransactionRetentionRead;
/// Write capability for policy-authorized RAW transaction retention transitions.
pub use self::capability::raw_retention::RawTransactionRetentionWrite;
/// Read capability for persisted RAW transaction observations. /// Read capability for persisted RAW transaction observations.
pub use self::capability::raw_transaction::RawTransactionObservationRead; pub use self::capability::raw_transaction::RawTransactionObservationRead;
/// Write capability for additional observations of already persisted RAW transactions. /// Write capability for additional observations of already persisted RAW transactions.
@@ -38,18 +42,48 @@ pub use self::capability::raw_transaction::RawTransactionObservationWrite;
pub use self::capability::raw_transaction::RawTransactionRead; pub use self::capability::raw_transaction::RawTransactionRead;
/// Write capability for canonical RAW transaction acquisitions. /// Write capability for canonical RAW transaction acquisitions.
pub use self::capability::raw_transaction::RawTransactionWrite; pub use self::capability::raw_transaction::RawTransactionWrite;
/// Error code used when a RAW write collides with divergent content for the same logical identity.
pub use self::error::ERROR_CODE_RAW_CONFLICT;
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants. /// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
pub use self::error::ERROR_CODE_RAW_MODEL_INVALID; pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract. /// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID; pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID;
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. /// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID; pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID;
/// Error code used when one RAW query or cursor violates backend-agnostic query invariants.
pub use self::error::ERROR_CODE_RAW_QUERY_INVALID;
/// Error code used when a RAW retention transition violates the logical lifecycle contract.
pub use self::error::ERROR_CODE_RAW_RETENTION_INVALID;
/// Persistable acquisition observation linked to one complete canonical RAW account state. /// Persistable acquisition observation linked to one complete canonical RAW account state.
pub use self::model::raw_account::RawAccountObservation; pub use self::model::raw_account::RawAccountObservation;
/// Canonical complete N1 RAW account state independent from acquisition transport. /// Canonical complete N1 RAW account state independent from acquisition transport.
pub use self::model::raw_account::RawAccountState; pub use self::model::raw_account::RawAccountState;
/// Durable backend-independent identity of one canonical RAW account state. /// Durable backend-independent identity of one canonical RAW account state.
pub use self::model::raw_account::RawAccountStateReference; pub use self::model::raw_account::RawAccountStateReference;
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;
/// Outcome for one canonical RAW entity in an idempotent persistence operation.
pub use self::model::raw_outcome::RawEntityWriteOutcome;
/// Outcome for one deterministic acquisition observation write.
pub use self::model::raw_outcome::RawObservationWriteOutcome;
/// Maximum opaque query cursor length admitted by the Store API.
pub use self::model::raw_pagination::MAX_RAW_PAGE_CURSOR_BYTES;
/// Backend-independent list query for complete canonical RAW account states.
pub use self::model::raw_pagination::RawAccountStateQuery;
/// One deterministic page of backend-independent Store results.
pub use self::model::raw_pagination::RawPage;
/// Opaque backend-owned cursor returned by one deterministic Store query.
pub use self::model::raw_pagination::RawPageCursor;
/// Caller-requested page size without an arbitrary KSP policy ceiling.
pub use self::model::raw_pagination::RawPageLimit;
/// Opaque-cursor page request used by backend-independent list operations.
pub use self::model::raw_pagination::RawPageRequest;
/// Optional inclusive Solana slot bounds for one Store query.
pub use self::model::raw_pagination::RawSlotRange;
/// Deterministic traversal direction for Store list queries.
pub use self::model::raw_pagination::RawSortDirection;
/// Backend-independent list query for canonical RAW transactions.
pub use self::model::raw_pagination::RawTransactionQuery;
/// Maximum complete RAW account-data length admitted by the Store API. /// Maximum complete RAW account-data length admitted by the Store API.
pub use self::model::raw_primitives::MAX_RAW_ACCOUNT_DATA_BYTES; pub use self::model::raw_primitives::MAX_RAW_ACCOUNT_DATA_BYTES;
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code. /// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
@@ -80,6 +114,16 @@ pub use self::model::raw_primitives::RawProvenanceCode;
pub use self::model::raw_primitives::RawTimestamp; pub use self::model::raw_primitives::RawTimestamp;
/// Canonical 64-byte Solana transaction signature used by Store identities. /// Canonical 64-byte Solana transaction signature used by Store identities.
pub use self::model::raw_primitives::RawTransactionSignature; pub use self::model::raw_primitives::RawTransactionSignature;
/// Logical availability state of one canonical RAW payload.
pub use self::model::raw_retention::RawRetentionState;
/// Outcome of one atomic RAW retention transition.
pub use self::model::raw_retention::RawRetentionWriteOutcome;
/// Explicit write mode for canonical RAW transaction acquisitions.
pub use self::model::raw_retention::RawTransactionAcquisitionMode;
/// Requested compare-and-transition operation for one RAW transaction retention state.
pub use self::model::raw_retention::RawTransactionRetentionTransition;
/// Minimal durable identity retained after a canonical RAW transaction payload is purged.
pub use self::model::raw_retention::RawTransactionTombstone;
/// Canonical source-independent N1 RAW transaction persisted by Store backends. /// Canonical source-independent N1 RAW transaction persisted by Store backends.
pub use self::model::raw_transaction::RawTransaction; pub use self::model::raw_transaction::RawTransaction;
/// Persistable acquisition observation linked to one canonical RAW transaction. /// Persistable acquisition observation linked to one canonical RAW transaction.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/model.rs // file: crates/ksp-store-api/src/model.rs
// version: 3 // version: 4
//! Private home for persistent Store models. //! Private home for persistent Store models.
//! //!
@@ -9,5 +9,8 @@
//! one or more models. //! one or more models.
pub(crate) mod raw_account; pub(crate) mod raw_account;
pub(crate) mod raw_outcome;
pub(crate) mod raw_pagination;
pub(crate) mod raw_primitives; pub(crate) mod raw_primitives;
pub(crate) mod raw_retention;
pub(crate) mod raw_transaction; pub(crate) mod raw_transaction;

View File

@@ -0,0 +1,55 @@
// file: crates/ksp-store-api/src/model/raw_outcome.rs
// version: 1
/// Outcome for one canonical RAW entity in an idempotent persistence operation.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawEntityWriteOutcome {
/// The canonical RAW entity was inserted for the first time.
Inserted,
/// Identical canonical content was already durable.
AlreadyPresent,
/// A previously purged RAW entity was explicitly rehydrated.
Rehydrated,
/// Normal persistence skipped a durable purged tombstone.
SkippedPurged,
}
/// Outcome for one deterministic acquisition observation write.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawObservationWriteOutcome {
/// The observation was inserted for the first time.
Inserted,
/// The same observation key already identified identical durable content.
AlreadyPresent,
/// No observation was recorded because the associated RAW entity was intentionally skipped.
NotRecorded,
}
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RawAcquisitionWriteOutcome {
entity: crate::RawEntityWriteOutcome,
observation: crate::RawObservationWriteOutcome,
}
impl RawAcquisitionWriteOutcome {
/// Creates one backend-independent atomic acquisition outcome.
#[must_use]
pub const fn new(entity: crate::RawEntityWriteOutcome, observation: crate::RawObservationWriteOutcome) -> Self {
return Self { entity, observation };
}
/// Returns the canonical RAW entity write outcome.
#[must_use]
pub const fn entity(&self) -> crate::RawEntityWriteOutcome {
return self.entity;
}
/// Returns the acquisition observation write outcome.
#[must_use]
pub const fn observation(&self) -> crate::RawObservationWriteOutcome {
return self.observation;
}
}

View File

@@ -0,0 +1,267 @@
// file: crates/ksp-store-api/src/model/raw_pagination.rs
// version: 1
/// Maximum opaque cursor length accepted by Store API queries.
///
/// This bounds untrusted token material only. It is not a policy limit on the
/// number of rows/results a backend may return.
pub const MAX_RAW_PAGE_CURSOR_BYTES: usize = 4 * 1024;
/// Opaque backend-owned cursor returned by one deterministic Store query.
#[derive(Clone, Eq, PartialEq)]
pub struct RawPageCursor(std::boxed::Box<[u8]>);
impl RawPageCursor {
/// Creates one non-empty bounded opaque cursor.
pub fn try_new(bytes: std::boxed::Box<[u8]>) -> crate::Result<Self> {
if bytes.is_empty() || bytes.len() > crate::MAX_RAW_PAGE_CURSOR_BYTES {
return std::result::Result::Err(raw_query_error("cursor"));
}
return std::result::Result::Ok(Self(bytes));
}
/// Returns the opaque cursor bytes unchanged.
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
return self.0.as_ref();
}
}
impl std::fmt::Debug for RawPageCursor {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("RawPageCursor").field("len", &self.0.len()).finish();
}
}
/// Caller-requested page size without an arbitrary KSP policy ceiling.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RawPageLimit(u64);
impl RawPageLimit {
/// Creates one strictly positive requested page size.
///
/// `ksp-store-api` deliberately imposes no smaller functional maximum.
/// Concrete backends may expose or return their real physical limitations.
pub fn new(value: u64) -> crate::Result<Self> {
if value == 0 {
return std::result::Result::Err(raw_query_error("limit"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the exact caller-requested item count.
#[must_use]
pub const fn get(&self) -> u64 {
return self.0;
}
}
/// Opaque-cursor page request used by backend-independent list operations.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawPageRequest {
cursor: std::option::Option<crate::RawPageCursor>,
limit: crate::RawPageLimit,
}
impl RawPageRequest {
/// Creates one first-page request.
#[must_use]
pub const fn first(limit: crate::RawPageLimit) -> Self {
return Self { cursor: std::option::Option::None, limit };
}
/// Creates one continuation-page request with an opaque backend cursor.
#[must_use]
pub const fn after(limit: crate::RawPageLimit, cursor: crate::RawPageCursor) -> Self {
return Self { cursor: std::option::Option::Some(cursor), limit };
}
/// Returns the opaque continuation cursor when present.
#[must_use]
pub fn cursor(&self) -> std::option::Option<&crate::RawPageCursor> {
return self.cursor.as_ref();
}
/// Returns the exact caller-requested page size.
#[must_use]
pub const fn limit(&self) -> crate::RawPageLimit {
return self.limit;
}
}
/// One deterministic page of backend-independent Store results.
#[derive(Debug)]
pub struct RawPage<T> {
items: std::vec::Vec<T>,
next_cursor: std::option::Option<crate::RawPageCursor>,
}
impl<T> RawPage<T> {
/// Creates one result page from backend-provided items and optional continuation cursor.
#[must_use]
pub fn new(items: std::vec::Vec<T>, next_cursor: std::option::Option<crate::RawPageCursor>) -> Self {
return Self { items, next_cursor };
}
/// Returns the current page items.
#[must_use]
pub fn items(&self) -> &[T] {
return self.items.as_slice();
}
/// Consumes the page and returns its items.
#[must_use]
pub fn into_items(self) -> std::vec::Vec<T> {
return self.items;
}
/// Returns the next opaque cursor when more results are available.
#[must_use]
pub fn next_cursor(&self) -> std::option::Option<&crate::RawPageCursor> {
return self.next_cursor.as_ref();
}
}
/// Deterministic traversal direction for Store list queries.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawSortDirection {
/// Oldest/lower ordered keys first.
Ascending,
/// Newest/higher ordered keys first.
Descending,
}
/// Optional inclusive Solana slot bounds for one Store query.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RawSlotRange {
end_inclusive: std::option::Option<u64>,
start_inclusive: std::option::Option<u64>,
}
impl RawSlotRange {
/// Creates one optional inclusive slot range and rejects reversed bounds.
pub fn new(start_inclusive: std::option::Option<u64>, end_inclusive: std::option::Option<u64>) -> crate::Result<Self> {
if let (std::option::Option::Some(start), std::option::Option::Some(end)) = (start_inclusive, end_inclusive) {
if start > end {
return std::result::Result::Err(raw_query_error("slot_range"));
}
}
return std::result::Result::Ok(Self { end_inclusive, start_inclusive });
}
/// Returns the optional inclusive ending slot.
#[must_use]
pub const fn end_inclusive(&self) -> std::option::Option<u64> {
return self.end_inclusive;
}
/// Returns the optional inclusive starting slot.
#[must_use]
pub const fn start_inclusive(&self) -> std::option::Option<u64> {
return self.start_inclusive;
}
}
/// Backend-independent list query for canonical RAW transactions.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionQuery {
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawPageRequest,
slots: crate::RawSlotRange,
}
impl RawTransactionQuery {
/// Creates one deterministic transaction-reference query.
#[must_use]
pub fn new(network: crate::RawNetworkId, slots: crate::RawSlotRange, direction: crate::RawSortDirection, page: crate::RawPageRequest) -> Self {
return Self { direction, network, page, slots };
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the required logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns pagination inputs without exposing backend cursor contents.
#[must_use]
pub fn page(&self) -> &crate::RawPageRequest {
return &self.page;
}
/// Returns optional inclusive slot bounds.
#[must_use]
pub const fn slots(&self) -> crate::RawSlotRange {
return self.slots;
}
}
/// Backend-independent list query for complete canonical RAW account states.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawAccountStateQuery {
direction: crate::RawSortDirection,
network: crate::RawNetworkId,
page: crate::RawPageRequest,
pubkey: std::option::Option<crate::Pubkey>,
slots: crate::RawSlotRange,
}
impl RawAccountStateQuery {
/// Creates one deterministic account-state-reference query.
#[must_use]
pub fn new(
network: crate::RawNetworkId,
pubkey: std::option::Option<crate::Pubkey>,
slots: crate::RawSlotRange,
direction: crate::RawSortDirection,
page: crate::RawPageRequest,
) -> Self {
return Self { direction, network, page, pubkey, slots };
}
/// Returns the requested deterministic traversal direction.
#[must_use]
pub const fn direction(&self) -> crate::RawSortDirection {
return self.direction;
}
/// Returns the required logical network scope.
#[must_use]
pub fn network(&self) -> &crate::RawNetworkId {
return &self.network;
}
/// Returns pagination inputs without exposing backend cursor contents.
#[must_use]
pub fn page(&self) -> &crate::RawPageRequest {
return &self.page;
}
/// Returns an optional account-address restriction.
#[must_use]
pub fn pubkey(&self) -> std::option::Option<&crate::Pubkey> {
return self.pubkey.as_ref();
}
/// Returns optional inclusive slot bounds.
#[must_use]
pub const fn slots(&self) -> crate::RawSlotRange {
return self.slots;
}
}
fn raw_query_error(field: &'static str) -> crate::Error {
return crate::Error::new(crate::ERROR_CODE_RAW_QUERY_INVALID, "invalid backend-agnostic RAW Store query").with_context("field", field);
}
#[cfg(test)]
#[path = "../../unit_tests/model/raw_pagination.rs"]
mod tests;

View File

@@ -0,0 +1,152 @@
// file: crates/ksp-store-api/src/model/raw_retention.rs
// version: 1
/// Logical availability state of one canonical RAW payload.
///
/// These states describe payload retention only. They never prove that a
/// STRUCTURAL/DECODED/DOMAIN processor has completed.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawRetentionState {
/// Complete canonical RAW payload is retained in ordinary Store availability.
Full,
/// Complete logical RAW content is retained in a compacted representation.
Compacted,
/// RAW content has been moved to an archive managed outside the hot Store path.
Archived,
/// RAW payload is absent while a durable tombstone remains.
Purged,
}
/// Explicit write mode for canonical RAW transaction acquisitions.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawTransactionAcquisitionMode {
/// Normal acquisition must skip an existing purged tombstone.
Normal,
/// Explicitly allow an existing purged transaction to be rehydrated.
ForceRehydrate,
}
/// Minimal durable identity retained after a canonical RAW transaction payload is purged.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionTombstone {
content_hash: crate::RawContentHash,
format_id: crate::RawFormatId,
format_version: u32,
reference: crate::RawTransactionReference,
slot: u64,
}
impl RawTransactionTombstone {
/// Creates one minimal transaction tombstone from canonical RAW identity metadata.
pub fn try_new(
reference: crate::RawTransactionReference,
slot: u64,
format_id: crate::RawFormatId,
format_version: u32,
content_hash: crate::RawContentHash,
) -> crate::Result<Self> {
if format_version == 0 {
return std::result::Result::Err(raw_retention_error("format_version"));
}
return std::result::Result::Ok(Self { content_hash, format_id, format_version, reference, slot });
}
/// Returns the canonical content hash retained after purge.
#[must_use]
pub const fn content_hash(&self) -> crate::RawContentHash {
return self.content_hash;
}
/// Returns the KSP-owned canonical RAW format identifier retained after purge.
#[must_use]
pub fn format_id(&self) -> &crate::RawFormatId {
return &self.format_id;
}
/// Returns the KSP-owned canonical RAW format version retained after purge.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.format_version;
}
/// Returns the durable backend-independent transaction identity.
#[must_use]
pub fn reference(&self) -> &crate::RawTransactionReference {
return &self.reference;
}
/// Returns the known transaction slot retained after purge.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
}
/// Requested compare-and-transition operation for one RAW transaction retention state.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawTransactionRetentionTransition {
expected: crate::RawRetentionState,
reference: crate::RawTransactionReference,
target: crate::RawRetentionState,
}
impl RawTransactionRetentionTransition {
/// Creates one allowed forward retention transition.
///
/// Rehydration from `Purged` is deliberately excluded and uses
/// [`crate::RawTransactionAcquisitionMode::ForceRehydrate`] instead.
pub fn try_new(reference: crate::RawTransactionReference, expected: crate::RawRetentionState, target: crate::RawRetentionState) -> crate::Result<Self> {
if !allowed_transition(expected, target) {
return std::result::Result::Err(raw_retention_error("transition"));
}
return std::result::Result::Ok(Self { expected, reference, target });
}
/// Returns the state the caller expects before applying the transition.
#[must_use]
pub const fn expected(&self) -> crate::RawRetentionState {
return self.expected;
}
/// Returns the durable transaction identity targeted by the transition.
#[must_use]
pub fn reference(&self) -> &crate::RawTransactionReference {
return &self.reference;
}
/// Returns the requested forward retention state.
#[must_use]
pub const fn target(&self) -> crate::RawRetentionState {
return self.target;
}
}
/// Outcome of one atomic RAW retention transition.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RawRetentionWriteOutcome {
/// The requested forward transition was applied.
Applied,
/// The entity was already at the requested target state.
AlreadyAtTarget,
}
fn allowed_transition(expected: crate::RawRetentionState, target: crate::RawRetentionState) -> bool {
return matches!(
(expected, target),
(crate::RawRetentionState::Full, crate::RawRetentionState::Compacted)
| (crate::RawRetentionState::Full, crate::RawRetentionState::Archived)
| (crate::RawRetentionState::Compacted, crate::RawRetentionState::Archived)
| (crate::RawRetentionState::Archived, crate::RawRetentionState::Purged)
);
}
fn raw_retention_error(field: &'static str) -> crate::Error {
return crate::Error::new(crate::ERROR_CODE_RAW_RETENTION_INVALID, "invalid RAW retention lifecycle request").with_context("field", field);
}
#[cfg(test)]
#[path = "../../unit_tests/model/raw_retention.rs"]
mod tests;

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-store-api/tests/dependency_boundary.rs // file: crates/ksp-store-api/tests/dependency_boundary.rs
// version: 4 // version: 5
//! Dependency canaries for the Store API RAW foundation. //! Dependency canaries for the Store API RAW foundation.
#[test] #[test]
fn pre_005_manifest_keeps_exact_core_only_runtime_dependency() { fn pre_006_manifest_keeps_exact_core_only_runtime_dependency() {
let manifest = include_str!("../Cargo.toml"); let manifest = include_str!("../Cargo.toml");
let dependencies_tail = manifest.split("[dependencies]").nth(1); let dependencies_tail = manifest.split("[dependencies]").nth(1);
assert!(dependencies_tail.is_some(), "Store API dependencies section must exist"); assert!(dependencies_tail.is_some(), "Store API dependencies section must exist");
@@ -49,24 +49,45 @@ fn pre_005_manifest_keeps_exact_core_only_runtime_dependency() {
} }
#[test] #[test]
fn pre_005_source_boundary_keeps_models_and_capabilities_backend_free() { fn pre_006_source_boundary_keeps_models_and_capabilities_backend_free() {
let crate_root = include_str!("../src/lib.rs"); let crate_root = include_str!("../src/lib.rs");
let model_home = include_str!("../src/model.rs"); let model_home = include_str!("../src/model.rs");
let raw_account = include_str!("../src/model/raw_account.rs"); let raw_account = include_str!("../src/model/raw_account.rs");
let raw_outcome = include_str!("../src/model/raw_outcome.rs");
let raw_pagination = include_str!("../src/model/raw_pagination.rs");
let raw_primitives = include_str!("../src/model/raw_primitives.rs"); let raw_primitives = include_str!("../src/model/raw_primitives.rs");
let raw_retention = include_str!("../src/model/raw_retention.rs");
let raw_transaction = include_str!("../src/model/raw_transaction.rs"); let raw_transaction = include_str!("../src/model/raw_transaction.rs");
let capability_home = include_str!("../src/capability.rs"); let capability_home = include_str!("../src/capability.rs");
let raw_account_capability = include_str!("../src/capability/raw_account.rs"); let raw_account_capability = include_str!("../src/capability/raw_account.rs");
let raw_retention_capability = include_str!("../src/capability/raw_retention.rs");
let raw_transaction_capability = include_str!("../src/capability/raw_transaction.rs"); let raw_transaction_capability = include_str!("../src/capability/raw_transaction.rs");
assert!(crate_root.contains("mod capability;")); assert!(crate_root.contains("mod capability;"));
assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("mod model;")); assert!(crate_root.contains("mod model;"));
assert!(model_home.contains("raw_account")); assert!(model_home.contains("raw_account"));
assert!(model_home.contains("raw_outcome"));
assert!(model_home.contains("raw_pagination"));
assert!(model_home.contains("raw_primitives")); assert!(model_home.contains("raw_primitives"));
assert!(model_home.contains("raw_retention"));
assert!(model_home.contains("raw_transaction")); assert!(model_home.contains("raw_transaction"));
assert!(capability_home.contains("raw_account")); assert!(capability_home.contains("raw_account"));
assert!(capability_home.contains("raw_retention"));
assert!(capability_home.contains("raw_transaction")); assert!(capability_home.contains("raw_transaction"));
for source in [crate_root, model_home, raw_account, raw_primitives, raw_transaction, capability_home, raw_account_capability, raw_transaction_capability] { for source in [
crate_root,
model_home,
raw_account,
raw_outcome,
raw_pagination,
raw_primitives,
raw_retention,
raw_transaction,
capability_home,
raw_account_capability,
raw_retention_capability,
raw_transaction_capability,
] {
for forbidden in [ for forbidden in [
"ksp_store_lib", "ksp_store_lib",
"ksp_store_postgres_lib", "ksp_store_postgres_lib",
@@ -85,12 +106,14 @@ fn pre_005_source_boundary_keeps_models_and_capabilities_backend_free() {
} }
assert!(!raw_transaction.contains("RawLog")); assert!(!raw_transaction.contains("RawLog"));
for forbidden in ["TransactionStatusObservation", "RawLogNotification", "RawSlotEvent", "RawVoteEvent", "RawBlock", "YellowstoneEntry"] { for forbidden in ["TransactionStatusObservation", "RawLogNotification", "RawSlotEvent", "RawVoteEvent", "RawBlock", "YellowstoneEntry"] {
assert!(!crate_root.contains(forbidden), "deferred pre.005 model leaked into Store API surface: {forbidden}"); assert!(!crate_root.contains(forbidden), "deferred pre.006 model leaked into Store API surface: {forbidden}");
} }
assert!(raw_transaction_capability.contains("trait RawTransactionRead")); assert!(raw_transaction_capability.contains("trait RawTransactionRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionWrite")); assert!(raw_transaction_capability.contains("trait RawTransactionWrite"));
assert!(raw_transaction_capability.contains("trait RawTransactionObservationRead")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationRead"));
assert!(raw_transaction_capability.contains("trait RawTransactionObservationWrite")); assert!(raw_transaction_capability.contains("trait RawTransactionObservationWrite"));
assert!(raw_retention_capability.contains("trait RawTransactionRetentionRead"));
assert!(raw_retention_capability.contains("trait RawTransactionRetentionWrite"));
assert!(raw_account_capability.contains("trait RawAccountStateRead")); assert!(raw_account_capability.contains("trait RawAccountStateRead"));
assert!(raw_account_capability.contains("trait RawAccountStateWrite")); assert!(raw_account_capability.contains("trait RawAccountStateWrite"));
assert!(raw_account_capability.contains("trait RawAccountObservationRead")); assert!(raw_account_capability.contains("trait RawAccountObservationRead"));
@@ -98,8 +121,11 @@ fn pre_005_source_boundary_keeps_models_and_capabilities_backend_free() {
for forbidden in ["trait StoreBackend", "trait Store", "PostgresStore", "MySqlStore", "Arc<dyn"] { for forbidden in ["trait StoreBackend", "trait Store", "PostgresStore", "MySqlStore", "Arc<dyn"] {
assert!(!capability_home.contains(forbidden)); assert!(!capability_home.contains(forbidden));
assert!(!raw_account_capability.contains(forbidden)); assert!(!raw_account_capability.contains(forbidden));
assert!(!raw_retention_capability.contains(forbidden));
assert!(!raw_transaction_capability.contains(forbidden)); assert!(!raw_transaction_capability.contains(forbidden));
} }
assert!(raw_pagination.contains("u64::MAX") == false);
assert!(!raw_pagination.contains("MAX_RAW_PAGE_ITEMS"));
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/external_backend.rs // file: crates/ksp-store-api/tests/external_backend.rs
// version: 1 // version: 2
//! External-implementation canary for object-safe Store API capabilities. //! External-implementation canary for object-safe Store API capabilities.
@@ -15,6 +15,16 @@ impl ksp_store_api::RawTransactionRead for ExternalMemoryBackend {
return std::result::Result::Ok(std::option::Option::None); return std::result::Result::Ok(std::option::Option::None);
}); });
} }
fn list_raw_transactions<'a>(
&'a self,
query: &'a ksp_store_api::RawTransactionQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>>> {
let _ = query;
return std::boxed::Box::pin(async {
return std::result::Result::Ok(ksp_store_api::RawPage::new(std::vec::Vec::new(), std::option::Option::None));
});
}
} }
impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend { impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend {
@@ -22,11 +32,16 @@ impl ksp_store_api::RawTransactionWrite for ExternalMemoryBackend {
&'a self, &'a self,
transaction: ksp_store_api::RawTransaction, transaction: ksp_store_api::RawTransaction,
observation: ksp_store_api::RawTransactionObservation, observation: ksp_store_api::RawTransactionObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<()>> { mode: ksp_store_api::RawTransactionAcquisitionMode,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
let _ = transaction; let _ = transaction;
let _ = observation; let _ = observation;
let _ = mode;
return std::boxed::Box::pin(async { return std::boxed::Box::pin(async {
return std::result::Result::Ok(()); return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(
ksp_store_api::RawEntityWriteOutcome::Inserted,
ksp_store_api::RawObservationWriteOutcome::Inserted,
));
}); });
} }
} }
@@ -47,10 +62,10 @@ impl ksp_store_api::RawTransactionObservationWrite for ExternalMemoryBackend {
fn record_raw_transaction_observation<'a>( fn record_raw_transaction_observation<'a>(
&'a self, &'a self,
observation: ksp_store_api::RawTransactionObservation, observation: ksp_store_api::RawTransactionObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<()>> { ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
let _ = observation; let _ = observation;
return std::boxed::Box::pin(async { return std::boxed::Box::pin(async {
return std::result::Result::Ok(()); return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::Inserted);
}); });
} }
} }
@@ -65,6 +80,16 @@ impl ksp_store_api::RawAccountStateRead for ExternalMemoryBackend {
return std::result::Result::Ok(std::option::Option::None); return std::result::Result::Ok(std::option::Option::None);
}); });
} }
fn list_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>>> {
let _ = query;
return std::boxed::Box::pin(async {
return std::result::Result::Ok(ksp_store_api::RawPage::new(std::vec::Vec::new(), std::option::Option::None));
});
}
} }
impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend { impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend {
@@ -72,11 +97,14 @@ impl ksp_store_api::RawAccountStateWrite for ExternalMemoryBackend {
&'a self, &'a self,
state: ksp_store_api::RawAccountState, state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation, observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<()>> { ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
let _ = state; let _ = state;
let _ = observation; let _ = observation;
return std::boxed::Box::pin(async { return std::boxed::Box::pin(async {
return std::result::Result::Ok(()); return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(
ksp_store_api::RawEntityWriteOutcome::Inserted,
ksp_store_api::RawObservationWriteOutcome::Inserted,
));
}); });
} }
} }
@@ -97,16 +125,50 @@ impl ksp_store_api::RawAccountObservationWrite for ExternalMemoryBackend {
fn record_raw_account_observation<'a>( fn record_raw_account_observation<'a>(
&'a self, &'a self,
observation: ksp_store_api::RawAccountObservation, observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<()>> { ) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
let _ = observation; let _ = observation;
return std::boxed::Box::pin(async { return std::boxed::Box::pin(async {
return std::result::Result::Ok(()); return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::Inserted);
});
}
}
impl ksp_store_api::RawTransactionRetentionRead for ExternalMemoryBackend {
fn get_raw_transaction_retention_state<'a>(
&'a self,
reference: &'a ksp_store_api::RawTransactionReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawRetentionState>>> {
let _ = reference;
return std::boxed::Box::pin(async {
return std::result::Result::Ok(std::option::Option::None);
});
}
fn get_raw_transaction_tombstone<'a>(
&'a self,
reference: &'a ksp_store_api::RawTransactionReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawTransactionTombstone>>> {
let _ = reference;
return std::boxed::Box::pin(async {
return std::result::Result::Ok(std::option::Option::None);
});
}
}
impl ksp_store_api::RawTransactionRetentionWrite for ExternalMemoryBackend {
fn transition_raw_transaction_retention<'a>(
&'a self,
transition: ksp_store_api::RawTransactionRetentionTransition,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawRetentionWriteOutcome>> {
let _ = transition;
return std::boxed::Box::pin(async {
return std::result::Result::Ok(ksp_store_api::RawRetentionWriteOutcome::Applied);
}); });
} }
} }
#[test] #[test]
fn pre_005_external_backend_implements_each_capability_without_store_runtime_crate() { fn pre_006_external_backend_implements_each_capability_without_store_runtime_crate() {
let backend = ExternalMemoryBackend; let backend = ExternalMemoryBackend;
let transaction_read: &dyn ksp_store_api::RawTransactionRead = &backend; let transaction_read: &dyn ksp_store_api::RawTransactionRead = &backend;
let transaction_write: &dyn ksp_store_api::RawTransactionWrite = &backend; let transaction_write: &dyn ksp_store_api::RawTransactionWrite = &backend;
@@ -116,6 +178,8 @@ fn pre_005_external_backend_implements_each_capability_without_store_runtime_cra
let account_write: &dyn ksp_store_api::RawAccountStateWrite = &backend; let account_write: &dyn ksp_store_api::RawAccountStateWrite = &backend;
let account_observation_read: &dyn ksp_store_api::RawAccountObservationRead = &backend; let account_observation_read: &dyn ksp_store_api::RawAccountObservationRead = &backend;
let account_observation_write: &dyn ksp_store_api::RawAccountObservationWrite = &backend; let account_observation_write: &dyn ksp_store_api::RawAccountObservationWrite = &backend;
let retention_read: &dyn ksp_store_api::RawTransactionRetentionRead = &backend;
let retention_write: &dyn ksp_store_api::RawTransactionRetentionWrite = &backend;
let _ = transaction_read; let _ = transaction_read;
let _ = transaction_write; let _ = transaction_write;
let _ = transaction_observation_read; let _ = transaction_observation_read;
@@ -124,5 +188,7 @@ fn pre_005_external_backend_implements_each_capability_without_store_runtime_cra
let _ = account_write; let _ = account_write;
let _ = account_observation_read; let _ = account_observation_read;
let _ = account_observation_write; let _ = account_observation_write;
let _ = retention_read;
let _ = retention_write;
return; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/public_api.rs // file: crates/ksp-store-api/tests/public_api.rs
// version: 4 // version: 5
//! Integration canaries for the public `ksp-store-api` surface. //! Integration canaries for the public `ksp-store-api` surface.
@@ -146,3 +146,36 @@ fn public_pre_005_capabilities_are_available_from_crate_root_and_dyn_compatible(
assert!(account_observation_write.is_none()); assert!(account_observation_write.is_none());
return; return;
} }
#[test]
fn public_pre_006_query_outcome_and_retention_contracts_are_available_from_crate_root() {
let limit = match ksp_store_api::RawPageLimit::new(5_000_000) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(limit.get(), 5_000_000);
assert!(ksp_store_api::RawPageLimit::new(u64::MAX).is_ok());
let slots = match ksp_store_api::RawSlotRange::new(std::option::Option::Some(1), std::option::Option::Some(2)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let query =
ksp_store_api::RawTransactionQuery::new(network, slots, ksp_store_api::RawSortDirection::Ascending, ksp_store_api::RawPageRequest::first(limit));
assert_eq!(query.page().limit().get(), 5_000_000);
let outcome =
ksp_store_api::RawAcquisitionWriteOutcome::new(ksp_store_api::RawEntityWriteOutcome::Inserted, ksp_store_api::RawObservationWriteOutcome::Inserted);
assert_eq!(outcome.entity(), ksp_store_api::RawEntityWriteOutcome::Inserted);
assert_eq!(outcome.observation(), ksp_store_api::RawObservationWriteOutcome::Inserted);
let retention_read: std::option::Option<&dyn ksp_store_api::RawTransactionRetentionRead> = std::option::Option::None;
let retention_write: std::option::Option<&dyn ksp_store_api::RawTransactionRetentionWrite> = std::option::Option::None;
assert!(retention_read.is_none());
assert!(retention_write.is_none());
assert_eq!(ksp_store_api::ERROR_CODE_RAW_CONFLICT.domain(), "store_api");
assert_eq!(ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID.domain(), "store_api");
assert_eq!(ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID.domain(), "store_api");
return;
}

View File

@@ -0,0 +1,54 @@
// file: crates/ksp-store-api/unit_tests/model/raw_pagination.rs
// version: 1
#[test]
fn page_limit_rejects_only_zero_and_keeps_no_ksp_policy_maximum() {
assert!(crate::RawPageLimit::new(0).is_err());
let maximum = crate::RawPageLimit::new(u64::MAX);
assert!(maximum.is_ok());
let maximum = match maximum {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(maximum.get(), u64::MAX);
return;
}
#[test]
fn cursor_is_opaque_bounded_and_debug_does_not_render_bytes() {
assert!(crate::RawPageCursor::try_new(std::boxed::Box::new([])).is_err());
assert!(crate::RawPageCursor::try_new(vec![7_u8; crate::MAX_RAW_PAGE_CURSOR_BYTES + 1].into_boxed_slice()).is_err());
let cursor = crate::RawPageCursor::try_new(vec![0xAB_u8; 8].into_boxed_slice());
assert!(cursor.is_ok());
let cursor = match cursor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let debug = format!("{cursor:?}");
assert!(debug.contains("len"));
assert!(!debug.contains("171"));
return;
}
#[test]
fn transaction_query_preserves_caller_limit_range_direction_and_network() {
let network = match crate::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let slots = match crate::RawSlotRange::new(std::option::Option::Some(10), std::option::Option::Some(20)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let limit = match crate::RawPageLimit::new(5_000_000) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let query = crate::RawTransactionQuery::new(network, slots, crate::RawSortDirection::Ascending, crate::RawPageRequest::first(limit));
assert_eq!(query.network().as_str(), "mainnet-beta");
assert_eq!(query.slots().start_inclusive(), std::option::Option::Some(10));
assert_eq!(query.page().limit().get(), 5_000_000);
assert_eq!(query.direction(), crate::RawSortDirection::Ascending);
assert!(crate::RawSlotRange::new(std::option::Option::Some(20), std::option::Option::Some(10)).is_err());
return;
}

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-store-api/unit_tests/model/raw_retention.rs
// version: 1
fn reference() -> std::option::Option<crate::RawTransactionReference> {
let network = match crate::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
return std::option::Option::Some(crate::RawTransactionReference::new(network, crate::RawTransactionSignature::new([3_u8; 64])));
}
#[test]
fn retention_transitions_are_forward_only_and_force_rehydrate_is_separate() {
let reference = match reference() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(crate::RawTransactionRetentionTransition::try_new(reference.clone(), crate::RawRetentionState::Full, crate::RawRetentionState::Compacted).is_ok());
assert!(crate::RawTransactionRetentionTransition::try_new(reference.clone(), crate::RawRetentionState::Full, crate::RawRetentionState::Archived).is_ok());
assert!(
crate::RawTransactionRetentionTransition::try_new(reference.clone(), crate::RawRetentionState::Compacted, crate::RawRetentionState::Archived).is_ok()
);
assert!(crate::RawTransactionRetentionTransition::try_new(reference.clone(), crate::RawRetentionState::Archived, crate::RawRetentionState::Purged).is_ok());
assert!(crate::RawTransactionRetentionTransition::try_new(reference.clone(), crate::RawRetentionState::Purged, crate::RawRetentionState::Full).is_err());
assert!(crate::RawTransactionRetentionTransition::try_new(reference, crate::RawRetentionState::Full, crate::RawRetentionState::Purged).is_err());
return;
}
#[test]
fn tombstone_keeps_minimal_identity_without_raw_payload() {
let reference = match reference() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let tombstone = crate::RawTransactionTombstone::try_new(reference, 99, format, 1, crate::RawContentHash::new([4_u8; 32]));
assert!(tombstone.is_ok());
let tombstone = match tombstone {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(tombstone.slot(), 99);
assert_eq!(tombstone.format_version(), 1);
let second_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(crate::RawTransactionTombstone::try_new(tombstone.reference().clone(), tombstone.slot(), second_format, 0, tombstone.content_hash()).is_err());
return;
}

243
deltas/0.3.1/pre.006.md Normal file
View File

@@ -0,0 +1,243 @@
<!-- file: deltas/0.3.1/pre.006.md -->
<!-- version: 1 -->
# Delta `0.3.1-pre.006` — queries, outcomes et lifecycle RAW
## Base requise
```text
0.3.1-pre.5
```
Le gate opérateur de `pre.005` est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets` et `cargo test -p ksp-store-api` passent.
## Recalibrage pagination
Le brainstorming opérateur précise une règle durable : Store fournit une primitive de query/pagination, **pas une policy d'exécution**.
Une limitation physique réelle d'un backend peut contraindre ce qu'il sert en une opération. En revanche `ksp-store-api`, `ksp-store-lib` et les crates backend ne doivent pas inventer un plafond métier inférieur (`100`, `500`, etc.) simplement pour gouverner le travail d'un futur executor.
`pre.006` matérialise donc :
```text
RawPageLimit > 0
aucun maximum fonctionnel KSP arbitraire
RawPageCursor opaque <= 4 KiB
```
La borne du cursor protège un token d'entrée hostile ; elle ne limite pas le nombre de résultats. Le futur job/worker choisira sa taille de travail et son backlog via ses propres policies/processing contracts.
## Objectif
Finaliser les contrats N1 encore nécessaires avant le hardening de release :
```text
queries/list cursorisés
outcomes idempotents
conflit stable
atomic acquisition finalisée
rétention logique transactionnelle
tombstone anti-rebackfill
normal skip / force rehydrate explicite
```
Aucune compression, archive physique, base de données, policy de purge ou executor n'est implémenté.
## Fichiers ajoutés
```text
crates/ksp-store-api/src/capability/raw_retention.rs
crates/ksp-store-api/src/model/raw_outcome.rs
crates/ksp-store-api/src/model/raw_pagination.rs
crates/ksp-store-api/src/model/raw_retention.rs
crates/ksp-store-api/unit_tests/model/raw_pagination.rs
crates/ksp-store-api/unit_tests/model/raw_retention.rs
deltas/0.3.1/pre.006.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-api/src/capability.rs
crates/ksp-store-api/src/capability/raw_account.rs
crates/ksp-store-api/src/capability/raw_transaction.rs
crates/ksp-store-api/src/error.rs
crates/ksp-store-api/src/lib.rs
crates/ksp-store-api/src/model.rs
crates/ksp-store-api/tests/dependency_boundary.rs
crates/ksp-store-api/tests/external_backend.rs
crates/ksp-store-api/tests/public_api.rs
docs/plans/022-V0_3_1_STORE_RAW_PLAN.md
docs/validation/018-V0_3_1_STORE_RAW.md
```
## Fichiers supprimés
```text
aucun
```
## Version
```text
workspace.package.version = 0.3.1-pre.6
```
## Queries et pagination
Surface ajoutée :
```text
RawPageCursor
RawPageLimit
RawPageRequest
RawPage<T>
RawSlotRange
RawSortDirection
RawTransactionQuery
RawAccountStateQuery
```
`RawPageLimit::new(0)` est invalide. Aucun maximum KSP n'est imposé ; `u64::MAX` reste représentable par le contrat. Les queries sont network-scoped et peuvent borner les slots ; les account queries peuvent en plus sélectionner un pubkey.
Les capabilities read gagnent :
```text
list_raw_transactions(query)
list_raw_account_states(query)
```
Les pages retournent des références compactes et un cursor opaque, pas des rows/PK/SQL backend.
## Outcomes et conflits
Surface ajoutée :
```text
RawEntityWriteOutcome
Inserted
AlreadyPresent
Rehydrated
SkippedPurged
RawObservationWriteOutcome
Inserted
AlreadyPresent
NotRecorded
RawAcquisitionWriteOutcome
entity + observation
```
Les opérations `persist_raw_*_acquisition` retournent désormais l'outcome combiné et conservent le contrat all-or-nothing RAW + observation.
Une même identité avec un contenu divergent doit produire :
```text
ERROR_CODE_RAW_CONFLICT
```
et jamais un overwrite silencieux.
Les écritures d'observation seules retournent `RawObservationWriteOutcome`.
## Lifecycle RAW transaction
Surface ajoutée :
```text
RawRetentionState
Full
Compacted
Archived
Purged
RawTransactionTombstone
RawTransactionRetentionTransition
RawRetentionWriteOutcome
RawTransactionAcquisitionMode
Normal
ForceRehydrate
RawTransactionRetentionRead
RawTransactionRetentionWrite
```
Transitions génériques admises :
```text
Full -> Compacted
Full -> Archived
Compacted -> Archived
Archived -> Purged
```
Interdits :
```text
Full -> Purged direct
Purged -> Full via transition générique
```
La réhydratation est un mode d'acquisition explicitement forcé. En mode normal, un tombstone purgé doit rester connu et produire `SkippedPurged` sans rebackfill automatique.
Le tombstone conserve uniquement l'identité transactionnelle, slot, format/version et hash canonique nécessaires au contrat anti-rebackfill ; aucun payload RAW n'y subsiste.
## Ownership rétention
Le Store applique la transition demandée mais ne décide jamais que les couches supérieures sont terminées. L'éligibilité compression/archive/purge appartient à un futur worker/job/maintenance policy fondé sur les preuves de processing versionnées.
La policy reste family-specific ; `RawAccountState` ne reçoit donc aucun lifecycle de purge copié artificiellement depuis `RawTransaction` dans cette tranche.
## Canari backend externe
Le backend externe de test est mis à jour pour :
```text
queries/pages
outcomes détaillés
mode Normal/ForceRehydrate
retention read/write
```
Il reste implémentable sans `ksp-store-lib`, PostgreSQL, runtime DB ou dépendance async supplémentaire.
## Hors scope confirmé
```text
ksp-store-lib
ksp-store-postgres-lib
PostgreSQL/tokio-postgres
Config/std.store
policy executor/backlog métier
processing ledger concret
compression/archive/purge physique
health runtime
TransactionStatusObservation commun
models event-only logs/slot/vote
RawBlock persistence
Yellowstone Entry persistence
N2 STRUCTURAL
N3 DECODED
N4 DOMAIN
```
## Validations opérateur de la base
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py ... PASS
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
```
## Validations de génération
Les audits structurels/Markdown sont rejoués sur l'overlay `pre.006`. `cargo`, `rustc` et `rustfmt` ne sont pas installés dans l'environnement de génération ; le gate Cargo final reste donc opérateur.
## Suite
`0.3.1-pre.007` effectue le hardening adversarial/completeness : cursor/provenance hostile, races de rétention, exact exports/modules, frontière Interface/Store, dependency firewall et scope négatif N2/N3/N4.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/022-V0_3_1_STORE_RAW_PLAN.md --> <!-- file: docs/plans/022-V0_3_1_STORE_RAW_PLAN.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Plan `0.3.1` — Store API RAW foundation # Plan `0.3.1` — Store API RAW foundation
@@ -75,7 +75,7 @@ modèles objet/struct persistants communs
observations d'acquisition persistantes lorsque leur conservation est utile observations d'acquisition persistantes lorsque leur conservation est utile
références durables références durables
outcomes d'écriture/idempotence outcomes d'écriture/idempotence
queries et pagination bornées queries cursorisées sans plafond métier arbitraire
health/readiness communs utiles health/readiness communs utiles
contrats/capabilities backend extensibles contrats/capabilities backend extensibles
référence/format canonique de wake-up pour une donnée déjà persistée, conformément aux règles KSP-NOTIFY référence/format canonique de wake-up pour une donnée déjà persistée, conformément aux règles KSP-NOTIFY
@@ -868,35 +868,58 @@ Les opérations `persist_raw_*_acquisition` signifient au contrat que le RAW et
Les opérations `record_raw_*_observation` supposent que la référence RAW ciblée existe déjà et permettent de retenir une acquisition supplémentaire sans retransmettre la donnée RAW complète. Les opérations `record_raw_*_observation` supposent que la référence RAW ciblée existe déjà et permettent de retenir une acquisition supplémentaire sans retransmettre la donnée RAW complète.
Les listes, queries, backlog, health commun et outcomes détaillés restent à finaliser dans `pre.006`; aucune transaction backend publique n'est nécessaire pour les exprimer. Les listes, queries et outcomes détaillés restent à finaliser dans `pre.006`; le backlog métier reste hors Store API et appartiendra au futur processing/job layer. Le health runtime reste une responsabilité de la future façade `ksp-store-lib` en `0.3.2`. Aucune transaction backend publique n'est nécessaire pour exprimer les opérations Store.
### 11.5 Outcomes ### 11.5 Outcomes
Le vocabulaire candidat : `pre.006` matérialise un vocabulaire commun d'idempotence :
```text ```text
RawWriteOutcome RawEntityWriteOutcome
Inserted Inserted
AlreadyPresent AlreadyPresent
Rehydrated
SkippedPurged
Conflict RawObservationWriteOutcome
= Error KSP stable, pas un succès silencieux Inserted
AlreadyPresent
NotRecorded
RawAcquisitionWriteOutcome
entity + observation
``` ```
Pour une acquisition combinée, l'outcome doit distinguer au minimum l'état du RAW et de l'observation sans révéler de clé backend. `Inserted` et `AlreadyPresent` sont des succès idempotents. Une même identité logique accompagnée d'un contenu divergent produit `ERROR_CODE_RAW_CONFLICT`; aucun backend ne peut résoudre ce cas par overwrite silencieux.
### 11.6 Pagination Pour `RawTransaction`, `Normal` et `ForceRehydrate` sont deux modes explicites : un tombstone purgé provoque `SkippedPurged` en mode normal ; seul `ForceRehydrate` autorise une réhydratation compatible. Le mode forcé n'annule pas les règles de conflit de contenu/format.
Le concept historique `PageRequest/PageSlice` est repris avec : ### 11.6 Pagination et queries
Le Store fournit un mécanisme de navigation, **pas une policy d'exécution**. `pre.006` introduit :
```text ```text
default = 100 RawPageLimit
maximum = 500 > 0
cursor opaque et borné aucun maximum fonctionnel KSP arbitraire
ordre déterministe par query
RawPageCursor
opaque
<= 4 KiB pour borner le token hostile
RawPageRequest
RawPage<T>
RawSlotRange
RawSortDirection
RawTransactionQuery
RawAccountStateQuery
``` ```
Le cursor est un token d'implémentation retourné par la façade et réinjecté tel quel par le consumer. Son contenu ne devient pas API et ne doit pas être utilisé comme transport d'un SQL fragment. Une requête de `5_000_000` éléments n'est pas ramenée silencieusement à `500`, `1000` ou une autre policy Store. Si un backend ne peut physiquement servir qu'une partie de la demande en une opération, il peut retourner cette partie avec un cursor de continuation ou une erreur backend réellement liée à sa capacité ; `ksp-store-lib` ne doit pas inventer un plafond inférieur par prudence.
Le cursor est un token d'implémentation retourné puis réinjecté tel quel par le consumer. Son contenu ne devient pas API, ne transporte jamais un fragment SQL public et sa borne de 4 KiB protège l'admission du token sans limiter le nombre de résultats.
Les queries `0.3.1` décrivent seulement des critères de données (network, slot range, compte optionnel, ordre, page). Elles ne définissent pas de backlog métier, de taille de batch worker, de priorité ou de policy executor. Le futur processing ledger/job layer décidera quoi traiter et dans quel volume.
## 12. Références, événements et cycle de vie N1 ## 12. Références, événements et cycle de vie N1
@@ -1105,7 +1128,7 @@ L'archive contient aussi les documents kbot2 historiques. Ils avaient déjà for
| logs transactionnels | conservés puis extraits dans la couche Core historique | REDESSINER | restent dans `RawTransaction`, puis deviennent des unités N2 STRUCTURAL ; pas de `RawLog` persistant séparé | | logs transactionnels | conservés puis extraits dans la couche Core historique | REDESSINER | restent dans `RawTransaction`, puis deviennent des unités N2 STRUCTURAL ; pas de `RawLog` persistant séparé |
| account observations/states | N1 account observation + N2 account state existaient | REDESSINER | prévoir `RawAccountState`/observation ; N2 seulement si une décomposition réellement utile est démontrée | | account observations/states | N1 account observation + N2 account state existaient | REDESSINER | prévoir `RawAccountState`/observation ; N2 seulement si une décomposition réellement utile est démontrée |
| repository traits | capabilities async séparées | REPRENDRE | capabilities read/write fines, aucune obligation de supporter toutes les familles | | repository traits | capabilities async séparées | REPRENDRE | capabilities read/write fines, aucune obligation de supporter toutes les familles |
| pagination | 100 par défaut, 500 max, cursor opaque | REPRENDRE | mêmes bornes candidates, cursor toujours opaque | | pagination | cursor opaque historique | REDESSINER | limite caller > 0 sans plafond métier KSP ; cursor hostile borné uniquement |
| replay contracts | sélection + force replay version-aware | REPRENDRE | futur replay piloté par processor/version/input hash, jamais par simple bool | | replay contracts | sélection + force replay version-aware | REPRENDRE | futur replay piloté par processor/version/input hash, jamais par simple bool |
| `processing_state` RAW | `received/core_extracted/failed` facilitait la queue | REDESSINER | peut servir d'index de travail futur mais ne constitue jamais la preuve durable unique de traitement | | `processing_state` RAW | `received/core_extracted/failed` facilitait la queue | REDESSINER | peut servir d'index de travail futur mais ne constitue jamais la preuve durable unique de traitement |
| processing ledger | stage + processor/version + input/hash + status | REPRENDRE | future preuve durable versionnée ; reportée aux couches/jobs concernés | | processing ledger | stage + processor/version + input/hash + status | REPRENDRE | future preuve durable versionnée ; reportée aux couches/jobs concernés |
@@ -1231,7 +1254,7 @@ Les invariants API qui contraignent déjà ce futur schéma restent fixés maint
identités logiques non dépendantes d'une PK SQL identités logiques non dépendantes d'une PK SQL
idempotence Inserted/AlreadyPresent/Conflict idempotence Inserted/AlreadyPresent/Conflict
atomicité RAW + observation atomicité RAW + observation
pagination bornée pagination cursorisée sans plafond métier KSP
payload versionné et borné payload versionné et borné
provenance sans secret provenance sans secret
round-trip exact du modèle objet commun round-trip exact du modèle objet commun
@@ -1296,7 +1319,7 @@ transaction logs remain owned by RawTransaction before STRUCTURAL extraction
account/status model compatibility canaries lorsqu'introduits account/status model compatibility canaries lorsqu'introduits
event-only model does not imply Store capability event-only model does not imply Store capability
atomic-operation contract fake backend atomic-operation contract fake backend
pagination bounds pagination cursor/zero-limit admission sans plafond métier arbitraire
retention state/tombstone transition invariants retention state/tombstone transition invariants
normal backfill skips purged tombstone; forced rehydrate remains explicit normal backfill skips purged tombstone; forced rehydrate remains explicit
processing proof is not represented by a lone boolean processing proof is not represented by a lone boolean
@@ -1332,7 +1355,7 @@ Aucun PostgreSQL live test n'appartient à `0.3.1` puisque le backend PostgreSQL
| processing ledger versionné | future contrat Store commun | hors surface initiale RAW | avec jobs/N2/N3 | preuve durable stage/version/input, pas bool | | processing ledger versionné | future contrat Store commun | hors surface initiale RAW | avec jobs/N2/N3 | preuve durable stage/version/input, pas bool |
| RAW references | `ksp-store-api` | public | `0.3.1` | reads/replay/lineage | | RAW references | `ksp-store-api` | public | `0.3.1` | reads/replay/lineage |
| write outcomes | `ksp-store-api` | public | `0.3.1` | sémantique idempotente commune | | write outcomes | `ksp-store-api` | public | `0.3.1` | sémantique idempotente commune |
| query/page | `ksp-store-api` | public | `0.3.1` | backlog/replay backend-agnostic | | query/page | `ksp-store-api` | public | `0.3.1` | navigation/replay backend-agnostic |
| health portable | `ksp-store-api` | public | `0.3.1` | consumer commun | | health portable | `ksp-store-api` | public | `0.3.1` | consumer commun |
| backend capabilities | `ksp-store-api` | public | `0.3.1` | implémentations externes | | backend capabilities | `ksp-store-api` | public | `0.3.1` | implémentations externes |
| `Store` facade | `ksp-store-lib` | public | `0.3.2` | point de consommation commun | | `Store` facade | `ksp-store-lib` | public | `0.3.2` | point de consommation commun |
@@ -1397,7 +1420,7 @@ Matérialiser les contracts read/write object-safe pour transaction/account et l
### `pre.006` — Queries, outcomes et lifecycle RAW ### `pre.006` — Queries, outcomes et lifecycle RAW
Finaliser reads/list/backlog bornés, atomic acquisition contract, `RawRetentionState`, tombstone minimal et sémantiques normal-skip/force-rehydrate sans implémenter compression/archive physique. Finaliser reads/list cursorisés sans plafond métier arbitraire, outcomes idempotents/conflict, atomic acquisition contract, `RawRetentionState`, tombstone minimal et sémantiques normal-skip/force-rehydrate sans implémenter compression/archive physique ni policy executor.
### `pre.007` — Boundary/adversarial hardening + completeness ### `pre.007` — Boundary/adversarial hardening + completeness
@@ -1467,7 +1490,7 @@ models et capabilities séparés
contrats backend communs stables contrats backend communs stables
backend externe implémentable sans ksp-store-lib backend externe implémentable sans ksp-store-lib
aucun type backend/SQL public aucun type backend/SQL public
queries/pagination bornées queries/pagination cursorisée sans plafond métier KSPs
idempotence/conflict semantics explicites idempotence/conflict semantics explicites
atomic acquisition contract explicite atomic acquisition contract explicite
références durables stables références durables stables

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/018-V0_3_1_STORE_RAW.md --> <!-- file: docs/validation/018-V0_3_1_STORE_RAW.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Validation `0.3.1` — Store API RAW foundation # Validation `0.3.1` — Store API RAW foundation
@@ -70,7 +70,7 @@ cycle de rétention logique + tombstone
| transaction handle | aucun handle SQL/backend public | `pre.005` | | transaction handle | aucun handle SQL/backend public | `pre.005` |
| atomic acquisition | méthode métier RAW + observation all-or-nothing | `pre.005/006` | | atomic acquisition | méthode métier RAW + observation all-or-nothing | `pre.005/006` |
| write outcome | `Inserted / AlreadyPresent`; divergence = `Err Conflict` | `pre.006` | | write outcome | `Inserted / AlreadyPresent`; divergence = `Err Conflict` | `pre.006` |
| page/cursor | default 100, max 500, cursor opaque borné | `pre.006` | | page/cursor | limit caller > 0 ; sans max KSP ; cursor opaque <= 4 KiB | `pre.006` |
| `RawRetentionState` | logique `Full/Compacted/Archived/Purged`, sans détail physique | `pre.006` | | `RawRetentionState` | logique `Full/Compacted/Archived/Purged`, sans détail physique | `pre.006` |
| tombstone | identité/hash/slot minimal durable après purge | `pre.006` | | tombstone | identité/hash/slot minimal durable après purge | `pre.006` |
| backfill normal après purge | skip distinct | `pre.006` | | backfill normal après purge | skip distinct | `pre.006` |
@@ -184,7 +184,7 @@ crate/test backend externe
| partial transaction+observation | interdit par atomic acquisition | `pre.005` PASS | | partial transaction+observation | interdit par atomic acquisition | `pre.005` PASS |
| event-only -> Store capability | absent par défaut | `pre.004/007` | | event-only -> Store capability | absent par défaut | `pre.004/007` |
| Interface/Store duplicate model | absent | `pre.007` | | Interface/Store duplicate model | absent | `pre.007` |
| page limit 0/>500 | rejet | `pre.006` | | page limit 0 | rejet ; aucun max KSP artificiel | `pre.006` |
| SQL/backend cursor leak | absent | `pre.006/007` | | SQL/backend cursor leak | absent | `pre.006/007` |
| external backend | implémente API sans Store lib | `pre.005` PASS | | external backend | implémente API sans Store lib | `pre.005` PASS |
| processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` | | processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` |
@@ -390,6 +390,41 @@ sécurité
-> PostgreSQL réel opt-in puis gate final obligatoire -> PostgreSQL réel opt-in puis gate final obligatoire
``` ```
### 8.4 Matérialisation `pre.006`
La tranche ajoute les contrats logiques suivants sans backend/runtime :
```text
RawPageCursor / RawPageLimit / RawPageRequest / RawPage<T>
RawSlotRange / RawSortDirection
RawTransactionQuery / RawAccountStateQuery
RawEntityWriteOutcome / RawObservationWriteOutcome / RawAcquisitionWriteOutcome
RawRetentionState
RawTransactionAcquisitionMode
RawTransactionTombstone
RawTransactionRetentionTransition / RawRetentionWriteOutcome
RawTransactionRetentionRead / RawTransactionRetentionWrite
```
Gates matérialisés :
```text
page limit 0 rejeté
u64::MAX admis comme demande représentable : aucun maximum métier KSP
cursor opaque borné à 4 KiB et Debug sans contenu
range de slots inversée rejetée
queries limitées aux critères de données, sans backlog/executor policy
Inserted / AlreadyPresent explicites
contenu divergent = ERROR_CODE_RAW_CONFLICT
RAW + observation gardent le contrat atomique
Full -> Compacted -> Archived -> Purged, avec Full -> Archived autorisé
aucun Full -> Purged direct
aucun Purged -> Full via transition générique
force rehydrate explicite via acquisition transactionnelle
tombstone minimal sans payload
retention policy choisie hors Store par worker/job/maintenance
```
## 11. État initial des tranches ## 11. État initial des tranches
| Tranche | Objet | État | | Tranche | Objet | État |
@@ -399,7 +434,7 @@ sécurité
| `pre.003` | primitives + RawTransaction | À FAIRE | | `pre.003` | primitives + RawTransaction | À FAIRE |
| `pre.004` | admission matrix + account/status models | PRÊT après gate local | | `pre.004` | admission matrix + account/status models | PRÊT après gate local |
| `pre.005` | backend contracts/capabilities | PRÊT après gate local | | `pre.005` | backend contracts/capabilities | PRÊT après gate local |
| `pre.006` | queries/outcomes/retention/tombstone | À FAIRE | | `pre.006` | queries/outcomes/retention/tombstone | PRÊT après gate local |
| `pre.007` | boundary/adversarial/completeness | À FAIRE | | `pre.007` | boundary/adversarial/completeness | À FAIRE |
| `pre.008` | gate technique final | À FAIRE | | `pre.008` | gate technique final | À FAIRE |
| `pre.009` | réconciliation documentaire | À FAIRE | | `pre.009` | réconciliation documentaire | À FAIRE |